From 15d00f1f4c7260dfa5c769bb07596dbdbac92b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 19 Oct 2025 16:12:36 +0800 Subject: [PATCH 1/6] feat: update new parameters --- README.zh-Hans.md | 626 +++++++++++++++--- sample/Cnblogs.DashScope.Sample/ISample.cs | 9 + sample/Cnblogs.DashScope.Sample/Program.cs | 107 +-- .../Text/ChatReasoningSample.cs | 58 ++ .../Text/ChatSample.cs | 49 ++ .../Text/ChatStreamSample.cs | 86 +++ .../Text/ChatThinkingBudgetSample.cs | 90 +++ .../Text/ChatToolCallingSample.cs | 79 +++ .../Text/ChatWebSearchSample.cs | 222 +++++++ src/Cnblogs.DashScope.Core/AsrOptions.cs | 24 + .../CacheControlOptions.cs | 12 + .../DashScopeDeepResearchInfo.cs | 12 + .../DashScopeDeepResearchReference.cs | 11 + .../DashScopeDeepResearchTask.cs | 42 ++ .../DashScopeDeepResearchWebsiteRef.cs | 10 + .../IMultimodalParameters.cs | 13 +- .../ITextGenerationParameters.cs | 29 +- .../Internals/IMessage.cs | 4 +- ...timodalMessageVideoContentJsonConverter.cs | 65 ++ .../MultimodalAnnotation.cs | 8 + .../MultimodalMessage.cs | 4 +- .../MultimodalMessageContent.cs | 44 +- .../MultimodalMessageVideoContent.cs | 47 ++ .../MultimodalMessageVideoContentType.cs | 17 + .../MultimodalParameters.cs | 3 + src/Cnblogs.DashScope.Core/TextChatMessage.cs | 15 + .../TextChatMessageExtra.cs | 12 + .../TextGenerationParameters.cs | 3 + .../TextGenerationPluginUsages.cs | 7 + .../TextGenerationSearchOptions.cs | 12 +- .../TextGenerationSearchPluginUsage.cs | 7 + .../TextGenerationTokenUsage.cs | 5 + .../TextGenerationWebSearchExtra.cs | 8 + .../TextGenerationWebSearchInfo.cs | 5 +- .../Utils/Snapshots.MultimodalGeneration.cs | 4 +- .../Utils/Snapshots.TextGeneration.cs | 47 +- 36 files changed, 1565 insertions(+), 231 deletions(-) create mode 100644 sample/Cnblogs.DashScope.Sample/ISample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/ChatReasoningSample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/ChatStreamSample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/ChatThinkingBudgetSample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs create mode 100644 src/Cnblogs.DashScope.Core/AsrOptions.cs create mode 100644 src/Cnblogs.DashScope.Core/CacheControlOptions.cs create mode 100644 src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs create mode 100644 src/Cnblogs.DashScope.Core/DashScopeDeepResearchReference.cs create mode 100644 src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs create mode 100644 src/Cnblogs.DashScope.Core/DashScopeDeepResearchWebsiteRef.cs create mode 100644 src/Cnblogs.DashScope.Core/Internals/MultimodalMessageVideoContentJsonConverter.cs create mode 100644 src/Cnblogs.DashScope.Core/MultimodalAnnotation.cs create mode 100644 src/Cnblogs.DashScope.Core/MultimodalMessageVideoContent.cs create mode 100644 src/Cnblogs.DashScope.Core/MultimodalMessageVideoContentType.cs create mode 100644 src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs create mode 100644 src/Cnblogs.DashScope.Core/TextGenerationPluginUsages.cs create mode 100644 src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs create mode 100644 src/Cnblogs.DashScope.Core/TextGenerationWebSearchExtra.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index aea5642..bdb306b 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -66,162 +66,590 @@ public class YourService(IDashScopeClient client) ## 支持的 API -- [对话](#对话) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景 +- [文本生成](#文本生成) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景 - [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景 - [语音合成](#语音合成) - CosyVoice,Sambert 等,支持 TTS 等应用场景 - [图像生成](#图像生成) - wanx2.1 等,支持文生图,人像风格重绘等应用场景 - [应用调用](#应用调用) - [文本向量](#文本向量) -### 对话 +## 文本生成 -使用 `dashScopeClient.GetTextCompletionAsync` 和 `dashScopeClient.GetTextCompletionStreamAsync` 来直接访问文本生成接口。 +使用 `dashScopeClient.GetTextCompletionAsync` 和 `dashScopeClient.GetTextCompletionStreamAsync()` 来访问文本生成 API。 -针对通义千问和 DeekSeek,我们提供了快捷方法进行调用: `GetQWenChatCompletionAsync` /`GetDeepSeekChatCompletionAsync` +常用模型:`qwen-max` `qwen-plus` `qwen-flush` 等 -相关文档:https://help.aliyun.com/zh/model-studio/user-guide/text-generation/ +基础示例: ```csharp -var history = new List -{ - ChatMessage.User("Please remember this number, 42"), - ChatMessage.Assistant("I have remembered this number."), - ChatMessage.User("What was the number I metioned before?") -} -var parameters = new TextGenerationParameters() -{ - ResultFormat = ResultFormats.Message -}; -var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters); -Console.WriteLine(completion.Output.Choices[0].Message.Content); // The number is 42 +var client = new DashScopeClient("your-api-key"); +var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() + { + Messages = new List() + { + TextChatMessage.System("You are a helpful assistant"), + TextChatMessage.User("你是谁?") + } + }, + Parameters = new TextGenerationParameters() { ResultFormat = "message" } + }); +Console.WriteLine(completion.Output.Choices![0].Message.Content) ``` -#### 推理 +### 多轮对话 + +#### 快速开始 -使用推理模型时,模型的思考过程可以通过 `ReasoningContent` 属性获取。 +核心是维护一个 `TextChatMessage` 数组作为对话历史。 ```csharp -var history = new List +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +while (true) { - TextChatMessage.User("Calculate 1+1") -}; -var completion = await client.GetDeepSeekChatCompletionAsync(DeepSeekLlm.DeepSeekR1, history); -Console.WriteLine(completion.Output.Choices[0]!.Message.ReasoningContent); -``` + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("使用默认输入:你是谁?"); + input = "你是谁?"; + } + + messages.Add(TextChatMessage.User(input)); + var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message" } + }); + Console.WriteLine("Assistant > " + completion.Output.Choices![0].Message.Content); + var usage = completion.Usage; + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } -对于支持的模型(例如 qwen3),可以使用 `TextGenerationParameters.EnableThinking` 决定是否使用模型的推理能力。 + messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); +} -```csharp -var stream = dashScopeClient - .GetQWenChatStreamAsync( - QWenLlm.QWenPlusLatest, - history, - new TextGenerationParameters - { - IncrementalOutput = true, - ResultFormat = ResultFormats.Message, - EnableThinking = true - }); +/* + * User > 你好,你今天过的怎么样? + * Assistant > 你好!谢谢你关心。虽然我是一个AI助手,没有真实的情感和体验,但我非常高兴能和你交流。今天过得挺好的,因为我可以和很多像你一样的朋友聊天,帮助大家解决问题,分享知识。你今天过得怎么样呢?有什么我可以帮你的吗? + * Usage: in(29)/out(59)/total(88) + */ ``` -#### 工具调用 +#### 思考模型 + +模型的思考过程会存放在独立的属性 `ReasoningContent` 中,存放到对话历史时要注意忽略它,仅保留模型的回复 `Content`。 + +有一些模型接受 `EnableThinking` 来设置是否开启深度思考,可以在 `Parameters` 里设置。 -创建一个可供模型使用的方法。 +思考模型的更多设置请见下文的 [深度思考](#深度思考) ```csharp -string GetCurrentWeather(GetCurrentWeatherParameters parameters) +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +while (true) { - // implementation is irrenlvent - return "Sunny" + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", EnableThinking = true } + }); + Console.WriteLine("Reasoning > " + completion.Output.Choices![0].Message.ReasoningContent); + Console.WriteLine("Assistant > " + completion.Output.Choices![0].Message.Content); + var usage = completion.Usage; + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } + + messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); } -public record GetCurrentWeatherParameters( - [property: Required] - [property: Description("The city and state, e.g. San Francisco, CA")] - string Location, - [property: JsonConverter(typeof(EnumStringConverter))] - TemperatureUnit Unit = TemperatureUnit.Celsius); +/* +User > 你好,今天感觉怎么样? +Reasoning > 好的,用户问“你好,今天感觉怎么样?”,我需要先理解他的意图。他可能是在关心我的状态,或者想开始一段对话。作为AI助手,我没有真实的情感,但应该以友好和积极的方式回应。 + +首先,我应该感谢他的问候,然后说明自己没有真实的情感,但愿意帮助他。接下来,可以询问他的情况,表现出关心,这样能促进进一步的交流。同时,保持语气自然,避免过于机械。 + +要注意用户可能的深层需求,比如他可能想寻求帮助,或者只是闲聊。所以回应要开放,让他知道我随时准备协助。另外,使用表情符号可以增加亲切感,但不要过多。 + +最后,确保回答简洁,不过于冗长,同时保持友好和专业。这样用户会觉得被重视,并且更愿意继续对话。 +Assistant > 你好呀!虽然我没有真实的情感体验,但很高兴能和你聊天!今天过得怎么样呢?有什么我可以帮你的吗? +Usage: in(24)/out(203)/reasoning(169)/total(227) + */ +``` + +#### 流式输出 -public enum TemperatureUnit +```cs +var request = new ModelRequest() { - Celsius, - Fahrenheit + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true + } } ``` -对话时带上方法的名称、描述和参数列表,参数列表以 JSON Schema 的形式提供(这里使用 `JsonSchema.Net` 库,您也可以使用其它具有类似功能的库)。 +可以通过 `client.GetTextCompletionStreamAsync` 来启用流式输出,同时建议开启 `Parameters` 里的 `IncrementOutput` 来启用增量输出。 -```csharp -var tools = new List() -{ - new( - ToolTypes.Function, - new FunctionDefinition( - nameof(GetCurrentWeather), - "获取当前天气", - new JsonSchemaBuilder().FromType().Build())) -}; +增量输出: -var history = new List -{ - ChatMessage.User("What is the weather today in C.A?") -}; +> 示例:["我爱","吃","苹果"] + +非增量输出: -var parameters = new TextGenerationParamters() +> 示例:["我爱","我爱吃","我爱吃苹果"] + +流式输出会返回一个 `IAsyncEnumerable`,推荐使用 `await foreach` 对它进行遍历,然后将增量输出的内容记录下来,最后再保存到对话历史中去。 + +```cs +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +while (true) { - ResultFormat = ResultFormats.Message, - Tools = tools -}; + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.Write("Reasoning > "); + reasoning = true; + } -// 向模型提问并提供可用的方法 -var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters); + Console.Write(choice.Message.ReasoningContent); + continue; + } -// 模型试图调用方法 -Console.WriteLine(completion.Output.Choice[0].Message.ToolCalls[0].Function.Name); // GetCurrentWeather -history.Add(completion.Output.Choice[0].Message); + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } -// 调用方法并将结果保存到聊天记录中 -var result = GetCurrentWeather(JsonSerializer.Deserialize(completion.Output.Choice[0].Message.ToolCalls[0].Function.Arguments)); -history.Add(new("tool", result, nameof(GetCurrentWeather))); + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } -// 模型根据调用结果返回答案 -completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters); -Console.WriteLine(completion.Output.Choice[0].Message.Content) // 现在浙江省杭州市的天气是大部多云,气温为 18 摄氏度。 -``` + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } +} -当模型认为应当调用工具时,返回消息中 `ToolCalls` 会提供调用的详情,本地在调用完成后可以把结果以 `tool` 角色返回。 +/* +User > 你好 +Reasoning > 好的,用户发来“你好”,我需要友好回应。首先,应该用中文回复,保持自然。可以问好并询问有什么可以帮助的,这样既礼貌又开放。注意不要用太正式的语言,让对话轻松一些。同时,要确保回复简洁,避免冗长。检查有没有需要特别注意的地方,比如用户可能的需求或之前的对话历史,但这里看起来是第一次交流。所以,确定回复内容应该是:“你好!有什么我可以帮你的吗?” 这样既友好 又明确,鼓励用户进一步说明需求。 +Assistant > 你好!有什么我可以帮你的吗? +Usage: in(19)/out(125)/reasoning(112)/total(144) + */ +``` -#### 上传文件(qwen-long) +### 深度思考 -使用长上下文模型时,需要先提前将文件上传到 DashScope 来获得 Id。 +通过 `EnableThinking` 来控制模型是否开启深度思考。 -```csharp -var file = new FileInfo("test.txt"); -var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name); +```cs +var request = new ModelRequest() +{ + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true + } +} ``` -使用文件 Id 初始化一个消息,内部会转换成 system 角色的一个文件引用。 +#### 限制思考长度 -```csharp -var history = new List +通过 `Parameters` 里的 `ThinkingBudget` 来限制模型的思考长度。 + +示例: + +```cs +const int budget = 10; +Console.WriteLine($"Set thinking budget to {budget} tokens"); +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +while (true) { - ChatMessage.File(uploadedFile.Id), // 多文件情况下可以直接传入文件 Id 数组, 例如:[file1.Id, file2.Id] - ChatMessage.User("总结一下文件的内容。") + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + ThinkingBudget = budget, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.Write("Reasoning > "); + reasoning = true; + } + + Console.Write(choice.Message.ReasoningContent); + continue; + } + + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } } -var parameters = new TextGenerationParameters() + +/* +Set thinking budget to 10 tokens +User > 你是谁? +Reasoning > 好的,用户问我“你是谁?”,我 +Assistant > 我是通义千问,是阿里巴巴集团研发的超大规模语言模型,可以回答问题、创作文字、编程、逻辑推理等多种任务。我旨在为用户提供帮助和便利。有什么我可以帮您的吗? +Usage: in(21)/out(59)/reasoning(10)/total(80) + */ +``` + +### 联网搜索 + +主要通过 `Parameters` 里的 `EnableSearch` 和 `SearchOptions` 来控制。 + +示例请求: + +```cs +var request = new ModelRequest() { - ResultFormat = ResultFormats.Message + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + EnableSearch = true, + SearchOptions = new TextGenerationSearchOptions() + { + SearchStrategy = "max", // max/turbo 主要控制搜索条目的多少 + EnableCitation = true, // 模型回复中添加来源引用 + CitationFormat = "[ref_]", // 模型回复的来源引用格式 + EnableSource = true, // 是否返回搜索来源列表,在 SearchInfo 中提供 + ForcedSearch = true,// 是否强制模型进行搜索 + EnableSearchExtension = true, // 开启垂直领域搜索,在 SearchInfo.Extra 里提供结果 + PrependSearchResult = true // 第一个返回包将只包含搜索结果,模型回复在随后的包内提供,不能和 EnableSearchExtension 同时开启 + } + } }; -var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenLong, history, parameters); -Console.WriteLine(completion.Output.Choices[0].Message.Content); ``` -如果需要,完成对话后可以使用 API 删除之前上传的文件。 +通过返回结果里的 `response.Output.SearchInfo` 来获取搜索结果,这个值会在模型搜索后一次性返回,并在之后的每次返回中都附带。因此,开启增量流式输出时,不需要通过 `StringBuilder` 等方式来缓存 `SearchInfo`。 ```csharp -var deletionResult = await dashScopeClient.DeleteFileAsync(uploadedFile.Id); +var messages = new List(); +while (true) +{ + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + EnableSearch = true, + SearchOptions = new TextGenerationSearchOptions() + { + SearchStrategy = "max", + EnableCitation = true, + CitationFormat = "[ref_]", + EnableSource = true, + EnableSearchExtension = true, + ForcedSearch = true + }, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var searching = false; + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + var search = chunk.Output.SearchInfo; + if (search != null) + { + if (!searching) + { + searching = true; + Console.WriteLine(); + Console.WriteLine("Search >"); + foreach (var re in search.SearchResults) + { + Console.WriteLine($"[{re.Index}].{re.Title} - {re.SiteName}, {re.Url}"); + } + + if (search.ExtraToolInfo != null) + { + foreach (var extra in search.ExtraToolInfo) + { + Console.WriteLine($"[{extra.Tool}]: {extra.Result}"); + } + } + } + } + + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.WriteLine(); + Console.Write("Reasoning > "); + reasoning = true; + } + + Console.Write(choice.Message.ReasoningContent); + continue; + } + + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/plugins({usage.Plugins?.Search?.Count})/total({usage.TotalTokens})"); + } +} + +/* +User > 阿里股价 + +Search > +[1].截至目前为止,外资机构对 - 无, https://xueqiu.com/9216592857/355356488 +[2].阿里巴巴 - QQ, https://gu.qq.com/usBABA.N +[3].$阿里巴巴(BABA)$2025年10月 - 新浪网, https://guba.sina.com.cn/?s=thread&tid=74408&bid=13015 +[4].阿里巴巴投資者關係-阿里巴巴集團 - 阿里巴巴集团, https://www.alibabagroup.com/zh-HK/investor-relations +[5].阿里巴巴(BABA)_美股行情_今日股价与走势图_新浪财经 - 新浪网, https://gu.sina.cn/us/hq/quotes.php?code=BABA&from=pc +[6].阿里巴巴-WR (89988.HK) 過往股價及數據 - , https://hk.finance.yahoo.com/quote/89988.HK/history/ +[7].阿里巴巴10月17日成交额为29.43亿美元 成交额较上个交易日增加59.59%。 - 同花顺财经网, https://stock.10jqka.com.cn/usstock/20251018/c671832899.shtml +[8].阿里巴巴(BABA)股票历史数据 - , https://cn.investing.com/equities/alibaba-historical-data +[9].阿里巴巴-W (9988.HK) 股價、新聞、報價和記錄 - , https://hk.finance.yahoo.com/quote/9988.HK/ +[stock]: 阿里巴巴美股: +实时价格167.05USD +上个交易日收盘价165.09USD +日环比%1.19% +月环比%-6.53 +日同比%66.33 +月同比%74.05 +历史价格列表[{"date":"2025-10-17","endPri":"167.050"},{"date":"2025-10-16","endPri":"165.090"},{"date":"2025-10-15","endPri":"165.910"},{"date":"2025-10-14","endPri":"162.860"},{"date":"2025-10-13","endPri":"166.810"},{"date":"2025-10-10","endPri":"159.010"},{"date":"2025-10-09","endPri":"173.680"},{"date":"2025-10-08","endPri":"181.120"},{"date":"2025-10-07","endPri":"181.330"},{"date":"2025-10-06","endPri":"187.220"},{"date":"2025-10-03","endPri":"188.030"},{"date":"2025-10-02","endPri":"189.340"},{"date":"2025-10-01","endPri":"182.780"},{"date":"2025-09-30","endPri":"178.730"},{"date":"2025-09-29","endPri":"179.900"},{"date":"2025-09-26","endPri":"171.910"},{"date":"2025-09-25","endPri":"175.470"},{"date":"2025-09-24","endPri":"176.440"},{"date":"2025-09-23","endPri":"163.080"},{"date":"2025-09-22","endPri":"164.250"},{"date":"2025-09-19","endPri":"162.810"},{"date":"2025-09-18","endPri":"162.480"},{"date":"2025-09-17","endPri":"166.170"},{"date":"2025-09-16","endPri":"162.210"},{"date":"2025-09-15","endPri":"158.040"},{"date":"2025-09-12","endPri":"155.060"},{"date":"2025-09-11","endPri":"155.440"},{"date":"2025-09-10","endPri":"143.930"},{"date":"2025-09-09","endPri":"147.100"},{"date":"2025-09-08","endPri":"141.200"}] + + + +Reasoning > 用户想了解阿里巴巴的股价信息。我需要从知识库中整理有关阿里巴巴股价的最新信息。 + +首先,让我查看知识库中有关阿里巴巴股价的最新数据: + +1. 从ref_7中可以看到:2025年10月17日,阿里巴巴(BABA)涨1.19%,报167.05美元,该日成交额为29.43亿美元,成交量为1776.57万。 + +2. 从ref_4中可以看到:2025年10月16日,阿里巴巴股价为$165.090,下跌了-0.820(-0.494%) + +3. 从ref_8中可以看到历史数据: + - 2025年10月15日: 165.91, 168.07 + - 2025年10月14日: 162.86, 160.05 + - 2025年10月13日: 166.81, 167.78 + - 2025年10月10日: 159.01, 170.03 + +4. 从ref_2中可以看到:2025年10月8日,阿里巴巴港股报价为181.12,下跌-0.12% + +5. 从ref_6中可以看到:2025年10月17日,阿里巴巴-WR (89988.HK)收市价为141.700,下跌-6.000 (-4.06%) + +6. 从ref_7中还提到:阿里巴巴(BABA)过去5个交易日涨5.06%,整个10月跌6.53%,年初至今涨97.02%,过去52周涨66.93% + +7. 从ref_5中可以看到一些股价指标:今开168.070,最高168.100,昨收162.860,最低164.600等 + +8. 从ref_1中可以看到:摩根大通在2025年10月2日发布的报告中,将阿里巴巴美股(BABA.N)的目标价大幅上调至245美元 + +9. 从ref_3中可以看到:2025年10月13日,高盛发布报告,上调阿里巴巴美股和港股目标价 + +10. 从ref_9中可以看到阿里巴巴-W (9988.HK)的一些价值评估数据,如市值3.43T,企业价值3.23T等 + +11. 从额外的"其他互联网信息"中提到:2025年10月19日,阿里巴巴集团的实际股票价格为155.30港币(但这条信息可能不准确,因为其他来源显示10月17日价格更高) + +12. 最后,系统提供的知识库中有一段最新的股市信息: +"股市信息:阿里巴巴美股: +实时价格167.05USD +上个交易日收盘价165.09USD +日环比%1.19% +月环比%-6.53 +日同比%66.33 +月同比%74.05" + +根据系统提供的知识库中的最新股市信息和ref_7的信息,可以确认截至2025年10月17日(星期五),阿里巴巴美股收盘价为167.05美元,较前一交易日上涨1.19%。 + +另外,从ref_4中可以看到10月16日的收盘价为165.09美元。 + +根据ref_7,10月17日的成交额为29.43亿美元,较上个交易日增加59.59%,成交量为1776.57万。 + +此外,还有一些分析师的目标价信息: +- 摩根大通在10月2日将目标价上调至245美元 +- 高盛在10月13日上调了目标价(具体数值未提供) + +综合以上信息,我可以提供阿里巴巴股价的最新情况,包括近期走势、分析师预测等。 +Assistant > # 阿里巴巴股价最新情况 + +## 美股(BABA)最新价格 +- 截至2025年10月17日,阿里巴巴美股收盘价为**167.05美元**,较前一交易日上涨1.19%[ref_7] +- 上一交易日(10月16日)收盘价为165.09美元,下跌0.494%[ref_4] +- 10月17日成交额达29.43亿美元,较上个交易日增加59.59%,当日成交量为1776.57万[ref_7] + +## 近期股价走势 +- 过去5个交易日累计上涨5.06%[ref_7] +- 整个10月下跌6.53%[ref_7] +- 年初至今上涨97.02%[ref_7] +- 过去52周上涨66.93%[ref_7] + +## 近期历史价格 +- 2025年10月15日: 165.91美元[ref_8] +- 2025年10月14日: 162.86美元[ref_8] +- 2025年10月13日: 166.81美元[ref_8] +- 2025年10月10日: 159.01美元[ref_8] + +## 港股情况 +- 阿里巴巴-WR (89988.HK)在2025年10月17日收市价为141.700港元,下跌6.000港元(-4.06%)[ref_6] +- 2025年10月8日,阿里巴巴港股报价为181.12港元,下跌0.12%[ref_2] + +## 分析师目标价 +- 摩根大通在2025年10月2日发布的报告中,将阿里巴巴美股目标价大幅上调至**245美元**[ref_1] +- 高盛于2025年10月13日发布报告,上调了阿里巴巴未来三年资本开支预测至4600亿元人民币,并上调其美股和港股目标价[ref_3] + +## 其他财务指标 +- 市盈率(TTM): 18.75[ref_5] +- 阿里巴巴-W (9988.HK)市值达3.43万亿港元[ref_9] +- 企业价值: 3.23万亿[ref_9] +Usage: in(2178)/out(1571)/reasoning(952)/plugins:(1)/total(3749) + */ ``` +### 工具调用 + +通过 `Parameter` 里的 `Tools` 来向模型提供可用的工具列表,模型会返回 `Tool` 角色的消息来调用工具。 + + + ### 多模态 使用 `dashScopeClient.GetMultimodalGenerationAsync` 和 `dashScopeClient.GetMultimodalGenerationStreamAsync` 来访问多模态文本生成接口。 diff --git a/sample/Cnblogs.DashScope.Sample/ISample.cs b/sample/Cnblogs.DashScope.Sample/ISample.cs new file mode 100644 index 0000000..eb5c50e --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/ISample.cs @@ -0,0 +1,9 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample; + +public interface ISample +{ + string Description { get; } + Task RunAsync(IDashScopeClient client); +} diff --git a/sample/Cnblogs.DashScope.Sample/Program.cs b/sample/Cnblogs.DashScope.Sample/Program.cs index 4189973..5c8f5bf 100644 --- a/sample/Cnblogs.DashScope.Sample/Program.cs +++ b/sample/Cnblogs.DashScope.Sample/Program.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Cnblogs.DashScope.Core; using Cnblogs.DashScope.Sample; +using Cnblogs.DashScope.Sample.Text; using Cnblogs.DashScope.Sdk; using Cnblogs.DashScope.Sdk.QWen; using Cnblogs.DashScope.Sdk.TextEmbedding; @@ -22,107 +23,29 @@ var dashScopeClient = new DashScopeClient(apiKey!); +var samples = typeof(ChatSample).Assembly.GetTypes() + .Where(t => t.IsAssignableTo(typeof(ISample)) && t is { IsClass: true, IsAbstract: false }) + .Select(x => Activator.CreateInstance(x) as ISample) + .Where(x => x != null) + .Select(x => x!) + .ToList(); + Console.WriteLine("Choose the sample you want to run:"); -foreach (var sampleType in Enum.GetValues()) +for (var i = 0; i < samples.Count; i++) { - Console.WriteLine($"{(int)sampleType}.{sampleType.GetDescription()}"); + Console.WriteLine($"{i}. {samples[i].Description}"); } Console.WriteLine(); Console.Write("Choose an option: "); -var type = (SampleType)int.Parse(Console.ReadLine()!); - -string userInput; -switch (type) +var parsed = int.TryParse(Console.ReadLine()?.Trim(), out var index); +if (parsed == false) { - case SampleType.TextCompletion: - Console.Write("Prompt > "); - userInput = Console.ReadLine()!; - await TextCompletionAsync(userInput); - break; - case SampleType.TextCompletionSse: - Console.Write("Prompt > "); - userInput = Console.ReadLine()!; - await TextCompletionStreamAsync(userInput); - break; - case SampleType.ChatCompletion: - await ChatStreamAsync(); - break; - case SampleType.ChatCompletionWithTool: - await ChatWithToolsAsync(); - break; - case SampleType.MultimodalCompletion: - await ChatWithImageAsync(); - break; - case SampleType.ChatCompletionWithFiles: - await ChatWithFilesAsync(); - break; - case SampleType.Text2Image: - await Text2ImageAsync(); - break; - case SampleType.MicrosoftExtensionsAi: - await ChatWithMicrosoftExtensions(); - break; - case SampleType.MicrosoftExtensionsAiToolCall: - await dashScopeClient.ToolCallWithExtensionAsync(); - break; - case SampleType.ApplicationCall: - Console.Write("Application Id > "); - var applicationId = Console.ReadLine()!; - Console.Write("Prompt > "); - userInput = Console.ReadLine()!; - await ApplicationCallAsync(applicationId, userInput); - break; - case SampleType.TextToSpeech: - { - using var tts = await dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync("cosyvoice-v2"); - var taskId = await tts.RunTaskAsync( - new SpeechSynthesizerParameters { Voice = "longxiaochun_v2", Format = "mp3" }); - await tts.ContinueTaskAsync(taskId, "博客园"); - await tts.ContinueTaskAsync(taskId, "代码改变世界"); - await tts.FinishTaskAsync(taskId); - var file = new FileInfo("tts.mp3"); - await using var stream = file.OpenWrite(); - await foreach (var b in tts.GetAudioAsync()) - { - stream.WriteByte(b); - } - - stream.Close(); - - var tokenUsage = 0; - await foreach (var message in tts.GetMessagesAsync()) - { - if (message.Payload.Usage?.Characters > tokenUsage) - { - tokenUsage = message.Payload.Usage.Characters; - } - } - - Console.WriteLine($"audio saved to {file.FullName}, token usage: {tokenUsage}"); - break; - } - - case SampleType.TextEmbedding: - Console.Write("text> "); - var text = Console.ReadLine(); - if (string.IsNullOrEmpty(text)) - { - text = "Coding changes world"; - Console.WriteLine($"using default text: {text}"); - } - - var response = await dashScopeClient.GetTextEmbeddingsAsync( - TextEmbeddingModel.TextEmbeddingV3, - [text], - new TextEmbeddingParameters() { Dimension = 512, }); - var array = response.Output.Embeddings.First().Embedding; - Console.WriteLine("Embedding"); - Console.WriteLine(string.Join('\n', array)); - Console.WriteLine($"Token usage: {response.Usage?.TotalTokens}"); - break; + Console.WriteLine("Invalid choice"); + return; } +await samples[index].RunAsync(dashScopeClient); return; // text completion diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatReasoningSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatReasoningSample.cs new file mode 100644 index 0000000..d746bdd --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatReasoningSample.cs @@ -0,0 +1,58 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class ChatReasoningSample : ISample +{ + /// + public string Description => "Chat with reasoning content"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", EnableThinking = true } + }); + Console.WriteLine("Reasoning > " + completion.Output.Choices![0].Message.ReasoningContent); + Console.WriteLine("Assistant > " + completion.Output.Choices![0].Message.Content); + var usage = completion.Usage; + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } + + messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); + } + } +} + +/* +User > 你好,今天感觉怎么样? +Reasoning > 好的,用户问“你好,今天感觉怎么样?”,我需要先理解他的意图。他可能是在关心我的状态,或者想开始一段对话。作为AI助手,我没有真实的情感,但应该以友好和积极的方式回应。 + +首先,我应该感谢他的问候,然后说明自己没有真实的情感,但愿意帮助他。接下来,可以询问他的情况,表现出关心,这样能促进进一步的交流。同时,保持语气自然,避免过于机械。 + +要注意用户可能的深层需求,比如他可能想寻求帮助,或者只是闲聊。所以回应要开放,让他知道我随时准备协助。另外,使用表情符号可以增加亲切感,但不要过多。 + +最后,确保回答简洁,不过于冗长,同时保持友好和专业。这样用户会觉得被重视,并且更愿意继续对话。 +Assistant > 你好呀!虽然我没有真实的情感体验,但很高兴能和你聊天!今天过得怎么样呢?有什么我可以帮你的吗? +Usage: in(24)/out(203)/reasoning(169)/total(227) + */ diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs new file mode 100644 index 0000000..84ae3e5 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs @@ -0,0 +1,49 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class ChatSample : ISample +{ + /// + public string Description => "Basic chat completion"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("使用默认输入:你是谁?"); + input = "你是谁?"; + } + + messages.Add(TextChatMessage.User(input)); + var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message" } + }); + Console.WriteLine("Assistant > " + completion.Output.Choices![0].Message.Content); + var usage = completion.Usage; + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + + messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); + } + } +} + +/* + * User > 你好,你今天过的怎么样? + * Assistant > 你好!谢谢你关心。虽然我是一个AI助手,没有真实的情感和体验,但我非常高兴能和你交流。今天过得挺好的,因为我可以和很多像你一样的朋友聊天,帮助大家解决问题,分享知识。你今天过得怎么样呢?有什么我可以帮你的吗? + * Usage: in(29)/out(59)/total(88) + */ diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatStreamSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatStreamSample.cs new file mode 100644 index 0000000..49a9f3e --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatStreamSample.cs @@ -0,0 +1,86 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class ChatStreamSample : ISample +{ + /// + public string Description => "Chat completion with stream output"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.Write("Reasoning > "); + reasoning = true; + } + + Console.Write(choice.Message.ReasoningContent); + continue; + } + + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } + } + } +} + +/* +User > 你好 +Reasoning > 好的,用户发来“你好”,我需要友好回应。首先,应该用中文回复,保持自然。可以问好并询问有什么可以帮助的,这样既礼貌又开放。注意不要用太正式的语言,让对话轻松一些。同时,要确保回复简洁,避免冗长。检查有没有需要特别注意的地方,比如用户可能的需求或之前的对话历史,但这里看起来是第一次交流。所以,确定回复内容应该是:“你好!有什么我可以帮你的吗?” 这样既友好 又明确,鼓励用户进一步说明需求。 +Assistant > 你好!有什么我可以帮你的吗? +Usage: in(19)/out(125)/reasoning(112)/total(144) + */ diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatThinkingBudgetSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatThinkingBudgetSample.cs new file mode 100644 index 0000000..684395a --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatThinkingBudgetSample.cs @@ -0,0 +1,90 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class ChatThinkingBudgetSample : ISample +{ + /// + public string Description => "Chat completion with thinking budget"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + const int budget = 10; + Console.WriteLine($"Set thinking budget to {budget} tokens"); + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + ThinkingBudget = budget, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.Write("Reasoning > "); + reasoning = true; + } + + Console.Write(choice.Message.ReasoningContent); + continue; + } + + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } + } + } +} + +/* +Set thinking budget to 10 tokens +User > 你是谁? +Reasoning > 好的,用户问我“你是谁?”,我 +Assistant > 我是通义千问,是阿里巴巴集团研发的超大规模语言模型,可以回答问题、创作文字、编程、逻辑推理等多种任务。我旨在为用户提供帮助和便利。有什么我可以帮您的吗? +Usage: in(21)/out(59)/reasoning(10)/total(80) + */ diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs new file mode 100644 index 0000000..67498a2 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs @@ -0,0 +1,79 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class ChatToolCallingSample : ISample +{ + /// + public string Description => "Chat with tool calling"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true, + } + }); + var reply = new StringBuilder(); + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.Write("Reasoning > "); + reasoning = true; + } + + Console.Write(choice.Message.ReasoningContent); + continue; + } + + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } + } + } +} diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs new file mode 100644 index 0000000..1477e2e --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs @@ -0,0 +1,222 @@ +using System.Text; +using System.Text.Json; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class ChatWebSearchSample : ISample +{ + /// + public string Description => "Chat with web search enabled"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + EnableSearch = true, + SearchOptions = new TextGenerationSearchOptions() + { + SearchStrategy = "max", + EnableCitation = true, + CitationFormat = "[ref_]", + EnableSource = true, + EnableSearchExtension = true, + ForcedSearch = true + }, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var searching = false; + var reasoning = false; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + var search = chunk.Output.SearchInfo; + if (search != null) + { + if (!searching) + { + searching = true; + Console.WriteLine(); + Console.WriteLine("Search >"); + foreach (var re in search.SearchResults) + { + Console.WriteLine($"[{re.Index}].{re.Title} - {re.SiteName}, {re.Url}"); + } + + if (search.ExtraToolInfo != null) + { + foreach (var extra in search.ExtraToolInfo) + { + Console.WriteLine($"[{extra.Tool}]: {extra.Result}"); + } + } + } + } + + if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + { + // reasoning + if (reasoning == false) + { + Console.WriteLine(); + Console.Write("Reasoning > "); + reasoning = true; + } + + Console.Write(choice.Message.ReasoningContent); + continue; + } + + if (reasoning) + { + reasoning = false; + Console.WriteLine(); + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/plugins({usage.Plugins?.Search?.Count})/total({usage.TotalTokens})"); + } + } + } +} + +/* +User > 阿里股价 + +Search > +[1].截至目前为止,外资机构对 - 无, https://xueqiu.com/9216592857/355356488 +[2].阿里巴巴 - QQ, https://gu.qq.com/usBABA.N +[3].$阿里巴巴(BABA)$2025年10月 - 新浪网, https://guba.sina.com.cn/?s=thread&tid=74408&bid=13015 +[4].阿里巴巴投資者關係-阿里巴巴集團 - 阿里巴巴集团, https://www.alibabagroup.com/zh-HK/investor-relations +[5].阿里巴巴(BABA)_美股行情_今日股价与走势图_新浪财经 - 新浪网, https://gu.sina.cn/us/hq/quotes.php?code=BABA&from=pc +[6].阿里巴巴-WR (89988.HK) 過往股價及數據 - , https://hk.finance.yahoo.com/quote/89988.HK/history/ +[7].阿里巴巴10月17日成交额为29.43亿美元 成交额较上个交易日增加59.59%。 - 同花顺财经网, https://stock.10jqka.com.cn/usstock/20251018/c671832899.shtml +[8].阿里巴巴(BABA)股票历史数据 - , https://cn.investing.com/equities/alibaba-historical-data +[9].阿里巴巴-W (9988.HK) 股價、新聞、報價和記錄 - , https://hk.finance.yahoo.com/quote/9988.HK/ +[stock]: 阿里巴巴美股: +实时价格167.05USD +上个交易日收盘价165.09USD +日环比%1.19% +月环比%-6.53 +日同比%66.33 +月同比%74.05 +历史价格列表[{"date":"2025-10-17","endPri":"167.050"},{"date":"2025-10-16","endPri":"165.090"},{"date":"2025-10-15","endPri":"165.910"},{"date":"2025-10-14","endPri":"162.860"},{"date":"2025-10-13","endPri":"166.810"},{"date":"2025-10-10","endPri":"159.010"},{"date":"2025-10-09","endPri":"173.680"},{"date":"2025-10-08","endPri":"181.120"},{"date":"2025-10-07","endPri":"181.330"},{"date":"2025-10-06","endPri":"187.220"},{"date":"2025-10-03","endPri":"188.030"},{"date":"2025-10-02","endPri":"189.340"},{"date":"2025-10-01","endPri":"182.780"},{"date":"2025-09-30","endPri":"178.730"},{"date":"2025-09-29","endPri":"179.900"},{"date":"2025-09-26","endPri":"171.910"},{"date":"2025-09-25","endPri":"175.470"},{"date":"2025-09-24","endPri":"176.440"},{"date":"2025-09-23","endPri":"163.080"},{"date":"2025-09-22","endPri":"164.250"},{"date":"2025-09-19","endPri":"162.810"},{"date":"2025-09-18","endPri":"162.480"},{"date":"2025-09-17","endPri":"166.170"},{"date":"2025-09-16","endPri":"162.210"},{"date":"2025-09-15","endPri":"158.040"},{"date":"2025-09-12","endPri":"155.060"},{"date":"2025-09-11","endPri":"155.440"},{"date":"2025-09-10","endPri":"143.930"},{"date":"2025-09-09","endPri":"147.100"},{"date":"2025-09-08","endPri":"141.200"}] + + + +Reasoning > 用户想了解阿里巴巴的股价信息。我需要从知识库中整理有关阿里巴巴股价的最新信息。 + +首先,让我查看知识库中有关阿里巴巴股价的最新数据: + +1. 从ref_7中可以看到:2025年10月17日,阿里巴巴(BABA)涨1.19%,报167.05美元,该日成交额为29.43亿美元,成交量为1776.57万。 + +2. 从ref_4中可以看到:2025年10月16日,阿里巴巴股价为$165.090,下跌了-0.820(-0.494%) + +3. 从ref_8中可以看到历史数据: + - 2025年10月15日: 165.91, 168.07 + - 2025年10月14日: 162.86, 160.05 + - 2025年10月13日: 166.81, 167.78 + - 2025年10月10日: 159.01, 170.03 + +4. 从ref_2中可以看到:2025年10月8日,阿里巴巴港股报价为181.12,下跌-0.12% + +5. 从ref_6中可以看到:2025年10月17日,阿里巴巴-WR (89988.HK)收市价为141.700,下跌-6.000 (-4.06%) + +6. 从ref_7中还提到:阿里巴巴(BABA)过去5个交易日涨5.06%,整个10月跌6.53%,年初至今涨97.02%,过去52周涨66.93% + +7. 从ref_5中可以看到一些股价指标:今开168.070,最高168.100,昨收162.860,最低164.600等 + +8. 从ref_1中可以看到:摩根大通在2025年10月2日发布的报告中,将阿里巴巴美股(BABA.N)的目标价大幅上调至245美元 + +9. 从ref_3中可以看到:2025年10月13日,高盛发布报告,上调阿里巴巴美股和港股目标价 + +10. 从ref_9中可以看到阿里巴巴-W (9988.HK)的一些价值评估数据,如市值3.43T,企业价值3.23T等 + +11. 从额外的"其他互联网信息"中提到:2025年10月19日,阿里巴巴集团的实际股票价格为155.30港币(但这条信息可能不准确,因为其他来源显示10月17日价格更高) + +12. 最后,系统提供的知识库中有一段最新的股市信息: +"股市信息:阿里巴巴美股: +实时价格167.05USD +上个交易日收盘价165.09USD +日环比%1.19% +月环比%-6.53 +日同比%66.33 +月同比%74.05" + +根据系统提供的知识库中的最新股市信息和ref_7的信息,可以确认截至2025年10月17日(星期五),阿里巴巴美股收盘价为167.05美元,较前一交易日上涨1.19%。 + +另外,从ref_4中可以看到10月16日的收盘价为165.09美元。 + +根据ref_7,10月17日的成交额为29.43亿美元,较上个交易日增加59.59%,成交量为1776.57万。 + +此外,还有一些分析师的目标价信息: +- 摩根大通在10月2日将目标价上调至245美元 +- 高盛在10月13日上调了目标价(具体数值未提供) + +综合以上信息,我可以提供阿里巴巴股价的最新情况,包括近期走势、分析师预测等。 +Assistant > # 阿里巴巴股价最新情况 + +## 美股(BABA)最新价格 +- 截至2025年10月17日,阿里巴巴美股收盘价为**167.05美元**,较前一交易日上涨1.19%[ref_7] +- 上一交易日(10月16日)收盘价为165.09美元,下跌0.494%[ref_4] +- 10月17日成交额达29.43亿美元,较上个交易日增加59.59%,当日成交量为1776.57万[ref_7] + +## 近期股价走势 +- 过去5个交易日累计上涨5.06%[ref_7] +- 整个10月下跌6.53%[ref_7] +- 年初至今上涨97.02%[ref_7] +- 过去52周上涨66.93%[ref_7] + +## 近期历史价格 +- 2025年10月15日: 165.91美元[ref_8] +- 2025年10月14日: 162.86美元[ref_8] +- 2025年10月13日: 166.81美元[ref_8] +- 2025年10月10日: 159.01美元[ref_8] + +## 港股情况 +- 阿里巴巴-WR (89988.HK)在2025年10月17日收市价为141.700港元,下跌6.000港元(-4.06%)[ref_6] +- 2025年10月8日,阿里巴巴港股报价为181.12港元,下跌0.12%[ref_2] + +## 分析师目标价 +- 摩根大通在2025年10月2日发布的报告中,将阿里巴巴美股目标价大幅上调至**245美元**[ref_1] +- 高盛于2025年10月13日发布报告,上调了阿里巴巴未来三年资本开支预测至4600亿元人民币,并上调其美股和港股目标价[ref_3] + +## 其他财务指标 +- 市盈率(TTM): 18.75[ref_5] +- 阿里巴巴-W (9988.HK)市值达3.43万亿港元[ref_9] +- 企业价值: 3.23万亿[ref_9] +Usage: in(2178)/out(1571)/reasoning(952)/plugins:(1)/total(3749) + */ diff --git a/src/Cnblogs.DashScope.Core/AsrOptions.cs b/src/Cnblogs.DashScope.Core/AsrOptions.cs new file mode 100644 index 0000000..e7039d1 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/AsrOptions.cs @@ -0,0 +1,24 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Options for speech recognition. +/// +public class AsrOptions +{ + /// + /// Language of the audio. Values in: zh, en, ja, de, ko, ru, fr, pt, ar, it, es. + /// + /// You can only set exactly 1 language. Leave this value as null if the audio file contains multiple languages. + public string? Language { get; set; } + + /// + /// Enable inverse text normalization(ITN). + /// + public bool? EnableItn { get; set; } + + /// + /// Return language identify result in response. + /// + /// If is set, this will return the value of . + public bool? EnableIld { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/CacheControlOptions.cs b/src/Cnblogs.DashScope.Core/CacheControlOptions.cs new file mode 100644 index 0000000..39f77f1 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/CacheControlOptions.cs @@ -0,0 +1,12 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Cache control options for model. +/// +public class CacheControlOptions +{ + /// + /// The cache type, no need to change, defaults to "ephemeral". + /// + public string Type { get; set; } = "ephemeral"; +} diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs new file mode 100644 index 0000000..79d99d2 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs @@ -0,0 +1,12 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Extra info from deep research model. +/// +public class DashScopeDeepResearchInfo +{ + /// + /// Current research result. + /// + public DashScopeDeepResearchTask? Research { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchReference.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchReference.cs new file mode 100644 index 0000000..e2cdbd7 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchReference.cs @@ -0,0 +1,11 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Represents a reference for deep search answer. +/// +/// The icon of the reference. +/// The description of the reference. +/// Index number of the reference. +/// Title of the reference. +/// The url of the reference. +public record DashScopeDeepResearchReference(string? Icon, string? Description, int IndexNumber, string Title, string Url); diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs new file mode 100644 index 0000000..ba2114c --- /dev/null +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace Cnblogs.DashScope.Core; + +/// +/// Represents a research task from deep research model. +/// +public class DashScopeDeepResearchTask +{ + /// + /// The id of the task. + /// + public int Id { get; set; } + + /// + /// Goal of the research. + /// + [JsonPropertyName("researchGoal")] + public string? ResearchGoal { get; set; } + + /// + /// Query string of the research. + /// + public string? Query { get; set; } + + /// + /// The websites reference. + /// + [JsonPropertyName("webSites")] + public List? WebSites { get; set; } + + /// + /// The content from tool calls. + /// + [JsonPropertyName("learningMap")] + public Dictionary? LearningMap { get; set; } + + /// + /// References of final answers. + /// + public List? References { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchWebsiteRef.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchWebsiteRef.cs new file mode 100644 index 0000000..18be1ea --- /dev/null +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchWebsiteRef.cs @@ -0,0 +1,10 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// The website reference that deep research model learned from search. +/// +/// The title of the website page. +/// The description of the website ref. +/// The url of the website. +/// The favicon of the website. +public record DashScopeDeepResearchWebsiteRef(string Title, string Description, string Url, string Favicon); diff --git a/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs b/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs index c1a2f7f..81482a7 100644 --- a/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs +++ b/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs @@ -4,11 +4,20 @@ /// Optional parameters for multi-model generation request. /// public interface IMultimodalParameters - : IProbabilityParameter, ISeedParameter, IIncrementalOutputParameter, IPenaltyParameter, IMaxTokenParameter, + : IProbabilityParameter, + ISeedParameter, + IIncrementalOutputParameter, + IPenaltyParameter, + IMaxTokenParameter, IStopTokenParameter { /// /// Allow higher resolution for inputs. When setting to true, increases the maximum input token from 1280 to 16384. Defaults to false. /// - public bool? VlHighResolutionImages { get; } + bool? VlHighResolutionImages { get; } + + /// + /// Options for speech recognition. + /// + AsrOptions? AsrOptions { get; } } diff --git a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs index 9c3b763..f3cbb42 100644 --- a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs @@ -20,7 +20,7 @@ public interface ITextGenerationParameters /// parameter.ResultFormat = ResultFormats.Message; /// /// - public string? ResultFormat { get; } + string? ResultFormat { get; } /// /// The format of response message, must be text or json_object @@ -34,55 +34,60 @@ public interface ITextGenerationParameters /// parameter.ResponseFormat = DashScopeResponseFormat.Json; /// /// - public DashScopeResponseFormat? ResponseFormat { get; } + DashScopeResponseFormat? ResponseFormat { get; } /// /// Enable internet search when generation. Defaults to false. /// - public bool? EnableSearch { get; } + bool? EnableSearch { get; } /// /// Search options. should set to true. /// - public TextGenerationSearchOptions? SearchOptions { get; set; } + TextGenerationSearchOptions? SearchOptions { get; set; } /// /// Thinking option. Valid for supported models.(e.g. qwen3) /// - public bool? EnableThinking { get; } + bool? EnableThinking { get; } /// /// Maximum length of thinking content. Valid for supported models.(e.g. qwen3) /// - public int? ThinkingBudget { get; set; } + int? ThinkingBudget { get; set; } /// /// Include log possibilities in response. /// - public bool? Logprobs { get; set; } + bool? Logprobs { get; set; } /// /// How many choices should be returned. Range: [0, 5] /// - public int? TopLogprobs { get; set; } + int? TopLogprobs { get; set; } /// /// Available tools for model to call. /// - public IEnumerable? Tools { get; } + IEnumerable? Tools { get; } /// /// Behavior when choosing tools. /// - public ToolChoice? ToolChoice { get; } + ToolChoice? ToolChoice { get; } /// /// Whether to enable parallel tool calling /// - public bool? ParallelToolCalls { get; } + bool? ParallelToolCalls { get; } /// /// Options when using QWen-MT models. /// - public TextGenerationTranslationOptions? TranslationOptions { get; set; } + TextGenerationTranslationOptions? TranslationOptions { get; set; } + + /// + /// Cache options when using qwen-coder models. + /// + CacheControlOptions? CacheControl { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/Internals/IMessage.cs b/src/Cnblogs.DashScope.Core/Internals/IMessage.cs index f6957b3..5c4bdca 100644 --- a/src/Cnblogs.DashScope.Core/Internals/IMessage.cs +++ b/src/Cnblogs.DashScope.Core/Internals/IMessage.cs @@ -6,10 +6,10 @@ internal interface IMessage /// /// Must be one of system, user or assistant. /// - public string Role { get; } + string Role { get; } /// /// The content of message. /// - public TContent Content { get; } + TContent Content { get; } } diff --git a/src/Cnblogs.DashScope.Core/Internals/MultimodalMessageVideoContentJsonConverter.cs b/src/Cnblogs.DashScope.Core/Internals/MultimodalMessageVideoContentJsonConverter.cs new file mode 100644 index 0000000..558436c --- /dev/null +++ b/src/Cnblogs.DashScope.Core/Internals/MultimodalMessageVideoContentJsonConverter.cs @@ -0,0 +1,65 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Cnblogs.DashScope.Core.Internals; + +internal class MultimodalMessageVideoContentJsonConverter : JsonConverter +{ + /// + public override MultimodalMessageVideoContent? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.String => ReadFromString(reader.GetString()), + JsonTokenType.Null => null, + JsonTokenType.StartArray => ReadFromArray(ref reader, options), + _ => throw new JsonException("Invalid type in stop array, must be string or string array") + }; + } + + /// + public override void Write( + Utf8JsonWriter writer, + MultimodalMessageVideoContent value, + JsonSerializerOptions options) + { + if (value.Type == MultimodalMessageVideoContentType.Video) + { + JsonSerializer.Serialize(writer, value.Urls.FirstOrDefault() ?? string.Empty, options); + } + else if (value.Type == MultimodalMessageVideoContentType.FrameSequence) + { + JsonSerializer.Serialize(writer, value.Urls, options); + } + else + { + throw new JsonException("Invalid video content type, must be Video or FrameSequence"); + } + } + + private static MultimodalMessageVideoContent? ReadFromArray( + ref Utf8JsonReader reader, + JsonSerializerOptions options) + { + var list = JsonSerializer.Deserialize>(ref reader, options); + if (list is null) + { + return null; + } + + return MultimodalMessageVideoContent.FrameSequence(list); + } + + private static MultimodalMessageVideoContent? ReadFromString(string? url) + { + if (url == null) + { + return null; + } + + return MultimodalMessageVideoContent.Video(url); + } +} diff --git a/src/Cnblogs.DashScope.Core/MultimodalAnnotation.cs b/src/Cnblogs.DashScope.Core/MultimodalAnnotation.cs new file mode 100644 index 0000000..34d1f1c --- /dev/null +++ b/src/Cnblogs.DashScope.Core/MultimodalAnnotation.cs @@ -0,0 +1,8 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Language annotation of the input file. +/// +/// The language of the file. +/// The type of the file. +public record MultimodalAnnotation(string Language, string Type); diff --git a/src/Cnblogs.DashScope.Core/MultimodalMessage.cs b/src/Cnblogs.DashScope.Core/MultimodalMessage.cs index f4e370e..86b6b44 100644 --- a/src/Cnblogs.DashScope.Core/MultimodalMessage.cs +++ b/src/Cnblogs.DashScope.Core/MultimodalMessage.cs @@ -8,10 +8,12 @@ namespace Cnblogs.DashScope.Core; /// The role associated with this message. /// The contents of this message. /// Thoughts from the model. +/// Language annotations from the model. public record MultimodalMessage( string Role, IReadOnlyList Content, - string? ReasoningContent = null) + string? ReasoningContent = null, + IReadOnlyList? Annotations = null) : IMessage> { /// diff --git a/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs b/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs index 2722009..6d1a5b0 100644 --- a/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs +++ b/src/Cnblogs.DashScope.Core/MultimodalMessageContent.cs @@ -9,13 +9,17 @@ /// Video urls. /// For qwen-vl-ocr only. Minimal pixels for ocr task. /// For qwen-vl-ocr only. Maximum pixels for ocr task. +/// For qwen-vl-ocr only. Rotate before ocr. +/// For video content, model will read the video by 1/fps seconds; for frame sequence, indicate that the frame is captured by 1/fps seconds. public record MultimodalMessageContent( string? Image = null, string? Text = null, string? Audio = null, - IEnumerable? Video = null, + MultimodalMessageVideoContent? Video = null, int? MinPixels = null, - int? MaxPixels = null) + int? MaxPixels = null, + bool? EnableRotate = null, + float? Fps = null) { private const string OssSchema = "oss://"; @@ -25,10 +29,15 @@ public record MultimodalMessageContent( /// Image url. /// For qwen-vl-ocr only. Minimal pixels for ocr task. /// For qwen-vl-ocr only. Maximum pixels for ocr task. + /// For OCR models only. Auto rotate images before OCR. /// - public static MultimodalMessageContent ImageContent(string url, int? minPixels = null, int? maxPixels = null) + public static MultimodalMessageContent ImageContent( + string url, + int? minPixels = null, + int? maxPixels = null, + bool? enableRotate = null) { - return new MultimodalMessageContent(url, MinPixels: minPixels, MaxPixels: maxPixels); + return new MultimodalMessageContent(url, MinPixels: minPixels, MaxPixels: maxPixels, EnableRotate: enableRotate); } /// @@ -38,17 +47,20 @@ public static MultimodalMessageContent ImageContent(string url, int? minPixels = /// Image media type. /// For qwen-vl-ocr only. Minimal pixels for ocr task. /// For qwen-vl-ocr only. Maximum pixels for ocr task. + /// For OCR models only. Auto rotate images before OCR. /// public static MultimodalMessageContent ImageContent( ReadOnlySpan bytes, string mediaType, int? minPixels = null, - int? maxPixels = null) + int? maxPixels = null, + bool? enableRotate = null) { return ImageContent( $"data:{mediaType};base64,{Convert.ToBase64String(bytes)}", minPixels, - maxPixels); + maxPixels, + enableRotate); } /// @@ -74,15 +86,27 @@ public static MultimodalMessageContent AudioContent(string audioUrl) /// /// Represents video contents. /// - /// The urls of the videos. + /// The urls of the frames. + /// The fps of the frame + /// + public static MultimodalMessageContent VideoFrames(IEnumerable frames, int? fps = null) + { + return new MultimodalMessageContent(Video: MultimodalMessageVideoContent.FrameSequence(frames), Fps: fps); + } + + /// + /// Represents a video content. + /// + /// The url of the video. + /// The fps for modal to capture frames by. /// - public static MultimodalMessageContent VideoContent(IEnumerable videoUrls) + public static MultimodalMessageContent VideoContent(string url, int? fps = null) { - return new MultimodalMessageContent(Video: videoUrls); + return new MultimodalMessageContent(Video: MultimodalMessageVideoContent.Video(url), Fps: fps); } internal bool IsOss() => Image?.StartsWith(OssSchema) == true || Audio?.StartsWith(OssSchema) == true - || Video?.Any(v => v.StartsWith(OssSchema)) == true; + || Video?.Urls.Any(v => v.StartsWith(OssSchema)) == true; } diff --git a/src/Cnblogs.DashScope.Core/MultimodalMessageVideoContent.cs b/src/Cnblogs.DashScope.Core/MultimodalMessageVideoContent.cs new file mode 100644 index 0000000..a9839c5 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/MultimodalMessageVideoContent.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; +using Cnblogs.DashScope.Core.Internals; + +namespace Cnblogs.DashScope.Core; + +/// +/// Represents video content of multimodal input. +/// +[JsonConverter(typeof(MultimodalMessageVideoContentJsonConverter))] +public class MultimodalMessageVideoContent +{ + /// + /// The type of the video input. + /// + public MultimodalMessageVideoContentType Type { get; set; } + + /// + /// The urls of the video file(s). + /// + public List Urls { get; set; } = new(); + + /// + /// Create a video content from a video file url. + /// + /// The url of the video file. + /// + public static MultimodalMessageVideoContent Video(string url) + { + return new MultimodalMessageVideoContent + { + Type = MultimodalMessageVideoContentType.Video, Urls = new List { url } + }; + } + + /// + /// Create a video input from still frames. + /// + /// The urls of the frames. + /// + public static MultimodalMessageVideoContent FrameSequence(IEnumerable urls) + { + return new MultimodalMessageVideoContent + { + Type = MultimodalMessageVideoContentType.FrameSequence, Urls = new List(urls) + }; + } +} diff --git a/src/Cnblogs.DashScope.Core/MultimodalMessageVideoContentType.cs b/src/Cnblogs.DashScope.Core/MultimodalMessageVideoContentType.cs new file mode 100644 index 0000000..0d75f98 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/MultimodalMessageVideoContentType.cs @@ -0,0 +1,17 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// The type of the video input. +/// +public enum MultimodalMessageVideoContentType +{ + /// + /// A video file. + /// + Video = 1, + + /// + /// A sequence of still frames. + /// + FrameSequence = 2 +} diff --git a/src/Cnblogs.DashScope.Core/MultimodalParameters.cs b/src/Cnblogs.DashScope.Core/MultimodalParameters.cs index dc6b98b..55c42ec 100644 --- a/src/Cnblogs.DashScope.Core/MultimodalParameters.cs +++ b/src/Cnblogs.DashScope.Core/MultimodalParameters.cs @@ -23,6 +23,9 @@ public class MultimodalParameters : IMultimodalParameters /// public bool? VlHighResolutionImages { get; set; } + /// + public AsrOptions? AsrOptions { get; set; } + /// public float? RepetitionPenalty { get; set; } diff --git a/src/Cnblogs.DashScope.Core/TextChatMessage.cs b/src/Cnblogs.DashScope.Core/TextChatMessage.cs index 3a3bfa2..1b7f83b 100644 --- a/src/Cnblogs.DashScope.Core/TextChatMessage.cs +++ b/src/Cnblogs.DashScope.Core/TextChatMessage.cs @@ -70,6 +70,21 @@ public TextChatMessage( /// Calls to the function. public List? ToolCalls { get; init; } + /// + /// Used by qwen-deep-research, indicate the phase of the research. + /// + public string? Phase { get; set; } + + /// + /// Used by qwen-deep-research, indicate the status of the model. + /// + public string? Status { get; set; } + + /// + /// Extra output from models. + /// + public TextChatMessageExtra? Extra { get; set; } + /// /// Creates a file message. /// diff --git a/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs b/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs new file mode 100644 index 0000000..a8af4f5 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs @@ -0,0 +1,12 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Extra output from different models. +/// +public class TextChatMessageExtra +{ + /// + /// Deep research output. + /// + public List? DeepResearch { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs index fc2fee9..fb4a7bf 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs @@ -65,6 +65,9 @@ public class TextGenerationParameters : ITextGenerationParameters /// public TextGenerationTranslationOptions? TranslationOptions { get; set; } + /// + public CacheControlOptions? CacheControl { get; set; } + /// public bool? IncrementalOutput { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/TextGenerationPluginUsages.cs b/src/Cnblogs.DashScope.Core/TextGenerationPluginUsages.cs new file mode 100644 index 0000000..40b5d9a --- /dev/null +++ b/src/Cnblogs.DashScope.Core/TextGenerationPluginUsages.cs @@ -0,0 +1,7 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Plugin usages. +/// +/// Usage of search plugin. +public record TextGenerationPluginUsages(TextGenerationSearchPluginUsage? Search); diff --git a/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs b/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs index d0d8cb4..d57c346 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationSearchOptions.cs @@ -26,7 +26,17 @@ public class TextGenerationSearchOptions public bool? ForcedSearch { get; set; } /// - /// How many search records should be provided to model. "standard" - 5 records. "pro" - 10 records. + /// How many search records should be provided to model. "turbo" or "max". /// public string? SearchStrategy { get; set; } + + /// + /// Enhanced search for specific areas. + /// + public bool? EnableSearchExtension { get; set; } + + /// + /// Return the search result first when using incremental output. + /// + public bool? PrependSearchResult { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs b/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs new file mode 100644 index 0000000..c332f39 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs @@ -0,0 +1,7 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Usage of the search plugin. +/// +/// Usage count. +public record TextGenerationSearchPluginUsage(int Count); diff --git a/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs b/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs index c908e55..b9b2ca1 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationTokenUsage.cs @@ -21,6 +21,11 @@ public class TextGenerationTokenUsage /// public TextGenerationOutputTokenDetails? OutputTokensDetails { get; set; } + /// + /// Usages of plugins. + /// + public TextGenerationPluginUsages? Plugins { get; set; } + /// /// The number of output token. /// diff --git a/src/Cnblogs.DashScope.Core/TextGenerationWebSearchExtra.cs b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchExtra.cs new file mode 100644 index 0000000..7cbb0e1 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchExtra.cs @@ -0,0 +1,8 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Extra info when is true. +/// +/// The results from extension tools. +/// The name of the tools. +public record TextGenerationWebSearchExtra(string Result, string Tool); diff --git a/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs index 27da418..454fb5c 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationWebSearchInfo.cs @@ -4,4 +4,7 @@ /// Web search information. /// /// Web search results. -public record TextGenerationWebSearchInfo(List SearchResults); +/// Extra tool infos when is true. +public record TextGenerationWebSearchInfo( + List SearchResults, + List? ExtraToolInfo); diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs index f413959..657ded1 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.MultimodalGeneration.cs @@ -462,7 +462,7 @@ public static class MultimodalGeneration MultimodalMessage.User( new List { - MultimodalMessageContent.VideoContent( + MultimodalMessageContent.VideoFrames( new List { "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg", @@ -520,7 +520,7 @@ public static class MultimodalGeneration MultimodalMessage.User( new List { - MultimodalMessageContent.VideoContent( + MultimodalMessageContent.VideoFrames( new List { "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241108/xzsgiz/football1.jpg", diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs index 65e5747..4a4cd04 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -367,7 +367,9 @@ public static class MessageFormat EnableCitation = true, CitationFormat = "[ref_]", ForcedSearch = true, - SearchStrategy = "standard" + SearchStrategy = "pro", + EnableSearchExtension = false, + PrependSearchResult = true } } }, @@ -385,14 +387,41 @@ public static class MessageFormat "截至2025年6月7日,博客园的dudu站长发布的内容包括了技术分享和个人经历总结。以下是对dudu最近博客内容的一个概括:\n\n1. 代码重构经验分享:dudu在一篇博客中分享了他在博客园后台开发过程中遇到的一次代码重构经历。这次重构涉及到两个列表的合并(union),他需要实现一个自定义的`EqualityComparer`,基于列表元素的`Id`字段来进行比较,而不是默认的对象引用比较。这表明dudu在持续关注和改进博客园的技术架构,以确保其高效和可维护性。[ref_2]\n\n2. 开源工具介绍:另一篇博客介绍了名为NBearMapping的开源对象映射工具,该工具可用于不同类型的对象、DataRow以及DataReader之间的数据映射。dudu提到这个工具对于开发者来说非常有用,因为它可以简化数据层与业务逻辑层之间的交互。[ref_3]\n\n此外,还有关于个人与博客园共同成长的感想,提到了在过去20年间,无论是个人还是博客园本身都经历了巨大的变化。dudu也提到了自己正面临一些个人生活中的挑战,并表达了对博客园社区理解和支持的感激之情。[ref_1]\n\n这些博客不仅展示了dudu作为技术人员的专业知识和技术分享的热情,还反映了他对博客园这个平台的深厚感情和个人投入。如果您需要更详细的博客内容或有其他问题,请告知我以便提供进一步的帮助。"), } }, - SearchInfo = new TextGenerationWebSearchInfo(new List() - { - new("CSDN - 专业开发者社区", "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", 1, "我与博客园的20年转载", "https://blog.csdn.net/weixin_40884228/article/details/148485212"), - new("博客园", "https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg", 2, "dudu - 博客园", "https://www.cnblogs.com/dudu"), - new("博客园", "https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg", 3, "dudu - 博客园", "https://www.cnblogs.com/dudu?page=36"), - new("阿里云官方网站", "https://img.alicdn.com/imgextra/i3/O1CN015NhUWq1Z1sdj3359l_!!6000000003135-55-tps-32-32.svg", 4, "玩转博客园的心路总结 - 阿里云开发者社区", "https://developer.aliyun.com/article/331235"), - new("CSDN - 专业开发者社区", "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", 5, "为.NET程序员打工的站长——博客园dudu 原创", "https://blog.csdn.net/Microsoft_MVP/article/details/2416055") - }) + SearchInfo = new TextGenerationWebSearchInfo( + new List() + { + new( + "CSDN - 专业开发者社区", + "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", + 1, + "我与博客园的20年转载", + "https://blog.csdn.net/weixin_40884228/article/details/148485212"), + new( + "博客园", + "https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg", + 2, + "dudu - 博客园", + "https://www.cnblogs.com/dudu"), + new( + "博客园", + "https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg", + 3, + "dudu - 博客园", + "https://www.cnblogs.com/dudu?page=36"), + new( + "阿里云官方网站", + "https://img.alicdn.com/imgextra/i3/O1CN015NhUWq1Z1sdj3359l_!!6000000003135-55-tps-32-32.svg", + 4, + "玩转博客园的心路总结 - 阿里云开发者社区", + "https://developer.aliyun.com/article/331235"), + new( + "CSDN - 专业开发者社区", + "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", + 5, + "为.NET程序员打工的站长——博客园dudu 原创", + "https://blog.csdn.net/Microsoft_MVP/article/details/2416055") + }, + null) }, RequestId = "80753a20-2750-9ab6-bc2a-1b851ef43efc", Usage = new TextGenerationTokenUsage From b85aa44c0c8c223f307ee13627fd8251c8cb82ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 19 Oct 2025 21:03:56 +0800 Subject: [PATCH 2/6] test: add snapshot for search with plugin --- README.zh-Hans.md | 4 ++ .../TextGenerationSearchPluginUsage.cs | 3 +- ...ion-message-search-nosse.request.body.json | 7 +- ...on-message-search-nosse.request.header.txt | 2 +- ...ion-message-search-nosse.response.body.txt | 2 +- ...n-message-search-nosse.response.header.txt | 13 ++-- .../Utils/Snapshots.TextGeneration.cs | 69 ++++++++++--------- 7 files changed, 56 insertions(+), 44 deletions(-) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index bdb306b..a4b927e 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -67,6 +67,10 @@ public class YourService(IDashScopeClient client) ## 支持的 API - [文本生成](#文本生成) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景 + - [多轮对话](#多轮对话) + - [深度思考](#深度思考) + - [联网搜索](#联网搜索) + - [工具调用](#工具调用) - [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景 - [语音合成](#语音合成) - CosyVoice,Sambert 等,支持 TTS 等应用场景 - [图像生成](#图像生成) - wanx2.1 等,支持文生图,人像风格重绘等应用场景 diff --git a/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs b/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs index c332f39..c456f84 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationSearchPluginUsage.cs @@ -4,4 +4,5 @@ /// Usage of the search plugin. /// /// Usage count. -public record TextGenerationSearchPluginUsage(int Count); +/// Search strategy. +public record TextGenerationSearchPluginUsage(int Count, string Strategy); diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.body.json index 7a30117..a53c03f 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.body.json +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.body.json @@ -1,10 +1,10 @@ { - "model": "qwen-max", + "model": "qwen-plus", "input": { "messages": [ { "role": "user", - "content": "总结博客园 dudu 的最新博客" + "content": "阿里股价" } ] }, @@ -16,7 +16,8 @@ "enable_citation": true, "citation_format": "[ref_]", "forced_search": true, - "search_strategy": "standard" + "search_strategy": "standard", + "enable_search_extension": true } } } diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.header.txt index 8f77480..8a83553 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.header.txt +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.request.header.txt @@ -5,4 +5,4 @@ Cache-Control: no-cache Host: dashscope.aliyuncs.com Accept-Encoding: gzip, deflate, br Connection: keep-alive -Content-Length: 592 +Content-Length: 581 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.body.txt index ed84299..9872480 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.body.txt +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.body.txt @@ -1 +1 @@ -{"output":{"search_info":{"search_results":[{"site_name":"CSDN - 专业开发者社区","icon":"https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg","index":1,"title":"我与博客园的20年转载","url":"https://blog.csdn.net/weixin_40884228/article/details/148485212"},{"site_name":"博客园","icon":"https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg","index":2,"title":"dudu - 博客园","url":"https://www.cnblogs.com/dudu"},{"site_name":"博客园","icon":"https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg","index":3,"title":"dudu - 博客园","url":"https://www.cnblogs.com/dudu?page=36"},{"site_name":"阿里云官方网站","icon":"https://img.alicdn.com/imgextra/i3/O1CN015NhUWq1Z1sdj3359l_!!6000000003135-55-tps-32-32.svg","index":4,"title":"玩转博客园的心路总结 - 阿里云开发者社区","url":"https://developer.aliyun.com/article/331235"},{"site_name":"CSDN - 专业开发者社区","icon":"https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg","index":5,"title":"为.NET程序员打工的站长——博客园dudu 原创","url":"https://blog.csdn.net/Microsoft_MVP/article/details/2416055"}]},"choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"截至2025年6月7日,博客园的dudu站长发布的内容包括了技术分享和个人经历总结。以下是对dudu最近博客内容的一个概括:\n\n1. 代码重构经验分享:dudu在一篇博客中分享了他在博客园后台开发过程中遇到的一次代码重构经历。这次重构涉及到两个列表的合并(union),他需要实现一个自定义的`EqualityComparer`,基于列表元素的`Id`字段来进行比较,而不是默认的对象引用比较。这表明dudu在持续关注和改进博客园的技术架构,以确保其高效和可维护性。[ref_2]\n\n2. 开源工具介绍:另一篇博客介绍了名为NBearMapping的开源对象映射工具,该工具可用于不同类型的对象、DataRow以及DataReader之间的数据映射。dudu提到这个工具对于开发者来说非常有用,因为它可以简化数据层与业务逻辑层之间的交互。[ref_3]\n\n此外,还有关于个人与博客园共同成长的感想,提到了在过去20年间,无论是个人还是博客园本身都经历了巨大的变化。dudu也提到了自己正面临一些个人生活中的挑战,并表达了对博客园社区理解和支持的感激之情。[ref_1]\n\n这些博客不仅展示了dudu作为技术人员的专业知识和技术分享的热情,还反映了他对博客园这个平台的深厚感情和个人投入。如果您需要更详细的博客内容或有其他问题,请告知我以便提供进一步的帮助。"}}]},"usage":{"plugins":{"search":{"count":1}},"total_tokens":800,"output_tokens":304,"input_tokens":496,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"80753a20-2750-9ab6-bc2a-1b851ef43efc"} +{"output":{"choices":[{"message":{"content":"截至2025年10月17日,阿里巴巴美股(BABA)的实时价格为167.05美元,较上个交易日收盘价165.09美元上涨1.19%[根据权威渠道的实时信息]。\n\n近期,多家券商上调了对阿里巴巴的目标股价。其中,摩根大通在2025年10月1日将阿里巴巴美股的目标价由170美元大幅上调至245美元,这是目前外资机构中的最高预测[ref_1][ref_3]。此外,大和证券、瑞银、花旗、高盛、摩根士丹利等也纷纷上调目标价并维持“买入”或类似评级[ref_1]。\n\n从市场表现来看,阿里巴巴股价在近期有所波动。例如,在2025年10月初,其美股价格一度接近189美元,随后有所回落[根据权威渠道的实时信息]。与此同时,港股方面,截至2025年10月3日收盘,阿里巴巴-SW(09988)报185.100港元,上涨2.000港元,涨幅1.09%[ref_2]。","role":"assistant"},"finish_reason":"stop"}],"search_info":{"extra_tool_info":[{"result":"阿里巴巴美股:\n实时价格167.05USD\n上个交易日收盘价165.09USD\n日环比%1.19%\n月环比%-6.53\n日同比%66.33\n月同比%74.05\n历史价格列表[{\"date\":\"2025-10-17\",\"endPri\":\"167.050\"},{\"date\":\"2025-10-16\",\"endPri\":\"165.090\"},{\"date\":\"2025-10-15\",\"endPri\":\"165.910\"},{\"date\":\"2025-10-14\",\"endPri\":\"162.860\"},{\"date\":\"2025-10-13\",\"endPri\":\"166.810\"},{\"date\":\"2025-10-10\",\"endPri\":\"159.010\"},{\"date\":\"2025-10-09\",\"endPri\":\"173.680\"},{\"date\":\"2025-10-08\",\"endPri\":\"181.120\"},{\"date\":\"2025-10-07\",\"endPri\":\"181.330\"},{\"date\":\"2025-10-06\",\"endPri\":\"187.220\"},{\"date\":\"2025-10-03\",\"endPri\":\"188.030\"},{\"date\":\"2025-10-02\",\"endPri\":\"189.340\"},{\"date\":\"2025-10-01\",\"endPri\":\"182.780\"},{\"date\":\"2025-09-30\",\"endPri\":\"178.730\"},{\"date\":\"2025-09-29\",\"endPri\":\"179.900\"},{\"date\":\"2025-09-26\",\"endPri\":\"171.910\"},{\"date\":\"2025-09-25\",\"endPri\":\"175.470\"},{\"date\":\"2025-09-24\",\"endPri\":\"176.440\"},{\"date\":\"2025-09-23\",\"endPri\":\"163.080\"},{\"date\":\"2025-09-22\",\"endPri\":\"164.250\"},{\"date\":\"2025-09-19\",\"endPri\":\"162.810\"},{\"date\":\"2025-09-18\",\"endPri\":\"162.480\"},{\"date\":\"2025-09-17\",\"endPri\":\"166.170\"},{\"date\":\"2025-09-16\",\"endPri\":\"162.210\"},{\"date\":\"2025-09-15\",\"endPri\":\"158.040\"},{\"date\":\"2025-09-12\",\"endPri\":\"155.060\"},{\"date\":\"2025-09-11\",\"endPri\":\"155.440\"},{\"date\":\"2025-09-10\",\"endPri\":\"143.930\"},{\"date\":\"2025-09-09\",\"endPri\":\"147.100\"},{\"date\":\"2025-09-08\",\"endPri\":\"141.200\"}]\n\n","tool":"stock"}],"search_results":[{"icon":"https://b.bdstatic.com/searchbox/mappconsole/image/20190805/1239163c-77cc-449e-b91f-9bb1d27e43a7.png","site_name":"无","index":1,"title":"各大券商不断调高 阿里 预期股价!1. 摩根大通:2025年10月1日,摩根大通发布报告将 阿里巴巴 (BABA.US)... - 雪球","url":"https://xueqiu.com/1692213155/355441155"},{"icon":"https://img.alicdn.com/imgextra/i3/O1CN0143d0Wi1XYHQYtbqJI_!!6000000002935-55-tps-32-32.svg","site_name":"无","index":2,"title":"阿里巴巴-SW(09988)_个股概览_股票价格_实时行情_走势图_新闻资讯_股评_财报_FinScope-AI让投资更简单","url":"https://gushitong.baidu.com/stock/hk-09988?code=09988&financeType=stock&market=hk&name=阿里巴巴-SW&subTab=2"},{"icon":"https://b.bdstatic.com/searchbox/mappconsole/image/20190805/1239163c-77cc-449e-b91f-9bb1d27e43a7.png","site_name":"无","index":3,"title":"截至目前为止,外资机构对","url":"https://xueqiu.com/9216592857/355356488"},{"icon":"https://static.alibabagroup.com/static/favicon.ico","site_name":"阿里巴巴集团","index":4,"title":"阿里巴巴投资者关系-阿里巴巴集团","url":"https://www.alibabagroup.com/redirect?path=/cn/ir/home"},{"icon":"https://static.alibabagroup.com/static/favicon.ico","site_name":"阿里巴巴集团","index":5,"title":"阿里巴巴投資者關係-阿里巴巴集團","url":"https://www.alibabagroup.com/zh-HK/investor-relations"}]}},"usage":{"total_tokens":2973,"output_tokens":266,"input_tokens":2707,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"cc2b017d-df02-4ad6-8942-858ad10a3f8a"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.header.txt index 8349277..b349be6 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.header.txt +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-nosse.response.header.txt @@ -1,15 +1,16 @@ HTTP/1.1 200 OK vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding content-type: application/json -x-request-id: 405d57ba-6cfc-9519-977f-0f519f712364 +x-request-id: cc2b017d-df02-4ad6-8942-858ad10a3f8a x-dashscope-call-gateway: true +x-dashscope-inner-csi: verified x-dashscope-finished: true x-dashscope-timeout: 298 -req-cost-time: 810 -req-arrive-time: 1751899675324 -resp-start-time: 1751899676135 -x-envoy-upstream-service-time: 802 +req-cost-time: 8863 +req-arrive-time: 1760877219936 +resp-start-time: 1760877228799 +x-envoy-upstream-service-time: 8856 content-encoding: gzip -date: Mon, 07 Jul 2025 14:47:55 GMT +date: Sun, 19 Oct 2025 12:33:48 GMT server: istio-envoy transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs index 4a4cd04..799760a 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -350,12 +350,12 @@ public static class MessageFormat "single-generation-message-search", new ModelRequest { - Model = "qwen-max", + Model = "qwen-plus", Input = new TextGenerationInput { Messages = - new List { TextChatMessage.User("总结博客园 dudu 的最新博客") } + new List { TextChatMessage.User("阿里股价") } }, Parameters = new TextGenerationParameters { @@ -365,11 +365,10 @@ public static class MessageFormat { EnableSource = true, EnableCitation = true, + EnableSearchExtension = true, CitationFormat = "[ref_]", ForcedSearch = true, - SearchStrategy = "pro", - EnableSearchExtension = false, - PrependSearchResult = true + SearchStrategy = "standard", } } }, @@ -384,52 +383,58 @@ public static class MessageFormat { FinishReason = "stop", Message = TextChatMessage.Assistant( - "截至2025年6月7日,博客园的dudu站长发布的内容包括了技术分享和个人经历总结。以下是对dudu最近博客内容的一个概括:\n\n1. 代码重构经验分享:dudu在一篇博客中分享了他在博客园后台开发过程中遇到的一次代码重构经历。这次重构涉及到两个列表的合并(union),他需要实现一个自定义的`EqualityComparer`,基于列表元素的`Id`字段来进行比较,而不是默认的对象引用比较。这表明dudu在持续关注和改进博客园的技术架构,以确保其高效和可维护性。[ref_2]\n\n2. 开源工具介绍:另一篇博客介绍了名为NBearMapping的开源对象映射工具,该工具可用于不同类型的对象、DataRow以及DataReader之间的数据映射。dudu提到这个工具对于开发者来说非常有用,因为它可以简化数据层与业务逻辑层之间的交互。[ref_3]\n\n此外,还有关于个人与博客园共同成长的感想,提到了在过去20年间,无论是个人还是博客园本身都经历了巨大的变化。dudu也提到了自己正面临一些个人生活中的挑战,并表达了对博客园社区理解和支持的感激之情。[ref_1]\n\n这些博客不仅展示了dudu作为技术人员的专业知识和技术分享的热情,还反映了他对博客园这个平台的深厚感情和个人投入。如果您需要更详细的博客内容或有其他问题,请告知我以便提供进一步的帮助。"), + "截至2025年10月17日,阿里巴巴美股(BABA)的实时价格为167.05美元,较上个交易日收盘价165.09美元上涨1.19%[根据权威渠道的实时信息]。\n\n近期,多家券商上调了对阿里巴巴的目标股价。其中,摩根大通在2025年10月1日将阿里巴巴美股的目标价由170美元大幅上调至245美元,这是目前外资机构中的最高预测[ref_1][ref_3]。此外,大和证券、瑞银、花旗、高盛、摩根士丹利等也纷纷上调目标价并维持“买入”或类似评级[ref_1]。\n\n从市场表现来看,阿里巴巴股价在近期有所波动。例如,在2025年10月初,其美股价格一度接近189美元,随后有所回落[根据权威渠道的实时信息]。与此同时,港股方面,截至2025年10月3日收盘,阿里巴巴-SW(09988)报185.100港元,上涨2.000港元,涨幅1.09%[ref_2]。"), } }, SearchInfo = new TextGenerationWebSearchInfo( new List() { new( - "CSDN - 专业开发者社区", - "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", + "无", + "https://b.bdstatic.com/searchbox/mappconsole/image/20190805/1239163c-77cc-449e-b91f-9bb1d27e43a7.png", 1, - "我与博客园的20年转载", - "https://blog.csdn.net/weixin_40884228/article/details/148485212"), + "各大券商不断调高 阿里 预期股价!1. 摩根大通:2025年10月1日,摩根大通发布报告将 阿里巴巴 (BABA.US)... - 雪球", + "https://xueqiu.com/1692213155/355441155"), new( - "博客园", - "https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg", + "无", + "https://img.alicdn.com/imgextra/i3/O1CN0143d0Wi1XYHQYtbqJI_!!6000000002935-55-tps-32-32.svg", 2, - "dudu - 博客园", - "https://www.cnblogs.com/dudu"), + "阿里巴巴-SW(09988)_个股概览_股票价格_实时行情_走势图_新闻资讯_股评_财报_FinScope-AI让投资更简单", + "https://gushitong.baidu.com/stock/hk-09988?code=09988&financeType=stock&market=hk&name=阿里巴巴-SW&subTab=2"), new( - "博客园", - "https://img.alicdn.com/imgextra/i2/O1CN01FzHbv01o253A3z2Gd_!!6000000005166-55-tps-32-32.svg", + "无", + "https://b.bdstatic.com/searchbox/mappconsole/image/20190805/1239163c-77cc-449e-b91f-9bb1d27e43a7.png", 3, - "dudu - 博客园", - "https://www.cnblogs.com/dudu?page=36"), + "截至目前为止,外资机构对", + "https://xueqiu.com/9216592857/355356488"), new( - "阿里云官方网站", - "https://img.alicdn.com/imgextra/i3/O1CN015NhUWq1Z1sdj3359l_!!6000000003135-55-tps-32-32.svg", + "阿里巴巴集团", + "https://static.alibabagroup.com/static/favicon.ico", 4, - "玩转博客园的心路总结 - 阿里云开发者社区", - "https://developer.aliyun.com/article/331235"), + "阿里巴巴投资者关系-阿里巴巴集团", + "https://www.alibabagroup.com/redirect?path=/cn/ir/home"), new( - "CSDN - 专业开发者社区", - "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", + "阿里巴巴集团", + "https://static.alibabagroup.com/static/favicon.ico", 5, - "为.NET程序员打工的站长——博客园dudu 原创", - "https://blog.csdn.net/Microsoft_MVP/article/details/2416055") + "阿里巴巴投資者關係-阿里巴巴集團", + "https://www.alibabagroup.com/zh-HK/investor-relations") }, - null) + new List() + { + new( + "阿里巴巴美股:\n实时价格167.05USD\n上个交易日收盘价165.09USD\n日环比%1.19%\n月环比%-6.53\n日同比%66.33\n月同比%74.05\n历史价格列表[{\"date\":\"2025-10-17\",\"endPri\":\"167.050\"},{\"date\":\"2025-10-16\",\"endPri\":\"165.090\"},{\"date\":\"2025-10-15\",\"endPri\":\"165.910\"},{\"date\":\"2025-10-14\",\"endPri\":\"162.860\"},{\"date\":\"2025-10-13\",\"endPri\":\"166.810\"},{\"date\":\"2025-10-10\",\"endPri\":\"159.010\"},{\"date\":\"2025-10-09\",\"endPri\":\"173.680\"},{\"date\":\"2025-10-08\",\"endPri\":\"181.120\"},{\"date\":\"2025-10-07\",\"endPri\":\"181.330\"},{\"date\":\"2025-10-06\",\"endPri\":\"187.220\"},{\"date\":\"2025-10-03\",\"endPri\":\"188.030\"},{\"date\":\"2025-10-02\",\"endPri\":\"189.340\"},{\"date\":\"2025-10-01\",\"endPri\":\"182.780\"},{\"date\":\"2025-09-30\",\"endPri\":\"178.730\"},{\"date\":\"2025-09-29\",\"endPri\":\"179.900\"},{\"date\":\"2025-09-26\",\"endPri\":\"171.910\"},{\"date\":\"2025-09-25\",\"endPri\":\"175.470\"},{\"date\":\"2025-09-24\",\"endPri\":\"176.440\"},{\"date\":\"2025-09-23\",\"endPri\":\"163.080\"},{\"date\":\"2025-09-22\",\"endPri\":\"164.250\"},{\"date\":\"2025-09-19\",\"endPri\":\"162.810\"},{\"date\":\"2025-09-18\",\"endPri\":\"162.480\"},{\"date\":\"2025-09-17\",\"endPri\":\"166.170\"},{\"date\":\"2025-09-16\",\"endPri\":\"162.210\"},{\"date\":\"2025-09-15\",\"endPri\":\"158.040\"},{\"date\":\"2025-09-12\",\"endPri\":\"155.060\"},{\"date\":\"2025-09-11\",\"endPri\":\"155.440\"},{\"date\":\"2025-09-10\",\"endPri\":\"143.930\"},{\"date\":\"2025-09-09\",\"endPri\":\"147.100\"},{\"date\":\"2025-09-08\",\"endPri\":\"141.200\"}]\n\n", + "stock") + }) }, - RequestId = "80753a20-2750-9ab6-bc2a-1b851ef43efc", + RequestId = "cc2b017d-df02-4ad6-8942-858ad10a3f8a", Usage = new TextGenerationTokenUsage { - TotalTokens = 800, - OutputTokens = 304, - InputTokens = 496, - PromptTokensDetails = new TextGenerationPromptTokenDetails(0) + TotalTokens = 2973, + OutputTokens = 266, + InputTokens = 2707, + PromptTokensDetails = new TextGenerationPromptTokenDetails(0), + Plugins = new TextGenerationPluginUsages(new TextGenerationSearchPluginUsage(1, "standard")) } }); From d9b76275cbe2b20879ffbfa084afeba52a30d0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 19 Oct 2025 21:35:15 +0800 Subject: [PATCH 3/6] test: add prepend search result snapshot --- .../Text/ChatWebSearchSample.cs | 2 +- .../TextGenerationSerializationTests.cs | 5 +- ...ation-message-search-sse.request.body.json | 22 +++ ...tion-message-search-sse.request.header.txt | 9 ++ ...ation-message-search-sse.response.body.txt | 125 ++++++++++++++++++ ...ion-message-search-sse.response.header.txt | 14 ++ .../Utils/Snapshots.TextGeneration.cs | 87 +++++++++++- 7 files changed, 260 insertions(+), 4 deletions(-) create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.body.json create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.header.txt diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs index 1477e2e..d6e6c65 100644 --- a/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs @@ -41,7 +41,7 @@ public async Task RunAsync(IDashScopeClient client) CitationFormat = "[ref_]", EnableSource = true, EnableSearchExtension = true, - ForcedSearch = true + ForcedSearch = true, }, IncrementalOutput = true } diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs index 7eb5642..ddb7eeb 100644 --- a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs +++ b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs @@ -177,12 +177,13 @@ public async Task ConversationCompletion_MessageFormatSse_SuccessAsync( Snapshots.TextGeneration.MessageFormat.SingleMessageJson, Snapshots.TextGeneration.MessageFormat.SingleMessageLogprobs, Snapshots.TextGeneration.MessageFormat.SingleMessageTranslation, - Snapshots.TextGeneration.MessageFormat.SingleMessageWebSearch); + Snapshots.TextGeneration.MessageFormat.SingleMessageWebSearchNoSse); public static readonly TheoryData, ModelResponse>> SingleGenerationMessageSseFormatData = new( Snapshots.TextGeneration.MessageFormat.SingleMessageIncremental, - Snapshots.TextGeneration.MessageFormat.SingleMessageReasoningIncremental); + Snapshots.TextGeneration.MessageFormat.SingleMessageReasoningIncremental, + Snapshots.TextGeneration.MessageFormat.SingleMessageWebSearchIncremental); public static readonly TheoryData, ModelResponse>> ConversationMessageFormatSseData = new( diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.body.json new file mode 100644 index 0000000..6ff4ad1 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.body.json @@ -0,0 +1,22 @@ +{ + "model": "qwen-plus", + "input": { + "messages": [ + { + "role": "user", + "content": "杭州明天的天气" + } + ] + }, + "parameters": { + "result_format": "message", + "enable_search": true, + "search_options": { + "enable_source": true, + "forced_search": true, + "prepend_search_result": true, + "search_strategy": "standard" + }, + "incremental_output": true + } +} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.header.txt new file mode 100644 index 0000000..47080c6 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.request.header.txt @@ -0,0 +1,9 @@ +POST /api/v1/services/aigc/text-generation/generation HTTP/1.1 +Accept: text/event-stream +Content-Type: application/json +User-Agent: PostmanRuntime/7.44.1 +Cache-Control: no-cache +Host: dashscope.aliyuncs.com +Accept-Encoding: gzip, deflate, br +Connection: keep-alive +Content-Length: 493 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.body.txt new file mode 100644 index 0000000..2706276 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.body.txt @@ -0,0 +1,125 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"","role":"assistant"},"finish_reason":"null"}],"search_info":{"search_results":[{"icon":"http://www.ip.cn/favicon.ico","site_name":"厦门时空科技有限公司","index":1,"title":"杭州市15天天气查询","url":"https://www.ip.cn/tianqi/zhejiang/hangzhou/15day.html"},{"icon":"https://img.alicdn.com/imgextra/i3/O1CN01kr9teP1wlRD8OH6TO_!!6000000006348-73-tps-16-16.ico","site_name":"eastday","index":2,"title":"杭州天气预报杭州2025年10月20日天气","url":"https://tianqi.eastday.com/tianqi/hangzhou/20251020.html"},{"icon":"http://www.ip.cn/favicon.ico","site_name":"厦门时空科技有限公司","index":3,"title":"杭州市2025年10月份天气查询","url":"https://www.ip.cn/tianqi/zhejiang/hangzhou/202510.html"},{"icon":"","site_name":"无","index":4,"title":"杭州","url":"http://www.suzhoutianqi114.com/hangzhou/10yuefen.html"},{"icon":"https://img.alicdn.com/imgextra/i3/O1CN01kr9teP1wlRD8OH6TO_!!6000000006348-73-tps-16-16.ico","site_name":"eastday","index":5,"title":">杭州历史天气 ","url":"https://tianqi.eastday.com/lishi/hangzhou.html"}]}},"usage":{},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:2 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"根据","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":710,"output_tokens":1,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:3 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"杭州市","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":711,"output_tokens":2,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:4 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"气象","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":712,"output_tokens":3,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:5 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"台","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":713,"output_tokens":4,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:6 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"2025","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":717,"output_tokens":8,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:7 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"年10月1","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":722,"output_tokens":13,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:8 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"9日发布的天气预报,","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":728,"output_tokens":19,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:9 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"杭州明天(1","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":732,"output_tokens":23,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:10 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"0月20日)","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":738,"output_tokens":29,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:11 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"的天气情况如下:\n\n*","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":744,"output_tokens":35,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:12 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":" **天气**:阴","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":750,"output_tokens":41,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:13 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"转多云\n* ","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":756,"output_tokens":47,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:14 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":" **气温**:最高气温","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":762,"output_tokens":53,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:15 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"20℃,最低","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":767,"output_tokens":58,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:16 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"气温18℃\n*","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":773,"output_tokens":64,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:17 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":" **风力","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":777,"output_tokens":68,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:18 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"**:北风3级","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":783,"output_tokens":74,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:19 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"\n* **空气质量**","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":789,"output_tokens":80,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:20 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":":优\n\n建议","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":793,"output_tokens":84,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:21 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"穿着单层棉麻","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":798,"output_tokens":89,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:22 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"面料的短套装","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":802,"output_tokens":93,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:23 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"、T恤衫等舒适的","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":808,"output_tokens":99,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:24 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"衣物。","role":"assistant"},"finish_reason":"null"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":810,"output_tokens":101,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + +id:25 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"","role":"assistant"},"finish_reason":"stop"}],"search_info":{"extra_tool_info":[],"search_results":[]}},"usage":{"total_tokens":810,"output_tokens":101,"input_tokens":709,"plugins":{"search":{"count":1,"strategy":"standard"}},"prompt_tokens_details":{"cached_tokens":0}},"request_id":"ed54a8da-a598-4a20-a849-3e0bc0ea3d4b"} + diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.header.txt new file mode 100644 index 0000000..f1d1a34 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-search-sse.response.header.txt @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers +x-request-id: ed54a8da-a598-4a20-a849-3e0bc0ea3d4b +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-timeout: 298 +x-dashscope-finished: false +req-cost-time: 826 +req-arrive-time: 1760879893898 +resp-start-time: 1760879894724 +x-envoy-upstream-service-time: 818 +date: Sun, 19 Oct 2025 13:18:14 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs index 799760a..25b50f7 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -346,7 +346,7 @@ public static class MessageFormat public static readonly RequestSnapshot, ModelResponse> - SingleMessageWebSearch = new( + SingleMessageWebSearchNoSse = new( "single-generation-message-search", new ModelRequest { @@ -438,6 +438,91 @@ public static class MessageFormat } }); + public static readonly RequestSnapshot, + ModelResponse> + SingleMessageWebSearchIncremental = new( + "single-generation-message-search", + new ModelRequest() + { + Model = "qwen-plus", + Input = new TextGenerationInput() + { + Messages = new List() { TextChatMessage.User("杭州明天的天气") } + }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableSearch = true, + IncrementalOutput = true, + SearchOptions = new TextGenerationSearchOptions() + { + ForcedSearch = true, + EnableSource = true, + PrependSearchResult = true, + SearchStrategy = "standard" + } + } + }, + new ModelResponse() + { + Output = new TextGenerationOutput() + { + SearchInfo = new TextGenerationWebSearchInfo( + new List() + { + new( + "厦门时空科技有限公司", + "http://www.ip.cn/favicon.ico", + 1, + "杭州市15天天气查询", + "https://www.ip.cn/tianqi/zhejiang/hangzhou/15day.html"), + new( + "eastday", + "https://img.alicdn.com/imgextra/i3/O1CN01kr9teP1wlRD8OH6TO_!!6000000006348-73-tps-16-16.ico", + 2, + "杭州天气预报杭州2025年10月20日天气", + "https://tianqi.eastday.com/tianqi/hangzhou/20251020.html"), + new( + "厦门时空科技有限公司", + "http://www.ip.cn/favicon.ico", + 3, + "杭州市2025年10月份天气查询", + "https://www.ip.cn/tianqi/zhejiang/hangzhou/202510.html"), + new( + "无", + string.Empty, + 4, + "杭州", + "http://www.suzhoutianqi114.com/hangzhou/10yuefen.html"), + new( + "eastday", + "https://img.alicdn.com/imgextra/i3/O1CN01kr9teP1wlRD8OH6TO_!!6000000006348-73-tps-16-16.ico", + 5, + ">杭州历史天气 ", + "https://tianqi.eastday.com/lishi/hangzhou.html"), + }, + null), + Choices = new List() + { + new() + { + FinishReason = "stop", + Message = TextChatMessage.Assistant( + "根据杭州市气象台2025年10月19日发布的天气预报,杭州明天(10月20日)的天气情况如下:\n\n* **天气**:阴转多云\n* **气温**:最高气温20℃,最低气温18℃\n* **风力**:北风3级\n* **空气质量**:优\n\n建议穿着单层棉麻面料的短套装、T恤衫等舒适的衣物。") + } + } + }, + Usage = new TextGenerationTokenUsage() + { + TotalTokens = 810, + InputTokens = 709, + OutputTokens = 101, + Plugins = + new TextGenerationPluginUsages(new TextGenerationSearchPluginUsage(1, "standard")), + PromptTokensDetails = new TextGenerationPromptTokenDetails(0) + } + }); + public static readonly RequestSnapshot, ModelResponse> SingleMessageJson = new( From 7bcb71907efd30f94916504b1b4ddec1cb8c82ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 19 Oct 2025 23:11:18 +0800 Subject: [PATCH 4/6] feat: add function call sample --- README.zh-Hans.md | 328 +++++++++++++++++- .../Text/ChatToolCallingSample.cs | 114 ++++-- src/Cnblogs.DashScope.Core/FunctionCall.cs | 29 +- src/Cnblogs.DashScope.Core/TextChatMessage.cs | 19 +- 4 files changed, 451 insertions(+), 39 deletions(-) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index a4b927e..89e6d25 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -67,10 +67,6 @@ public class YourService(IDashScopeClient client) ## 支持的 API - [文本生成](#文本生成) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景 - - [多轮对话](#多轮对话) - - [深度思考](#深度思考) - - [联网搜索](#联网搜索) - - [工具调用](#工具调用) - [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景 - [语音合成](#语音合成) - CosyVoice,Sambert 等,支持 TTS 等应用场景 - [图像生成](#图像生成) - wanx2.1 等,支持文生图,人像风格重绘等应用场景 @@ -437,7 +433,9 @@ var request = new ModelRequest() }; ``` -通过返回结果里的 `response.Output.SearchInfo` 来获取搜索结果,这个值会在模型搜索后一次性返回,并在之后的每次返回中都附带。因此,开启增量流式输出时,不需要通过 `StringBuilder` 等方式来缓存 `SearchInfo`。 +通过返回结果里的 `response.Output.SearchInfo` 来获取搜索结果,这个值会在第一个包随模型回复完整返回。因此,开启增量流式输出时,不需要通过 `StringBuilder` 等方式来缓存 `SearchInfo`。 + +联网搜索的调用次数可以通过最后一个包的 `response.Usage.Plugins.Search.Count` 获取。 ```csharp var messages = new List(); @@ -650,7 +648,325 @@ Usage: in(2178)/out(1571)/reasoning(952)/plugins:(1)/total(3749) ### 工具调用 -通过 `Parameter` 里的 `Tools` 来向模型提供可用的工具列表,模型会返回 `Tool` 角色的消息来调用工具。 +通过 `Parameter` 里的 `Tools` 来向模型提供可用的工具列表,模型会返回带有 `ToolCall` 属性的消息来调用工具。 + +接收到消息后,服务端需要调用对应工具并将结果作为 `Tool` 角色的消息插入到对话记录中再发起请求,模型会根据工具调用的结果总结答案。 + +默认情况下,模型每次只会调用一次工具。如果输入的问题需要多次调用同一工具,或需要同时调用多个工具,可以在 `Parameter` 里启用 `ParallelToolCalls` 来允许模型同时发起多次工具调用请求。 + +这个示例中,我们先定义一个获取天气的 C# 方法: + +```csharp +public record WeatherReportParameters( + [property: Required] + [property: Description("要获取天气的省市名称,例如浙江省杭州市")] + string Location, + [property: JsonConverter(typeof(EnumStringConverter))] + [property: Description("温度单位")] + TemperatureUnit Unit = TemperatureUnit.Celsius); + +public enum TemperatureUnit +{ + Celsius, + Fahrenheit +} + +private string GetWeather(WeatherReportParameters payload) + => "大部多云,气温 " + + payload.Unit switch + { + TemperatureUnit.Celsius => "18 摄氏度", + TemperatureUnit.Fahrenheit => "64 华氏度", + _ => throw new InvalidOperationException() + }; +``` + +随后构造工具数组向模型提供工具定义,参数列表需要以 JSON Schema 的形式提供。这里我们使用 `JsonSchema.Net.Generation` 库来自动生成 JSON Schema,您也可以使用其他类似功能的库。 + +``` +var tools = new List +{ + new( + ToolTypes.Function, + new FunctionDefinition( + nameof(GetWeather), + "获得当前天气", + new JsonSchemaBuilder().FromType().Build())) +}; +``` + +随后我们将这个工具定义附加到 `Parameters` 里,随消息一同发送(每次请求时都需要附带 tools 信息)。 + +```csharp +var request = new ModelRequest() +{ + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true, + Tools = tools, + ToolChoice = ToolChoice.AutoChoice, // 允许模型自行决定是否需要调用模型 + ParallelToolCalls = true // 允许模型同时发起多次工具调用 + } +} +``` + +模型会返回一个带有 `ToolCalls` 的消息尝试调用工具,我们需要解析并将结果附加到消息数组中去。当开启流式增量输出时,会先输出除 `arguments` 外的所有信息,随后增量输出 `arguments`。 + +模型回复示例,可以看到 `arguments` 是增量流式输出的,每次调用的第一个包都包含了 Index 和 Id 信息。 + +``` +{"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"}]} + +{"choices":[{"message":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"arguments":" \"浙江省杭州市\", \""}}],"role":"assistant"},"index":0,"finish_reason":"null"}]} + +{"choices":[{"message":{"content":"","tool_calls":[{"index":0,"id":"","type":"function","function":{"arguments":"unit\": \"celsius\"}"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]} + +{"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"}]} + +{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"","type":"function","function":{"arguments":"\": \"上海市\", \"unit"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]} + +{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"","type":"function","function":{"arguments":"\": \"celsius\"}"}}],"role":"assistant"},"index":0,"finish_reason":"null"}]} + +{"choices":[{"message":{"content":"","tool_calls":[{"index":1,"id":"","type":"function","function":{}}],"role":"assistant"},"index":0,"finish_reason":"tool_calls"}]} +``` + +我们需要构建一个字典来收集模型输出的 `arguments` 信息并组装成完整的工具调用数组。 + +```csharp +List? pendingToolCalls = null; // 收集到的 toolCalls 信息 +var argumentDictionary = new Dictionary(); // toolcalls 的 index-arguemnt 字典 +await foreach (var chunk in response) +{ + usage = chunk.Usage; + var choice = chunk.Output.Choices![0]; + if (choice.Message.ToolCalls != null && choice.Message.ToolCalls.Count != 0) + { + pendingToolCalls ??= new List(); + foreach (var call in choice.Message.ToolCalls) + { + var hasPartial = argumentDictionary.TryGetValue(call.Index, out var partialArgument); + if (!hasPartial || partialArgument == null) + { + partialArgument = new StringBuilder(); + argumentDictionary[call.Index] = partialArgument; + pendingToolCalls.Add(call); + } + + partialArgument.Append(call.Function.Arguments); + } + + continue; + } + + // ...如果没有工具调用则正常处理模型回复 +} + +// 组装工具调用结果 +if (argumentDictionary.Count != 0) +{ + if (firstReplyChunk) + { + Console.Write("Assistant > "); + } + + pendingToolCalls?.ForEach(p => + { + p.Function.Arguments = argumentDictionary[p.Index].ToString(); + Console.Write($"调用:{p.Function.Name}({p.Function.Arguments}); "); + }); +} + +// 将模型的工具调用信息保存到对话记录 +messages.Add(TextChatMessage.Assistant(reply.ToString(), toolCalls: pendingToolCalls)); +``` + +收集到完整的工具调用信息后,我们需要调用对应的方法并将结果以 `Tool` 消息附加到模型回复之后 + +``` +if (pendingToolCalls?.Count > 0) +{ + // call tools + foreach (var call in pendingToolCalls) + { + // 这里我们已知只有一种工具,生产环境需要根据 call.Function.Name 动态的选择工具进行调用。 + var payload = JsonSerializer.Deserialize(call.Function.Arguments!)!; + var response = GetWeather(payload); + Console.WriteLine("Tool > " + response); + // 附加调用结果 + messages.Add(TextChatMessage.Tool(response, call.Id)); + } + + pendingToolCalls = null; +} +``` + +此时 messages 的角色顺序应该是,最后一个或多个消息是 Tool 角色消息,取决于模型回复的 ToolCalls 数量。 + +``` +User +Assistant(包含 ToolCalls) +Tool +(Tool) +``` + +随后再次发起请求,模型将总结工具调用结果并给出回答。 + +完整代码: + +```csharp +var tools = new List +{ + new( + ToolTypes.Function, + new FunctionDefinition( + nameof(GetWeather), + "获得当前天气", + new JsonSchemaBuilder().FromType().Build())) +}; +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +List? pendingToolCalls = null; +while (true) +{ + if (pendingToolCalls?.Count > 0) + { + // call tools + foreach (var call in pendingToolCalls) + { + var payload = JsonSerializer.Deserialize(call.Function.Arguments!)!; + var response = GetWeather(payload); + Console.WriteLine("Tool > " + response); + messages.Add(TextChatMessage.Tool(response, call.Id)); + } + + pendingToolCalls = null; + } + else + { + // get user input + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + } + + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = false, + IncrementalOutput = true, + Tools = tools, + ToolChoice = ToolChoice.AutoChoice, + ParallelToolCalls = true + } + }); + var reply = new StringBuilder(); + TextGenerationTokenUsage? usage = null; + var argumentDictionary = new Dictionary(); + var firstReplyChunk = true; + await foreach (var chunk in completion) + { + usage = chunk.Usage; + var choice = chunk.Output.Choices![0]; + if (choice.Message.ToolCalls != null && choice.Message.ToolCalls.Count != 0) + { + pendingToolCalls ??= new List(); + foreach (var call in choice.Message.ToolCalls) + { + var hasPartial = argumentDictionary.TryGetValue(call.Index, out var partialArgument); + if (!hasPartial || partialArgument == null) + { + partialArgument = new StringBuilder(); + argumentDictionary[call.Index] = partialArgument; + pendingToolCalls.Add(call); + } + + partialArgument.Append(call.Function.Arguments); + } + + continue; + } + + if (firstReplyChunk) + { + Console.Write("Assistant > "); + firstReplyChunk = false; + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + } + + if (argumentDictionary.Count != 0) + { + if (firstReplyChunk) + { + Console.Write("Assistant > "); + } + + pendingToolCalls?.ForEach(p => + { + p.Function.Arguments = argumentDictionary[p.Index].ToString(); + Console.Write($"调用:{p.Function.Name}({p.Function.Arguments}); "); + }); + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString(), toolCalls: pendingToolCalls)); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } +} + +string GetWeather(WeatherReportParameters payload) + => $"{payload.Location} 大部多云,气温 " + + payload.Unit switch + { + TemperatureUnit.Celsius => "18 摄氏度", + TemperatureUnit.Fahrenheit => "64 华氏度", + _ => throw new InvalidOperationException() + }; + +public record WeatherReportParameters( + [property: Required] + [property: Description("要获取天气的省市名称,例如浙江省杭州市")] + string Location, + [property: JsonConverter(typeof(EnumStringConverter))] + [property: Description("温度单位")] + TemperatureUnit Unit = TemperatureUnit.Celsius); + +public enum TemperatureUnit +{ + Celsius, + Fahrenheit +} + +/* +User > 杭州和上海的天气怎么样? +Assistant > 调用:GetWeather({"Location": "浙江省杭州市", "Unit": "Celsius"}); 调用:GetWeather({"Location": "上海市", "Unit": "Celsius"}); +Usage: in(196)/out(54)/total(250) +Tool > 浙江省杭州市 大部多云,气温 18 摄氏度 +Tool > 上海市 大部多云,气温 18 摄氏度 +Assistant > 浙江省杭州市和上海市的天气大部多云,气温均为18摄氏度。 +Usage: in(302)/out(19)/total(321) + */ +``` diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs index 67498a2..a9382aa 100644 --- a/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatToolCallingSample.cs @@ -1,5 +1,9 @@ using System.Text; +using System.Text.Json; using Cnblogs.DashScope.Core; +using Cnblogs.DashScope.Sdk; +using Json.Schema; +using Json.Schema.Generation; namespace Cnblogs.DashScope.Sample.Text; @@ -11,19 +15,47 @@ public class ChatToolCallingSample : ISample /// public async Task RunAsync(IDashScopeClient client) { + var tools = new List + { + new( + ToolTypes.Function, + new FunctionDefinition( + nameof(GetWeather), + "获得当前天气", + new JsonSchemaBuilder().FromType().Build())) + }; var messages = new List(); messages.Add(TextChatMessage.System("You are a helpful assistant")); + List? pendingToolCalls = null; while (true) { - Console.Write("User > "); - var input = Console.ReadLine(); - if (string.IsNullOrEmpty(input)) + if (pendingToolCalls?.Count > 0) + { + // call tools + foreach (var call in pendingToolCalls) + { + var payload = JsonSerializer.Deserialize(call.Function.Arguments!)!; + var response = GetWeather(payload); + Console.WriteLine("Tool > " + response); + messages.Add(TextChatMessage.Tool(response, call.Id)); + } + + pendingToolCalls = null; + } + else { - Console.WriteLine("Please enter a user input."); - return; + // get user input + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); } - messages.Add(TextChatMessage.User(input)); var completion = client.GetTextCompletionStreamAsync( new ModelRequest() { @@ -32,48 +64,90 @@ public async Task RunAsync(IDashScopeClient client) Parameters = new TextGenerationParameters() { ResultFormat = "message", - EnableThinking = true, + EnableThinking = false, IncrementalOutput = true, + Tools = tools, + ToolChoice = ToolChoice.AutoChoice, + ParallelToolCalls = true } }); var reply = new StringBuilder(); - var reasoning = false; TextGenerationTokenUsage? usage = null; + var argumentDictionary = new Dictionary(); + var firstReplyChunk = true; await foreach (var chunk in completion) { + usage = chunk.Usage; var choice = chunk.Output.Choices![0]; - if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) + if (choice.Message.ToolCalls != null && choice.Message.ToolCalls.Count != 0) { - // reasoning - if (reasoning == false) + pendingToolCalls ??= new List(); + foreach (var call in choice.Message.ToolCalls) { - Console.Write("Reasoning > "); - reasoning = true; + var hasPartial = argumentDictionary.TryGetValue(call.Index, out var partialArgument); + if (!hasPartial || partialArgument == null) + { + partialArgument = new StringBuilder(); + argumentDictionary[call.Index] = partialArgument; + pendingToolCalls.Add(call); + } + + partialArgument.Append(call.Function.Arguments); } - Console.Write(choice.Message.ReasoningContent); continue; } - if (reasoning) + if (firstReplyChunk) { - reasoning = false; - Console.WriteLine(); Console.Write("Assistant > "); + firstReplyChunk = false; } Console.Write(choice.Message.Content); reply.Append(choice.Message.Content); - usage = chunk.Usage; + } + + if (argumentDictionary.Count != 0) + { + if (firstReplyChunk) + { + Console.Write("Assistant > "); + } + + pendingToolCalls?.ForEach(p => + { + p.Function.Arguments = argumentDictionary[p.Index].ToString(); + Console.Write($"调用:{p.Function.Name}({p.Function.Arguments}); "); + }); } Console.WriteLine(); - messages.Add(TextChatMessage.Assistant(reply.ToString())); + messages.Add(TextChatMessage.Assistant(reply.ToString(), toolCalls: pendingToolCalls)); if (usage != null) { Console.WriteLine( - $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); } } } + + private string GetWeather(WeatherReportParameters payload) + => $"{payload.Location} 大部多云,气温 " + + payload.Unit switch + { + TemperatureUnit.Celsius => "18 摄氏度", + TemperatureUnit.Fahrenheit => "64 华氏度", + _ => throw new InvalidOperationException() + }; } + +/* +User > 杭州和上海的天气怎么样? +Assistant > 调用:GetWeather({"Location": "浙江省杭州市", "Unit": "Celsius"}); 调用:GetWeather({"Location": "上海市", "Unit": "Celsius"}); +Usage: in(196)/out(54)/total(250) +Tool > 浙江省杭州市 大部多云,气温 18 摄氏度 +Tool > 上海市 大部多云,气温 18 摄氏度 +Assistant > 浙江省杭州市和上海市的天气大部多云,气温均为18摄氏度。 +Usage: in(302)/out(19)/total(321) + */ diff --git a/src/Cnblogs.DashScope.Core/FunctionCall.cs b/src/Cnblogs.DashScope.Core/FunctionCall.cs index a1a4266..58f49a3 100644 --- a/src/Cnblogs.DashScope.Core/FunctionCall.cs +++ b/src/Cnblogs.DashScope.Core/FunctionCall.cs @@ -3,6 +3,29 @@ /// /// Represents a call to function. /// -/// Name of the function to call. -/// Arguments of this call, usually a json string. -public record FunctionCall(string Name, string? Arguments); +public class FunctionCall +{ + /// + /// Create an empty function call. + /// + public FunctionCall() + { + } + + /// + /// Create a function call. + /// + /// Name of the function to be called. + /// Arguments that passed to the function. + public FunctionCall(string name, string? arguments) + { + Name = name; + Arguments = arguments; + } + + /// Name of the function to call. + public string Name { get; set; } = string.Empty; + + /// Arguments of this call, usually a json string. + public string? Arguments { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/TextChatMessage.cs b/src/Cnblogs.DashScope.Core/TextChatMessage.cs index 1b7f83b..c9301c1 100644 --- a/src/Cnblogs.DashScope.Core/TextChatMessage.cs +++ b/src/Cnblogs.DashScope.Core/TextChatMessage.cs @@ -31,7 +31,7 @@ public TextChatMessage(IEnumerable fileIds) /// /// The role of this message. /// The content of this message. - /// Used when role is tool, represents the function name of this message generated by. + /// Used when role is tool, represents the function name of this message generated by. /// Notify model that next message should use this message as prefix. /// Reasoning content for reasoning model. /// Calls to the function. @@ -39,14 +39,14 @@ public TextChatMessage(IEnumerable fileIds) public TextChatMessage( string role, string content, - string? name = null, + string? toolCallId = null, bool? partial = null, string? reasoningContent = null, List? toolCalls = null) { Role = role; Content = content; - Name = name; + ToolCallId = toolCallId; Partial = partial; ReasoningContent = reasoningContent; ToolCalls = toolCalls; @@ -59,7 +59,7 @@ public TextChatMessage( public string Content { get; init; } /// Used when role is tool, represents the function name of this message generated by. - public string? Name { get; init; } + public string? ToolCallId { get; init; } /// Notify model that next message should use this message as prefix. public bool? Partial { get; init; } @@ -109,11 +109,10 @@ public static TextChatMessage File(IEnumerable fileIds) /// Create a user message. /// /// Content of the message. - /// Author name. /// - public static TextChatMessage User(string content, string? name = null) + public static TextChatMessage User(string content) { - return new TextChatMessage(DashScopeRoleNames.User, content, name); + return new TextChatMessage(DashScopeRoleNames.User, content); } /// @@ -149,10 +148,10 @@ public static TextChatMessage Assistant( /// Create a tool message. /// /// The output from tool. - /// The name of the tool. + /// The id of the tool call. /// - public static TextChatMessage Tool(string content, string? name = null) + public static TextChatMessage Tool(string content, string? toolCallId = null) { - return new TextChatMessage(DashScopeRoleNames.Tool, content, name); + return new TextChatMessage(DashScopeRoleNames.Tool, content, toolCallId); } } From 3d7ce604fb8fd6bdf5e236fe18d2521089e5af91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Mon, 20 Oct 2025 22:09:07 +0800 Subject: [PATCH 5/6] test: add toolcall snapshot --- sample/Cnblogs.DashScope.Sample/Program.cs | 1 - .../TextGenerationSerializationTests.cs | 3 +- ...n-message-with-tools-sse.request.body.json | 73 ++++++++++++++ ...-message-with-tools-sse.request.header.txt | 8 ++ ...n-message-with-tools-sse.response.body.txt | 56 +++++++++++ ...message-with-tools-sse.response.header.txt | 14 +++ .../Utils/Snapshots.TextGeneration.cs | 94 ++++++++++--------- 7 files changed, 204 insertions(+), 45 deletions(-) create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.header.txt diff --git a/sample/Cnblogs.DashScope.Sample/Program.cs b/sample/Cnblogs.DashScope.Sample/Program.cs index 5c8f5bf..95fe7e8 100644 --- a/sample/Cnblogs.DashScope.Sample/Program.cs +++ b/sample/Cnblogs.DashScope.Sample/Program.cs @@ -6,7 +6,6 @@ using Cnblogs.DashScope.Sample.Text; using Cnblogs.DashScope.Sdk; using Cnblogs.DashScope.Sdk.QWen; -using Cnblogs.DashScope.Sdk.TextEmbedding; using Cnblogs.DashScope.Sdk.Wanx; using Json.Schema; using Json.Schema.Generation; diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs index ddb7eeb..07cdf20 100644 --- a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs +++ b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs @@ -183,7 +183,8 @@ public async Task ConversationCompletion_MessageFormatSse_SuccessAsync( ModelResponse>> SingleGenerationMessageSseFormatData = new( Snapshots.TextGeneration.MessageFormat.SingleMessageIncremental, Snapshots.TextGeneration.MessageFormat.SingleMessageReasoningIncremental, - Snapshots.TextGeneration.MessageFormat.SingleMessageWebSearchIncremental); + Snapshots.TextGeneration.MessageFormat.SingleMessageWebSearchIncremental, + Snapshots.TextGeneration.MessageFormat.SingleMessageWithToolsIncremental); public static readonly TheoryData, ModelResponse>> ConversationMessageFormatSseData = new( diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json new file mode 100644 index 0000000..ac6f824 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.body.json @@ -0,0 +1,73 @@ +{ + "model": "qwen-plus", + "input": { + "messages": [ + { + "role": "user", + "content": "杭州上海现在的天气如何?" + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + { + "function": { + "name": "get_current_weather", + "arguments": "{\"location\": \"浙江省杭州市\"}" + }, + "index": 0, + "id": "call_cec4c19d27624537b583af", + "type": "function" + }, + { + "function": { + "name": "get_current_weather", + "arguments": "{\"location\": \"上海市\"}" + }, + "index": 1, + "id": "call_dxjdop3d27624537b583af", + "type": "function" + } + ] + }, + { + "role": "tool", + "content": "浙江省杭州市 大部多云,摄氏 18 度", + "tool_call_id": "call_cec4c19d27624537b583af" + }, + { + "role": "tool", + "content": "上海市 多云转小雨,摄氏 19 度", + "tool_call_id": "call_dxjdop3d27624537b583af" + } + ] + }, + "parameters": { + "result_format": "message", + "seed": 6999, + "max_tokens": 1500, + "incremental_output": true, + "tools": [ + { + "type": "function", + "function": { + "name": "get_current_weather", + "description": "获取现在的天气", + "parameters": { + "type": "object", + "properties": { + "location": { + "type": "string", + "description": "要获取天气的省市名称,例如浙江省杭州市" + } + }, + "required": [ + "location" + ] + } + } + } + ], + "parallel_tool_calls": true + } +} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.header.txt new file mode 100644 index 0000000..14f76d4 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.request.header.txt @@ -0,0 +1,8 @@ +POST /api/v1/services/aigc/text-generation/generation HTTP/1.1 +Accept: text/event-stream +Content-Type: application/json +Cache-Control: no-cache +Host: dashscope.aliyuncs.com +Accept-Encoding: gzip, deflate, br +Connection: keep-alive +Content-Length: 2894 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.body.txt new file mode 100644 index 0000000..d34e51f --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.body.txt @@ -0,0 +1,56 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"目前","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":284,"output_tokens":1,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:2 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"杭州和","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":286,"output_tokens":3,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:3 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"上海的","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":288,"output_tokens":5,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:4 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"天气情况如下","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":291,"output_tokens":8,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:5 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":":\n\n- **杭州**:","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":297,"output_tokens":14,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:6 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"大部多云","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":301,"output_tokens":18,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:7 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":",气温为18℃","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":307,"output_tokens":24,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:8 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"。\n- **上海**:","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":313,"output_tokens":30,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:9 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"多云转小雨,","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":319,"output_tokens":36,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:10 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"气温为19℃。\n\n","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":325,"output_tokens":42,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:11 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"请注意天气变化,出门","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":330,"output_tokens":47,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:12 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"携带雨具以防","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":334,"output_tokens":51,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:13 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"下雨。","role":"assistant"},"index":0,"finish_reason":"null"}]},"usage":{"total_tokens":336,"output_tokens":53,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} +id:14 +event:result +:HTTP_STATUS/200 +data:{"output":{"choices":[{"message":{"content":"","role":"assistant"},"index":0,"finish_reason":"stop"}]},"usage":{"total_tokens":336,"output_tokens":53,"input_tokens":283,"prompt_tokens_details":{"cached_tokens":0}},"request_id":"dd51401b-146e-42a0-96d9-4067a5fac75a"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.header.txt new file mode 100644 index 0000000..bf91772 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-with-tools-sse.response.header.txt @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers +x-request-id: dd51401b-146e-42a0-96d9-4067a5fac75a +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-timeout: 298 +x-dashscope-finished: false +req-cost-time: 166 +req-arrive-time: 1760967863780 +resp-start-time: 1760967863946 +x-envoy-upstream-service-time: 159 +date: Mon, 20 Oct 2025 13:44:23 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs index 25b50f7..cafeeb4 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -807,44 +807,63 @@ public static readonly public static readonly RequestSnapshot, - ModelResponse> SingleMessageChatClientWithTools = + ModelResponse> SingleMessageWithToolsIncremental = new( "single-generation-message-with-tools", new ModelRequest { - Model = "qwen-max", + Model = "qwen-plus", Input = new TextGenerationInput { Messages = - new List { TextChatMessage.User("杭州现在的天气如何?") } + new List + { + TextChatMessage.User("杭州上海现在的天气如何?"), + TextChatMessage.Assistant( + string.Empty, + toolCalls: new List() + { + new( + "call_cec4c19d27624537b583af", + "function", + 0, + new FunctionCall( + "get_current_weather", + "{\"location\": \"浙江省杭州市\"}")), + new( + "call_dxjdop3d27624537b583af", + "function", + 1, + new FunctionCall( + "get_current_weather", + "{\"location\": \"上海市\"}")), + }), + TextChatMessage.Tool("浙江省杭州市 大部多云,摄氏 18 度", "call_cec4c19d27624537b583af"), + TextChatMessage.Tool("上海市 多云转小雨,摄氏 19 度", "call_dxjdop3d27624537b583af") + } }, Parameters = new TextGenerationParameters { ResultFormat = "message", - Seed = 1234, + Seed = 6999, MaxTokens = 1500, - TopP = 0.8f, - TopK = 100, - RepetitionPenalty = 1.1f, - PresencePenalty = 1.2f, - Temperature = 0.85f, - Tools = - new List - { - new( - "function", - new FunctionDefinition( - "get_current_weather", - "获取现在的天气", - new JsonSchemaBuilder().FromType( - new SchemaGeneratorConfiguration - { - PropertyNameResolver = - PropertyNameResolvers.LowerSnakeCase - }) - .Build())) - }, - ToolChoice = ToolChoice.FunctionChoice("get_current_weather") + IncrementalOutput = true, + Tools = new List + { + new( + "function", + new FunctionDefinition( + "get_current_weather", + "获取现在的天气", + new JsonSchemaBuilder().FromType( + new SchemaGeneratorConfiguration + { + PropertyNameResolver = + PropertyNameResolvers.LowerSnakeCase + }) + .Build())) + }, + ParallelToolCalls = true } }, new ModelResponse @@ -857,28 +876,17 @@ public static readonly new() { FinishReason = "stop", - Message = TextChatMessage.Assistant( - string.Empty, - toolCalls: - new List - { - new( - "call_cec4c19d27624537b583af", - ToolTypes.Function, - 0, - new FunctionCall( - "get_current_weather", - "{\"location\": \"浙江省杭州市\"}")) - }) + Message = TextChatMessage.Assistant("目前杭州和上海的天气情况如下:\n\n- **杭州**:大部多云,气温为18℃。\n- **上海**:多云转小雨,气温为19℃。\n\n请注意天气变化,出门携带雨具以防下雨。") } } }, - RequestId = "67300049-c108-9987-b1c1-8e0ee2de6b5d", + RequestId = "dd51401b-146e-42a0-96d9-4067a5fac75a", Usage = new TextGenerationTokenUsage { - InputTokens = 211, - OutputTokens = 8, - TotalTokens = 219 + InputTokens = 283, + OutputTokens = 53, + TotalTokens = 336, + PromptTokensDetails = new TextGenerationPromptTokenDetails(0) } }); From b11d4aca4dfe16360ca0cc79d8a32d784f3f588c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Mon, 20 Oct 2025 22:23:55 +0800 Subject: [PATCH 6/6] docs: add json output sample --- README.zh-Hans.md | 81 +++++++++++++++++++ .../Text/JsonOutputSample.cs | 71 ++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 sample/Cnblogs.DashScope.Sample/Text/JsonOutputSample.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 89e6d25..94c3a23 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -968,6 +968,87 @@ Usage: in(302)/out(19)/total(321) */ ``` +### 结构化输出(JSON 输出) + +设置 `Parameter` 里的 `ResponseFormat` (注意不是 `ResultFormat` )为 JSON 即可强制大模型以 JSON 格式输出。 + +示例请求: + +```csharp +var request = new ModelRequest() +{ + Model = "qwen-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + ResponseFormat = DashScopeResponseFormat.Json, + IncrementalOutput = true + } +} +``` + +示例代码,大模型会以 JSON 输出用户输入的字数信息: + +```csharp +var messages = new List(); +messages.Add(TextChatMessage.System("使用 JSON 输出用户输入的字数信息")); +while (true) +{ + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + ResponseFormat = DashScopeResponseFormat.Json, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var firstChunk = true; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (firstChunk) + { + firstChunk = false; + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } +} + +/* +User > 你好 +Assistant > {"word_count": 2} +Usage: in(25)/out(7)/total(32) + */ +``` + ### 多模态 diff --git a/sample/Cnblogs.DashScope.Sample/Text/JsonOutputSample.cs b/sample/Cnblogs.DashScope.Sample/Text/JsonOutputSample.cs new file mode 100644 index 0000000..c40174d --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/JsonOutputSample.cs @@ -0,0 +1,71 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class JsonOutputSample : ISample +{ + /// + public string Description => "JSON output text sample"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + messages.Add(TextChatMessage.System("使用 JSON 输出用户输入的字数信息")); + while (true) + { + Console.Write("User > "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + Console.WriteLine("Please enter a user input."); + return; + } + + messages.Add(TextChatMessage.User(input)); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + ResponseFormat = DashScopeResponseFormat.Json, + IncrementalOutput = true + } + }); + var reply = new StringBuilder(); + var firstChunk = true; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (firstChunk) + { + firstChunk = false; + Console.Write("Assistant > "); + } + + Console.Write(choice.Message.Content); + reply.Append(choice.Message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(reply.ToString())); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + } + } +} + +/* +User > 你好 +Assistant > {"word_count": 2} +Usage: in(25)/out(7)/total(32) + */