@@ -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
443441var 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 ` 是增量流式输出的,每次调用的第一个包都包含了 Index 和 Id 信息。
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