Skip to content

Commit 7bcb719

Browse files
committed
feat: add function call sample
1 parent d9b7627 commit 7bcb719

File tree

4 files changed

+451
-39
lines changed

4 files changed

+451
-39
lines changed

README.zh-Hans.md

Lines changed: 322 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,6 @@ public class YourService(IDashScopeClient client)
6767
## 支持的 API
6868

6969
- [文本生成](#文本生成) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景
70-
- [多轮对话](#多轮对话)
71-
- [深度思考](#深度思考)
72-
- [联网搜索](#联网搜索)
73-
- [工具调用](#工具调用)
7470
- [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景
7571
- [语音合成](#语音合成) - CosyVoice,Sambert 等,支持 TTS 等应用场景
7672
- [图像生成](#图像生成) - wanx2.1 等,支持文生图,人像风格重绘等应用场景
@@ -437,7 +433,9 @@ var request = new ModelRequest<TextGenerationInput, ITextGenerationParameters>()
437433
};
438434
```
439435

440-
通过返回结果里的 `response.Output.SearchInfo` 来获取搜索结果,这个值会在模型搜索后一次性返回,并在之后的每次返回中都附带。因此,开启增量流式输出时,不需要通过 `StringBuilder` 等方式来缓存 `SearchInfo`
436+
通过返回结果里的 `response.Output.SearchInfo` 来获取搜索结果,这个值会在第一个包随模型回复完整返回。因此,开启增量流式输出时,不需要通过 `StringBuilder` 等方式来缓存 `SearchInfo`
437+
438+
联网搜索的调用次数可以通过最后一个包的 `response.Usage.Plugins.Search.Count` 获取。
441439

442440
```csharp
443441
var messages = new List<TextChatMessage>();
@@ -650,7 +648,325 @@ Usage: in(2178)/out(1571)/reasoning(952)/plugins:(1)/total(3749)
650648

651649
### 工具调用
652650

653-
通过 `Parameter` 里的 `Tools` 来向模型提供可用的工具列表,模型会返回 `Tool` 角色的消息来调用工具。
651+
通过 `Parameter` 里的 `Tools` 来向模型提供可用的工具列表,模型会返回带有 `ToolCall` 属性的消息来调用工具。
652+
653+
接收到消息后,服务端需要调用对应工具并将结果作为 `Tool` 角色的消息插入到对话记录中再发起请求,模型会根据工具调用的结果总结答案。
654+
655+
默认情况下,模型每次只会调用一次工具。如果输入的问题需要多次调用同一工具,或需要同时调用多个工具,可以在 `Parameter` 里启用 `ParallelToolCalls` 来允许模型同时发起多次工具调用请求。
656+
657+
这个示例中,我们先定义一个获取天气的 C# 方法:
658+
659+
```csharp
660+
public record WeatherReportParameters(
661+
[property: Required]
662+
[property: Description("要获取天气的省市名称,例如浙江省杭州市")]
663+
string Location,
664+
[property: JsonConverter(typeof(EnumStringConverter<TemperatureUnit>))]
665+
[property: Description("温度单位")]
666+
TemperatureUnit Unit = TemperatureUnit.Celsius);
667+
668+
public enum TemperatureUnit
669+
{
670+
Celsius,
671+
Fahrenheit
672+
}
673+
674+
private string GetWeather(WeatherReportParameters payload)
675+
=> "大部多云,气温 "
676+
+ payload.Unit switch
677+
{
678+
TemperatureUnit.Celsius => "18 摄氏度",
679+
TemperatureUnit.Fahrenheit => "64 华氏度",
680+
_ => throw new InvalidOperationException()
681+
};
682+
```
683+
684+
随后构造工具数组向模型提供工具定义,参数列表需要以 JSON Schema 的形式提供。这里我们使用 `JsonSchema.Net.Generation` 库来自动生成 JSON Schema,您也可以使用其他类似功能的库。
685+
686+
```
687+
var tools = new List<ToolDefinition>
688+
{
689+
new(
690+
ToolTypes.Function,
691+
new FunctionDefinition(
692+
nameof(GetWeather),
693+
"获得当前天气",
694+
new JsonSchemaBuilder().FromType<WeatherReportParameters>().Build()))
695+
};
696+
```
697+
698+
随后我们将这个工具定义附加到 `Parameters` 里,随消息一同发送(每次请求时都需要附带 tools 信息)。
699+
700+
```csharp
701+
var request = new ModelRequest<TextGenerationInput, ITextGenerationParameters>()
702+
{
703+
Model = "qwen-turbo",
704+
Input = new TextGenerationInput() { Messages = messages },
705+
Parameters = new TextGenerationParameters()
706+
{
707+
ResultFormat = "message",
708+
EnableThinking = true,
709+
IncrementalOutput = true,
710+
Tools = tools,
711+
ToolChoice = ToolChoice.AutoChoice, // 允许模型自行决定是否需要调用模型
712+
ParallelToolCalls = true // 允许模型同时发起多次工具调用
713+
}
714+
}
715+
```
716+
717+
模型会返回一个带有 `ToolCalls` 的消息尝试调用工具,我们需要解析并将结果附加到消息数组中去。当开启流式增量输出时,会先输出除 `arguments` 外的所有信息,随后增量输出 `arguments`。
718+
719+
模型回复示例,可以看到 `arguments` 是增量流式输出的,每次调用的第一个包都包含了 IndexId 信息。
720+
721+
```
722+
{"choices":[{"message":{"content":"","tool_calls":[{"index":0,"id":"call_30817ba5d0b349ed88ddcc","type":"function","function":{"name":"get_current_weather","arguments":"{\"location\":"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]}
723+
724+
{"choices":[{"message":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"arguments":" \"浙江省杭州市\", \""}}],"role":"assistant"},"index":0,"finish_reason":"null"}]}
725+
726+
{"choices":[{"message":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"arguments":"unit\": \"celsius\"}"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]}
727+
728+
{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"call_566994452aec418d930430","type":"function","function":{"name":"get_current_weather","arguments":"{\"location"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]}
729+
730+
{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"","type":"function","function":{"arguments":"\": \"上海市\", \"unit"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]}
731+
732+
{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"","type":"function","function":{"arguments":"\": \"celsius\"}"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]}
733+
734+
{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"","type":"function","function":{}}],"role":"assistant"},"index":0,"finish_reason":"tool_calls"}]}
735+
```
736+
737+
我们需要构建一个字典来收集模型输出的 `arguments` 信息并组装成完整的工具调用数组。
738+
739+
```csharp
740+
List<ToolCall>? pendingToolCalls = null; // 收集到的 toolCalls 信息
741+
var argumentDictionary = new Dictionary<int, StringBuilder>(); // toolcalls 的 index-arguemnt 字典
742+
await foreach (var chunk in response)
743+
{
744+
usage = chunk.Usage;
745+
var choice = chunk.Output.Choices![0];
746+
if (choice.Message.ToolCalls != null && choice.Message.ToolCalls.Count != 0)
747+
{
748+
pendingToolCalls ??= new List<ToolCall>();
749+
foreach (var call in choice.Message.ToolCalls)
750+
{
751+
var hasPartial = argumentDictionary.TryGetValue(call.Index, out var partialArgument);
752+
if (!hasPartial || partialArgument == null)
753+
{
754+
partialArgument = new StringBuilder();
755+
argumentDictionary[call.Index] = partialArgument;
756+
pendingToolCalls.Add(call);
757+
}
758+
759+
partialArgument.Append(call.Function.Arguments);
760+
}
761+
762+
continue;
763+
}
764+
765+
// ...如果没有工具调用则正常处理模型回复
766+
}
767+
768+
// 组装工具调用结果
769+
if (argumentDictionary.Count != 0)
770+
{
771+
if (firstReplyChunk)
772+
{
773+
Console.Write("Assistant > ");
774+
}
775+
776+
pendingToolCalls?.ForEach(p =>
777+
{
778+
p.Function.Arguments = argumentDictionary[p.Index].ToString();
779+
Console.Write($"调用:{p.Function.Name}({p.Function.Arguments}); ");
780+
});
781+
}
782+
783+
// 将模型的工具调用信息保存到对话记录
784+
messages.Add(TextChatMessage.Assistant(reply.ToString(), toolCalls: pendingToolCalls));
785+
```
786+
787+
收集到完整的工具调用信息后,我们需要调用对应的方法并将结果以 `Tool` 消息附加到模型回复之后
788+
789+
```
790+
if (pendingToolCalls?.Count > 0)
791+
{
792+
// call tools
793+
foreach (var call in pendingToolCalls)
794+
{
795+
// 这里我们已知只有一种工具,生产环境需要根据 call.Function.Name 动态的选择工具进行调用。
796+
var payload = JsonSerializer.Deserialize<WeatherReportParameters>(call.Function.Arguments!)!;
797+
var response = GetWeather(payload);
798+
Console.WriteLine("Tool > " + response);
799+
// 附加调用结果
800+
messages.Add(TextChatMessage.Tool(response, call.Id));
801+
}
802+
803+
pendingToolCalls = null;
804+
}
805+
```
806+
807+
此时 messages 的角色顺序应该是,最后一个或多个消息是 Tool 角色消息,取决于模型回复的 ToolCalls 数量。
808+
809+
```
810+
User
811+
Assistant(包含 ToolCalls)
812+
Tool
813+
(Tool)
814+
```
815+
816+
随后再次发起请求,模型将总结工具调用结果并给出回答。
817+
818+
完整代码:
819+
820+
```csharp
821+
var tools = new List<ToolDefinition>
822+
{
823+
new(
824+
ToolTypes.Function,
825+
new FunctionDefinition(
826+
nameof(GetWeather),
827+
"获得当前天气",
828+
new JsonSchemaBuilder().FromType<WeatherReportParameters>().Build()))
829+
};
830+
var messages = new List<TextChatMessage>();
831+
messages.Add(TextChatMessage.System("You are a helpful assistant"));
832+
List<ToolCall>? pendingToolCalls = null;
833+
while (true)
834+
{
835+
if (pendingToolCalls?.Count > 0)
836+
{
837+
// call tools
838+
foreach (var call in pendingToolCalls)
839+
{
840+
var payload = JsonSerializer.Deserialize<WeatherReportParameters>(call.Function.Arguments!)!;
841+
var response = GetWeather(payload);
842+
Console.WriteLine("Tool > " + response);
843+
messages.Add(TextChatMessage.Tool(response, call.Id));
844+
}
845+
846+
pendingToolCalls = null;
847+
}
848+
else
849+
{
850+
// get user input
851+
Console.Write("User > ");
852+
var input = Console.ReadLine();
853+
if (string.IsNullOrEmpty(input))
854+
{
855+
Console.WriteLine("Please enter a user input.");
856+
return;
857+
}
858+
859+
messages.Add(TextChatMessage.User(input));
860+
}
861+
862+
var completion = client.GetTextCompletionStreamAsync(
863+
new ModelRequest<TextGenerationInput, ITextGenerationParameters>()
864+
{
865+
Model = "qwen-turbo",
866+
Input = new TextGenerationInput() { Messages = messages },
867+
Parameters = new TextGenerationParameters()
868+
{
869+
ResultFormat = "message",
870+
EnableThinking = false,
871+
IncrementalOutput = true,
872+
Tools = tools,
873+
ToolChoice = ToolChoice.AutoChoice,
874+
ParallelToolCalls = true
875+
}
876+
});
877+
var reply = new StringBuilder();
878+
TextGenerationTokenUsage? usage = null;
879+
var argumentDictionary = new Dictionary<int, StringBuilder>();
880+
var firstReplyChunk = true;
881+
await foreach (var chunk in completion)
882+
{
883+
usage = chunk.Usage;
884+
var choice = chunk.Output.Choices![0];
885+
if (choice.Message.ToolCalls != null && choice.Message.ToolCalls.Count != 0)
886+
{
887+
pendingToolCalls ??= new List<ToolCall>();
888+
foreach (var call in choice.Message.ToolCalls)
889+
{
890+
var hasPartial = argumentDictionary.TryGetValue(call.Index, out var partialArgument);
891+
if (!hasPartial || partialArgument == null)
892+
{
893+
partialArgument = new StringBuilder();
894+
argumentDictionary[call.Index] = partialArgument;
895+
pendingToolCalls.Add(call);
896+
}
897+
898+
partialArgument.Append(call.Function.Arguments);
899+
}
900+
901+
continue;
902+
}
903+
904+
if (firstReplyChunk)
905+
{
906+
Console.Write("Assistant > ");
907+
firstReplyChunk = false;
908+
}
909+
910+
Console.Write(choice.Message.Content);
911+
reply.Append(choice.Message.Content);
912+
}
913+
914+
if (argumentDictionary.Count != 0)
915+
{
916+
if (firstReplyChunk)
917+
{
918+
Console.Write("Assistant > ");
919+
}
920+
921+
pendingToolCalls?.ForEach(p =>
922+
{
923+
p.Function.Arguments = argumentDictionary[p.Index].ToString();
924+
Console.Write($"调用:{p.Function.Name}({p.Function.Arguments}); ");
925+
});
926+
}
927+
928+
Console.WriteLine();
929+
messages.Add(TextChatMessage.Assistant(reply.ToString(), toolCalls: pendingToolCalls));
930+
if (usage != null)
931+
{
932+
Console.WriteLine(
933+
$"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})");
934+
}
935+
}
936+
937+
string GetWeather(WeatherReportParameters payload)
938+
=> $"{payload.Location} 大部多云,气温 "
939+
+ payload.Unit switch
940+
{
941+
TemperatureUnit.Celsius => "18 摄氏度",
942+
TemperatureUnit.Fahrenheit => "64 华氏度",
943+
_ => throw new InvalidOperationException()
944+
};
945+
946+
public record WeatherReportParameters(
947+
[property: Required]
948+
[property: Description("要获取天气的省市名称,例如浙江省杭州市")]
949+
string Location,
950+
[property: JsonConverter(typeof(EnumStringConverter<TemperatureUnit>))]
951+
[property: Description("温度单位")]
952+
TemperatureUnit Unit = TemperatureUnit.Celsius);
953+
954+
public enum TemperatureUnit
955+
{
956+
Celsius,
957+
Fahrenheit
958+
}
959+
960+
/*
961+
User > 杭州和上海的天气怎么样?
962+
Assistant > 调用:GetWeather({"Location": "浙江省杭州市", "Unit": "Celsius"}); 调用:GetWeather({"Location": "上海市", "Unit": "Celsius"});
963+
Usage: in(196)/out(54)/total(250)
964+
Tool > 浙江省杭州市 大部多云,气温 18 摄氏度
965+
Tool > 上海市 大部多云,气温 18 摄氏度
966+
Assistant > 浙江省杭州市和上海市的天气大部多云,气温均为18摄氏度。
967+
Usage: in(302)/out(19)/total(321)
968+
*/
969+
```
654970

655971

656972

0 commit comments

Comments
 (0)