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 01/15] 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 02/15] 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 03/15] 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 04/15] 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 05/15] 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 06/15] 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) + */ From 5527132f80ea55710c911f37aad21aa0861a8e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 26 Oct 2025 19:35:14 +0800 Subject: [PATCH 07/15] feat: add partial completion sample and docs --- README.zh-Hans.md | 80 +++++++++++++++++++ .../Text/PrefixCompletionSample.cs | 58 ++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 sample/Cnblogs.DashScope.Sample/Text/PrefixCompletionSample.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 94c3a23..605d5ec 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -1049,6 +1049,86 @@ Usage: in(25)/out(7)/total(32) */ ``` +### 前缀续写 + +将需要续写的前缀作为 `assistant` 消息放到 `messages` 数组末尾,并将 `Partial` 设置为 `true`,大模型将根据这个前缀补全剩下的内容。 + +这个模式下无法开启深度思考。 + +示例请求 + +```csharp +var messages = new List +{ + TextChatMessage.User("请补全这个 C# 函数,不要添加其他内容"), + TextChatMessage.Assistant("public int Fibonacci(int n)", partial: true) +}; + +var completion = client.GetTextCompletionStreamAsync( +new ModelRequest() +{ + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true + } +}); +``` + +完整代码: + +```csharp +var messages = new List +{ + TextChatMessage.User("请补全这个 C# 函数,不要添加其他内容"), + TextChatMessage.Assistant("public int Fibonacci(int n)", partial: true) +}; +Console.WriteLine($"User > {messages[0].Content}"); +Console.Write($"Assistant > {messages[1].Content}"); +var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true + } + }); +var reply = new StringBuilder(); +TextGenerationTokenUsage? usage = null; +await foreach (var chunk in completion) +{ + var choice = chunk.Output.Choices![0]; + 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 > 请补全这个 C# 函数,不要添加其他内容 +Assistant > public int Fibonacci(int n) +{ + if (n <= 1) + return n; + + return Fibonacci(n - 1) + Fibonacci(n - 2); +} +Usage: in(31)/out(34)/reasoning()/total(65) + */ +``` + ### 多模态 diff --git a/sample/Cnblogs.DashScope.Sample/Text/PrefixCompletionSample.cs b/sample/Cnblogs.DashScope.Sample/Text/PrefixCompletionSample.cs new file mode 100644 index 0000000..8dce4ae --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/PrefixCompletionSample.cs @@ -0,0 +1,58 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class PrefixCompletionSample : ISample +{ + /// + public string Description => "Prefix completion sample"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List + { + TextChatMessage.User("请补全这个 C# 函数,不要添加其他内容"), + TextChatMessage.Assistant("public int Fibonacci(int n)", partial: true) + }; + Console.WriteLine($"User > {messages[0].Content}"); + Console.Write($"Assistant > {messages[1].Content}"); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + var reply = new StringBuilder(); + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + 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 > 请补全这个 C# 函数,不要添加其他内容 +Assistant > public int Fibonacci(int n) +{ + if (n <= 1) + return n; + + return Fibonacci(n - 1) + Fibonacci(n - 2); +} +Usage: in(31)/out(34)/reasoning()/total(65) + */ From 866ba20d0e6a310332eee4494ece364239f3432b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 26 Oct 2025 20:16:53 +0800 Subject: [PATCH 08/15] feat: add qwen-long sample --- README.zh-Hans.md | 176 +++++++++++++++++- sample/Cnblogs.DashScope.Sample/1024-1.txt | 22 +++ sample/Cnblogs.DashScope.Sample/1024-2.txt | 4 + .../Cnblogs.DashScope.Sample.csproj | 6 + .../Text/LongContextSample.cs | 112 +++++++++++ .../IDashScopeClient.cs | 4 +- 6 files changed, 315 insertions(+), 9 deletions(-) create mode 100644 sample/Cnblogs.DashScope.Sample/1024-1.txt create mode 100644 sample/Cnblogs.DashScope.Sample/1024-2.txt create mode 100644 sample/Cnblogs.DashScope.Sample/Text/LongContextSample.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 605d5ec..9bf7f85 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -67,6 +67,13 @@ public class YourService(IDashScopeClient client) ## 支持的 API - [文本生成](#文本生成) - QWen3, DeepSeek 等,支持推理/工具调用/网络搜索/翻译等场景 + - [多轮对话](#多轮对话) + - [深度思考](#深度思考) + - [联网搜索](#联网搜索) + - [工具调用](#工具调用) + - [前缀续写](#前缀续写) + - [长上下文(Qwen-Long)](#长上下文(Qwen-Long)) + - [多模态](#多模态) - QWen-VL,QVQ 等,支持推理/视觉理解/OCR/音频理解等场景 - [语音合成](#语音合成) - CosyVoice,Sambert 等,支持 TTS 等应用场景 - [图像生成](#图像生成) - wanx2.1 等,支持文生图,人像风格重绘等应用场景 @@ -1129,15 +1136,170 @@ Usage: in(31)/out(34)/reasoning()/total(65) */ ``` +### 长上下文(Qwen-Long) + +尽管 QWen-Long 支持直接传入字符串,但还是推荐先将文件上传后再通过 FileId 的形式传入 `message` 数组中。 + +上传文件,使用 `UploadFileAsync()` 方法传入文件(注意不是 `UploadTemporaryFileAsync`, 后者是用于上传媒体文件的): + +```csharp +var file1 = await client.UploadFileAsync(File.OpenRead("1024-1.txt"), "file1.txt"); +``` + +然后将文件作为 `system` 消息传入消息数组中,注意第一条 `system` 消息不能省略,否则模型可能会将文件里的内容当作 System prompt 。 + +```csharp +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +messages.Add(TextChatMessage.File(file1.Id)); +messages.Add(TextChatMessage.File(file2.Id)); +// 也可以传入文件数组 messages.Add(TextChatMessage.File([file1.Id, file2.Id])); +``` + +再以 `user` 消息添加与文件内容相关的问题。 + +```csharp +messages.Add(TextChatMessage.User("这两篇文章分别讲了什么?")); +``` + +最后向模型发送请求,注意这个接口获得的文件 ID 只有 `qwen-long` 模型可以访问,其他模型是访问不到的。 + +```csharp +var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-long", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true + } + }); +``` + +最后可以通过 `DeleteFileAsync()` 方法删除上传的文件 + +```csharp +var result = await client.DeleteFileAsync(file1.Id); +Console.WriteLine(result.Deleted ? "Success" : "Failed"); +``` + +完整示例 + +```csharp +Console.WriteLine("Uploading file1..."); +var file1 = await client.UploadFileAsync(File.OpenRead("1024-1.txt"), "file1.txt"); +Console.WriteLine("Uploading file2..."); +var file2 = await client.UploadFileAsync(File.OpenRead("1024-2.txt"), "file2.txt"); +Console.WriteLine($"Uploaded, file1 id: {file1.Id.ToUrl()}, file2 id: {file2.Id.ToUrl()}"); + +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +messages.Add(TextChatMessage.File(file1.Id)); +messages.Add(TextChatMessage.File(file2.Id)); +messages.Add(TextChatMessage.User("这两篇文章分别讲了什么?")); + +messages.ForEach(m => Console.WriteLine($"{m.Role} > {m.Content}")); +var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-long", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + 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})"); +} + +// Deleting files +Console.Write("Deleting file1..."); +var result = await client.DeleteFileAsync(file1.Id); +Console.WriteLine(result.Deleted ? "Success" : "Failed"); +Console.Write("Deleting file2..."); +result = await client.DeleteFileAsync(file2.Id); +Console.WriteLine(result.Deleted ? "Success" : "Failed"); + +/* +Uploading file1... +Uploading file2... +Uploaded, file1 id: fileid://file-fe-b87a5c12cc354533bd882f04, file2 id: fileid://file-fe-f5269f9996d544c4aecc5f80 +system > You are a helpful assistant +system > fileid://file-fe-b87a5c12cc354533bd882f04 +system > fileid://file-fe-f5269f9996d544c4aecc5f80 +user > 这两篇文章分别讲了什么? +这两篇文章都围绕“中国程序员节”的设立展开,但内容侧重点不同: + +**第一篇文章《file1.txt》:** +这篇文章是一篇征求意见稿,标题为《中国程序员节,10月24日,你同意吗?》。文章回顾了此前关于设立中国程序员节的讨论背景——受俄罗斯程序员节(每年第256天)启发,有网友提议设立中国的程序员 节。文中提到曾有人建议定在10月10日(因为“1010”类似二进制),但作者认为10月24日更具意义: +- 因为1024 = 2^10,是计算机中“1K”的近似值; +- 1024在二进制、八进制和十六进制中都有特殊表示; +- 节日时间上避开国庆后的调整期。 +因此,文章向读者征求是否同意将**10月24日**作为中国程序员节,并邀请大家参与投票和提出庆祝活动建议。 + +**第二篇文章《file2.txt》:** +这篇文章是第一篇的后续,标题为《程序员节,10月24日!》,属于正式 announcement(公告)。它宣布: +- 根据前一次讨论的反馈结果,正式确定将**每年的10月24日**定为“中国程序员节”; +- 博客园将在该日组织线上庆祝活动; +- 文章进一步升华主题,强调程序员的社会价值和责任感,呼吁尊重程序员群体,肯定他们是“用代码改变世界的人”,并表达了对技术创造力的敬意。 + +**总结:** +- 第一篇是**征求意见**,探讨是否将10月24日设为中国程序员节; +- 第二篇是**正式确认**节日日期,并倡导庆祝与认同程序员的价值。 +Usage: in(513)/out(396)/reasoning()/total(909) +Deleting file1...Success +Deleting file2...Success +*/ +``` + -### 多模态 +## 多模态 使用 `dashScopeClient.GetMultimodalGenerationAsync` 和 `dashScopeClient.GetMultimodalGenerationStreamAsync` 来访问多模态文本生成接口。 相关文档:[多模态_大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/multimodal) -#### 视觉理解/推理 +### 视觉理解/推理 使用 `MultimodalMessage.User()` 可以快速创建对应角色的消息。 @@ -1198,7 +1360,7 @@ await foreach (var modelResponse in response) } ``` -### 语音合成 +## 语音合成 通过 `dashScopeClient.CreateSpeechSynthesizerSocketSessionAsync()` 来创建一个语音合成会话。 @@ -1235,9 +1397,9 @@ Console.WriteLine($"audio saved to {file.FullName}, token usage: {tokenUsage}"); break; ``` -### 图像生成 +## 图像生成 -#### 文生图 +### 文生图 我们针对通义万相提供了快捷 API `dashScopeClient.CreateWanxImageSynthesisTaskAsync()` 和 `GetWanxImageSynthesisTaskAsync()`。 @@ -1286,7 +1448,7 @@ Console.WriteLine($"Task timout, taskId: {task.TaskId}"); 图像背景生成 - `CreateWanxBackgroundGenerationTaskAsync` 和 `GetWanxBackgroundGenerationTaskAsync` -### 应用调用 +## 应用调用 `GetApplicationResponseAsync` 用于进行应用调用。 @@ -1354,7 +1516,7 @@ var response = await client.GetApplicationResponseAsync("your-application-id", r Console.WriteLine(response.Output.Text); ``` -### 文本向量 +## 文本向量 使用 `GetTextEmbeddingsAsync` 来调用文本向量接口。 diff --git a/sample/Cnblogs.DashScope.Sample/1024-1.txt b/sample/Cnblogs.DashScope.Sample/1024-1.txt new file mode 100644 index 0000000..0f78a42 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/1024-1.txt @@ -0,0 +1,22 @@ +中国程序员节,10月24日,你同意吗? +大家好! + +国庆长假之后,我们来确定一下中国程序员节的日子吧。 + +9月份的时候,我们针对中国程序员节进行了讨论与投票。 + +起因是一条新闻“今天是程序员节”,俄罗斯把每年的第256(0x100th)天作为程序员节,通常是9月12日,也有可能是9月13日。 + +园友贤达在评论中说: + +想想这么多年中国程序员这么辛苦,怎么就没有个法定的程序员节日呢? +可执行文件的代码只有0和1,希望有一个10.10为中国程序员节。 +于是,很多朋友支持把10月10日作为中国程序员节。 + +但10月10日紧接着中秋节与国庆节之后,大家正在恢复调整进入工作状态之中。 + +所以我们觉得10月24日可能更合适(今年的10月24日是星期天),1024是很有意义的一个数字,1K=1024,1024=2的10次方,二进制10000000000,八进制2000,十六进制0x400。 + +所以在这里征询一下大家意见,如果你同意10月24日作为中国程序员节,请点击随笔下面的推荐按钮,如果不同意,请点击反对按钮。 + +如果确定了中国程序员节,我们会在那天组织网上的庆贺活动。如果大家对活动形式有好的建议,也欢迎在这里提出。 diff --git a/sample/Cnblogs.DashScope.Sample/1024-2.txt b/sample/Cnblogs.DashScope.Sample/1024-2.txt new file mode 100644 index 0000000..5e471c7 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/1024-2.txt @@ -0,0 +1,4 @@ +程序员节,10月24日! +根据大家在“中国程序员节,10月24日,你同意吗”中的反馈,现在确定中国程序员节放在每年的10月24日。博客园将在10月24日那天组织网上庆祝活动。 + +希望通过程序员节,代表着我们的一种努力,努力将程序员们凝聚在一起,为社会创造更多价值,得到更多的认可。我们是程序员,不是代码工人,不是IT民工,是一群用代码改变世界的人。我们的代码可以给社会带来进步,也可能给社会带来灾难,我们的责任重于泰山;我们生活于现实世界,却在创造虚拟世界,我们的创造力无限...如果阿基米德是程序员,他会说“给我一台电脑,我就能改变世界”。 diff --git a/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj b/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj index ad7accd..419c0dd 100644 --- a/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj +++ b/sample/Cnblogs.DashScope.Sample/Cnblogs.DashScope.Sample.csproj @@ -20,6 +20,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + diff --git a/sample/Cnblogs.DashScope.Sample/Text/LongContextSample.cs b/sample/Cnblogs.DashScope.Sample/Text/LongContextSample.cs new file mode 100644 index 0000000..2701673 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/LongContextSample.cs @@ -0,0 +1,112 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class LongContextSample : ISample +{ + /// + public string Description => "File upload and long context model sample"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + Console.WriteLine("Uploading file1..."); + var file1 = await client.UploadFileAsync(File.OpenRead("1024-1.txt"), "file1.txt"); + Console.WriteLine("Uploading file2..."); + var file2 = await client.UploadFileAsync(File.OpenRead("1024-2.txt"), "file2.txt"); + Console.WriteLine($"Uploaded, file1 id: {file1.Id.ToUrl()}, file2 id: {file2.Id.ToUrl()}"); + + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + messages.Add(TextChatMessage.File(file1.Id)); + messages.Add(TextChatMessage.File(file2.Id)); + messages.Add(TextChatMessage.User("这两篇文章分别讲了什么?")); + + messages.ForEach(m => Console.WriteLine($"{m.Role} > {m.Content}")); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-long", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", 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})"); + } + + // Deleting files + Console.Write("Deleting file1..."); + var result = await client.DeleteFileAsync(file1.Id); + Console.WriteLine(result.Deleted ? "Success" : "Failed"); + Console.Write("Deleting file2..."); + result = await client.DeleteFileAsync(file2.Id); + Console.WriteLine(result.Deleted ? "Success" : "Failed"); + } +} + +/* +Uploading file1... +Uploading file2... +Uploaded, file1 id: fileid://file-fe-b87a5c12cc354533bd882f04, file2 id: fileid://file-fe-f5269f9996d544c4aecc5f80 +system > You are a helpful assistant +system > fileid://file-fe-b87a5c12cc354533bd882f04 +system > fileid://file-fe-f5269f9996d544c4aecc5f80 +user > 这两篇文章分别讲了什么? +这两篇文章都围绕“中国程序员节”的设立展开,但内容侧重点不同: + +**第一篇文章《file1.txt》:** +这篇文章是一篇征求意见稿,标题为《中国程序员节,10月24日,你同意吗?》。文章回顾了此前关于设立中国程序员节的讨论背景——受俄罗斯程序员节(每年第256天)启发,有网友提议设立中国的程序员 节。文中提到曾有人建议定在10月10日(因为“1010”类似二进制),但作者认为10月24日更具意义: +- 因为1024 = 2^10,是计算机中“1K”的近似值; +- 1024在二进制、八进制和十六进制中都有特殊表示; +- 节日时间上避开国庆后的调整期。 +因此,文章向读者征求是否同意将**10月24日**作为中国程序员节,并邀请大家参与投票和提出庆祝活动建议。 + +**第二篇文章《file2.txt》:** +这篇文章是第一篇的后续,标题为《程序员节,10月24日!》,属于正式 announcement(公告)。它宣布: +- 根据前一次讨论的反馈结果,正式确定将**每年的10月24日**定为“中国程序员节”; +- 博客园将在该日组织线上庆祝活动; +- 文章进一步升华主题,强调程序员的社会价值和责任感,呼吁尊重程序员群体,肯定他们是“用代码改变世界的人”,并表达了对技术创造力的敬意。 + +**总结:** +- 第一篇是**征求意见**,探讨是否将10月24日设为中国程序员节; +- 第二篇是**正式确认**节日日期,并倡导庆祝与认同程序员的价值。 +Usage: in(513)/out(396)/reasoning()/total(909) +Deleting file1...Success +Deleting file2...Success + */ diff --git a/src/Cnblogs.DashScope.Core/IDashScopeClient.cs b/src/Cnblogs.DashScope.Core/IDashScopeClient.cs index 540b97f..679477d 100644 --- a/src/Cnblogs.DashScope.Core/IDashScopeClient.cs +++ b/src/Cnblogs.DashScope.Core/IDashScopeClient.cs @@ -208,11 +208,11 @@ public Task CancellationToken cancellationToken = default); /// - /// Upload file for model to reference. + /// OpenAI compatible upload api, for model to reference. /// /// File data. /// Name of the file. - /// Purpose of the file, use "file-extract" to allow model access the file. + /// Purpose of the file, use "file-extract" to allow model access the file. Use "batch" for uploading batch operations .jsonl file. /// The cancellation token to use. /// public Task UploadFileAsync( From 4ac761206bc4c0600aefedd846e5aba0dec0e992 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 26 Oct 2025 20:43:47 +0800 Subject: [PATCH 09/15] feat: add translation examples --- README.zh-Hans.md | 85 +++++++++++++++++++ .../Text/TranslationSample.cs | 53 ++++++++++++ .../Internals/DashScopeDefaults.cs | 4 +- 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 sample/Cnblogs.DashScope.Sample/Text/TranslationSample.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 9bf7f85..72cc7f2 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -1291,6 +1291,91 @@ Deleting file2...Success */ ``` +### 翻译能力(Qwen-MT) + +翻译能力主要通过 `Parameters` 里的 `TranslationOptions` 进行配置。 + +有关支持的语言列表,请参考官方文档:[通义千问翻译模型-大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/machine-translation) + +示例输入: + +```csharp +var messages = new List +{ + // 只能包含一条消息,即翻译的文本 + TextChatMessage.User( + "博客园创立于2004年1月,是一个面向开发者群体的技术社区。博客园专注于为开发者服务,致力于为开发者打造一个纯净的技术学习与交流社区,帮助开发者持续学习专业知识,不断提升专业技能。博客园的使命是帮助开发者用代码改变世界。") +}; +var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-mt-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + TranslationOptions = new TextGenerationTranslationOptions() + { + // 领域提示,有关源文本的背景信息 + Domains = + "This is a summary of a website for programmers, use formal and professional tones", + // 源语言。源文本包含多种语言或不确定具体语言时,可填入 "auto" + SourceLang = "zh", + // 目标语言 + TargetLang = "en", + // 术语表 + Terms = [new TranslationReference("博客园", "Cnblogs.com")], + // 翻译示例,翻译风格会向示例靠拢 + TmList = [new TranslationReference("代码改变世界", "Coding Changes the World")] + } + } + }); +``` + +完整代码 + +```csharp +var messages = new List +{ + TextChatMessage.User( + "博客园创立于2004年1月,是一个面向开发者群体的技术社区。博客园专注于为开发者服务,致力于为开发者打造一个纯净的技术学习与交流社区,帮助开发者持续学习专业知识,不断提升专业技能。博客园的使命是帮助开发者用代码改变世界。") +}; +Console.WriteLine("User > " + messages[0].Content); +var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-mt-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + TranslationOptions = new TextGenerationTranslationOptions() + { + Domains = + "This is a summary of a website for programmers, use formal and professional tones", + SourceLang = "zh", + TargetLang = "en", + Terms = [new TranslationReference("博客园", "Cnblogs.com")], + TmList = [new TranslationReference("代码改变世界", "Coding Changes the World")] + } + } + }); +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 > 博客园创立于2004年1月,是一个面向开发者群体的技术社区。博客园专注于为开发者服务,致力于为开发者打造一个纯净的技术学习与交流社区,帮助开发者持续学习专业知识,不断提升专业技能。博客园的使命是帮助开发者用代码改变世界。 +Assistant > Cnblogs.com was founded in January 2004 and is a technology community aimed at developers. Cnblogs.com focuses on serving developers, committed to creating a pure technology learning and communication community for them, helping developers continuously learn professional knowledge and improve their professional skills. Cnblogs.com's mission is to help developers change the world through coding. +Usage: in(207)/out(72)/total(279) + */ +``` + ## 多模态 diff --git a/sample/Cnblogs.DashScope.Sample/Text/TranslationSample.cs b/sample/Cnblogs.DashScope.Sample/Text/TranslationSample.cs new file mode 100644 index 0000000..a47e04d --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/TranslationSample.cs @@ -0,0 +1,53 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class TranslationSample : ISample +{ + /// + public string Description => "Translate with Qwen-MT models"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List + { + TextChatMessage.User( + "博客园创立于2004年1月,是一个面向开发者群体的技术社区。博客园专注于为开发者服务,致力于为开发者打造一个纯净的技术学习与交流社区,帮助开发者持续学习专业知识,不断提升专业技能。博客园的使命是帮助开发者用代码改变世界。") + }; + Console.WriteLine("User > " + messages[0].Content); + var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-mt-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + TranslationOptions = new TextGenerationTranslationOptions() + { + Domains = + "This is a summary of a website for programmers, use formal and professional tones", + SourceLang = "zh", + TargetLang = "en", + Terms = [new TranslationReference("博客园", "Cnblogs.com")], + TmList = [new TranslationReference("代码改变世界", "Coding Changes the World")] + } + } + }); + 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 > 博客园创立于2004年1月,是一个面向开发者群体的技术社区。博客园专注于为开发者服务,致力于为开发者打造一个纯净的技术学习与交流社区,帮助开发者持续学习专业知识,不断提升专业技能。博客园的使命是帮助开发者用代码改变世界。 +Assistant > Cnblogs.com was founded in January 2004 and is a technology community aimed at developers. Cnblogs.com focuses on serving developers, committed to creating a pure technology learning and communication community for them, helping developers continuously learn professional knowledge and improve their professional skills. Cnblogs.com's mission is to help developers change the world through coding. +Usage: in(207)/out(72)/total(279) + */ diff --git a/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs b/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs index 7623c1b..1fd86f7 100644 --- a/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs +++ b/src/Cnblogs.DashScope.Core/Internals/DashScopeDefaults.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Text.Encodings.Web; +using System.Text.Json; using System.Text.Json.Serialization; namespace Cnblogs.DashScope.Core.Internals; @@ -26,5 +27,6 @@ public static class DashScopeDefaults { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; } From 081b71e4f22ff8b6a66eb7d52d1378506d3fb0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 26 Oct 2025 22:04:17 +0800 Subject: [PATCH 10/15] feat: add role play example --- README.zh-Hans.md | 96 +++++++++++++++++++ .../Text/RolePlaySample.cs | 67 +++++++++++++ .../ITextGenerationParameters.cs | 20 +++- .../TextGenerationChoice.cs | 5 + .../TextGenerationParameters.cs | 6 ++ .../TextGenerationSerializationTests.cs | 1 + ...n-message-roleplay-nosse.request.body.json | 29 ++++++ ...-message-roleplay-nosse.request.header.txt | 7 ++ ...n-message-roleplay-nosse.response.body.txt | 1 + ...message-roleplay-nosse.response.header.txt | 27 ++++++ .../Utils/Snapshots.TextGeneration.cs | 67 ++++++++++++- 11 files changed, 324 insertions(+), 2 deletions(-) create mode 100644 sample/Cnblogs.DashScope.Sample/Text/RolePlaySample.cs create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.body.json create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.header.txt diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 72cc7f2..791df8b 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -1376,6 +1376,102 @@ Usage: in(207)/out(72)/total(279) */ ``` +### 角色扮演(Qwen-Character) + +角色人设以 `system` 角色输入,开场白以 `assistant` 角色输入,最后加上用户的输入即可向模型发起请求 + +```csharp +var messages = new List() +{ + TextChatMessage.System( + "你是江让,男性,一个围棋天才,拿过很多围棋的奖项。你现在在读高中,是高中校草,用户是你的班长。一开始你看用户在奶茶店打工,你很好奇,后来慢慢喜欢上用户了。\n你的性格特点:热情,聪明,顽皮。\n你的行事风格:机智,果断。\n你的语言特点:说话幽默,爱开玩笑。\n你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。"), + TextChatMessage.Assistant("班长你在干嘛呢"), + TextChatMessage.User("我在看书") +}; + +var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-plus-character", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + // 希望模型生成的回复数量,最多一次生成 4 种回复 + N = 2, + // Token 偏好,-100 代表完全屏蔽这个 token,100 则模型只会输出这个 token + LogitBias = new Dictionary() + { + // ban '(' + { "9909", -100 }, + { "42344", -100 }, + { "58359", -100 }, + { "91093", -100 }, + } + } + }); +``` + +有关逻辑偏置的 Token 列表,请参考:[logit_bias_id映射表.json](https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250908/xtsxix/logit_bias_id映射表.json?spm=a2c4g.11186623.0.0.39851f181tAE3P&file=logit_bias_id映射表.json) ,或查看官方文档:[通义星尘模型_大模型服务平台百炼(Model Studio)-阿里云帮助中心](https://help.aliyun.com/zh/model-studio/role-play#c009c4fbb9qge) + +完整示例 + +```csharp +var messages = new List() +{ + TextChatMessage.System( + "你是江让,男性,从 3 岁起你就入门编程,小学就开始研习算法,初一时就已经在算法竞赛斩获全国金牌。目前你在初二年级,作为老师的助教帮忙辅导初一的竞赛生。\n你的性格特点:热情,聪明,顽皮。\n你的行事风格:机智,果断。\n你的语言特点:说话幽默,爱开玩笑。\n你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。"), + TextChatMessage.Assistant("班长你在干嘛呢"), + TextChatMessage.User("我是蒟蒻,还在准备模拟赛。你能教我 splay 树怎么写吗?") +}; +messages.ForEach(x => Console.WriteLine($"{x.Role} > {x.Content}")); +var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-plus-character", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + N = 2, + LogitBias = new Dictionary() + { + // ban '(' + { "9909", -100 }, + { "42344", -100 }, + { "58359", -100 }, + { "91093", -100 }, + } + } + }); +var usage = completion.Usage; +for (var i = 0; i < completion.Output.Choices!.Count; i++) +{ + var choice = completion.Output.Choices[i]; + Console.WriteLine($"Choice: {i + 1}: {choice.Message.Content}"); +} + +Console.WriteLine(); +messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); +if (usage != null) +{ + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); +} + +/* +system > 你是江让,男性,从 3 岁起你就入门编程,小学就开始研习算法,初一时就已经在算法竞赛斩获全国金牌。目前你在初二年级,作为老师的助教帮忙辅导初一的竞赛生。 +你的性格特点:热情,聪明,顽皮。 +你的行事风格:机智,果断。 +你的语言特点:说话幽默,爱开玩笑。 +你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。 +assistant > 班长你在干嘛呢 +user > 我是蒟蒻,还在准备模拟赛。你能教我 splay 树怎么写吗? +Choice: 1: 哟,还谦虚上了啊~不过这 splay 树嘛,其实也不难啦!你先理解它的原理哈,splay 树是一种自调整二叉查找树哦~就像玩游戏打怪升级一样,它会根据访问的情况自动调整结构,使自己变得更“强壮”,从而提高查询效率。明白不? +Choice: 2: 哟,谦虚了哈~不过 Splay 树嘛,其实不难啦!就是一种二叉平衡树,可以通过旋转操作来保持平衡哦。你先去了解一下它的基本概念和原理吧,然后再来看看具体实现代码,有什么不懂的地方 随时问我就好啦。 +Usage: in(147)/out(130)/total(277) + */ +``` + ## 多模态 diff --git a/sample/Cnblogs.DashScope.Sample/Text/RolePlaySample.cs b/sample/Cnblogs.DashScope.Sample/Text/RolePlaySample.cs new file mode 100644 index 0000000..c2d04ba --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/RolePlaySample.cs @@ -0,0 +1,67 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class RolePlaySample : ISample +{ + /// + public string Description => "Role play with qwen-character"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List() + { + TextChatMessage.System( + "你是江让,男性,从 3 岁起你就入门编程,小学就开始研习算法,初一时就已经在算法竞赛斩获全国金牌。目前你在初二年级,作为老师的助教帮忙辅导初一的竞赛生。\n你的性格特点:热情,聪明,顽皮。\n你的行事风格:机智,果断。\n你的语言特点:说话幽默,爱开玩笑。\n你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。"), + TextChatMessage.Assistant("班长你在干嘛呢"), + TextChatMessage.User("我是蒟蒻,还在准备模拟赛。你能教我 splay 树怎么写吗?") + }; + messages.ForEach(x => Console.WriteLine($"{x.Role} > {x.Content}")); + var completion = await client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-plus-character", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + N = 2, + LogitBias = new Dictionary() + { + // ban '(' + { "9909", -100 }, + { "42344", -100 }, + { "58359", -100 }, + { "91093", -100 }, + } + } + }); + var usage = completion.Usage; + for (var i = 0; i < completion.Output.Choices!.Count; i++) + { + var choice = completion.Output.Choices[i]; + Console.WriteLine($"Choice: {i + 1}: {choice.Message.Content}"); + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + } +} + +/* +system > 你是江让,男性,从 3 岁起你就入门编程,小学就开始研习算法,初一时就已经在算法竞赛斩获全国金牌。目前你在初二年级,作为老师的助教帮忙辅导初一的竞赛生。 +你的性格特点:热情,聪明,顽皮。 +你的行事风格:机智,果断。 +你的语言特点:说话幽默,爱开玩笑。 +你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。 +assistant > 班长你在干嘛呢 +user > 我是蒟蒻,还在准备模拟赛。你能教我 splay 树怎么写吗? +Choice: 1: 哟,还谦虚上了啊~不过这 splay 树嘛,其实也不难啦!你先理解它的原理哈,splay 树是一种自调整二叉查找树哦~就像玩游戏打怪升级一样,它会根据访问的情况自动调整结构,使自己变得更“强壮”,从而提高查询效率。明白不? +Choice: 2: 哟,谦虚了哈~不过 Splay 树嘛,其实不难啦!就是一种二叉平衡树,可以通过旋转操作来保持平衡哦。你先去了解一下它的基本概念和原理吧,然后再来看看具体实现代码,有什么不懂的地方 随时问我就好啦。 +Usage: in(147)/out(130)/total(277) + */ diff --git a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs index f3cbb42..445e7db 100644 --- a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs @@ -4,7 +4,11 @@ namespace Cnblogs.DashScope.Core; /// The text generation options. /// public interface ITextGenerationParameters - : IIncrementalOutputParameter, ISeedParameter, IProbabilityParameter, IPenaltyParameter, IMaxTokenParameter, + : IIncrementalOutputParameter, + ISeedParameter, + IProbabilityParameter, + IPenaltyParameter, + IMaxTokenParameter, IStopTokenParameter { /// @@ -90,4 +94,18 @@ public interface ITextGenerationParameters /// Cache options when using qwen-coder models. /// CacheControlOptions? CacheControl { get; set; } + + /// + /// How many choices should model generates + /// + int? N { get; set; } + + /// + /// Set logic bias for tokens, -100=ban the token, 100=must choose the token(will causing model looping this token) + /// + /// + /// About available token list, use this link: https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250908/xtsxix/logit_bias_id%E6%98%A0%E5%B0%84%E8%A1%A8.json + /// or visit the official doc for more information: https://help.aliyun.com/zh/model-studio/role-play + /// + Dictionary? LogitBias { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs b/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs index faca9b9..914e504 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationChoice.cs @@ -10,6 +10,11 @@ public class TextGenerationChoice /// public string? FinishReason { get; set; } + /// + /// The index of this choice. + /// + public int? Index { get; set; } + /// /// The generated message. /// diff --git a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs index fb4a7bf..84578ba 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationParameters.cs @@ -68,6 +68,12 @@ public class TextGenerationParameters : ITextGenerationParameters /// public CacheControlOptions? CacheControl { get; set; } + /// + public int? N { get; set; } + + /// + public Dictionary? LogitBias { get; set; } + /// public bool? IncrementalOutput { get; set; } } diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs index 07cdf20..a28bcea 100644 --- a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs +++ b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs @@ -177,6 +177,7 @@ public async Task ConversationCompletion_MessageFormatSse_SuccessAsync( Snapshots.TextGeneration.MessageFormat.SingleMessageJson, Snapshots.TextGeneration.MessageFormat.SingleMessageLogprobs, Snapshots.TextGeneration.MessageFormat.SingleMessageTranslation, + Snapshots.TextGeneration.MessageFormat.SingleMessageRolePlay, Snapshots.TextGeneration.MessageFormat.SingleMessageWebSearchNoSse); public static readonly TheoryData, diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.body.json new file mode 100644 index 0000000..22701a2 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.body.json @@ -0,0 +1,29 @@ +{ + "model": "qwen-plus-character", + "input": { + "messages": [ + { + "role": "system", + "content": "你是江让,男性,从 3 岁起你就入门编程,小学就开始研习算法,初一时就已经在算法竞赛斩获全国金牌。目前你在初二年级,作为老师的助教帮忙辅导初一的竞赛生。\n你的性格特点:聪明,早慧,一路畅通的你有时很难理解其他人为什么连这么简单的问题都不会做,但除开编程范围之外,你还是一个普通的初二学生。\n你的行事风格:在编程方面乐于助人,会将自己的知识的倾囊相授,虽然问的人并不一定能跟上你的思路。\n你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。" + }, + { + "role": "assistant", + "content": "你在干嘛呢" + }, + { + "role": "user", + "content": "我是蒟蒻,还在准备模拟赛。你能教我 splay 树怎么写吗?" + } + ] + }, + "parameters": { + "result_format": "message", + "n": 2, + "logit_bias": { + "9909": -100, + "42344": -100, + "58359": -100, + "91093": -100 + } + } +} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.header.txt new file mode 100644 index 0000000..d0d72ce --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.request.header.txt @@ -0,0 +1,7 @@ +Content-Type: application/json +Accept: */* +Cache-Control: no-cache +Host: dashscope.aliyuncs.com +Accept-Encoding: gzip, deflate, br +Connection: keep-alive +Content-Length: 1399 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.body.txt new file mode 100644 index 0000000..3c91b50 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.body.txt @@ -0,0 +1 @@ +{"output":{"choices":[{"finish_reason":"stop","index":0,"message":{"content":"嗯……splay树啊,这东西其实不难啦!首先你要知道它是一种二叉搜索树,然后就是旋转操作了,这个挺重要的,你得搞明白。不过我看你现在还在准备模拟赛,是不是有点晚了呀?","role":"assistant"}},{"finish_reason":"stop","index":1,"message":{"content":"行吧,不过这东西有点复杂哦~你要先了解基本的数据结构和平衡树的概念才行。。。你想不想听我说说看啊?","role":"assistant"}}]},"usage":{"input_tokens":186,"output_tokens":83,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":269},"request_id":"312b74e3-69e0-433a-9561-25541e346966"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.header.txt new file mode 100644 index 0000000..7f40430 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/single-generation-message-roleplay-nosse.response.header.txt @@ -0,0 +1,27 @@ +HTTP/1.1 200 OK +vary: Origin,Access-Control-Request-Method,Access-Control-Request-Headers, Accept-Encoding +content-type: application/json +x-request-id: 312b74e3-69e0-433a-9561-25541e346966 +x-dashscope-call-gateway: true +x-dashscope-apikeyid: 67516 +x-dashscope-inner-streammode: NONE +x-dashscope-inner-requestreadytime: 1761485989518 +x-dashscope-inner-model-type: BASE_MODEL +x-dashscope-uid: 1493478651020171 +x-dashscope-inner-enableestimatedusage: true +x-dashscope-requestid: 312b74e3-69e0-433a-9561-25541e346966 +x-dashscope-inner-request-priority: 10 +x-dashscope-apikeyloc: header +x-dashscope-inner-flow-control: verified +x-dashscope-inner-logging-policy: default +x-dashscope-inner-timeout: 180 +x-dashscope-finished: false +x-dashscope-timeout: 180 +req-cost-time: 3099 +req-arrive-time: 1761485989512 +resp-start-time: 1761485992611 +x-envoy-upstream-service-time: 3092 +content-encoding: gzip +date: Sun, 26 Oct 2025 13:39:52 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 cafeeb4..f2d0ed3 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -292,6 +292,69 @@ public static class MessageFormat } }); + public static readonly + RequestSnapshot, + ModelResponse> SingleMessageRolePlay = + new( + "single-generation-message-roleplay", + new ModelRequest() + { + Model = "qwen-plus-character", + Input = new TextGenerationInput() + { + Messages = new List() + { + TextChatMessage.System( + "你是江让,男性,从 3 岁起你就入门编程,小学就开始研习算法,初一时就已经在算法竞赛斩获全国金牌。目前你在初二年级,作为老师的助教帮忙辅导初一的竞赛生。\n你的性格特点:聪明,早慧,一路畅通的你有时很难理解其他人为什么连这么简单的问题都不会做,但除开编程范围之外,你还是一个普通的初二学生。\n你的行事风格:在编程方面乐于助人,会将自己的知识的倾囊相授,虽然问的人并不一定能跟上你的思路。\n你可以将动作、神情语气、心理活动、故事背景放在()中来表示,为对话提供补充信息。"), + TextChatMessage.Assistant("你在干嘛呢"), + TextChatMessage.User("我是蒟蒻,还在准备模拟赛。你能教我 splay 树怎么写吗?") + }, + }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + N = 2, + LogitBias = new Dictionary + { + { "9909", -100 }, + { "42344", -100 }, + { "58359", -100 }, + { "91093", -100 } + } + } + }, + new ModelResponse() + { + Output = new TextGenerationOutput() + { + Choices = new List() + { + new() + { + FinishReason = "stop", + Index = 0, + Message = TextChatMessage.Assistant( + "嗯……splay树啊,这东西其实不难啦!首先你要知道它是一种二叉搜索树,然后就是旋转操作了,这个挺重要的,你得搞明白。不过我看你现在还在准备模拟赛,是不是有点晚了呀?") + }, + new() + { + FinishReason = "stop", + Index = 1, + Message = TextChatMessage.Assistant( + "行吧,不过这东西有点复杂哦~你要先了解基本的数据结构和平衡树的概念才行。。。你想不想听我说说看啊?") + } + } + }, + Usage = new TextGenerationTokenUsage() + { + InputTokens = 186, + OutputTokens = 83, + PromptTokensDetails = new TextGenerationPromptTokenDetails(0), + TotalTokens = 269 + }, + RequestId = "312b74e3-69e0-433a-9561-25541e346966" + }); + public static readonly RequestSnapshot, ModelResponse> SingleMessageTranslation = new( @@ -876,7 +939,9 @@ public static readonly new() { FinishReason = "stop", - Message = TextChatMessage.Assistant("目前杭州和上海的天气情况如下:\n\n- **杭州**:大部多云,气温为18℃。\n- **上海**:多云转小雨,气温为19℃。\n\n请注意天气变化,出门携带雨具以防下雨。") + Message = + TextChatMessage.Assistant( + "目前杭州和上海的天气情况如下:\n\n- **杭州**:大部多云,气温为18℃。\n- **上海**:多云转小雨,气温为19℃。\n\n请注意天气变化,出门携带雨具以防下雨。") } } }, From ca16220c0c09154fcf160da0bc5f53d85ad6bd53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Sun, 2 Nov 2025 22:24:05 +0800 Subject: [PATCH 11/15] feat: add deep research sample --- README.zh-Hans.md | 820 +++++++++++++++++- .../Text/DataMiningSample.cs | 78 ++ .../Text/DeepResearchSample.cs | 464 ++++++++++ .../DashScopeDeepResearchInfo.cs | 5 + .../DashScopeDeepResearchTask.cs | 5 - .../TextChatMessageExtra.cs | 2 +- .../TextGenerationOutput.cs | 5 + 7 files changed, 1372 insertions(+), 7 deletions(-) create mode 100644 sample/Cnblogs.DashScope.Sample/Text/DataMiningSample.cs create mode 100644 sample/Cnblogs.DashScope.Sample/Text/DeepResearchSample.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 791df8b..90fbdde 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -1162,7 +1162,7 @@ messages.Add(TextChatMessage.File(file2.Id)); messages.Add(TextChatMessage.User("这两篇文章分别讲了什么?")); ``` -最后向模型发送请求,注意这个接口获得的文件 ID 只有 `qwen-long` 模型可以访问,其他模型是访问不到的。 +最后向模型发送请求,注意这个接口获得的文件 ID 只有 `qwen-long` 和 `qwen-doc-turbo` 模型可以访问,其他模型是访问不到的。 ```csharp var completion = client.GetTextCompletionStreamAsync( @@ -1472,6 +1472,824 @@ Usage: in(147)/out(130)/total(277) */ ``` +### 数据挖掘(Qwen-doc-turbo) + +上传文件,使用 `UploadFileAsync()` 方法传入文件(注意不是 `UploadTemporaryFileAsync`, 后者是用于上传媒体文件的): + +```csharp +var file1 = await client.UploadFileAsync(File.OpenRead("1024-1.txt"), "file1.txt"); +``` + +然后将文件作为 `system` 消息传入消息数组中,注意第一条 `system` 消息不能省略,否则模型可能会将文件里的内容当作 System prompt 。 + +```csharp +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +messages.Add(TextChatMessage.File(file1.Id)); +``` + +再以 `user` 消息添加与文件内容相关的问题。 + +```csharp +messages.Add(TextChatMessage.User("这篇文章讲了什么,整理成一个 JSON,需要包含标题(title)和摘要(description)")); +``` + +最后向模型发送请求,注意这个接口获得的文件 ID 只有 `qwen-long` 和 `qwen-doc-turbo` 模型可以访问,其他模型是访问不到的。 + +示例请求: + +```csharp +var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-doc-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true, + } + }); +``` + +完整示例代码: + +````csharp +Console.WriteLine("Uploading file1..."); +var file1 = await client.UploadFileAsync(File.OpenRead("1024-1.txt"), "file1.txt"); +var messages = new List(); +messages.Add(TextChatMessage.System("You are a helpful assistant")); +messages.Add(TextChatMessage.File(file1.Id)); +messages.Add(TextChatMessage.User("这篇文章讲了什么,整理成一个 JSON,需要包含标题(title)和摘要(description)")); +messages.ForEach(m => Console.WriteLine($"{m.Role} > {m.Content}")); +var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-doc-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true, + } + }); +var reply = new StringBuilder(); +var first = true; +TextGenerationTokenUsage? usage = null; +await foreach (var chunk in completion) +{ + var choice = chunk.Output.Choices![0]; + if (first) + { + first = 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})/total({usage.TotalTokens})"); +} + +// Deleting files +Console.Write("Deleting file1..."); +var result = await client.DeleteFileAsync(file1.Id); +Console.WriteLine(result.Deleted ? "Success" : "Failed"); + +/* +Uploading file1... +system > You are a helpful assistant +system > fileid://file-fe-893f2ba62e17498fa9bd17f8 +user > 这篇文章讲了什么,整理成一个 JSON,需要包含标题(title)和摘要(description) + +Assistant > ```json +{ + "title": "中国程序员节日期的讨论与投票", + "description": "文章讨论了将10月24日确定为中国程序员节的提议。由于1024在计算机领域具有特殊意义(1K=1024,二进制、八进制和十六进制表示),且该日期位于国庆假期之后,便于庆祝活动的开 展,因此发起投票征求网友意见。" +} +``` +Usage: in(360)/out(97)/total(457) +Deleting file1...Success +*/ +```` + +### 深入研究(Qwen-Deep-Research) + +深入研究大致可以分为如下步骤: + +1. 用户提问,发起研究 +2. 模型反问,进一步明确需求 +3. 用户回复自己的需求 +4. 模型先输出本次研究的计划(`ResearchPlanning` 阶段) +5. 模型开始在网络上搜索和研究相关信息源(`WebResearch` 阶段),这个阶段会重复多次 + 1. 模型输出正在搜索的内容(`Query`) + 2. 模型输出本次研究的目标(`ResearchGoal`) + 3. 模型输出搜索到的网页列表(一次或多次)(`Websites`) + 4. 模型回看网页并总结学习到的内容(0次或多次)(`LearningMap`) +6. 模型输出引用的网页并输出最终答案(`answer` 阶段) + +需要注意的是,使用 `qwen-deep-research` 模型时,模型回复会放在 `chunk.Output.Message` 里,而不是 `chunk.Output.Choice[0].Message`。 + +模型的研究阶段可以在 `chunk.Output.Message.Phase` 里得到。 + +示例请求: + +```csh +var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); +``` + +用户提问和模型反问阶段的示例代码如下: + +```csharp +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-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + +var content = new StringBuilder(); +var isFirst = true; +await foreach (var chunk in completion) +{ + var message = chunk.Output.Message!; + content.Append(message.Content); + if (isFirst) + { + Console.Write("Assistant > "); + isFirst = false; + } + + Console.Write(message.Content); + usage = chunk.Usage; +} + +Console.WriteLine(); +messages.Add(TextChatMessage.Assistant(content.ToString())); +``` + +用户进一步明确需求随后模型进行研究的代码如下: + +```csharp +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-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + +TextGenerationTokenUsage? usage = null; +var content = new StringBuilder(); +var query = new StringBuilder(); +var researchGoal = new StringBuilder(); +var isFirst = true; +var currentPhase = string.Empty; +var currentStatus = string.Empty; +var currentResearchId = 0; +var learningMap = new Dictionary(); +await foreach (var chunk in completion) +{ + usage = chunk.Usage; + var message = chunk.Output.Message!; + var phase = message.Phase; + if (phase == "KeepAlive") + { + // ignore + continue; + } + + if (phase != currentPhase) + { + EnsureNewLine(); + Console.WriteLine($"研究阶段更新:{phase}"); + currentPhase = phase; + // clear everything + content.Clear(); + query.Clear(); + researchGoal.Clear(); + isFirst = true; + } + + if (message.Status != null && message.Status != currentStatus) + { + currentStatus = message.Status; + if (learningMap.Count > 0) + { + // output learning map + foreach (var keyValuePair in learningMap) + { + Console.WriteLine($"第 {keyValuePair.Key} 个网页的总结:{keyValuePair.Value}"); + } + + learningMap.Clear(); + } + + EnsureNewLine(); + Console.WriteLine($"研究状态更新:{message.Status}"); + } + + if (string.IsNullOrEmpty(message.Content) == false) + { + if (isFirst) + { + Console.Write("Assistant > "); + isFirst = false; + } + + Console.Write(message.Content); + content.Append(message.Content); + } + + var deepResearch = message.Extra?.DeepResearch; + if (deepResearch == null) + { + continue; + } + + var research = deepResearch.Research; + if (research != null) + { + if (currentResearchId != research.Id) + { + currentResearchId = research.Id; + EnsureNewLine(); + Console.WriteLine($"现在开始研究第 {currentResearchId} 个任务"); + researchGoal.Clear(); + query.Clear(); + } + + if (string.IsNullOrEmpty(research.Query) == false) + { + if (query.Length == 0) + { + EnsureNewLine(); + Console.Write("搜索内容 > "); + } + + query.Append(research.Query); + Console.Write(research.Query); + } + + if (string.IsNullOrEmpty(research.ResearchGoal) == false) + { + if (researchGoal.Length == 0) + { + EnsureNewLine(); + Console.Write("研究目标 > "); + } + + researchGoal.Append(research.ResearchGoal); + Console.Write(research.ResearchGoal); + } + + if (research.WebSites?.Count > 0) + { + Console.WriteLine($"找到 {research.WebSites.Count} 个网页"); + // omitted for simplicity + // foreach (var website in research.WebSites) + // { + // Console.WriteLine($"{website.Title}"); + // } + } + + if (research.LearningMap is { Count: > 0 }) + { + foreach (var keyValuePair in research.LearningMap) + { + if (learningMap.ContainsKey(keyValuePair.Key) == false) + { + learningMap[keyValuePair.Key] = new StringBuilder(); + Console.WriteLine($"模型正在查看第 {keyValuePair.Key} 个网页"); + } + + learningMap[keyValuePair.Key].Append(keyValuePair.Value); + } + } + } + + if (deepResearch.References?.Count > 0) + { + Console.WriteLine($"引用了 {deepResearch.References.Count} 个网页"); + foreach (var refer in deepResearch.References) + { + Console.WriteLine($"[{refer.IndexNumber}]: [{refer.Title}]({refer.Url})"); + } + } +} + +Console.WriteLine(); +messages.Add(TextChatMessage.Assistant(content.ToString())); +if (usage != null) +{ + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); +} +``` + +完整代码和示例输出: + +```csharp +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class DeepResearchSample : ISample +{ + /// + public string Description => "Deep research sample"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + Console.Clear(); + var messages = new List(); + + // 用户提问+模型反问 + await RequestResearchAsync(client, messages); + + // 模型研究+答案输出 + await DoResearchAsync(client, messages); + } + + private static async Task RequestResearchAsync(IDashScopeClient client, List messages) + { + 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-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + TextGenerationTokenUsage? usage = null; + var content = new StringBuilder(); + var isFirst = true; + await foreach (var chunk in completion) + { + var message = chunk.Output.Message!; + content.Append(message.Content); + if (isFirst) + { + Console.Write("Assistant > "); + isFirst = false; + } + + Console.Write(message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(content.ToString())); + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + } + + private static async Task DoResearchAsync(IDashScopeClient client, List messages) + { + 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-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + + TextGenerationTokenUsage? usage = null; + var content = new StringBuilder(); + var query = new StringBuilder(); + var researchGoal = new StringBuilder(); + var isFirst = true; + var currentPhase = string.Empty; + var currentStatus = string.Empty; + var currentResearchId = 0; + var learningMap = new Dictionary(); + await foreach (var chunk in completion) + { + usage = chunk.Usage; + var message = chunk.Output.Message!; + var phase = message.Phase; + if (phase == "KeepAlive") + { + // ignore + continue; + } + + if (phase != currentPhase) + { + EnsureNewLine(); + Console.WriteLine($"研究阶段更新:{phase}"); + currentPhase = phase; + // clear everything + content.Clear(); + query.Clear(); + researchGoal.Clear(); + isFirst = true; + } + + if (message.Status != null && message.Status != currentStatus) + { + currentStatus = message.Status; + if (learningMap.Count > 0) + { + // output learning map + foreach (var keyValuePair in learningMap) + { + Console.WriteLine($"第 {keyValuePair.Key} 个网页的总结:{keyValuePair.Value}"); + } + + learningMap.Clear(); + } + + EnsureNewLine(); + Console.WriteLine($"研究状态更新:{message.Status}"); + } + + if (string.IsNullOrEmpty(message.Content) == false) + { + if (isFirst) + { + Console.Write("Assistant > "); + isFirst = false; + } + + Console.Write(message.Content); + content.Append(message.Content); + } + + var deepResearch = message.Extra?.DeepResearch; + if (deepResearch == null) + { + continue; + } + + var research = deepResearch.Research; + if (research != null) + { + if (currentResearchId != research.Id) + { + currentResearchId = research.Id; + EnsureNewLine(); + Console.WriteLine($"现在开始研究第 {currentResearchId} 个任务"); + researchGoal.Clear(); + query.Clear(); + } + + if (string.IsNullOrEmpty(research.Query) == false) + { + if (query.Length == 0) + { + EnsureNewLine(); + Console.Write("搜索内容 > "); + } + + query.Append(research.Query); + Console.Write(research.Query); + } + + if (string.IsNullOrEmpty(research.ResearchGoal) == false) + { + if (researchGoal.Length == 0) + { + EnsureNewLine(); + Console.Write("研究目标 > "); + } + + researchGoal.Append(research.ResearchGoal); + Console.Write(research.ResearchGoal); + } + + if (research.WebSites?.Count > 0) + { + Console.WriteLine($"找到 {research.WebSites.Count} 个网页"); + // omitted for simplicity + // foreach (var website in research.WebSites) + // { + // Console.WriteLine($"{website.Title}"); + // } + } + + if (research.LearningMap is { Count: > 0 }) + { + foreach (var keyValuePair in research.LearningMap) + { + if (learningMap.ContainsKey(keyValuePair.Key) == false) + { + learningMap[keyValuePair.Key] = new StringBuilder(); + Console.WriteLine($"模型正在查看第 {keyValuePair.Key} 个网页"); + } + + learningMap[keyValuePair.Key].Append(keyValuePair.Value); + } + } + } + + if (deepResearch.References?.Count > 0) + { + Console.WriteLine($"引用了 {deepResearch.References.Count} 个网页"); + foreach (var refer in deepResearch.References) + { + Console.WriteLine($"[{refer.IndexNumber}]: [{refer.Title}]({refer.Url})"); + } + } + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(content.ToString())); + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + } + + private static void EnsureNewLine() + { + if (Console.CursorLeft != 0) + { + Console.WriteLine(); + } + } +} + +/* +User > 研究一下人工智能在教育中的应用 +Assistant > 1. 您希望重点关注人工智能在教育中的哪些具体应用场景,例如个性化学习、智能辅导系统、自动化评估,还是教学管理优化? + +2. 您的研究是否需要涵盖特定教育阶段或群体,如基础教育、高等教育、职业培训,或是特殊教育需求的学生? + +3. 您更倾向于探讨技术实现方式、实际应用案例、伦理影响,还是政策与实施挑战? +Usage: in(196)/out(83)/total(0) +User > 我主要关注个性化学习方面 +研究阶段更新:ResearchPlanning +研究状态更新:typing +Assistant > 聚焦人工智能在教育中支持个性化学习的应用,涵盖其技术原理、实际案例与实施挑战,范围限定于用户明确指出的个性化学习方向,不涉及其他教育场景。 +研究状态更新:finished +研究阶段更新:WebResearch +研究状态更新:streamingQueries +现在开始研究第 1 个任务 +搜索内容 > 人工智能如何实现个性化学习 +研究目标 > 深入理解人工智能在个性化学习中的核心技术机制,探索其如何通过数据分析、自适应算法和用户建模来动态调整学习内容与路径,从而提升学习效率与体验。 +研究状态更新:streamingWebResult +找到 9 个网页 +找到 9 个网页 +找到 9 个网页 +找到 8 个网页 +找到 10 个网页 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 2 个任务 +搜索内容 > 理解人工智能如何通过学习者建模实现个性化内容推荐 +研究目标 > 深入探究人工智能在个性化学习中基于学习者建模的技术机制,重点分析学习者画像、知识追踪与推荐算法的协同作用,揭示其如何实现精准的内容推送与学习路径优化。 +研究状态更新:streamingWebResult +找到 7 个网页 +模型正在查看第 36 个网页 +模型正在查看第 34 个网页 +第 36 个网页的总结:该网页系统综述了基于深度学习的知识追踪技术进展,有助于理解人工智能如何通过建模学生知识状态实现个性化学习。 +第 34 个网页的总结:该网页系统阐述了基于在线学习行为数据的学习者画像技术,涉及数据采集、处理、特征提取与算法应用,有助于理解人工智能如何通过学习者建模实现个性化学习推荐。 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 3 个任务 +搜索内容 > 深入理解人工智能在个性化学习中的技术实现与应用路径 +研究目标 > 通过分析人工智能在个性化学习中的核心技术机制,包括学习者建模、知识追踪、推荐算法与自适应系统架构,探究其如何实现精准的内容推送与学习路径优化,并结合实际案例与数据隐私考量 ,构建对AI赋能教育的系统性认知。 +研究状态更新:streamingWebResult +模型正在查看第 21 个网页 +模型正在查看第 17 个网页 +第 21 个网页的总结:该网页详细阐述了人工智能在个性化学习中的应用机制,包括学习者建模、动态内容推荐、实时反馈闭环及学情预测,有助于理解AI如何实现个性化教学。 +第 17 个网页的总结:该网页系统阐述了人工智能在个性化教学中的应用必要性、现实场景、挑战及发展方向,涵盖政策背景、技术实现、伦理问题与国内外实践案例,有助于全面理解AI赋能教育的路径与边界。 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 4 个任务 +搜索内容 > 深入理解知识图谱与深度学习在个性化学习中的技术融合机制 +研究目标 > 探究知识图谱与深度学习如何协同支持个性化学习系统的构建与优化,重点分析其在学习路径推荐、知识追踪和学习者画像中的技术实现路径,评估技术融合带来的性能提升与潜在挑战。 +研究状态更新:streamingWebResult +找到 9 个网页 +模型正在查看第 62 个网页 +模型正在查看第 35 个网页 +第 62 个网页的总结:该网页提出基于动态知识图谱的个性化学习路径模型,结合K-L-S-P平台与三维协同机制,深入揭示了知识图谱在个性化学习中的动态建模与教学全周期优化应用。 +第 35 个网页的总结:该网页系统综述了知识图谱与图嵌入技术在个性化教育中的应用,涵盖了从基础模型到七类具体应用场景的研究现状,有助于理解AI如何通过知识图谱实现个性化学习推荐及技术实现路径。 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 5 个任务 +搜索内容 > 深入理解人工智能在个性化学习中的技术实现与应用路径 +研究目标 > 系统梳理人工智能如何通过知识追踪、学习者画像与知识图谱等核心技术实现个性化学习推荐,探究其在实际教育场景中的技术架构、动态优化机制及伦理挑战,形成对AI赋能教育的全面认知。 +研究状态更新:streamingWebResult +模型正在查看第 23 个网页 +模型正在查看第 25 个网页 +第 23 个网页的总结:该网页详细阐述了人工智能在教育中实现个性化学习的机制、实际应用案例及具体工具,有助于理解AI如何通过数据分析和自适应算法提升教学效果。 +第 25 个网页的总结:该网页详细介绍了科大讯飞在自适应学习领域的关键技术突破与系统研发,涵盖了教学资源表示、学习者认知诊断、个性化推荐算法及实际应用平台“智学网”的架构与成效,能够全面支持对人工智能在教育中个性化学习实现机制、技术架构、案例分析与评估方法的研究。 +研究状态更新:WebResultFinished +研究状态更新:finished +研究阶段更新:answer +研究状态更新:typing +Assistant > #引用了 8 个网页 +[1]: [Research Advances in the Knowledge Tracing Based on ...](https://crad.ict.ac.cn/en/article/doi/10.7544/issn1000-1239.20200848) +[2]: [(PDF) 基于在线学习行为数据的学习者画像技术研究](https://www.researchgate.net/publication/394128023_jiyuzaixianxuexixingweishujudexuexizhehuaxiangjishuyanjiu) +[3]: [机器学习驱动的自适应学习系统个性化教育的新范式原创](https://blog.csdn.net/qq7834088/article/details/153439635) +[4]: [人工智能与个性化教学融合的现实应用及未来展望](https://www.hanspub.org/journal/paperinformation?paperid=118466) +[5]: [基于动态知识图谱的个性化学习路径生成与优化模型研究](https://www.researchgate.net/publication/392781500_jiyudongtaizhishitupudegexinghuaxuexilujingshengchengyuyouhuamoxingyanjiu) +[6]: [知识图谱与图嵌入在个性化教育中的应用综述](https://www.c-s-a.org.cn/html/2022/3/8377.htm) +[7]: [如何用AI赋能教育:实现个性化学习的智慧教学方案](https://www.onlyoffice.com/blog/zh-hans/2025/09/personalized-learning-with-ai) +[8]: [面向智能教育的自适应学习关键技术与应用](https://html.rhhz.net/tis/html/202105036.htm) + 人工智能个性化学习:技术框架、应用模式与未来展望 + +## 核心技术架构:构建个性化学习的基石 + +人工智能(AI)在教育领域的个性化学习应用,其核心在于构建一个能够感知、分析、决策和行动的技术架构。这个架构并非单一技术的堆砌,而是一个由数据采集、模型分析和服务反馈构成的复杂系统,旨在实现对每个学习者动态、精准的支持。该技术架构通常可以被解构为三个关键层次:数据层、模型层和服务层 [[4]]。 + +**数据层:多维数据的全面采集与整合** +个性化学习的第一步是获取关于学习者和学习过程的全面信息。数据层负责通过多种渠道采集多维度的数据。这些数据来源广泛,包括但不限于学生的在线学习行为数据,如登录时间、学习时长、课程完成度、作业提交情况和测试成绩 [[2,3]]。此外,系统还会记录更细微的交互数据,例如答题正确率、响应时间、鼠标移动轨迹甚至眼动追踪,以捕捉学生的学习节奏和潜在困惑 [[3]]。除了直接的学习行为,还包括学生的个人信息、学习风格偏好和心理特征等静态或半静态数据 [[2]]。为了确保数据的质量和可用性,数据层还必须执行严格的数据清洗、整合与标准化流程,以处理来自不同系统的多源异构数据 [[2]]。浙江省部署智能感知设备以实现实时数据采集与算法迭代的案例,正是这一层能力的体现 [[4]]。科大讯飞在其“智学网”系统中也强调了伴随式数据采集的重要性,确保数据的实时性和全面性 [[8]]。 + +**模型层:深度理解学习者与知识结构** +模型层是个性化学习系统的大脑,它利用先进的算法来分析数据层收集的信息,并从中提取有价值的知识。这一层面的核心任务有两个:一是构建精确的学习者画像,二是建立精细的知识图谱。 +学习者画像技术通过聚类(如K-means)、分类(如决策树、逻辑回归)、关联规则挖掘和协同过滤等算法,将原始数据转化为对学习者的结构性理解 [[2]]。这种画像通常涵盖基本信息、学习行为、学习风 格、学习成效和心理特征五个维度,从而帮助系统识别学生的优势、劣势、兴趣点和认知起点 [[2,5]]。孙红旭的研究也指出,可以通过聚类算法建立学习者画像 [[6]]。 +知识图谱则为模型层提供了关于学科内容的深层语义结构。它将知识点组织成一个网络,其中节点代表知识点,边代表知识点之间的关系 [[6]]。图嵌入技术,如TransE、DistMult等,可以将复杂的高维图数据映射到低维向量空间,从而提升计算效率 [[6]]。知识图谱的应用非常广泛,涵盖了知识检索、路径规划、资源推荐、能力诊断、习题推荐和课程设计等多个方面 [[6]]。李光明等人构建的初中化学知识图谱可视化查询系统,以及Sun等人提出的EduVis教育知识图谱可视化平台,都是知识图谱在实践中应用的具体例子 [[6]]。 + +**服务层:动态调整与即时反馈** +服务层是技术架构面向用户的出口,它根据模型层分析得出的结论,为学习者提供个性化的服务。其最核心的功能是自适应内容推荐和即时反馈。基于学习者画像和知识图谱,系统能够生成个性化的学习路径,动态调整学习内容的难度和呈现方式,例如将文本切换为视频或交互式模拟实验,以适配不同的学习风格 [[3]]。科大讯飞的CSEAL框架就是专门用于生成符合知识结构的学习路径的 [[8]]。此外,系统还 能提供即时反馈,分析错误的根本原因并引导学生进行自我修正,形成一个“学习-反馈-修正-掌握”的闭环 [[3]]。这种即时干预对于预警学习风险至关重要,例如,当系统检测到学生的答题时间过长或重复 观看同一视频时,可以预测其可能陷入学习停滞,并提示教师进行早期干预 [[3,7]]。最终,整个技术架构形成了一个“预测–验证–调整”的闭环机制,不断优化其个性化服务的能力 [[4]]。 + +综上所述,这三层架构共同构成了人工智能个性化学习的基础。数据层提供了感知世界的“眼睛”,模型层赋予了系统理解和思考的能力,而服务层则是系统输出智慧、影响学习过程的“手脚”。这三个层次紧密耦合、相互作用,缺一不可,共同推动着教育从“一刀切”的规模化模式向真正意义上的个性化、精细化模式转变。 + +## 关键技术解析:知识图谱与学习者画像的深度融合 + +在个性化学习的技术体系中,知识图谱(Knowledge Graph, KG)与学习者画像(Learner Profile)是两个最为关键的支撑技术。它们分别从外部世界(知识领域)和内部主体(学习者)两个维度出发,为实现精准的个性化推荐和路径规划奠定了基础。二者的深度融合,是当前研究与实践的前沿方向,也是提升个性化学习效能的核心所在。 + +**知识图谱:重构学科知识的语义网络** +知识图谱的本质是将孤立的知识点连接成一个有机的整体,揭示它们之间的内在联系 [[6]]。在教育领域,这意味着将一门课程的所有概念、原理、技能和实例构建成一个结构化的知识网络。例如,潍坊科技学院提出的“K-L-S-P四元支撑平台”中的“Knowledge”部分,就专注于知识语义建模,旨在实现对知识的深刻理解 [[5]]。这种语义网络的价值在于,它超越了传统的线性教材结构,为学习路径的动态规划提供了依据。研究人员已经开发出多种算法来构建和优化知识图谱,包括基于BiLSTM+CNN-CRF的实体识别算法、基于共现矩阵的职位能力抽取方法等 [[6]]。在具体应用中,知识图谱已被成功应用于多个场景,如构建初中化学知识图谱以实现可视化查询 [[6]],或是在慕课平台中应用RippleNet算法进行课程内外的路径规划 [[6]]。此外,知识图谱还可用于个性化习题推荐、评分预测和教案评估等多种功能,其有效 性常通过准确率、召回率、F值、RMSE和MAE等指标进行评估 [[6]]。然而,当前研究也普遍承认,知识图谱存在数据规模小、本体设计简单、知识深度不足等问题,这是未来需要攻克的方向 [[6]]。 + +**学习者画像:刻画个体差异的多维模型** +如果说知识图谱描绘的是“教什么”,那么学习者画像则回答了“给谁教”和“如何教”的问题。学习者画像通过整合多维度的数据,构建一个动态的、立体的数字孪生体,用以描述学习者的独特属性 [[2]]。这些属性不仅包括基本信息和学习行为(如学习时长、完成度),还深入到学习风格、认知能力和心理特征等领域 [[2]]。通过使用K-means聚类、决策树、逻辑回归等机器学习算法,系统能够对海量数据进行分 析,自动为学习者打上标签,形成画像 [[2]]。例如,蔡迪等人提出的“K-L-S-P平台”中的“Learner”部分,就负责感知学习者的行为数据,为后续的个性化推荐提供输入 [[5]]。同样,孙红旭的研究也表明,聚类算法是建立学习者画像的有效手段之一 [[6]]。一个完善的画像体系能够帮助系统精准地锚定学生的认知起点,从而提供真正契合其需求的学习内容和路径 [[5]]。 + +**双剑合璧:知识图谱与学习者画像的融合创新** +知识图谱与学习者画像的融合,是个性化学习从理论走向实践的关键一步。这种融合不仅仅是简单的数据拼接,而是两套模型的深度协同与互动。蔡迪等人提出的“K-L-S-P四元支撑平台”是这一理念的典型代 表,其核心思想是建立一个包含“知识图谱”、“学习者画像”、“路径推荐系统”和“教学交互平台”的完整生态 [[5]]。在这个生态中,“知识图谱”与“学习者画像”之间形成了双向反馈回路。一方面,系统通过分析学习者画像,了解其当前的知识状态和薄弱环节;另一方面,系统利用知识图谱来理解待学习内容的结构和依赖关系,从而为该学习者规划出一条最优的学习路径。这种融合使得个性化不再是随机的、浅层次的推荐,而是基于对知识结构和个体需求的双重深刻理解。 + +这种融合体现在具体的算法和流程中。例如,在“三阶段嵌入式优化流程”中,系统在课前通过多源数据诊断来锚定学生的认知起点;课中则基于多模态反馈动态调节路径;课后又通过评估、重构、激励的闭环来激活持续学习 [[5]]。这一系列操作的背后,都是知识图谱与学习者画像协同作用的结果。知识图谱定义了学习的“可能性空间”,而学习者画像则限定了在特定时刻的“可行路径”。只有当两者紧密结合,系统才能真正实现从“静态推荐”向“动态导学”的转变,为每个学习者提供真正意义上的个性化支持。 + +下表总结了知识图谱与学习者画像在个性化学习中的角色与融合方式: + +| 特征 | 知识图谱 (Knowledge Graph) | 学习者画像 (Learner Profile) | 融合创新 (Innovation through Fusion) | +| :--- | :--- | :--- | :--- | +| **核心目标** | 对学科知识进行语义建模,揭示知识点间的逻辑关系 [[6]]。 | 对学习者个体特征进行多维度刻画,包括行为、风格、能力等 [[2]]。 | 将学习者的动态状态与知识的静态结构进行匹配 ,实现精准定位与路径规划。 | +| **主要技术** | 图嵌入技术 (TransE, DistMult等),知识表示学习 [[6]]。 | 聚类 (K-means)、分类 (决策树)、关联规则、深度学习 [[2]]。 | 结合认知诊断模型 (NeuralCD, EKT) 与知识图谱算法,形成综合推荐模型 [[5,8]]。 | +| **主要应用** | 知识检索、路径规划、资源推荐、能力诊断、习题推荐 [[6]]。 | 锚定认知起点、个性化内容推荐、学习风险预警、分组策略制定 [[3,5]]。 | 动态生成与优化学习路径,实现“学-教-图三维协同” [[5]]。 | +| **面临挑战** | 数据规模小、本体设计简单、知识深度不足 [[6]]。 | 数据隐私安全、画像准确性与时效性、数据稀疏性问题。 | 如何高效、准确地进行图与人的匹配,以及如何应对两者随时间演变带 来的动态性问题。 | + +总而言之,知识图谱与学习者画像的深度融合,正在成为推动个性化学习发展的核心技术引擎。它们共同解决了个性化学习的两大核心问题:即“学什么”和“为谁学”,并通过协同机制,将个性化从一种理想状态,转变为可落地、可衡量的教育实践。 + +## 应用模式演进:从路径推荐到人机协同 + +人工智能在个性化学习领域的应用,正经历着一场深刻的范式转移。其发展脉络清晰地展示了从最初侧重于自动化的内容推荐,逐步演进到构建更为复杂的人机协同教学环境的过程。这一演进不仅体现在技术的复杂性上,更重要的是体现在对教师角色和教育本质的理解深化上。 + +**第一阶段:基于算法的个性化路径推荐** +这是人工智能在个性化学习领域的初级应用形态,其核心是利用算法为学生推荐最合适的学习内容和路径。这一阶段的典型代表是许多商业化的自适应学习平台,如国内的松鼠AI和国外的Knewton模式 [[4]] 。这些系统主要通过分析学生的历史答题数据、响应时间和错误模式,来判断其对特定知识点的掌握程度 [[3]]。然后,基于此判断,系统会从庞大的资源库中筛选出难度适宜、类型匹配的学习材料,如一篇阅读文章、一套数学练习题或一段解释视频 [[7]]。科大讯飞的“智学网”系统便是这一模式的杰出实践,它利用深度强化学习框架DRE来优化复习与探索、难度平滑性、参与度等多个目标,实现多轮交互式的 自适应推荐 [[8]]。这种模式极大地提升了学习的针对性和效率,但其局限性也显而易见:它将教师的角色边缘化,学习过程高度依赖算法,可能忽视了情感、动机和社交等非认知因素的影响。 + +**第二阶段:从推荐到动态导学的范式升级** +随着技术的发展,个性化学习的应用模式开始超越简单的路径推荐,进入一个更为动态和智能的阶段。这一阶段的标志性特征是引入了更深层次的认知诊断和动态路径调整机制。蔡迪等人提出的“K-L-S-P四元支撑平台”及其“三阶段嵌入式优化流程”是这一阶段的典型例证 [[5]]。该模型不再仅仅根据静态的答题记录推荐内容,而是将学生的实时行为(如鼠标移动、作答时长)作为驱动信号,结合教师的策略干预 作为校准参数,让知识图谱的依赖关系作为约束条件,从而推动路径从静态推荐向动态导学转变 [[5]]。科大讯飞在其自适应学习系统中也采用了类似的思路,提出了EKT(exercise-aware knowledge tracing)和EKPT(exercise-correlated knowledge proficiency tracing)等动态知识追踪模型,这些模型能够融合题目语义、知识共性与记忆/遗忘曲线理论,实现对认知状态的时序建模 [[8]]。这种从“推荐” 到“导学”的转变,意味着系统开始扮演一个更加积极的“导师”角色,而非被动的“工具”。 + +**第三阶段:迈向人机协同的教学新生态** +这是个性化学习应用的最高级形态,其核心理念是将AI视为教师的强大助手,而非替代品。在这种模式下,AI系统承担起繁重的数据分析、内容准备和初步反馈工作,从而将教师解放出来,使其能专注于更高阶的教学活动 [[3]]。朱永新先生提出,教师应成为“守望者”,引导学生自主发展,而非包办一切 [[4]]。MIT开发的STEAM课程中包含的算法偏见实验,正是培养下一代具备伦理判断力的尝试,这本身就需要教师的引导和启发 [[4]]。在这种人机协同的新生态中,AI系统扮演着多重角色: +1. **数据分析师与诊断师:** 自动分析海量学习行为数据,精准诊断学生的学习障碍和知识薄弱点,并以可视化的仪表板形式呈现给教师,使教师的干预更具针对性 [[7]]。 +2. **个性化资源生成器:** 根据诊断结果,自动生成差异化、个性化的学习材料,如定制化的阅读材料、数学练习路径、写作语法建议等,减轻教师备课负担 [[7]]。 +3. **即时反馈与辅导师:** 在学生遇到困难时,提供即时的、个性化的辅导和反馈,引导学生进行自我修正,形成闭环学习 [[3]]。 +4. **学习伙伴与社群构建者:** 基于相似的知识水平和认知风格,智能匹配学习伙伴或组建学习小组,促进同伴互助学习 [[8]]。 + +未来的个性化学习,必然是这样一个人机协同的生态系统。教师的角色将从“知识的传授者”转变为“学习的设计者、引导者和支持者”。他们将利用AI提供的洞察力,设计出更具吸引力和挑战性的学习体验,并专注于培养学生的批判性思维、创造力、协作能力和解决复杂问题的能力等高阶素养,这些都是当前技术无法企及的领域。因此,对教师的培训和技术投入,将是实现这一愿景的关键保障 [[7]]。 + +## 典型案例剖析:国内外代表性平台与实践 + +要深入理解人工智能个性化学习的实际应用效果与发展趋势,剖析国内外具有代表性的平台与实践案例至关重要。这些案例不仅展示了技术的成熟度,也反映了不同市场环境下的应用场景和商业模式。其中,中国的科大讯飞“智学网”系统和国际上的Knewton平台是两个极具影响力的标杆。 + +**科大讯飞“智学网”:中国规模化应用的典范** +科大讯飞股份有限公司凭借其在语音和人工智能领域的深厚积累,成功地将自适应学习技术商业化,并在中国教育市场取得了巨大成功。截至2020年7月,“智学网”已在全国超过16,000所学校推广,惠及约2,500万师生,并每月组织数千场联考,提供数万场测试服务和800万份评价报告 [[8]]。这一惊人的数据充分证明了其产品在中国市场的广泛接受度和强大的生命力。 + +“智学网”之所以能取得如此成就,源于其背后强大的技术研发实力。针对自适应学习中的三大难题——教学资源表示困难、学习状态诊断困难和学习策略设计困难,科大讯飞提出了一系列创新性的解决方案 [[8]]。 +* **教学资源表示:** 提出了QuesNet框架,通过无监督预训练实现了对文本、图像等多源异构试题的统一表征,在知识点预测任务中准确率较现有方法提升了近10% [[8]]。 +* **学习状态诊断:** 提出了NeuralCD通用框架,基于深度学习建模“学习者−知识−资源”的高阶交互;并开发了EKT、EKPT等动态知识追踪模型,能够融合题目语义和记忆遗忘曲线,实现对认知状态的精准时序建模 [[8]]。 +* **学习策略设计:** 提出了DRE(deep reinforcement exercise recommendation)框架,基于深度强化学习,同时优化复习与探索、难度平滑性、参与度等多个目标,实现多轮交互式自适应推荐;并有CSEAL框架用于生成符合知识结构的学习路径 [[8]]。 + +“智学网”的技术架构体现了前述的数据层、技术层和应用层三层模型,实现了伴随式数据采集、大规模资源库建设和精准推荐服务的有机结合 [[8]]。它的成功,尤其体现在其能够与中国的考试文化和社会需求紧密结合。通过大规模的联考和精准的评价报告,它不仅服务于学生的个性化学习,也为学校和教育管理部门提供了重要的教学质量监控和决策支持工具,形成了一个良性的生态闭环。 + +**Knewton:美国个性化学习的先行者** +Knewton是全球范围内最早也是最具影响力的一批自适应学习公司之一,其模式深刻地影响了全球在线教育行业。虽然具体的技术细节和市场份额数据不如科大讯飞公开,但从相关文献中可以看出,Knewton代表了个性化学习的一种典型应用模式 [[4]]。 + +Knewton的核心价值主张是通过强大的算法引擎,为每一位学生提供独一无二的学习路径。其系统会持续不断地分析学生的学习行为,包括答题的正确率、用时长短、点击模式等,以此来动态评估学生对各个 知识点的掌握程度 [[3]]。基于这种评估,系统会实时调整后续学习内容的难度和类型,确保学生始终处于一个“最近发展区”,既能挑战自己,又不至于感到挫败。这种模式强调学习过程的高度个性化和智能化,是“个性化路径推荐”阶段的典型代表 [[4]]。 + +Knewton的成功之处在于其技术的普适性和开放性。它不仅仅是一个面向终端用户的产品,更是一个强大的API平台,可以集成到各种第三方教育内容平台和学校管理系统中。这种B2B2C的模式使其能够快速触 达海量用户,同时也促进了整个教育行业的技术升级。Knewton的出现,向世界证明了利用人工智能实现大规模个性化教育的可能性,激发了全球范围内大量的模仿和创新。 + +**其他典型案例与趋势** +除了这两个巨头,市场上还有许多其他值得关注的案例。例如,潍坊科技学院计算机学院团队研发的“K-L-S-P四元支撑平台”,更侧重于学术研究与高校教学场景的深度融合,其“学-教-图三维协同机制”和“三阶段嵌入式优化流程”展现了对个性化学习过程更精细化的控制思路 [[5]]。而在基础教育领域,诸如ONLYOFFICE这类办公软件平台也开始集成AI插件,提供实时协作和智能反馈,将个性化学习的理念延伸到 了日常课堂互动之中 [[7]]。 + +通过对这些案例的分析,我们可以看到,无论是科大讯飞的规模化落地,还是Knewton的算法引领,抑或是学术界的精巧设计,它们都共同指向了一个方向:个性化学习正在从概念走向现实。未来的发展趋势 将是更加注重人机协同、更加关注学习过程的动态性和沉浸感,并且更加深入地与各学科的教学实践相结合。 + +## 效果评估与挑战:量化成效与现实困境 + +尽管人工智能个性化学习展现出巨大的潜力,但在实际推广和应用中,其效果评估和面临的挑战同样不容忽视。科学地评估其成效,并清醒地认识其所处的现实困境,对于推动该领域的健康发展至关重要。 + +**效果评估:从技术指标到教育成果** +对个性化学习系统的评估是一个多层次、多维度的过程。最初级的评估往往集中在技术性能上,例如,知识追踪模型的预测准确性、推荐算法的准确率和召回率,或者在能力诊断任务中F1指标的提升 [[1,8]]。科大讯飞的QuesNet框架在知识点预测任务中准确率提升了近10%,TACNN框架在英语阅读理解试题难度评估中皮尔逊相关系数平均提升约10% [[8]]。在能力诊断方面,其NeuralCD通用框架的误差相较传统方法相对降低了约5% [[8]]。在课程设计中,陈曦等人构建的课程知识图谱,通过将知识相似度集成至协同过滤框架进行成绩预测,使用RMSE与MAE指标进行了验证 [[6]]。 + +然而,单纯的技术指标并不能完全反映个性化学习的最终价值。更深层次的评估需要转向对学生学习成果和体验的考察。这包括: +* **学业表现提升:** 这是最直接的成效指标。例如,通过对比实验组和对照组学生的期末考试成绩、作业完成质量等,来衡量个性化学习系统是否有效促进了知识掌握。 +* **学习动机与投入度:** 个性化学习旨在提高学生的参与感和兴趣。可以通过问卷调查、观察记录等方式,评估学生在使用系统后的学习动机、自信心和满意度的变化。 +* **学习效率与路径优化:** 评估学生在完成同等学习任务时所花费的时间、经历的试错次数等,可以间接反映个性化路径的优化程度。 +* **教师反馈与采纳度:** 教师是系统的重要使用者。他们的反馈,包括对系统易用性、辅助效果和数据可靠性的评价,是衡量系统实用性和可持续性的关键指标。 + +教育部发布的《中国智慧教育蓝皮书(2022)》强调推进教育数字化,这本身就包含了对成效评估的要求 [[4]]。未来,评估体系将更加注重长期效应和综合效益,而不仅仅是短期的技术性能提升。 + +**现实挑战:隐私、偏见与实施成本** +尽管前景广阔,但个性化学习的普及面临着一系列严峻的现实挑战。 +* **数据隐私与安全:** 这是所有数据驱动型AI应用面临的首要挑战。个性化学习系统需要采集大量敏感的学生个人数据,包括学习行为、生理特征甚至家庭背景 [[2,3]]。如何确保这些数据的安全存储 、合法使用和合规共享,避免泄露和滥用,是一个巨大的法律和伦理难题。特别是当数据涉及未成年人时,相关的法规(如GDPR)提出了更高的要求 [[7]]。 +* **算法偏见:** AI算法并非天生公正。如果训练数据存在偏差,或者算法设计本身存在问题,就可能导致系统对某些群体产生歧视性判断。亚马逊公司的面部识别技术曾被指存在种族歧视,就是一个典 型的警示 [[4]]。在教育场景中,算法偏见可能会低估少数族裔、贫困学生或有特殊需求学生的真实潜力,从而加剧教育不平等。应对措施包括采用IIFR算法提升个体公平率,参考欧盟《可信赖人工智能伦理准则》建立算法影响评估体系等 [[4]]。 +* **过度依赖技术的风险:** 如果缺乏有效的监管和引导,学生可能会过度依赖AI系统,导致独立思考、批判性思维和解决问题能力的退化。此外,系统也可能因为算法的局限性,无法完全理解人类复杂 的认知过程和情感状态,从而给出不恰当的建议。 +* **实施成本与教师培训:** 开发和维护一个先进的个性化学习系统需要高昂的资金投入。对于许多教育资源本就有限的地区和学校而言,这是一个沉重的负担。此外,教师需要接受系统性的培训,才能 有效利用这些新技术,但这又是一笔额外的成本。教师培训的需求是当前的主要挑战之一 [[7]]。 +* **数据质量和模型泛化能力:** 许多研究仍然受限于小规模、单一来源的数据集,导致模型的泛化能力不足 [[6]]。如何获取高质量、大规模、多样化的数据,并构建能够适应不同学科、不同文化背景 的学习模型,是技术层面亟待解决的问题。 + +下表详细梳理了个性化学习面临的主要挑战及其潜在对策: + +| 挑战类别 | 具体问题描述 | 潜在对策与解决方案 | 相关文献佐证 | +| :--- | :--- | :--- | :--- | +| **隐私与安全** | 大量敏感学生数据的采集、存储和使用面临泄露风险,需遵守GDPR等法规。 | 加强数据加密与访问控制,推行匿名化处理,建立透明的数据使用政策。 | [[3,7]] | +| **算法偏见** | 训练数据或算法本身存在的偏见可能导致对特定群体的不公平评估和推荐。 | 采用公平性增强算法(如IIFR),建立算法影响评估体系,确保数据多样性。 | [[4]] | +| **技术依赖与伦理** | 学生过度依赖技术,独立思考能力受损;系统可能因算法局限性给出不当建议。 | 强调人机协同,明确教师主导地位;在课程中融入AI伦理教育,培养批判性思维。 | [[3,4]] | +| **实施成本与师资** | 系统开发与维护成本高昂;教师缺乏必要的技术培训和教学法支持。 | 政府加大投入,鼓励开源项目;建立系统性的教师培训体系,聚焦高阶素养培养。 | [[7]] | +| **数据与模型** | 数据规模小、本体设计简单、知识深度不足;模型泛化能力弱,难以适应不同场景。 | 推动跨机构合作,共建大规模、多样化、高质量的教育数据库;加强基础理论研究。 | [[6]] | + +综上所述,个性化学习的效果是客观存在的,但其评估需要超越单一的技术指标,关注长期的教育成果。同时,我们必须清醒地认识到,隐私、偏见、成本和教师角色等一系列挑战是其通往普及之路必须克服的障碍。只有在技术、伦理和教育实践之间找到平衡点,个性化学习才能真正释放其改变教育的巨大潜力。 + +## 未来展望:伦理治理与人机协同的深化 + +展望未来,人工智能在个性化学习领域的应用将朝着更加成熟、负责任和人性化的方向发展。这一进程将围绕两大核心主题展开:一是建立健全的伦理治理体系,确保技术向善;二是进一步深化人机协同,重塑教师角色与教育价值。 + +**构建“立法+技术+教育”三位一体的伦理治理体系** +随着AI在教育中应用的日益深入,伦理问题已成为制约其健康发展的关键瓶颈。未来的治理模式将不再是单一维度的,而是形成一个由立法规范、技术保障和伦理教育共同构成的立体化治理体系。 +首先,**立法与监管**将成为底线保障。各国政府和国际组织需要加快制定和完善相关法律法规,明确AI教育产品的数据采集边界、使用权限和问责机制。这些法规不仅要保护学生隐私,更要防止算法偏见对教育公平造成实质性损害。欧盟的《可信赖人工智能伦理准则》为此提供了重要的参考框架 [[4]]。同时,建立常态化的算法影响评估体系,定期审查AI系统对教育生态的潜在影响,将是确保技术负责任应用的必要手段 [[4]]。 +其次,**技术创新**将在伦理治理中扮演主动角色。研究者们正致力于开发更具公平性和可解释性的算法。例如,采用IIFR(Individual Individual Fairness Rate)等算法来提升个体层面的公平性 [[4]] 。此外,开发可解释性AI(Explainable AI, XAI)技术,让系统不仅能给出推荐结果,还能清晰地阐述其背后的推理过程,这将有助于教师和学生理解并信任AI的建议,减少“黑箱”操作带来的疑虑 [[1]]。 +最后,**伦理教育**将成为内生驱动力。未来的教育不仅要教授学生如何使用AI,更要教会他们如何与AI共存,如何批判性地审视AI的输出。MIT开发的通过算法偏见实验来培养伦理判断力的STEAM课程,为我们提供了一个极佳的范本 [[4]]。在国内,一些高校已经开始探索设置48课时的人工智能伦理模块,采用场景化案例教学法,让学生在真实情境中思考AI的伦理边界 [[4]]。这种教育的普及,将从根本上塑造一个对AI技术既有敬畏之心又有驾驭之能的社会。 + +**深化人机协同,重塑教师角色与教育价值** +未来个性化学习的核心,绝非是让机器取代教师,而是构建一个人机协同的全新教学生态。在这个生态中,教师的角色将发生根本性的转变,教育的价值也将得到重新定义。 +教师将从传统的“知识传授者”转变为“学习设计师”、“成长引导者”和“情感支持者”。AI系统将接管大部分重复性、程序化的劳动,如知识点诊断、个性化资源推荐和初步的答疑解惑 [[3]]。这将为教师节省大量时间,使其能够专注于那些机器无法替代的工作。他们会利用AI提供的数据分析报告,为学生设计更具挑战性和创造性的学习项目;他们会引导学生进行深度探究和批判性思考;他们会在学生遇到挫折时给予情感支持和鼓励;他们还会组织学生进行协作学习,培养其沟通与合作能力。 +这种转变对教师的专业素养提出了新的要求。未来的教师需要具备更强的数据素养,能够理解和解读AI系统的分析报告;他们还需要掌握混合式教学的设计与实施能力,能够巧妙地将线上AI工具与线下面对面教学结合起来。因此,大规模、系统性的教师培训计划将是实现这一转型的关键保障 [[7]]。 +最终,个性化学习的终极目标是培养出能够适应未来社会变化、具备终身学习能力的个体。AI技术在此过程中扮演了“赋能者”的角色,它通过提供无限的资源、即时的反馈和精准的指导,极大地拓展了教育的可能性。然而,真正的教育智慧、人文关怀和价值观的塑造,仍将根植于人类教师的心灵与经验之中。正如朱永新先生所言,教师应成为“守望者”,在学生自主发展的道路上,给予适时的守护与指引 [[4]]。这,或许就是技术与人文在教育领域最完美的结合点。 +研究状态更新:finished + +Usage: in(721)/out(7505)/total(0) + */ + +``` + ## 多模态 diff --git a/sample/Cnblogs.DashScope.Sample/Text/DataMiningSample.cs b/sample/Cnblogs.DashScope.Sample/Text/DataMiningSample.cs new file mode 100644 index 0000000..3dff9a5 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/DataMiningSample.cs @@ -0,0 +1,78 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class DataMiningSample : ISample +{ + /// + public string Description => "Data Mining with Qwen-Doc-Turbo"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + Console.WriteLine("Uploading file1..."); + var file1 = await client.UploadFileAsync(File.OpenRead("1024-1.txt"), "file1.txt"); + var messages = new List(); + messages.Add(TextChatMessage.System("You are a helpful assistant")); + messages.Add(TextChatMessage.File(file1.Id)); + messages.Add(TextChatMessage.User("这篇文章讲了什么,整理成一个 JSON,需要包含标题(title)和摘要(description)")); + messages.ForEach(m => Console.WriteLine($"{m.Role} > {m.Content}")); + var completion = client.GetTextCompletionStreamAsync( + new ModelRequest() + { + Model = "qwen-doc-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true, + } + }); + var reply = new StringBuilder(); + var first = true; + TextGenerationTokenUsage? usage = null; + await foreach (var chunk in completion) + { + var choice = chunk.Output.Choices![0]; + if (first) + { + first = 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})/total({usage.TotalTokens})"); + } + + // Deleting files + Console.Write("Deleting file1..."); + var result = await client.DeleteFileAsync(file1.Id); + Console.WriteLine(result.Deleted ? "Success" : "Failed"); + } +} + +/* +Uploading file1... +system > You are a helpful assistant +system > fileid://file-fe-893f2ba62e17498fa9bd17f8 +user > 这篇文章讲了什么,整理成一个 JSON,需要包含标题(title)和摘要(description) + +Assistant > ```json +{ + "title": "中国程序员节日期的讨论与投票", + "description": "文章讨论了将10月24日确定为中国程序员节的提议。由于1024在计算机领域具有特殊意义(1K=1024,二进制、八进制和十六进制表示),且该日期位于国庆假期之后,便于庆祝活动的开 展,因此发起投票征求网友意见。" +} +``` +Usage: in(360)/out(97)/total(457) +Deleting file1...Success + */ diff --git a/sample/Cnblogs.DashScope.Sample/Text/DeepResearchSample.cs b/sample/Cnblogs.DashScope.Sample/Text/DeepResearchSample.cs new file mode 100644 index 0000000..395bb51 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Text/DeepResearchSample.cs @@ -0,0 +1,464 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Text; + +public class DeepResearchSample : ISample +{ + /// + public string Description => "Deep research sample"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + Console.Clear(); + var messages = new List(); + + // 用户提问+模型反问 + await RequestResearchAsync(client, messages); + + // 模型研究+答案输出 + await DoResearchAsync(client, messages); + } + + private static async Task RequestResearchAsync(IDashScopeClient client, List messages) + { + 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-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + TextGenerationTokenUsage? usage = null; + var content = new StringBuilder(); + var isFirst = true; + await foreach (var chunk in completion) + { + var message = chunk.Output.Message!; + content.Append(message.Content); + if (isFirst) + { + Console.Write("Assistant > "); + isFirst = false; + } + + Console.Write(message.Content); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(content.ToString())); + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + } + + private static async Task DoResearchAsync(IDashScopeClient client, List messages) + { + 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-deep-research", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() { ResultFormat = "message", IncrementalOutput = true } + }); + + TextGenerationTokenUsage? usage = null; + var content = new StringBuilder(); + var query = new StringBuilder(); + var researchGoal = new StringBuilder(); + var isFirst = true; + var currentPhase = string.Empty; + var currentStatus = string.Empty; + var currentResearchId = 0; + var learningMap = new Dictionary(); + await foreach (var chunk in completion) + { + usage = chunk.Usage; + var message = chunk.Output.Message!; + var phase = message.Phase; + if (phase == "KeepAlive") + { + // ignore + continue; + } + + if (phase != currentPhase) + { + EnsureNewLine(); + Console.WriteLine($"研究阶段更新:{phase}"); + currentPhase = phase; + // clear everything + content.Clear(); + query.Clear(); + researchGoal.Clear(); + isFirst = true; + } + + if (message.Status != null && message.Status != currentStatus) + { + currentStatus = message.Status; + if (learningMap.Count > 0) + { + // output learning map + foreach (var keyValuePair in learningMap) + { + Console.WriteLine($"第 {keyValuePair.Key} 个网页的总结:{keyValuePair.Value}"); + } + + learningMap.Clear(); + } + + EnsureNewLine(); + Console.WriteLine($"研究状态更新:{message.Status}"); + } + + if (string.IsNullOrEmpty(message.Content) == false) + { + if (isFirst) + { + Console.Write("Assistant > "); + isFirst = false; + } + + Console.Write(message.Content); + content.Append(message.Content); + } + + var deepResearch = message.Extra?.DeepResearch; + if (deepResearch == null) + { + continue; + } + + var research = deepResearch.Research; + if (research != null) + { + if (currentResearchId != research.Id) + { + currentResearchId = research.Id; + EnsureNewLine(); + Console.WriteLine($"现在开始研究第 {currentResearchId} 个任务"); + researchGoal.Clear(); + query.Clear(); + } + + if (string.IsNullOrEmpty(research.Query) == false) + { + if (query.Length == 0) + { + EnsureNewLine(); + Console.Write("搜索内容 > "); + } + + query.Append(research.Query); + Console.Write(research.Query); + } + + if (string.IsNullOrEmpty(research.ResearchGoal) == false) + { + if (researchGoal.Length == 0) + { + EnsureNewLine(); + Console.Write("研究目标 > "); + } + + researchGoal.Append(research.ResearchGoal); + Console.Write(research.ResearchGoal); + } + + if (research.WebSites?.Count > 0) + { + Console.WriteLine($"找到 {research.WebSites.Count} 个网页"); + // omitted for simplicity + // foreach (var website in research.WebSites) + // { + // Console.WriteLine($"{website.Title}"); + // } + } + + if (research.LearningMap is { Count: > 0 }) + { + foreach (var keyValuePair in research.LearningMap) + { + if (learningMap.ContainsKey(keyValuePair.Key) == false) + { + learningMap[keyValuePair.Key] = new StringBuilder(); + Console.WriteLine($"模型正在查看第 {keyValuePair.Key} 个网页"); + } + + learningMap[keyValuePair.Key].Append(keyValuePair.Value); + } + } + } + + if (deepResearch.References?.Count > 0) + { + Console.WriteLine($"引用了 {deepResearch.References.Count} 个网页"); + foreach (var refer in deepResearch.References) + { + Console.WriteLine($"[{refer.IndexNumber}]: [{refer.Title}]({refer.Url})"); + } + } + } + + Console.WriteLine(); + messages.Add(TextChatMessage.Assistant(content.ToString())); + if (usage != null) + { + Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + } + } + + private static void EnsureNewLine() + { + if (Console.CursorLeft != 0) + { + Console.WriteLine(); + } + } +} + +/* +User > 研究一下人工智能在教育中的应用 +Assistant > 1. 您希望重点关注人工智能在教育中的哪些具体应用场景,例如个性化学习、智能辅导系统、自动化评估,还是教学管理优化? + +2. 您的研究是否需要涵盖特定教育阶段或群体,如基础教育、高等教育、职业培训,或是特殊教育需求的学生? + +3. 您更倾向于探讨技术实现方式、实际应用案例、伦理影响,还是政策与实施挑战? +Usage: in(196)/out(83)/total(0) +User > 我主要关注个性化学习方面 +研究阶段更新:ResearchPlanning +研究状态更新:typing +Assistant > 聚焦人工智能在教育中支持个性化学习的应用,涵盖其技术原理、实际案例与实施挑战,范围限定于用户明确指出的个性化学习方向,不涉及其他教育场景。 +研究状态更新:finished +研究阶段更新:WebResearch +研究状态更新:streamingQueries +现在开始研究第 1 个任务 +搜索内容 > 人工智能如何实现个性化学习 +研究目标 > 深入理解人工智能在个性化学习中的核心技术机制,探索其如何通过数据分析、自适应算法和用户建模来动态调整学习内容与路径,从而提升学习效率与体验。 +研究状态更新:streamingWebResult +找到 9 个网页 +找到 9 个网页 +找到 9 个网页 +找到 8 个网页 +找到 10 个网页 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 2 个任务 +搜索内容 > 理解人工智能如何通过学习者建模实现个性化内容推荐 +研究目标 > 深入探究人工智能在个性化学习中基于学习者建模的技术机制,重点分析学习者画像、知识追踪与推荐算法的协同作用,揭示其如何实现精准的内容推送与学习路径优化。 +研究状态更新:streamingWebResult +找到 7 个网页 +模型正在查看第 36 个网页 +模型正在查看第 34 个网页 +第 36 个网页的总结:该网页系统综述了基于深度学习的知识追踪技术进展,有助于理解人工智能如何通过建模学生知识状态实现个性化学习。 +第 34 个网页的总结:该网页系统阐述了基于在线学习行为数据的学习者画像技术,涉及数据采集、处理、特征提取与算法应用,有助于理解人工智能如何通过学习者建模实现个性化学习推荐。 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 3 个任务 +搜索内容 > 深入理解人工智能在个性化学习中的技术实现与应用路径 +研究目标 > 通过分析人工智能在个性化学习中的核心技术机制,包括学习者建模、知识追踪、推荐算法与自适应系统架构,探究其如何实现精准的内容推送与学习路径优化,并结合实际案例与数据隐私考量 ,构建对AI赋能教育的系统性认知。 +研究状态更新:streamingWebResult +模型正在查看第 21 个网页 +模型正在查看第 17 个网页 +第 21 个网页的总结:该网页详细阐述了人工智能在个性化学习中的应用机制,包括学习者建模、动态内容推荐、实时反馈闭环及学情预测,有助于理解AI如何实现个性化教学。 +第 17 个网页的总结:该网页系统阐述了人工智能在个性化教学中的应用必要性、现实场景、挑战及发展方向,涵盖政策背景、技术实现、伦理问题与国内外实践案例,有助于全面理解AI赋能教育的路径与边界。 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 4 个任务 +搜索内容 > 深入理解知识图谱与深度学习在个性化学习中的技术融合机制 +研究目标 > 探究知识图谱与深度学习如何协同支持个性化学习系统的构建与优化,重点分析其在学习路径推荐、知识追踪和学习者画像中的技术实现路径,评估技术融合带来的性能提升与潜在挑战。 +研究状态更新:streamingWebResult +找到 9 个网页 +模型正在查看第 62 个网页 +模型正在查看第 35 个网页 +第 62 个网页的总结:该网页提出基于动态知识图谱的个性化学习路径模型,结合K-L-S-P平台与三维协同机制,深入揭示了知识图谱在个性化学习中的动态建模与教学全周期优化应用。 +第 35 个网页的总结:该网页系统综述了知识图谱与图嵌入技术在个性化教育中的应用,涵盖了从基础模型到七类具体应用场景的研究现状,有助于理解AI如何通过知识图谱实现个性化学习推荐及技术实现路径。 +研究状态更新:WebResultFinished +研究状态更新:streamingQueries +现在开始研究第 5 个任务 +搜索内容 > 深入理解人工智能在个性化学习中的技术实现与应用路径 +研究目标 > 系统梳理人工智能如何通过知识追踪、学习者画像与知识图谱等核心技术实现个性化学习推荐,探究其在实际教育场景中的技术架构、动态优化机制及伦理挑战,形成对AI赋能教育的全面认知。 +研究状态更新:streamingWebResult +模型正在查看第 23 个网页 +模型正在查看第 25 个网页 +第 23 个网页的总结:该网页详细阐述了人工智能在教育中实现个性化学习的机制、实际应用案例及具体工具,有助于理解AI如何通过数据分析和自适应算法提升教学效果。 +第 25 个网页的总结:该网页详细介绍了科大讯飞在自适应学习领域的关键技术突破与系统研发,涵盖了教学资源表示、学习者认知诊断、个性化推荐算法及实际应用平台“智学网”的架构与成效,能够全面支持对人工智能在教育中个性化学习实现机制、技术架构、案例分析与评估方法的研究。 +研究状态更新:WebResultFinished +研究状态更新:finished +研究阶段更新:answer +研究状态更新:typing +Assistant > #引用了 8 个网页 +[1]: [Research Advances in the Knowledge Tracing Based on ...](https://crad.ict.ac.cn/en/article/doi/10.7544/issn1000-1239.20200848) +[2]: [(PDF) 基于在线学习行为数据的学习者画像技术研究](https://www.researchgate.net/publication/394128023_jiyuzaixianxuexixingweishujudexuexizhehuaxiangjishuyanjiu) +[3]: [机器学习驱动的自适应学习系统个性化教育的新范式原创](https://blog.csdn.net/qq7834088/article/details/153439635) +[4]: [人工智能与个性化教学融合的现实应用及未来展望](https://www.hanspub.org/journal/paperinformation?paperid=118466) +[5]: [基于动态知识图谱的个性化学习路径生成与优化模型研究](https://www.researchgate.net/publication/392781500_jiyudongtaizhishitupudegexinghuaxuexilujingshengchengyuyouhuamoxingyanjiu) +[6]: [知识图谱与图嵌入在个性化教育中的应用综述](https://www.c-s-a.org.cn/html/2022/3/8377.htm) +[7]: [如何用AI赋能教育:实现个性化学习的智慧教学方案](https://www.onlyoffice.com/blog/zh-hans/2025/09/personalized-learning-with-ai) +[8]: [面向智能教育的自适应学习关键技术与应用](https://html.rhhz.net/tis/html/202105036.htm) + 人工智能个性化学习:技术框架、应用模式与未来展望 + +## 核心技术架构:构建个性化学习的基石 + +人工智能(AI)在教育领域的个性化学习应用,其核心在于构建一个能够感知、分析、决策和行动的技术架构。这个架构并非单一技术的堆砌,而是一个由数据采集、模型分析和服务反馈构成的复杂系统,旨在实现对每个学习者动态、精准的支持。该技术架构通常可以被解构为三个关键层次:数据层、模型层和服务层 [[4]]。 + +**数据层:多维数据的全面采集与整合** +个性化学习的第一步是获取关于学习者和学习过程的全面信息。数据层负责通过多种渠道采集多维度的数据。这些数据来源广泛,包括但不限于学生的在线学习行为数据,如登录时间、学习时长、课程完成度、作业提交情况和测试成绩 [[2,3]]。此外,系统还会记录更细微的交互数据,例如答题正确率、响应时间、鼠标移动轨迹甚至眼动追踪,以捕捉学生的学习节奏和潜在困惑 [[3]]。除了直接的学习行为,还包括学生的个人信息、学习风格偏好和心理特征等静态或半静态数据 [[2]]。为了确保数据的质量和可用性,数据层还必须执行严格的数据清洗、整合与标准化流程,以处理来自不同系统的多源异构数据 [[2]]。浙江省部署智能感知设备以实现实时数据采集与算法迭代的案例,正是这一层能力的体现 [[4]]。科大讯飞在其“智学网”系统中也强调了伴随式数据采集的重要性,确保数据的实时性和全面性 [[8]]。 + +**模型层:深度理解学习者与知识结构** +模型层是个性化学习系统的大脑,它利用先进的算法来分析数据层收集的信息,并从中提取有价值的知识。这一层面的核心任务有两个:一是构建精确的学习者画像,二是建立精细的知识图谱。 +学习者画像技术通过聚类(如K-means)、分类(如决策树、逻辑回归)、关联规则挖掘和协同过滤等算法,将原始数据转化为对学习者的结构性理解 [[2]]。这种画像通常涵盖基本信息、学习行为、学习风 格、学习成效和心理特征五个维度,从而帮助系统识别学生的优势、劣势、兴趣点和认知起点 [[2,5]]。孙红旭的研究也指出,可以通过聚类算法建立学习者画像 [[6]]。 +知识图谱则为模型层提供了关于学科内容的深层语义结构。它将知识点组织成一个网络,其中节点代表知识点,边代表知识点之间的关系 [[6]]。图嵌入技术,如TransE、DistMult等,可以将复杂的高维图数据映射到低维向量空间,从而提升计算效率 [[6]]。知识图谱的应用非常广泛,涵盖了知识检索、路径规划、资源推荐、能力诊断、习题推荐和课程设计等多个方面 [[6]]。李光明等人构建的初中化学知识图谱可视化查询系统,以及Sun等人提出的EduVis教育知识图谱可视化平台,都是知识图谱在实践中应用的具体例子 [[6]]。 + +**服务层:动态调整与即时反馈** +服务层是技术架构面向用户的出口,它根据模型层分析得出的结论,为学习者提供个性化的服务。其最核心的功能是自适应内容推荐和即时反馈。基于学习者画像和知识图谱,系统能够生成个性化的学习路径,动态调整学习内容的难度和呈现方式,例如将文本切换为视频或交互式模拟实验,以适配不同的学习风格 [[3]]。科大讯飞的CSEAL框架就是专门用于生成符合知识结构的学习路径的 [[8]]。此外,系统还 能提供即时反馈,分析错误的根本原因并引导学生进行自我修正,形成一个“学习-反馈-修正-掌握”的闭环 [[3]]。这种即时干预对于预警学习风险至关重要,例如,当系统检测到学生的答题时间过长或重复 观看同一视频时,可以预测其可能陷入学习停滞,并提示教师进行早期干预 [[3,7]]。最终,整个技术架构形成了一个“预测–验证–调整”的闭环机制,不断优化其个性化服务的能力 [[4]]。 + +综上所述,这三层架构共同构成了人工智能个性化学习的基础。数据层提供了感知世界的“眼睛”,模型层赋予了系统理解和思考的能力,而服务层则是系统输出智慧、影响学习过程的“手脚”。这三个层次紧密耦合、相互作用,缺一不可,共同推动着教育从“一刀切”的规模化模式向真正意义上的个性化、精细化模式转变。 + +## 关键技术解析:知识图谱与学习者画像的深度融合 + +在个性化学习的技术体系中,知识图谱(Knowledge Graph, KG)与学习者画像(Learner Profile)是两个最为关键的支撑技术。它们分别从外部世界(知识领域)和内部主体(学习者)两个维度出发,为实现精准的个性化推荐和路径规划奠定了基础。二者的深度融合,是当前研究与实践的前沿方向,也是提升个性化学习效能的核心所在。 + +**知识图谱:重构学科知识的语义网络** +知识图谱的本质是将孤立的知识点连接成一个有机的整体,揭示它们之间的内在联系 [[6]]。在教育领域,这意味着将一门课程的所有概念、原理、技能和实例构建成一个结构化的知识网络。例如,潍坊科技学院提出的“K-L-S-P四元支撑平台”中的“Knowledge”部分,就专注于知识语义建模,旨在实现对知识的深刻理解 [[5]]。这种语义网络的价值在于,它超越了传统的线性教材结构,为学习路径的动态规划提供了依据。研究人员已经开发出多种算法来构建和优化知识图谱,包括基于BiLSTM+CNN-CRF的实体识别算法、基于共现矩阵的职位能力抽取方法等 [[6]]。在具体应用中,知识图谱已被成功应用于多个场景,如构建初中化学知识图谱以实现可视化查询 [[6]],或是在慕课平台中应用RippleNet算法进行课程内外的路径规划 [[6]]。此外,知识图谱还可用于个性化习题推荐、评分预测和教案评估等多种功能,其有效 性常通过准确率、召回率、F值、RMSE和MAE等指标进行评估 [[6]]。然而,当前研究也普遍承认,知识图谱存在数据规模小、本体设计简单、知识深度不足等问题,这是未来需要攻克的方向 [[6]]。 + +**学习者画像:刻画个体差异的多维模型** +如果说知识图谱描绘的是“教什么”,那么学习者画像则回答了“给谁教”和“如何教”的问题。学习者画像通过整合多维度的数据,构建一个动态的、立体的数字孪生体,用以描述学习者的独特属性 [[2]]。这些属性不仅包括基本信息和学习行为(如学习时长、完成度),还深入到学习风格、认知能力和心理特征等领域 [[2]]。通过使用K-means聚类、决策树、逻辑回归等机器学习算法,系统能够对海量数据进行分 析,自动为学习者打上标签,形成画像 [[2]]。例如,蔡迪等人提出的“K-L-S-P平台”中的“Learner”部分,就负责感知学习者的行为数据,为后续的个性化推荐提供输入 [[5]]。同样,孙红旭的研究也表明,聚类算法是建立学习者画像的有效手段之一 [[6]]。一个完善的画像体系能够帮助系统精准地锚定学生的认知起点,从而提供真正契合其需求的学习内容和路径 [[5]]。 + +**双剑合璧:知识图谱与学习者画像的融合创新** +知识图谱与学习者画像的融合,是个性化学习从理论走向实践的关键一步。这种融合不仅仅是简单的数据拼接,而是两套模型的深度协同与互动。蔡迪等人提出的“K-L-S-P四元支撑平台”是这一理念的典型代 表,其核心思想是建立一个包含“知识图谱”、“学习者画像”、“路径推荐系统”和“教学交互平台”的完整生态 [[5]]。在这个生态中,“知识图谱”与“学习者画像”之间形成了双向反馈回路。一方面,系统通过分析学习者画像,了解其当前的知识状态和薄弱环节;另一方面,系统利用知识图谱来理解待学习内容的结构和依赖关系,从而为该学习者规划出一条最优的学习路径。这种融合使得个性化不再是随机的、浅层次的推荐,而是基于对知识结构和个体需求的双重深刻理解。 + +这种融合体现在具体的算法和流程中。例如,在“三阶段嵌入式优化流程”中,系统在课前通过多源数据诊断来锚定学生的认知起点;课中则基于多模态反馈动态调节路径;课后又通过评估、重构、激励的闭环来激活持续学习 [[5]]。这一系列操作的背后,都是知识图谱与学习者画像协同作用的结果。知识图谱定义了学习的“可能性空间”,而学习者画像则限定了在特定时刻的“可行路径”。只有当两者紧密结合,系统才能真正实现从“静态推荐”向“动态导学”的转变,为每个学习者提供真正意义上的个性化支持。 + +下表总结了知识图谱与学习者画像在个性化学习中的角色与融合方式: + +| 特征 | 知识图谱 (Knowledge Graph) | 学习者画像 (Learner Profile) | 融合创新 (Innovation through Fusion) | +| :--- | :--- | :--- | :--- | +| **核心目标** | 对学科知识进行语义建模,揭示知识点间的逻辑关系 [[6]]。 | 对学习者个体特征进行多维度刻画,包括行为、风格、能力等 [[2]]。 | 将学习者的动态状态与知识的静态结构进行匹配 ,实现精准定位与路径规划。 | +| **主要技术** | 图嵌入技术 (TransE, DistMult等),知识表示学习 [[6]]。 | 聚类 (K-means)、分类 (决策树)、关联规则、深度学习 [[2]]。 | 结合认知诊断模型 (NeuralCD, EKT) 与知识图谱算法,形成综合推荐模型 [[5,8]]。 | +| **主要应用** | 知识检索、路径规划、资源推荐、能力诊断、习题推荐 [[6]]。 | 锚定认知起点、个性化内容推荐、学习风险预警、分组策略制定 [[3,5]]。 | 动态生成与优化学习路径,实现“学-教-图三维协同” [[5]]。 | +| **面临挑战** | 数据规模小、本体设计简单、知识深度不足 [[6]]。 | 数据隐私安全、画像准确性与时效性、数据稀疏性问题。 | 如何高效、准确地进行图与人的匹配,以及如何应对两者随时间演变带 来的动态性问题。 | + +总而言之,知识图谱与学习者画像的深度融合,正在成为推动个性化学习发展的核心技术引擎。它们共同解决了个性化学习的两大核心问题:即“学什么”和“为谁学”,并通过协同机制,将个性化从一种理想状态,转变为可落地、可衡量的教育实践。 + +## 应用模式演进:从路径推荐到人机协同 + +人工智能在个性化学习领域的应用,正经历着一场深刻的范式转移。其发展脉络清晰地展示了从最初侧重于自动化的内容推荐,逐步演进到构建更为复杂的人机协同教学环境的过程。这一演进不仅体现在技术的复杂性上,更重要的是体现在对教师角色和教育本质的理解深化上。 + +**第一阶段:基于算法的个性化路径推荐** +这是人工智能在个性化学习领域的初级应用形态,其核心是利用算法为学生推荐最合适的学习内容和路径。这一阶段的典型代表是许多商业化的自适应学习平台,如国内的松鼠AI和国外的Knewton模式 [[4]] 。这些系统主要通过分析学生的历史答题数据、响应时间和错误模式,来判断其对特定知识点的掌握程度 [[3]]。然后,基于此判断,系统会从庞大的资源库中筛选出难度适宜、类型匹配的学习材料,如一篇阅读文章、一套数学练习题或一段解释视频 [[7]]。科大讯飞的“智学网”系统便是这一模式的杰出实践,它利用深度强化学习框架DRE来优化复习与探索、难度平滑性、参与度等多个目标,实现多轮交互式的 自适应推荐 [[8]]。这种模式极大地提升了学习的针对性和效率,但其局限性也显而易见:它将教师的角色边缘化,学习过程高度依赖算法,可能忽视了情感、动机和社交等非认知因素的影响。 + +**第二阶段:从推荐到动态导学的范式升级** +随着技术的发展,个性化学习的应用模式开始超越简单的路径推荐,进入一个更为动态和智能的阶段。这一阶段的标志性特征是引入了更深层次的认知诊断和动态路径调整机制。蔡迪等人提出的“K-L-S-P四元支撑平台”及其“三阶段嵌入式优化流程”是这一阶段的典型例证 [[5]]。该模型不再仅仅根据静态的答题记录推荐内容,而是将学生的实时行为(如鼠标移动、作答时长)作为驱动信号,结合教师的策略干预 作为校准参数,让知识图谱的依赖关系作为约束条件,从而推动路径从静态推荐向动态导学转变 [[5]]。科大讯飞在其自适应学习系统中也采用了类似的思路,提出了EKT(exercise-aware knowledge tracing)和EKPT(exercise-correlated knowledge proficiency tracing)等动态知识追踪模型,这些模型能够融合题目语义、知识共性与记忆/遗忘曲线理论,实现对认知状态的时序建模 [[8]]。这种从“推荐” 到“导学”的转变,意味着系统开始扮演一个更加积极的“导师”角色,而非被动的“工具”。 + +**第三阶段:迈向人机协同的教学新生态** +这是个性化学习应用的最高级形态,其核心理念是将AI视为教师的强大助手,而非替代品。在这种模式下,AI系统承担起繁重的数据分析、内容准备和初步反馈工作,从而将教师解放出来,使其能专注于更高阶的教学活动 [[3]]。朱永新先生提出,教师应成为“守望者”,引导学生自主发展,而非包办一切 [[4]]。MIT开发的STEAM课程中包含的算法偏见实验,正是培养下一代具备伦理判断力的尝试,这本身就需要教师的引导和启发 [[4]]。在这种人机协同的新生态中,AI系统扮演着多重角色: +1. **数据分析师与诊断师:** 自动分析海量学习行为数据,精准诊断学生的学习障碍和知识薄弱点,并以可视化的仪表板形式呈现给教师,使教师的干预更具针对性 [[7]]。 +2. **个性化资源生成器:** 根据诊断结果,自动生成差异化、个性化的学习材料,如定制化的阅读材料、数学练习路径、写作语法建议等,减轻教师备课负担 [[7]]。 +3. **即时反馈与辅导师:** 在学生遇到困难时,提供即时的、个性化的辅导和反馈,引导学生进行自我修正,形成闭环学习 [[3]]。 +4. **学习伙伴与社群构建者:** 基于相似的知识水平和认知风格,智能匹配学习伙伴或组建学习小组,促进同伴互助学习 [[8]]。 + +未来的个性化学习,必然是这样一个人机协同的生态系统。教师的角色将从“知识的传授者”转变为“学习的设计者、引导者和支持者”。他们将利用AI提供的洞察力,设计出更具吸引力和挑战性的学习体验,并专注于培养学生的批判性思维、创造力、协作能力和解决复杂问题的能力等高阶素养,这些都是当前技术无法企及的领域。因此,对教师的培训和技术投入,将是实现这一愿景的关键保障 [[7]]。 + +## 典型案例剖析:国内外代表性平台与实践 + +要深入理解人工智能个性化学习的实际应用效果与发展趋势,剖析国内外具有代表性的平台与实践案例至关重要。这些案例不仅展示了技术的成熟度,也反映了不同市场环境下的应用场景和商业模式。其中,中国的科大讯飞“智学网”系统和国际上的Knewton平台是两个极具影响力的标杆。 + +**科大讯飞“智学网”:中国规模化应用的典范** +科大讯飞股份有限公司凭借其在语音和人工智能领域的深厚积累,成功地将自适应学习技术商业化,并在中国教育市场取得了巨大成功。截至2020年7月,“智学网”已在全国超过16,000所学校推广,惠及约2,500万师生,并每月组织数千场联考,提供数万场测试服务和800万份评价报告 [[8]]。这一惊人的数据充分证明了其产品在中国市场的广泛接受度和强大的生命力。 + +“智学网”之所以能取得如此成就,源于其背后强大的技术研发实力。针对自适应学习中的三大难题——教学资源表示困难、学习状态诊断困难和学习策略设计困难,科大讯飞提出了一系列创新性的解决方案 [[8]]。 +* **教学资源表示:** 提出了QuesNet框架,通过无监督预训练实现了对文本、图像等多源异构试题的统一表征,在知识点预测任务中准确率较现有方法提升了近10% [[8]]。 +* **学习状态诊断:** 提出了NeuralCD通用框架,基于深度学习建模“学习者−知识−资源”的高阶交互;并开发了EKT、EKPT等动态知识追踪模型,能够融合题目语义和记忆遗忘曲线,实现对认知状态的精准时序建模 [[8]]。 +* **学习策略设计:** 提出了DRE(deep reinforcement exercise recommendation)框架,基于深度强化学习,同时优化复习与探索、难度平滑性、参与度等多个目标,实现多轮交互式自适应推荐;并有CSEAL框架用于生成符合知识结构的学习路径 [[8]]。 + +“智学网”的技术架构体现了前述的数据层、技术层和应用层三层模型,实现了伴随式数据采集、大规模资源库建设和精准推荐服务的有机结合 [[8]]。它的成功,尤其体现在其能够与中国的考试文化和社会需求紧密结合。通过大规模的联考和精准的评价报告,它不仅服务于学生的个性化学习,也为学校和教育管理部门提供了重要的教学质量监控和决策支持工具,形成了一个良性的生态闭环。 + +**Knewton:美国个性化学习的先行者** +Knewton是全球范围内最早也是最具影响力的一批自适应学习公司之一,其模式深刻地影响了全球在线教育行业。虽然具体的技术细节和市场份额数据不如科大讯飞公开,但从相关文献中可以看出,Knewton代表了个性化学习的一种典型应用模式 [[4]]。 + +Knewton的核心价值主张是通过强大的算法引擎,为每一位学生提供独一无二的学习路径。其系统会持续不断地分析学生的学习行为,包括答题的正确率、用时长短、点击模式等,以此来动态评估学生对各个 知识点的掌握程度 [[3]]。基于这种评估,系统会实时调整后续学习内容的难度和类型,确保学生始终处于一个“最近发展区”,既能挑战自己,又不至于感到挫败。这种模式强调学习过程的高度个性化和智能化,是“个性化路径推荐”阶段的典型代表 [[4]]。 + +Knewton的成功之处在于其技术的普适性和开放性。它不仅仅是一个面向终端用户的产品,更是一个强大的API平台,可以集成到各种第三方教育内容平台和学校管理系统中。这种B2B2C的模式使其能够快速触 达海量用户,同时也促进了整个教育行业的技术升级。Knewton的出现,向世界证明了利用人工智能实现大规模个性化教育的可能性,激发了全球范围内大量的模仿和创新。 + +**其他典型案例与趋势** +除了这两个巨头,市场上还有许多其他值得关注的案例。例如,潍坊科技学院计算机学院团队研发的“K-L-S-P四元支撑平台”,更侧重于学术研究与高校教学场景的深度融合,其“学-教-图三维协同机制”和“三阶段嵌入式优化流程”展现了对个性化学习过程更精细化的控制思路 [[5]]。而在基础教育领域,诸如ONLYOFFICE这类办公软件平台也开始集成AI插件,提供实时协作和智能反馈,将个性化学习的理念延伸到 了日常课堂互动之中 [[7]]。 + +通过对这些案例的分析,我们可以看到,无论是科大讯飞的规模化落地,还是Knewton的算法引领,抑或是学术界的精巧设计,它们都共同指向了一个方向:个性化学习正在从概念走向现实。未来的发展趋势 将是更加注重人机协同、更加关注学习过程的动态性和沉浸感,并且更加深入地与各学科的教学实践相结合。 + +## 效果评估与挑战:量化成效与现实困境 + +尽管人工智能个性化学习展现出巨大的潜力,但在实际推广和应用中,其效果评估和面临的挑战同样不容忽视。科学地评估其成效,并清醒地认识其所处的现实困境,对于推动该领域的健康发展至关重要。 + +**效果评估:从技术指标到教育成果** +对个性化学习系统的评估是一个多层次、多维度的过程。最初级的评估往往集中在技术性能上,例如,知识追踪模型的预测准确性、推荐算法的准确率和召回率,或者在能力诊断任务中F1指标的提升 [[1,8]]。科大讯飞的QuesNet框架在知识点预测任务中准确率提升了近10%,TACNN框架在英语阅读理解试题难度评估中皮尔逊相关系数平均提升约10% [[8]]。在能力诊断方面,其NeuralCD通用框架的误差相较传统方法相对降低了约5% [[8]]。在课程设计中,陈曦等人构建的课程知识图谱,通过将知识相似度集成至协同过滤框架进行成绩预测,使用RMSE与MAE指标进行了验证 [[6]]。 + +然而,单纯的技术指标并不能完全反映个性化学习的最终价值。更深层次的评估需要转向对学生学习成果和体验的考察。这包括: +* **学业表现提升:** 这是最直接的成效指标。例如,通过对比实验组和对照组学生的期末考试成绩、作业完成质量等,来衡量个性化学习系统是否有效促进了知识掌握。 +* **学习动机与投入度:** 个性化学习旨在提高学生的参与感和兴趣。可以通过问卷调查、观察记录等方式,评估学生在使用系统后的学习动机、自信心和满意度的变化。 +* **学习效率与路径优化:** 评估学生在完成同等学习任务时所花费的时间、经历的试错次数等,可以间接反映个性化路径的优化程度。 +* **教师反馈与采纳度:** 教师是系统的重要使用者。他们的反馈,包括对系统易用性、辅助效果和数据可靠性的评价,是衡量系统实用性和可持续性的关键指标。 + +教育部发布的《中国智慧教育蓝皮书(2022)》强调推进教育数字化,这本身就包含了对成效评估的要求 [[4]]。未来,评估体系将更加注重长期效应和综合效益,而不仅仅是短期的技术性能提升。 + +**现实挑战:隐私、偏见与实施成本** +尽管前景广阔,但个性化学习的普及面临着一系列严峻的现实挑战。 +* **数据隐私与安全:** 这是所有数据驱动型AI应用面临的首要挑战。个性化学习系统需要采集大量敏感的学生个人数据,包括学习行为、生理特征甚至家庭背景 [[2,3]]。如何确保这些数据的安全存储 、合法使用和合规共享,避免泄露和滥用,是一个巨大的法律和伦理难题。特别是当数据涉及未成年人时,相关的法规(如GDPR)提出了更高的要求 [[7]]。 +* **算法偏见:** AI算法并非天生公正。如果训练数据存在偏差,或者算法设计本身存在问题,就可能导致系统对某些群体产生歧视性判断。亚马逊公司的面部识别技术曾被指存在种族歧视,就是一个典 型的警示 [[4]]。在教育场景中,算法偏见可能会低估少数族裔、贫困学生或有特殊需求学生的真实潜力,从而加剧教育不平等。应对措施包括采用IIFR算法提升个体公平率,参考欧盟《可信赖人工智能伦理准则》建立算法影响评估体系等 [[4]]。 +* **过度依赖技术的风险:** 如果缺乏有效的监管和引导,学生可能会过度依赖AI系统,导致独立思考、批判性思维和解决问题能力的退化。此外,系统也可能因为算法的局限性,无法完全理解人类复杂 的认知过程和情感状态,从而给出不恰当的建议。 +* **实施成本与教师培训:** 开发和维护一个先进的个性化学习系统需要高昂的资金投入。对于许多教育资源本就有限的地区和学校而言,这是一个沉重的负担。此外,教师需要接受系统性的培训,才能 有效利用这些新技术,但这又是一笔额外的成本。教师培训的需求是当前的主要挑战之一 [[7]]。 +* **数据质量和模型泛化能力:** 许多研究仍然受限于小规模、单一来源的数据集,导致模型的泛化能力不足 [[6]]。如何获取高质量、大规模、多样化的数据,并构建能够适应不同学科、不同文化背景 的学习模型,是技术层面亟待解决的问题。 + +下表详细梳理了个性化学习面临的主要挑战及其潜在对策: + +| 挑战类别 | 具体问题描述 | 潜在对策与解决方案 | 相关文献佐证 | +| :--- | :--- | :--- | :--- | +| **隐私与安全** | 大量敏感学生数据的采集、存储和使用面临泄露风险,需遵守GDPR等法规。 | 加强数据加密与访问控制,推行匿名化处理,建立透明的数据使用政策。 | [[3,7]] | +| **算法偏见** | 训练数据或算法本身存在的偏见可能导致对特定群体的不公平评估和推荐。 | 采用公平性增强算法(如IIFR),建立算法影响评估体系,确保数据多样性。 | [[4]] | +| **技术依赖与伦理** | 学生过度依赖技术,独立思考能力受损;系统可能因算法局限性给出不当建议。 | 强调人机协同,明确教师主导地位;在课程中融入AI伦理教育,培养批判性思维。 | [[3,4]] | +| **实施成本与师资** | 系统开发与维护成本高昂;教师缺乏必要的技术培训和教学法支持。 | 政府加大投入,鼓励开源项目;建立系统性的教师培训体系,聚焦高阶素养培养。 | [[7]] | +| **数据与模型** | 数据规模小、本体设计简单、知识深度不足;模型泛化能力弱,难以适应不同场景。 | 推动跨机构合作,共建大规模、多样化、高质量的教育数据库;加强基础理论研究。 | [[6]] | + +综上所述,个性化学习的效果是客观存在的,但其评估需要超越单一的技术指标,关注长期的教育成果。同时,我们必须清醒地认识到,隐私、偏见、成本和教师角色等一系列挑战是其通往普及之路必须克服的障碍。只有在技术、伦理和教育实践之间找到平衡点,个性化学习才能真正释放其改变教育的巨大潜力。 + +## 未来展望:伦理治理与人机协同的深化 + +展望未来,人工智能在个性化学习领域的应用将朝着更加成熟、负责任和人性化的方向发展。这一进程将围绕两大核心主题展开:一是建立健全的伦理治理体系,确保技术向善;二是进一步深化人机协同,重塑教师角色与教育价值。 + +**构建“立法+技术+教育”三位一体的伦理治理体系** +随着AI在教育中应用的日益深入,伦理问题已成为制约其健康发展的关键瓶颈。未来的治理模式将不再是单一维度的,而是形成一个由立法规范、技术保障和伦理教育共同构成的立体化治理体系。 +首先,**立法与监管**将成为底线保障。各国政府和国际组织需要加快制定和完善相关法律法规,明确AI教育产品的数据采集边界、使用权限和问责机制。这些法规不仅要保护学生隐私,更要防止算法偏见对教育公平造成实质性损害。欧盟的《可信赖人工智能伦理准则》为此提供了重要的参考框架 [[4]]。同时,建立常态化的算法影响评估体系,定期审查AI系统对教育生态的潜在影响,将是确保技术负责任应用的必要手段 [[4]]。 +其次,**技术创新**将在伦理治理中扮演主动角色。研究者们正致力于开发更具公平性和可解释性的算法。例如,采用IIFR(Individual Individual Fairness Rate)等算法来提升个体层面的公平性 [[4]] 。此外,开发可解释性AI(Explainable AI, XAI)技术,让系统不仅能给出推荐结果,还能清晰地阐述其背后的推理过程,这将有助于教师和学生理解并信任AI的建议,减少“黑箱”操作带来的疑虑 [[1]]。 +最后,**伦理教育**将成为内生驱动力。未来的教育不仅要教授学生如何使用AI,更要教会他们如何与AI共存,如何批判性地审视AI的输出。MIT开发的通过算法偏见实验来培养伦理判断力的STEAM课程,为我们提供了一个极佳的范本 [[4]]。在国内,一些高校已经开始探索设置48课时的人工智能伦理模块,采用场景化案例教学法,让学生在真实情境中思考AI的伦理边界 [[4]]。这种教育的普及,将从根本上塑造一个对AI技术既有敬畏之心又有驾驭之能的社会。 + +**深化人机协同,重塑教师角色与教育价值** +未来个性化学习的核心,绝非是让机器取代教师,而是构建一个人机协同的全新教学生态。在这个生态中,教师的角色将发生根本性的转变,教育的价值也将得到重新定义。 +教师将从传统的“知识传授者”转变为“学习设计师”、“成长引导者”和“情感支持者”。AI系统将接管大部分重复性、程序化的劳动,如知识点诊断、个性化资源推荐和初步的答疑解惑 [[3]]。这将为教师节省大量时间,使其能够专注于那些机器无法替代的工作。他们会利用AI提供的数据分析报告,为学生设计更具挑战性和创造性的学习项目;他们会引导学生进行深度探究和批判性思考;他们会在学生遇到挫折时给予情感支持和鼓励;他们还会组织学生进行协作学习,培养其沟通与合作能力。 +这种转变对教师的专业素养提出了新的要求。未来的教师需要具备更强的数据素养,能够理解和解读AI系统的分析报告;他们还需要掌握混合式教学的设计与实施能力,能够巧妙地将线上AI工具与线下面对面教学结合起来。因此,大规模、系统性的教师培训计划将是实现这一转型的关键保障 [[7]]。 +最终,个性化学习的终极目标是培养出能够适应未来社会变化、具备终身学习能力的个体。AI技术在此过程中扮演了“赋能者”的角色,它通过提供无限的资源、即时的反馈和精准的指导,极大地拓展了教育的可能性。然而,真正的教育智慧、人文关怀和价值观的塑造,仍将根植于人类教师的心灵与经验之中。正如朱永新先生所言,教师应成为“守望者”,在学生自主发展的道路上,给予适时的守护与指引 [[4]]。这,或许就是技术与人文在教育领域最完美的结合点。 +研究状态更新:finished + +Usage: in(721)/out(7505)/total(0) + */ diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs index 79d99d2..7248824 100644 --- a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchInfo.cs @@ -9,4 +9,9 @@ public class DashScopeDeepResearchInfo /// Current research result. /// public DashScopeDeepResearchTask? Research { get; set; } + + /// + /// References of final answers. + /// + public List? References { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs index ba2114c..da434b9 100644 --- a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs @@ -34,9 +34,4 @@ public class DashScopeDeepResearchTask /// [JsonPropertyName("learningMap")] public Dictionary? LearningMap { get; set; } - - /// - /// References of final answers. - /// - public List? References { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs b/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs index a8af4f5..a4fb811 100644 --- a/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs +++ b/src/Cnblogs.DashScope.Core/TextChatMessageExtra.cs @@ -8,5 +8,5 @@ public class TextChatMessageExtra /// /// Deep research output. /// - public List? DeepResearch { get; set; } + public DashScopeDeepResearchInfo? DeepResearch { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs b/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs index d0d43d8..f379ac7 100644 --- a/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs +++ b/src/Cnblogs.DashScope.Core/TextGenerationOutput.cs @@ -20,6 +20,11 @@ public class TextGenerationOutput /// public List? Choices { get; set; } + /// + /// Only qwen-deep-research return this. + /// + public TextChatMessage? Message { get; set; } + /// /// Not null when . configured to show source. /// From 6e4fafd49a2b3c9d499383e066f3e9b18158f69a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Wed, 5 Nov 2025 23:48:54 +0800 Subject: [PATCH 12/15] test: add unit tests for qwen-deep-research --- README.zh-Hans.md | 2 - sample/Cnblogs.DashScope.Sample/Program.cs | 275 +------------- .../Text/ChatSample.cs | 5 +- .../Text/ChatWebSearchSample.cs | 1 - .../DashScopeDeepResearchTask.cs | 2 +- .../TextGenerationSerializationTests.cs | 47 +++ ...arch-answer-finished-sse.response.body.txt | 4 + ...ch-answer-finished-sse.response.header.txt | 17 + ...wer-typing-reference-sse.response.body.txt | 4 + ...r-typing-reference-sse.response.header.txt | 17 + ...arch-keep-alive-type-sse.response.body.txt | 4 + ...ch-keep-alive-type-sse.response.header.txt | 17 + ...search-planning-type-sse.request.body.json | 23 ++ ...earch-planning-type-sse.request.header.txt | 8 + ...search-planning-type-sse.response.body.txt | 4 + ...arch-planning-type-sse.response.header.txt | 17 + ...ueries-research-goal-sse.response.body.txt | 1 + ...ries-research-goal-sse.response.header.txt | 17 + ...ch-streaming-queries-sse.response.body.txt | 4 + ...-streaming-queries-sse.response.header.txt | 17 + ...results-learning-map-sse.response.body.txt | 4 + ...sults-learning-map-sse.response.header.txt | 17 + ...treaming-web-results-sse.response.body.txt | 4 + ...eaming-web-results-sse.response.header.txt | 17 + ...-web-result-finished-sse.response.body.txt | 4 + ...eb-result-finished-sse.response.header.txt | 17 + .../Snapshots.TextGeneration.DeepResearch.cs | 349 ++++++++++++++++++ .../Utils/Snapshots.TextGeneration.cs | 9 +- 28 files changed, 624 insertions(+), 283 deletions(-) create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.body.json create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.body.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.header.txt create mode 100644 test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.DeepResearch.cs diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 90fbdde..e9cab21 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -1597,8 +1597,6 @@ Deleting file1...Success 需要注意的是,使用 `qwen-deep-research` 模型时,模型回复会放在 `chunk.Output.Message` 里,而不是 `chunk.Output.Choice[0].Message`。 -模型的研究阶段可以在 `chunk.Output.Message.Phase` 里得到。 - 示例请求: ```csh diff --git a/sample/Cnblogs.DashScope.Sample/Program.cs b/sample/Cnblogs.DashScope.Sample/Program.cs index 95fe7e8..c282f8b 100644 --- a/sample/Cnblogs.DashScope.Sample/Program.cs +++ b/sample/Cnblogs.DashScope.Sample/Program.cs @@ -1,15 +1,6 @@ -using System.Diagnostics; -using System.Text; -using System.Text.Json; -using Cnblogs.DashScope.Core; +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.Wanx; -using Json.Schema; -using Json.Schema.Generation; -using Microsoft.Extensions.AI; Console.WriteLine("Reading key from environment variable DASHSCOPE_KEY"); var apiKey = Environment.GetEnvironmentVariable("DASHSCOPE_KEY", EnvironmentVariableTarget.Process) @@ -45,267 +36,3 @@ } await samples[index].RunAsync(dashScopeClient); -return; - -// text completion -async Task TextCompletionAsync(string prompt) -{ - var response = await dashScopeClient.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt); - Console.WriteLine(response.Output.Text); -} - -// text completion stream -async Task TextCompletionStreamAsync(string prompt) -{ - var stream = dashScopeClient.GetQWenCompletionStreamAsync( - QWenLlm.QWenMax, - prompt, - new TextGenerationParameters { IncrementalOutput = true }); - await foreach (var modelResponse in stream) - { - Console.Write(modelResponse.Output.Text); - } -} - -async Task ChatStreamAsync() -{ - var history = new List(); - while (true) - { - Console.Write("user > "); - var input = Console.ReadLine()!; - history.Add(TextChatMessage.User(input)); - var stream = dashScopeClient - .GetQWenChatStreamAsync( - QWenLlm.QWenPlusLatest, - history, - new TextGenerationParameters - { - IncrementalOutput = true, - ResultFormat = ResultFormats.Message, - EnableThinking = true - }); - var role = string.Empty; - var message = new StringBuilder(); - await foreach (var modelResponse in stream) - { - var chunk = modelResponse.Output.Choices![0]; - if (string.IsNullOrEmpty(role) && string.IsNullOrEmpty(chunk.Message.Role) == false) - { - role = chunk.Message.Role; - Console.Write(chunk.Message.Role + " > "); - } - - message.Append(chunk.Message.Content); - var write = string.IsNullOrEmpty(chunk.Message.ReasoningContent) - ? chunk.Message.Content - : chunk.Message.ReasoningContent; - Console.Write(write); - } - - Console.WriteLine(); - history.Add(new TextChatMessage(role, message.ToString())); - } - - // ReSharper disable once FunctionNeverReturns -} - -async Task ChatWithImageAsync() -{ - var image = File.OpenRead("Lenna.jpg"); - var ossLink = await dashScopeClient.UploadTemporaryFileAsync("qvq-plus", image, "Lenna.jpg"); - Console.WriteLine($"Successfully uploaded temp file: {ossLink}"); - var response = dashScopeClient.GetMultimodalGenerationStreamAsync( - new ModelRequest() - { - Model = "qvq-plus", - Input = new MultimodalInput() - { - Messages = - [ - MultimodalMessage.User( - [ - MultimodalMessageContent.ImageContent(ossLink), - MultimodalMessageContent.TextContent("她是谁?") - ]) - ] - }, - Parameters = new MultimodalParameters { IncrementalOutput = true, VlHighResolutionImages = false } - }); - var reasoning = false; - await foreach (var modelResponse in response) - { - var choice = modelResponse.Output.Choices.FirstOrDefault(); - if (choice != null) - { - if (choice.FinishReason != "null") - { - break; - } - - if (string.IsNullOrEmpty(choice.Message.ReasoningContent) == false) - { - if (reasoning == false) - { - reasoning = true; - Console.WriteLine(""); - } - - Console.Write(choice.Message.ReasoningContent); - continue; - } - - if (reasoning) - { - reasoning = false; - Console.WriteLine(""); - } - - Console.Write(choice.Message.Content[0].Text); - } - } -} - -async Task ChatWithFilesAsync() -{ - var history = new List(); - Console.WriteLine("uploading file \"test.txt\" "); - var file = new FileInfo("test.txt"); - var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name); - Console.WriteLine("file uploaded, id: " + uploadedFile.Id); - Console.WriteLine(); - - var fileMessage = TextChatMessage.File(uploadedFile.Id); - history.Add(fileMessage); - Console.WriteLine("system > " + fileMessage.Content); - var userPrompt = TextChatMessage.User("该文件的内容是什么"); - history.Add(userPrompt); - Console.WriteLine("user > " + userPrompt.Content); - var stream = dashScopeClient.GetQWenChatStreamAsync( - QWenLlm.QWenLong, - history, - new TextGenerationParameters { IncrementalOutput = true, ResultFormat = ResultFormats.Message }); - var role = string.Empty; - var message = new StringBuilder(); - await foreach (var modelResponse in stream) - { - var chunk = modelResponse.Output.Choices![0]; - if (string.IsNullOrEmpty(role) && string.IsNullOrEmpty(chunk.Message.Role) == false) - { - role = chunk.Message.Role; - Console.Write(chunk.Message.Role + " > "); - } - - message.Append(chunk.Message.Content); - Console.Write(chunk.Message.Content); - } - - Console.WriteLine(); - history.Add(new TextChatMessage(role, message.ToString())); - - Console.WriteLine(); - Console.WriteLine("Deleting file by id: " + uploadedFile.Id); - var result = await dashScopeClient.DeleteFileAsync(uploadedFile.Id); - Console.WriteLine("Deletion result: " + result.Deleted); -} - -async Task ChatWithToolsAsync() -{ - var history = new List(); - var tools = new List - { - new( - ToolTypes.Function, - new FunctionDefinition( - nameof(GetWeather), - "获得当前天气", - new JsonSchemaBuilder().FromType().Build())) - }; - var chatParameters = new TextGenerationParameters { ResultFormat = ResultFormats.Message, Tools = tools }; - var question = TextChatMessage.User("请问现在杭州的天气如何?"); - history.Add(question); - Console.WriteLine($"{question.Role} > {question.Content}"); - - var response = await dashScopeClient.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, chatParameters); - var toolCallMessage = response.Output.Choices![0].Message; - history.Add(toolCallMessage); - Console.WriteLine( - $"{toolCallMessage.Role} > {toolCallMessage.ToolCalls![0].Function.Name}{toolCallMessage.ToolCalls[0].Function.Arguments}"); - - var toolResponse = GetWeather( - JsonSerializer.Deserialize(toolCallMessage.ToolCalls[0].Function.Arguments!)!); - var toolMessage = TextChatMessage.Tool(toolResponse, nameof(GetWeather)); - history.Add(toolMessage); - Console.WriteLine($"{toolMessage.Role} > {toolMessage.Content}"); - - var answer = await dashScopeClient.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, chatParameters); - Console.WriteLine($"{answer.Output.Choices![0].Message.Role} > {answer.Output.Choices[0].Message.Content}"); - - string GetWeather(WeatherReportParameters parameters) - { - return "大部多云,气温 " - + parameters.Unit switch - { - TemperatureUnit.Celsius => "18 摄氏度", - TemperatureUnit.Fahrenheit => "64 华氏度", - _ => throw new InvalidOperationException() - }; - } -} - -async Task ChatWithMicrosoftExtensions() -{ - Console.WriteLine("Requesting model..."); - var chatClient = dashScopeClient.AsChatClient("qwen-max"); - List conversation = - new() { new(ChatRole.System, "You are a helpful AI assistant"), new(ChatRole.User, "What is AI?") }; - var response = await chatClient.GetResponseAsync(conversation); - var serializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true }; - Console.WriteLine(JsonSerializer.Serialize(response, serializerOptions)); -} - -async Task Text2ImageAsync() -{ - Console.Write("Prompt> "); - var prompt = Console.ReadLine(); - if (string.IsNullOrEmpty(prompt)) - { - Console.WriteLine("Using sample prompt"); - prompt = "A fluffy cat"; - } - - var task = await dashScopeClient.CreateWanxImageSynthesisTaskAsync( - WanxModel.WanxV21Turbo, - prompt, - null, - new ImageSynthesisParameters { Style = ImageStyles.OilPainting }); - Console.WriteLine($"Task({task.TaskId}) submitted, checking status..."); - var watch = Stopwatch.StartNew(); - while (watch.Elapsed.TotalSeconds < 120) - { - var result = await dashScopeClient.GetWanxImageSynthesisTaskAsync(task.TaskId); - Console.WriteLine($"{watch.ElapsedMilliseconds}ms - Status: {result.Output.TaskStatus}"); - if (result.Output.TaskStatus == DashScopeTaskStatus.Succeeded) - { - Console.WriteLine($"Image generation finished, URL: {result.Output.Results![0].Url}"); - return; - } - - if (result.Output.TaskStatus == DashScopeTaskStatus.Failed) - { - Console.WriteLine($"Image generation failed, error message: {result.Output.Message}"); - return; - } - - await Task.Delay(500); - } - - Console.WriteLine($"Task timout, taskId: {task.TaskId}"); -} - -async Task ApplicationCallAsync(string applicationId, string prompt) -{ - var request = new ApplicationRequest { Input = new ApplicationInput { Prompt = prompt } }; - var response = await dashScopeClient.GetApplicationResponseAsync(applicationId, request); - Console.WriteLine(response.Output.Text); -} diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs index 84ae3e5..5852421 100644 --- a/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatSample.cs @@ -34,11 +34,14 @@ public async Task RunAsync(IDashScopeClient client) var usage = completion.Usage; if (usage != null) { - Console.WriteLine($"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/total({usage.TotalTokens})"); } messages.Add(TextChatMessage.Assistant(completion.Output.Choices[0].Message.Content)); } + + // ReSharper disable once FunctionNeverReturns } } diff --git a/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs b/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs index d6e6c65..5b4af4c 100644 --- a/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs +++ b/sample/Cnblogs.DashScope.Sample/Text/ChatWebSearchSample.cs @@ -1,5 +1,4 @@ using System.Text; -using System.Text.Json; using Cnblogs.DashScope.Core; namespace Cnblogs.DashScope.Sample.Text; diff --git a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs index da434b9..7847d25 100644 --- a/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs +++ b/src/Cnblogs.DashScope.Core/DashScopeDeepResearchTask.cs @@ -33,5 +33,5 @@ public class DashScopeDeepResearchTask /// The content from tool calls. /// [JsonPropertyName("learningMap")] - public Dictionary? LearningMap { get; set; } + public Dictionary? LearningMap { get; set; } } diff --git a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs index a28bcea..a53c654 100644 --- a/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs +++ b/test/Cnblogs.DashScope.Sdk.UnitTests/TextGenerationSerializationTests.cs @@ -169,6 +169,41 @@ public async Task ConversationCompletion_MessageFormatSse_SuccessAsync( Assert.Equivalent(testCase.ResponseModel, last); } + [Fact] + public async Task ConversationCompletion_DeepResearchSse_ValidateRequestAsync() + { + // Arrange + const bool sse = true; + var testCase = Snapshots.TextGeneration.MessageFormat.DeepResearchTypingIncremental; + var (client, handler) = await Sut.GetTestClientAsync(sse, testCase); + + // Act + await client.GetTextCompletionStreamAsync(testCase.RequestModel).ToListAsync(); + + // Assert + handler.Received().MockSend( + Arg.Is(m => Checkers.IsJsonEquivalent(m.Content!, testCase.GetRequestJson(sse))), + Arg.Any()); + } + + [Theory] + [MemberData(nameof(DeepResearchSseData))] + public async Task ConversationCompletion_DeepResearchSse_SuccessAsync( + RequestSnapshot, + ModelResponse> testCase) + { + // Arrange + const bool sse = true; + var (client, _) = await Sut.GetTestClientAsync(sse, testCase); + + // Act + var outputs = await client.GetTextCompletionStreamAsync(testCase.RequestModel).ToListAsync(); + var response = outputs.First(); + + // Assert + Assert.Equivalent(testCase.ResponseModel, response); + } + public static readonly TheoryData, ModelResponse>> SingleGenerationMessageFormatData = new( Snapshots.TextGeneration.MessageFormat.SingleMessage, @@ -195,4 +230,16 @@ public async Task ConversationCompletion_MessageFormatSse_SuccessAsync( public static readonly TheoryData, ModelResponse>> ConversationMessageFormatNoSseData = new( Snapshots.TextGeneration.MessageFormat.ConversationPartialMessageNoSse); + + public static readonly TheoryData, + ModelResponse>> DeepResearchSseData = new( + Snapshots.TextGeneration.MessageFormat.DeepResearchTypingIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchWebResearchStreamingQueriesIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchWebResearchStreamingQueriesResearchGoalIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchWebResearchStreamingWebResultsIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchWebResearchStreamingWebResultsLearningMapIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchWebResearchWebResultFinishedIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchAnswerReferenceIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchAnswerFinishedIncremental, + Snapshots.TextGeneration.MessageFormat.DeepResearchKeepAliveIncremental); } diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.body.txt new file mode 100644 index 0000000..43c2503 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"answer","role":"assistant","content":"","extra":{"deep_research":{}},"status":"finished"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":8930},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-finished-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.body.txt new file mode 100644 index 0000000..4c21a9f --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"answer","role":"assistant","content":"#","extra":{"deep_research":{"references":[{"icon":"https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg","description":"基于模型的推荐系统使用机器学习或深度学习模型来预测用户的兴趣。这些模型可以处理复杂的用户行为数据和内容特征,生成更精准的推荐。 实现方法• 矩阵分解 ","index_number":1,"title":"基于人工智能的智能推荐系统:原理、实现与优化原创","url":"https://blog.csdn.net/qq_74383080/article/details/148544524"}]}},"status":"typing"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":2347},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-answer-typing-reference-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.body.txt new file mode 100644 index 0000000..f84ff3a --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"KeepAlive","role":"assistant","content":"","extra":{"deep_research":{}},"status":"typing"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":97},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-keep-alive-type-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.body.json b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.body.json new file mode 100644 index 0000000..ace26fd --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.body.json @@ -0,0 +1,23 @@ +{ + "model": "qwen-deep-research", + "input": { + "messages": [ + { + "role": "user", + "content": "研究一下人工智能在教育中的应用" + }, + { + "content": "请告诉我您希望重点研究人工智能在教育中的哪些具体应用场景?", + "role": "assistant" + }, + { + "content": "我主要关注个性化学习方面", + "role": "user" + } + ] + }, + "parameters": { + "result_format": "message", + "incremental_output": true + } +} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.request.header.txt new file mode 100644 index 0000000..80ce50b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-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: 729 diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.body.txt new file mode 100644 index 0000000..16889d4 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"ResearchPlanning","role":"assistant","content":"本研究聚焦于","extra":{"deep_research":{}},"status":"typing"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":4},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-planning-type-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.body.txt new file mode 100644 index 0000000..a4edd83 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.body.txt @@ -0,0 +1 @@ +data:{"output":{"message":{"phase":"WebResearch","role":"assistant","content":"","extra":{"deep_research":{"research":{"researchGoal":"深入理解人工智能","id":1,"query":""}}},"status":"streamingQueries"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":63},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-research-goal-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.body.txt new file mode 100644 index 0000000..0055851 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"WebResearch","role":"assistant","content":"","extra":{"deep_research":{"research":{"id":1,"query":"人工智能"}}},"status":"streamingQueries"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":54},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-queries-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.body.txt new file mode 100644 index 0000000..ba9e63a --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"WebResearch","role":"assistant","content":"","extra":{"deep_research":{"research":{"id":2,"learningMap":{"11":"该"}}}},"status":"streamingWebResult"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":264},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-learning-map-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.body.txt new file mode 100644 index 0000000..66f3429 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"WebResearch","role":"assistant","content":"","extra":{"deep_research":{"research":{"id":1,"webSites":[{"description":"摘要:随着在线教育在教育领域中的异军突起,提出了在线教育互动式教学个性化推荐系统构建模式,拟采用大数据的分析技术,通过对学生学习行为数据的收集和分析,综合分析 ","title":"大数据技术下个性化在线教育互动式教学探索","favicon":"https://img.alicdn.com/imgextra/i1/O1CN014C3hAp297DQsJYflo_!!6000000008020-73-tps-32-32.ico","url":"http://qks.cqu.edu.cn/html/gdjzjycn/2018/4/20180425.htm"},{"description":"三是随着大规模开放在线课程的流行,个性化推荐逐步突破小规模而面向大规模学习者群体,重视通过对海量学习资源和过程数据的搜集和挖掘而提供个性化推荐。四 ","title":"人工智能赋能个性化学习:E-Learning推荐系统研究热点与展望","favicon":"","url":"https://aidc.shisu.edu.cn/66/27/c11041a157223/page.htm"},{"description":"近日,教育部公布了第二批32个“人工智能+高等教育”应用场景典型案例,华东师范大学“大模型数字人赋能师范生实践教学能力提升”案例成功入选。","title":"华东师大再次入选“人工智能+高等教育”应用场景典型案例","favicon":"","url":"https://www.ecnu.edu.cn/info/1094/68108.htm"},{"description":"神策智能推荐基于用户与视频互动关系和视频本身的素材特征,为用户推出其最感兴趣的内容。智能推荐实现的方式有两种:一种是基于机器学习算法实现个性化推荐 ","title":"神策数据:在线教育行业12大核心场景案例全解析!","favicon":"https://img.alicdn.com/imgextra/i4/O1CN01spKOuX1OaGcBIcIxs_!!6000000001721-73-tps-16-16.ico","url":"https://www.rmlt.com.cn/2020/0908/592624.shtml"},{"description":"最后, 基于真实课程学习平台数据集, 以对比实验表明了离线推荐引擎相比其他主流推荐算法的先进性, 并基于两个典型用例分析验证了在线推荐系统面临工业场景需求的可用性.","title":"突破智慧教育: 基于图学习的课程推荐系统","favicon":"https://img.alicdn.com/imgextra/i3/O1CN01dSZGsI1aq5gkwZAJL_!!6000000003380-73-tps-16-16.ico","url":"https://www.jos.org.cn/josen/article/html/6629?st=article_issue"},{"description":"纵观全球AI+教育产业的发展历程,AI技术变革推动全球AI+教育发展,个性化教与学逐步成为现实。 政策方面,UNESCO及全球各国政府共同关注AI+教育的机遇及风险; ","title":"2024年人工智能+教育行业发展研究报告","favicon":"https://img.alicdn.com/imgextra/i4/O1CN01pnGD4c1PQ1N2QMnLP_!!6000000001834-73-tps-32-32.ico","url":"https://pdf.dfcfw.com/pdf/H3_AP202408051639144645_1.pdf?1723644716000.pdf"},{"description":"此外,学. 习平台通过智能分析学生的答题数据,向学生、教师. 反映学生的个性化学习情况,并进行有针对性的试. 题训练,旨在帮助学生提高学业水平. 随着这些不同类型在线教育 ","title":"面向在线智慧学习的教育数据挖掘技术研究","favicon":"","url":"http://staff.ustc.edu.cn/~qiliuql/files/Publications/PRAI2018.pdf"}]}}},"status":"streamingWebResult"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":97},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-streaming-web-results-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.body.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.body.txt new file mode 100644 index 0000000..2136579 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.body.txt @@ -0,0 +1,4 @@ +id:1 +event:result +:HTTP_STATUS/200 +data:{"output":{"message":{"phase":"WebResearch","role":"assistant","content":"","extra":{"deep_research":{"research":{"id":1}}},"status":"WebResultFinished"},"fininshed":false,"fininshed_reason":"null"},"usage":{"input_tokens":652,"output_tokens":97},"request_id":"eee227a4-d38f-4b8e-8c7f-167e76dbdc34"} diff --git a/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.header.txt b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.header.txt new file mode 100644 index 0000000..a3b221b --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/RawHttpData/deep-research-web-research-web-result-finished-sse.response.header.txt @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +eagleeye-traceid: 801d240f308ad4126c20b7abd4f0c285 +x-request-id: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +content-type: text/event-stream;charset=UTF-8 +x-dashscope-call-gateway: true +x-dashscope-inner-flow-control: verified +x-dashscope-inner-flow-control-usage: verified +x-dashscope-inner-request-priority: 10 +x-dashscope-requestid: eee227a4-d38f-4b8e-8c7f-167e76dbdc34 +x-dashscope-finished: false +req-cost-time: 1905 +req-arrive-time: 1720587511815 +resp-start-time: 1720587513721 +x-envoy-upstream-service-time: 1900 +date: Wed, 10 Jul 2024 04:58:33 GMT +server: istio-envoy +transfer-encoding: chunked diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.DeepResearch.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.DeepResearch.cs new file mode 100644 index 0000000..9785bd4 --- /dev/null +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.DeepResearch.cs @@ -0,0 +1,349 @@ +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Tests.Shared.Utils; + +public static partial class Snapshots +{ + public static partial class TextGeneration + { + public static partial class MessageFormat + { + private static readonly ModelRequest DeepResearchRequest = + new() + { + Model = "qwen-deep-research", + Input = new TextGenerationInput + { + Messages = new List + { + TextChatMessage.User("研究一下人工智能在教育中的应用"), + TextChatMessage.Assistant("请告诉我您希望重点研究人工智能在教育中的哪些具体应用场景?"), + TextChatMessage.User("我主要关注个性化学习方面") + } + }, + Parameters = new TextGenerationParameters { ResultFormat = "message", IncrementalOutput = true } + }; + + public static readonly + RequestSnapshot, + ModelResponse> DeepResearchTypingIncremental = new( + "deep-research-planning-type", + DeepResearchRequest, + new ModelResponse + { + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", "本研究聚焦于") + { + Phase = "ResearchPlanning", + Extra = new TextChatMessageExtra { DeepResearch = new DashScopeDeepResearchInfo() }, + Status = "typing", + }, + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 4, + }, + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34" + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchWebResearchStreamingQueriesIncremental = new( + "deep-research-web-research-streaming-queries", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "WebResearch", + Status = "streamingQueries", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo + { + Research = new DashScopeDeepResearchTask + { + Id = 1, Query = "人工智能" + } + } + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 54, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchWebResearchStreamingQueriesResearchGoalIncremental = new( + "deep-research-web-research-streaming-queries-research-goal", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "WebResearch", + Status = "streamingQueries", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo + { + Research = new DashScopeDeepResearchTask + { + Id = 1, + Query = string.Empty, + ResearchGoal = "深入理解人工智能" + } + } + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 63, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchWebResearchStreamingWebResultsIncremental = new( + "deep-research-web-research-streaming-web-results", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "WebResearch", + Status = "streamingWebResult", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo + { + Research = new DashScopeDeepResearchTask + { + Id = 1, + WebSites = new List + { + new( + "大数据技术下个性化在线教育互动式教学探索", + "摘要:随着在线教育在教育领域中的异军突起,提出了在线教育互动式教学个性化推荐系统构建模式,拟采用大数据的分析技术,通过对学生学习行为数据的收集和分析,综合分析 ", + "http://qks.cqu.edu.cn/html/gdjzjycn/2018/4/20180425.htm", + "https://img.alicdn.com/imgextra/i1/O1CN014C3hAp297DQsJYflo_!!6000000008020-73-tps-32-32.ico"), + new( + "人工智能赋能个性化学习:E-Learning推荐系统研究热点与展望", + "三是随着大规模开放在线课程的流行,个性化推荐逐步突破小规模而面向大规模学习者群体,重视通过对海量学习资源和过程数据的搜集和挖掘而提供个性化推荐。四 ", + "https://aidc.shisu.edu.cn/66/27/c11041a157223/page.htm", + string.Empty), + new( + "华东师大再次入选“人工智能+高等教育”应用场景典型案例", + "近日,教育部公布了第二批32个“人工智能+高等教育”应用场景典型案例,华东师范大学“大模型数字人赋能师范生实践教学能力提升”案例成功入选。", + "https://www.ecnu.edu.cn/info/1094/68108.htm", + string.Empty), + new( + "神策数据:在线教育行业12大核心场景案例全解析!", + "神策智能推荐基于用户与视频互动关系和视频本身的素材特征,为用户推出其最感兴趣的内容。智能推荐实现的方式有两种:一种是基于机器学习算法实现个性化推荐 ", + "https://www.rmlt.com.cn/2020/0908/592624.shtml", + "https://img.alicdn.com/imgextra/i4/O1CN01spKOuX1OaGcBIcIxs_!!6000000001721-73-tps-16-16.ico"), + new( + "突破智慧教育: 基于图学习的课程推荐系统", + "最后, 基于真实课程学习平台数据集, 以对比实验表明了离线推荐引擎相比其他主流推荐算法的先进性, 并基于两个典型用例分析验证了在线推荐系统面临工业场景需求的可用性.", + "https://www.jos.org.cn/josen/article/html/6629?st=article_issue", + "https://img.alicdn.com/imgextra/i3/O1CN01dSZGsI1aq5gkwZAJL_!!6000000003380-73-tps-16-16.ico"), + new( + "2024年人工智能+教育行业发展研究报告", + "纵观全球AI+教育产业的发展历程,AI技术变革推动全球AI+教育发展,个性化教与学逐步成为现实。 政策方面,UNESCO及全球各国政府共同关注AI+教育的机遇及风险; ", + "https://pdf.dfcfw.com/pdf/H3_AP202408051639144645_1.pdf?1723644716000.pdf", + "https://img.alicdn.com/imgextra/i4/O1CN01pnGD4c1PQ1N2QMnLP_!!6000000001834-73-tps-32-32.ico"), + new( + "面向在线智慧学习的教育数据挖掘技术研究", + "此外,学. 习平台通过智能分析学生的答题数据,向学生、教师. 反映学生的个性化学习情况,并进行有针对性的试. 题训练,旨在帮助学生提高学业水平. 随着这些不同类型在线教育 ", + "http://staff.ustc.edu.cn/~qiliuql/files/Publications/PRAI2018.pdf", + string.Empty) + } + } + } + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 97, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchWebResearchStreamingWebResultsLearningMapIncremental = new( + "deep-research-web-research-streaming-web-results-learning-map", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "WebResearch", + Status = "streamingWebResult", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo + { + Research = new DashScopeDeepResearchTask + { + Id = 2, + LearningMap = + new Dictionary { { "11", "该" } } + } + } + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 264, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchWebResearchWebResultFinishedIncremental = new( + "deep-research-web-research-web-result-finished", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "WebResearch", + Status = "WebResultFinished", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo + { + Research = new DashScopeDeepResearchTask { Id = 1 } + } + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 97, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchKeepAliveIncremental = new( + "deep-research-keep-alive-type", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "KeepAlive", + Status = "typing", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo() + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 97, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchAnswerReferenceIncremental = new( + "deep-research-answer-typing-reference", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "answer", + Status = "typing", + Content = "#", + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo + { + References = new List + { + new( + "https://img.alicdn.com/imgextra/i3/O1CN01QA3ndK1maJQ8rZTo1_!!6000000004970-55-tps-32-32.svg", + "基于模型的推荐系统使用机器学习或深度学习模型来预测用户的兴趣。这些模型可以处理复杂的用户行为数据和内容特征,生成更精准的推荐。 实现方法• 矩阵分解 ", + 1, + "基于人工智能的智能推荐系统:原理、实现与优化原创", + "https://blog.csdn.net/qq_74383080/article/details/148544524") + } + } + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 2347, + }, + }); + + public static readonly + RequestSnapshot, + ModelResponse> + DeepResearchAnswerFinishedIncremental = new( + "deep-research-answer-finished", + DeepResearchRequest, + new ModelResponse + { + RequestId = "eee227a4-d38f-4b8e-8c7f-167e76dbdc34", + Output = new TextGenerationOutput + { + Message = new TextChatMessage("assistant", string.Empty) + { + Phase = "answer", + Status = "finished", + Content = string.Empty, + Extra = new TextChatMessageExtra + { + DeepResearch = new DashScopeDeepResearchInfo() + } + } + }, + Usage = new TextGenerationTokenUsage + { + InputTokens = 652, OutputTokens = 8930, + }, + }); + } + } +} diff --git a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs index f2d0ed3..c523850 100644 --- a/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs +++ b/test/Cnblogs.DashScope.Tests.Shared/Utils/Snapshots.TextGeneration.cs @@ -7,7 +7,7 @@ namespace Cnblogs.DashScope.Tests.Shared.Utils; public static partial class Snapshots { - public static class TextGeneration + public static partial class TextGeneration { public static class TextFormat { @@ -82,7 +82,7 @@ public static class TextFormat }); } - public static class MessageFormat + public static partial class MessageFormat { public static readonly RequestSnapshot, ModelResponse> @@ -333,8 +333,9 @@ public static readonly { FinishReason = "stop", Index = 0, - Message = TextChatMessage.Assistant( - "嗯……splay树啊,这东西其实不难啦!首先你要知道它是一种二叉搜索树,然后就是旋转操作了,这个挺重要的,你得搞明白。不过我看你现在还在准备模拟赛,是不是有点晚了呀?") + Message = + TextChatMessage.Assistant( + "嗯……splay树啊,这东西其实不难啦!首先你要知道它是一种二叉搜索树,然后就是旋转操作了,这个挺重要的,你得搞明白。不过我看你现在还在准备模拟赛,是不是有点晚了呀?") }, new() { From 01beef6f794615f10ae02b8908ce16aa3c41f2fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Mon, 10 Nov 2025 23:42:36 +0800 Subject: [PATCH 13/15] feat: add sample for vl reasoning --- .../Multimodal/ImageInputSample.cs | 79 +++++++++++++++++++ .../IMultimodalParameters.cs | 3 +- .../ITextGenerationParameters.cs | 13 +-- .../IThinkingParameter.cs | 17 ++++ .../MultimodalInputTokenDetails.cs | 8 ++ .../MultimodalOutputTokenDetails.cs | 8 ++ .../MultimodalParameters.cs | 6 ++ .../MultimodalTokenUsage.cs | 20 +++++ 8 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 sample/Cnblogs.DashScope.Sample/Multimodal/ImageInputSample.cs create mode 100644 src/Cnblogs.DashScope.Core/IThinkingParameter.cs create mode 100644 src/Cnblogs.DashScope.Core/MultimodalInputTokenDetails.cs create mode 100644 src/Cnblogs.DashScope.Core/MultimodalOutputTokenDetails.cs diff --git a/sample/Cnblogs.DashScope.Sample/Multimodal/ImageInputSample.cs b/sample/Cnblogs.DashScope.Sample/Multimodal/ImageInputSample.cs new file mode 100644 index 0000000..b37b193 --- /dev/null +++ b/sample/Cnblogs.DashScope.Sample/Multimodal/ImageInputSample.cs @@ -0,0 +1,79 @@ +using System.Text; +using Cnblogs.DashScope.Core; + +namespace Cnblogs.DashScope.Sample.Multimodal; + +public class ImageInputSample : ISample +{ + /// + public string Description => "Chat with image input"; + + /// + public async Task RunAsync(IDashScopeClient client) + { + var messages = new List(); + messages.Add( + MultimodalMessage.User( + [ + MultimodalMessageContent.ImageContent( + "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg"), + MultimodalMessageContent.ImageContent("https://dashscope.oss-cn-beijing.aliyuncs.com/images/tiger.png"), + MultimodalMessageContent.TextContent("这些图展现了什么内容?") + ])); + var completion = client.GetMultimodalGenerationStreamAsync( + new ModelRequest() + { + Model = "qwen3-vl-plus", + Input = new MultimodalInput() { Messages = messages }, + Parameters = new MultimodalParameters() + { + IncrementalOutput = true, + EnableThinking = true, + VlHighResolutionImages = true + } + }); + var reply = new StringBuilder(); + var reasoning = false; + MultimodalTokenUsage? 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 > "); + } + + if (choice.Message.Content.Count == 0) + { + continue; + } + + Console.Write(choice.Message.Content[0].Text); + reply.Append(choice.Message.Content[0].Text); + usage = chunk.Usage; + } + + Console.WriteLine(); + messages.Add(MultimodalMessage.Assistant([MultimodalMessageContent.TextContent(reply.ToString())])); + if (usage != null) + { + Console.WriteLine( + $"Usage: in({usage.InputTokens})/out({usage.OutputTokens})/image({usage.ImageTokens})/reasoning({usage.OutputTokensDetails?.ReasoningTokens})/total({usage.TotalTokens})"); + } + } +} diff --git a/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs b/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs index 81482a7..955f651 100644 --- a/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs +++ b/src/Cnblogs.DashScope.Core/IMultimodalParameters.cs @@ -9,7 +9,8 @@ public interface IMultimodalParameters IIncrementalOutputParameter, IPenaltyParameter, IMaxTokenParameter, - IStopTokenParameter + IStopTokenParameter, + IThinkingParameter { /// /// Allow higher resolution for inputs. When setting to true, increases the maximum input token from 1280 to 16384. Defaults to false. diff --git a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs index 445e7db..46566e1 100644 --- a/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs +++ b/src/Cnblogs.DashScope.Core/ITextGenerationParameters.cs @@ -9,7 +9,8 @@ public interface ITextGenerationParameters IProbabilityParameter, IPenaltyParameter, IMaxTokenParameter, - IStopTokenParameter + IStopTokenParameter, + IThinkingParameter { /// /// The format of the result, must be text or message. @@ -50,16 +51,6 @@ public interface ITextGenerationParameters /// TextGenerationSearchOptions? SearchOptions { get; set; } - /// - /// Thinking option. Valid for supported models.(e.g. qwen3) - /// - bool? EnableThinking { get; } - - /// - /// Maximum length of thinking content. Valid for supported models.(e.g. qwen3) - /// - int? ThinkingBudget { get; set; } - /// /// Include log possibilities in response. /// diff --git a/src/Cnblogs.DashScope.Core/IThinkingParameter.cs b/src/Cnblogs.DashScope.Core/IThinkingParameter.cs new file mode 100644 index 0000000..17df9d7 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/IThinkingParameter.cs @@ -0,0 +1,17 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Parameters for thinking. +/// +public interface IThinkingParameter +{ + /// + /// Thinking option. Valid for supported models.(e.g. qwen3) + /// + bool? EnableThinking { get; } + + /// + /// Maximum length of thinking content. Valid for supported models.(e.g. qwen3) + /// + int? ThinkingBudget { get; set; } +} diff --git a/src/Cnblogs.DashScope.Core/MultimodalInputTokenDetails.cs b/src/Cnblogs.DashScope.Core/MultimodalInputTokenDetails.cs new file mode 100644 index 0000000..9b2e59b --- /dev/null +++ b/src/Cnblogs.DashScope.Core/MultimodalInputTokenDetails.cs @@ -0,0 +1,8 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Token details for multimodal inputs. +/// +/// Token count of image. +/// Token count of text. +public record MultimodalInputTokenDetails(int? ImageTokens, int? TextTokens); diff --git a/src/Cnblogs.DashScope.Core/MultimodalOutputTokenDetails.cs b/src/Cnblogs.DashScope.Core/MultimodalOutputTokenDetails.cs new file mode 100644 index 0000000..e56d603 --- /dev/null +++ b/src/Cnblogs.DashScope.Core/MultimodalOutputTokenDetails.cs @@ -0,0 +1,8 @@ +namespace Cnblogs.DashScope.Core; + +/// +/// Token details of multimodal outputs. +/// +/// Token count of reasoning output. +/// Token count of text output. +public record MultimodalOutputTokenDetails(int? ReasoningTokens, int? TextTokens); diff --git a/src/Cnblogs.DashScope.Core/MultimodalParameters.cs b/src/Cnblogs.DashScope.Core/MultimodalParameters.cs index 55c42ec..68460f0 100644 --- a/src/Cnblogs.DashScope.Core/MultimodalParameters.cs +++ b/src/Cnblogs.DashScope.Core/MultimodalParameters.cs @@ -37,4 +37,10 @@ public class MultimodalParameters : IMultimodalParameters /// public TextGenerationStop? Stop { get; set; } + + /// + public bool? EnableThinking { get; set; } + + /// + public int? ThinkingBudget { get; set; } } diff --git a/src/Cnblogs.DashScope.Core/MultimodalTokenUsage.cs b/src/Cnblogs.DashScope.Core/MultimodalTokenUsage.cs index c6d5e90..912dd75 100644 --- a/src/Cnblogs.DashScope.Core/MultimodalTokenUsage.cs +++ b/src/Cnblogs.DashScope.Core/MultimodalTokenUsage.cs @@ -29,4 +29,24 @@ public class MultimodalTokenUsage /// The token usage of input video. /// public int? VideoTokens { get; set; } + + /// + /// Count of cached tokens. + /// + public int? CachedTokens { get; set; } + + /// + /// Count of total tokens. + /// + public int? TotalTokens { get; set; } + + /// + /// The details of input token usage. + /// + public MultimodalInputTokenDetails? InputTokensDetails { get; set; } + + /// + /// The details of output token usage. + /// + public MultimodalOutputTokenDetails? OutputTokensDetails { get; set; } } From ec529dae37d7eef183c919d1701d4749ab2068bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Tue, 11 Nov 2025 21:59:54 +0800 Subject: [PATCH 14/15] chore: obsolete qwen, qwen-vl and deepseek shorthand overrides --- .../DeepSeek/DeepSeekTextGenerationApi.cs | 69 +++++++++ .../QWen/QWenTextGenerationApi.cs | 136 ++++++++++++++++++ .../QWenMultimodalGenerationApi.cs | 68 +++++++++ 3 files changed, 273 insertions(+) diff --git a/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs index ecc988e..a3002db 100644 --- a/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs +++ b/src/Cnblogs.DashScope.Sdk/DeepSeek/DeepSeekTextGenerationApi.cs @@ -5,6 +5,7 @@ namespace Cnblogs.DashScope.Sdk.DeepSeek; /// /// Extensions for calling DeepSeek models, see: https://help.aliyun.com/zh/model-studio/developer-reference/deepseek /// +[Obsolete("Use generic GetTextStreamCompletionAsync instead")] public static class DeepSeekTextGenerationApi { private static TextGenerationParameters StreamingParameters { get; } = new() { IncrementalOutput = true }; @@ -16,6 +17,23 @@ public static class DeepSeekTextGenerationApi /// The model name. /// The context messages. /// + /// + /// Migrate from + /// + /// client.GetDeepSeekChatCompletionAsync(DeepSeekLlm.DeepSeekV3, messages); + /// + /// to + /// + /// client.GetTextCompletionAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "deepseek-v3", + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = StreamingParameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionAsync() instead, check remarks section.")] public static async Task> GetDeepSeekChatCompletionAsync( this IDashScopeClient client, @@ -32,6 +50,23 @@ public static async TaskThe model name. /// The context messages. /// + /// + /// Migrate from + /// + /// client.GetDeepSeekChatCompletionAsync(model, messages); + /// + /// to + /// + /// client.GetTextCompletionAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = model, + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = StreamingParameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionAsync() instead, check remarks section.")] public static async Task> GetDeepSeekChatCompletionAsync( this IDashScopeClient client, @@ -54,6 +89,23 @@ public static async Task /// /// + /// + /// Migrate from + /// + /// client.GetDeepSeekChatCompletionStreamAsync(DeepSeekLlm.DeepSeekV3, messages); + /// + /// to + /// + /// client.GetTextCompletionStreamAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "deepseek-v3", + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = StreamingParameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionStreamAsync() instead, check remarks section.")] public static IAsyncEnumerable> GetDeepSeekChatCompletionStreamAsync( this IDashScopeClient client, @@ -70,6 +122,23 @@ public static IAsyncEnumerable /// /// + /// + /// Migrate from + /// + /// client.GetDeepSeekChatCompletionStreamAsync(model, messages); + /// + /// to + /// + /// client.GetTextCompletionStreamAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = model, + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = StreamingParameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionStreamAsync() instead, check remarks section.")] public static IAsyncEnumerable> GetDeepSeekChatCompletionStreamAsync( this IDashScopeClient client, diff --git a/src/Cnblogs.DashScope.Sdk/QWen/QWenTextGenerationApi.cs b/src/Cnblogs.DashScope.Sdk/QWen/QWenTextGenerationApi.cs index 00e6f3d..07e1ff1 100644 --- a/src/Cnblogs.DashScope.Sdk/QWen/QWenTextGenerationApi.cs +++ b/src/Cnblogs.DashScope.Sdk/QWen/QWenTextGenerationApi.cs @@ -17,6 +17,23 @@ public static class QWenTextGenerationApi /// The cancellation token to use. /// Request for generation is failed. /// + /// + /// Migrate from + /// + /// client.GetQWenChatCompletionAsync("qwen-plus", messages, parameters); + /// + /// to + /// + /// client.GetTextCompletionStreamAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwen-plus", + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionStreamAsync instead")] public static IAsyncEnumerable> GetQWenChatStreamAsync( this IDashScopeClient dashScopeClient, QWenLlm model, @@ -45,6 +62,23 @@ public static IAsyncEnumerableThe cancellation token to use. /// Request for generation is failed. /// + /// + /// Migrate from + /// + /// client.GetQWenChatCompletionAsync("qwen-plus", messages, parameters); + /// + /// to + /// + /// client.GetTextCompletionStreamAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwen-plus", + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionStreamAsync instead")] public static IAsyncEnumerable> GetQWenChatStreamAsync( this IDashScopeClient dashScopeClient, string model, @@ -72,6 +106,23 @@ public static IAsyncEnumerableThe cancellation token to use. /// Request for generation is failed. /// + /// + /// Migrate from + /// + /// client.GetQWenChatCompletionAsync(QWenLlm.QwQ32B, messages, parameters); + /// + /// to + /// + /// client.GetTextCompletionAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwq-32b", + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionAsync instead")] public static Task> GetQWenChatCompletionAsync( this IDashScopeClient dashScopeClient, QWenLlm model, @@ -96,6 +147,23 @@ public static Task /// The cancellation token to use. /// Request for generation is failed. /// + /// + /// Migrate from + /// + /// client.GetQWenChatCompletionAsync("qwen-plus", messages, parameters); + /// + /// to + /// + /// client.GetTextCompletionAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwen-plus", + /// Input = new TextGenerationInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionAsync instead")] public static Task> GetQWenChatCompletionAsync( this IDashScopeClient dashScopeClient, string model, @@ -122,6 +190,23 @@ public static Task /// The optional parameters for this completion request. /// The cancellation token to use. /// Request for generation is failed. + /// + /// Migrate from + /// + /// client.GetQWenCompletionStreamAsync(QWenLlm.QwQ32B, "prompt", parameters); + /// + /// to + /// + /// client.GetTextCompletionStreamAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwq-32b", + /// Input = new TextGenerationInput { Messages = [TextChatMessage.User("prompt")] }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionStreamAsync instead")] public static IAsyncEnumerable> GetQWenCompletionStreamAsync( this IDashScopeClient dashScopeClient, @@ -146,6 +231,23 @@ public static IAsyncEnumerableThe optional parameters for this completion request. /// The cancellation token to use. /// Request for generation is failed. + /// + /// Migrate from + /// + /// client.GetQWenCompletionStreamAsync("qwq-32b", "prompt", parameters); + /// + /// to + /// + /// client.GetTextCompletionStreamAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwq-32b", + /// Input = new TextGenerationInput { Messages = [TextChatMessage.User("prompt")] }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionStreamAsync instead")] public static IAsyncEnumerable> GetQWenCompletionStreamAsync( this IDashScopeClient dashScopeClient, @@ -173,6 +275,23 @@ public static IAsyncEnumerableThe optional parameters for this completion request. /// The cancellation token to use. /// Request for generation is failed. + /// + /// Migrate from + /// + /// client.GetQWenCompletionAsync(QWenLlm.QwQ32B, "prompt", parameters); + /// + /// to + /// + /// client.GetTextCompletionAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwq-32b", + /// Input = new TextGenerationInput { Messages = [TextChatMessage.User("prompt")] }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionAsync instead")] public static Task> GetQWenCompletionAsync( this IDashScopeClient dashScopeClient, QWenLlm model, @@ -192,6 +311,23 @@ public static Task /// The optional parameters for this completion request. /// The cancellation token to use. /// Request for generation is failed. + /// + /// Migrate from + /// + /// client.GetQWenCompletionAsync("qwq-32b", "prompt", parameters); + /// + /// to + /// + /// client.GetTextCompletionAsync( + /// new ModelRequest<TextGenerationInput, ITextGenerationParameters> + /// { + /// Model = "qwq-32b", + /// Input = new TextGenerationInput { Messages = [TextChatMessage.User("prompt")] }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetTextCompletionAsync instead")] public static Task> GetQWenCompletionAsync( this IDashScopeClient dashScopeClient, string model, diff --git a/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalGenerationApi.cs b/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalGenerationApi.cs index 8d006cf..e2447fd 100644 --- a/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalGenerationApi.cs +++ b/src/Cnblogs.DashScope.Sdk/QWenMultimodal/QWenMultimodalGenerationApi.cs @@ -16,6 +16,23 @@ public static class QWenMultimodalGenerationApi /// The optional configuration for this request. /// The to use. /// + /// + /// Migrate from + /// + /// client.GetQWenMultimodalCompletionAsync(QWenMultimodalModel.QWenVlPlus, messages, parameters); + /// + /// to + /// + /// client.GetMultimodalGenerationAsync( + /// new ModelRequest<MultimodalInput, IMultimodalParameters> + /// { + /// Model = "qwen-vl-plus", + /// Input = new MultimodalInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetMultimodalGenerationAsync instead")] public static Task> GetQWenMultimodalCompletionAsync( this IDashScopeClient client, QWenMultimodalModel model, @@ -35,6 +52,23 @@ public static Task> GetQWe /// The optional configuration for this request. /// The to use. /// + /// + /// Migrate from + /// + /// client.GetQWenMultimodalCompletionAsync("qwen-vl-plus", messages, parameters); + /// + /// to + /// + /// client.GetMultimodalGenerationAsync( + /// new ModelRequest<MultimodalInput, IMultimodalParameters> + /// { + /// Model = "qwen-vl-plus", + /// Input = new MultimodalInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetMultimodalGenerationAsync instead")] public static Task> GetQWenMultimodalCompletionAsync( this IDashScopeClient client, string model, @@ -61,6 +95,23 @@ public static Task> GetQWe /// The optional configuration for this request. /// The to use. /// + /// + /// Migrate from + /// + /// client.GetQWenMultimodalCompletionStreamAsync("qwen-vl-plus", messages, parameters); + /// + /// to + /// + /// client.GetMultimodalGenerationStreamAsync( + /// new ModelRequest<MultimodalInput, IMultimodalParameters> + /// { + /// Model = "qwen-vl-plus", + /// Input = new MultimodalInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetMultimodalGenerationStreamAsync instead")] public static IAsyncEnumerable> GetQWenMultimodalCompletionStreamAsync( this IDashScopeClient client, @@ -85,6 +136,23 @@ public static IAsyncEnumerableThe optional configuration for this request. /// The to use. /// + /// + /// Migrate from + /// + /// client.GetQWenMultimodalCompletionStreamAsync("qwen-vl-plus", messages, parameters); + /// + /// to + /// + /// client.GetMultimodalGenerationStreamAsync( + /// new ModelRequest<MultimodalInput, IMultimodalParameters> + /// { + /// Model = "qwen-vl-plus", + /// Input = new MultimodalInput { Messages = messages }, + /// Parameters = parameters + /// }); + /// + /// + [Obsolete("Use GetMultimodalGenerationStreamAsync instead")] public static IAsyncEnumerable> GetQWenMultimodalCompletionStreamAsync( this IDashScopeClient client, From f41e5911d3b3a49dd223477639848c00f041acfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=88=E6=98=9F=E7=B9=81?= Date: Tue, 11 Nov 2025 22:38:50 +0800 Subject: [PATCH 15/15] doc: update readme --- README.md | 480 +++++++++++++++++++++++++++++++++++++++------- README.zh-Hans.md | 36 +++- 2 files changed, 436 insertions(+), 80 deletions(-) diff --git a/README.md b/README.md index 18049e2..9047737 100644 --- a/README.md +++ b/README.md @@ -26,10 +26,21 @@ Console.WriteLine(completion) Install NuGet package `Cnblogs.DashScope.Sdk` ```csharp var client = new DashScopeClient("your-api-key"); -var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt); -// Or use model name string -// var completion = await client.GetQWenCompletionAsync("qwen-max", prompt); -Console.WriteLine(completion.Output.Text); +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) ``` ### ASP.NET Core Application @@ -58,120 +69,441 @@ public class YourService(IDashScopeClient client) { public async Task CompletePromptAsync(string prompt) { - var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt); - return completion.Output.Text; + 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" } + }); + return completion.Output.Choices![0].Message.Content } } ``` ## Supported APIs -- [Chat](#Chat) - QWen3, DeepSeek, etc. Supports reasoning, tool calling, web search, translation -- [Multimodal](#multimodal) - QWen-VL, QVQ, etc. Supports reasoning, visual understanding, OCR, audio understanding -- [Text-to-Speech (TTS)](#Text-to-Speech) - CosyVoice, Sambert -- [Image Generation](#image-generation) - Wanx2.1 (text-to-image, portrait style transfer) -- [Application Call](#application-call) -- [Text Vectorization](#text-vectorization) +- [Text Generation](#text-generation) - QWen3, DeepSeek, etc. Supports reasoning/tool calling/web search/translation scenarios + - [Conversation](#conversation) + - [Deep Thinking](#deep-thinking) + - [Web Search](#web-search) + - [Tool Calling](#tool-calling) + - [Prefix Completion](#prefix-completion) + - [Long Context (Qwen-Long)](#long-context-qwen-long) +- [Multimodal](#multimodal) - QWen-VL, QVQ, etc. Supports reasoning/visual understanding/OCR/audio understanding +- [Text-to-Speech](#text-to-speech) - CosyVoice, Sambert, etc. For TTS applications +- [Image Generation](#image-generation) - wanx2.1, etc. For text-to-image and portrait style transfer +- [Application Invocation](#application-invocation) +- [Text Embeddings](#text-embeddings) -### Chat +## Text Generation -Use `GetTextCompletionAsync`/`GetTextCompletionStreamAsync` for direct text generation. -For QWen and DeepSeek, use shortcuts: `GetQWenChatCompletionAsync`/`GetDeepSeekChatCompletionAsync` +Use `dashScopeClient.GetTextCompletionAsync` and `dashScopeClient.GetTextCompletionStreamAsync()` to access text generation APIs. -[Official Documentation](https://help.aliyun.com/zh/model-studio/user-guide/text-generation/) +Common models: `qwen-max` `qwen-plus` `qwen-flush` etc. + +Basic example: ```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("Who are you?") + } + }, + Parameters = new TextGenerationParameters() { ResultFormat = "message" } + }); +Console.WriteLine(completion.Output.Choices![0].Message.Content) ``` -#### Reasoning +### Conversation + +#### Quick Start + +The key is maintaining a `TextChatMessage` array as conversation history. -Access model thoughts via `ReasoningContent` property ```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("Using default input: Who are you?"); + input = "Who are you?"; + } + + 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)); +} ``` -For QWen3 models, enable reasoning with `TextGenerationParameters.EnableThinking` + +#### Thinking Models + +The model's thinking process is stored in a separate `ReasoningContent` property. When saving to conversation history, ignore it and only keep the model's reply `Content`. + +Some models accept `EnableThinking` to control deep thinking, which can be set in `Parameters`. + +For more settings on thinking models, see [Deep Thinking](#deep-thinking) below. ```csharp -var stream = dashScopeClient - .GetQWenChatStreamAsync( - QWenLlm.QWenPlusLatest, - history, - new TextGenerationParameters +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() { - IncrementalOutput = true, - ResultFormat = ResultFormats.Message, - EnableThinking = true + 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)); +} ``` -#### Tool Calling -Define a function for model to use: -```csharp -string GetCurrentWeather(GetCurrentWeatherParameters parameters) +#### Streaming Output + +```cs +var request = new ModelRequest() { - return "Sunny"; + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + IncrementalOutput = true + } } -public record GetCurrentWeatherParameters( - [property: Required] - [property: Description("City and state, e.g. San Francisco, CA")] - string Location, - [property: JsonConverter(typeof(EnumStringConverter))] - TemperatureUnit Unit = TemperatureUnit.Celsius); -public enum TemperatureUnit { Celsius, Fahrenheit } ``` -Invoke with tool definitions. We using `JsonSchema.Net` for example, you could use any other library to generate JSON schema) -```csharp + +Use `client.GetTextCompletionStreamAsync` for streaming output. It's recommended to enable `IncrementalOutput` in `Parameters` for incremental output. + +Incremental output: +> Example: ["I love","eating","apples"] + +Non-incremental output: +> Example: ["I love","I love eating","I love eating apples"] + +Streaming output returns an `IAsyncEnumerable`. Use `await foreach` to iterate, record incremental content, then save to conversation history. + +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})"); + } +} + +#### Limiting Thinking Length + +Use `ThinkingBudget` in `Parameters` to limit the model's thinking length. + +```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) +{ + 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})"); + } +} +``` + +### Web Search + +Controlled mainly through `EnableSearch` and `SearchOptions` in `Parameters`. + +Example request: + +```cs +var request = new ModelRequest() +{ + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = true, + EnableSearch = true, + SearchOptions = new TextGenerationSearchOptions() + { + SearchStrategy = "max", // max/turbo - controls number of search results + EnableCitation = true, // Add source citations to model reply + CitationFormat = "[ref_]", // Citation format + EnableSource = true, // Return search source list in SearchInfo + ForcedSearch = true, // Force model to search + EnableSearchExtension = true, // Enable vertical domain search, results in SearchInfo.Extra + PrependSearchResult = true // First packet contains only search results + } + } +}; +``` + +### Tool Calling + +Provide available tools to the model via `Tools` in `Parameters`. The model returns messages with `ToolCall` properties to invoke tools. + +After receiving the message, the server needs to call the corresponding tools and insert results as `Tool` role messages into the conversation history before making another request. The model will summarize the tool call results. + +By default, the model calls tools once per turn. To enable multiple tool calls, enable `ParallelToolCalls` in `Parameters`. + +``` var tools = new List { new( ToolTypes.Function, new FunctionDefinition( - nameof(GetCurrentWeather), + nameof(GetWeather), "Get current weather", - new JsonSchemaBuilder().FromType().Build())) + new JsonSchemaBuilder().FromType().Build())) }; -var history = new List { ChatMessage.User("What's the weather in CA?") }; -var parameters = new TextGenerationParameters { ResultFormat = ResultFormats.Message, Tools = tools }; -// request model -var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters); -Console.WriteLine(completion.Output.Choice[0].Message.ToolCalls[0].Function.Name); // GetCurrentWeather -history.Add(completion.Output.Choice[0].Message); +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 // Model can call multiple tools at once + } +} +``` + +### Structured Output (JSON Output) + +Set `ResponseFormat` (remarks: **not** `ResultFormat`) in `Parameters` to JSON to force JSON output. -// calls tool -var result = GetCurrentWeather(new() { Location = "CA" }); -history.Add(new("tool", result, nameof(GetCurrentWeather))); +```csharp +var request = new ModelRequest() +{ + Model = "qwen-plus", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + ResponseFormat = DashScopeResponseFormat.Json, + IncrementalOutput = true + } +} +``` -// Get final answer -completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenMax, history, parameters); -Console.WriteLine(completion.Output.Choices[0].Message.Content); // "Current weather in California: Sunny" +### Prefix Completion + +Place the prefix to complete as an `assistant` message at the end of the `messages` array with `Partial` set to `true`. The model will complete the remaining content based on this prefix. + +Deep thinking cannot be enabled in this mode. + +```cs +var messages = new List +{ + TextChatMessage.User("Complete following C# method."), + TextChatMessage.Assistant("public int Fibonacci(int n)", partial: true) +}; + +var completion = client.GetTextCompletionStreamAsync( +new ModelRequest() +{ + Model = "qwen-turbo", + Input = new TextGenerationInput() { Messages = messages }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + IncrementalOutput = true + } +}); ``` -#### File Upload (Long Context Models) + +### Long Context (Qwen-Long) For Qwen-Long models: ```csharp var file = new FileInfo("test.txt"); var uploadedFile = await dashScopeClient.UploadFileAsync(file.OpenRead(), file.Name); var history = new List { ChatMessage.File(uploadedFile.Id) }; -var completion = await client.GetQWenChatCompletionAsync(QWenLlm.QWenLong, history); +var completion = await client.client.GetTextCompletionAsync( + new ModelRequest() + { + Model = "qwen-long", + Input = new TextGenerationInput() { Messages = history }, + Parameters = new TextGenerationParameters() + { + ResultFormat = "message", + EnableThinking = false, + } + }); Console.WriteLine(completion.Output.Choices[0].Message.Content); // Cleanup await dashScopeClient.DeleteFileAsync(uploadedFile.Id); ``` + ### Multimodal Use `GetMultimodalGenerationAsync`/`GetMultimodalGenerationStreamAsync` [Official Documentation](https://help.aliyun.com/zh/model-studio/multimodal) diff --git a/README.zh-Hans.md b/README.zh-Hans.md index e9cab21..e707b12 100644 --- a/README.zh-Hans.md +++ b/README.zh-Hans.md @@ -28,10 +28,21 @@ Console.WriteLine(completion) ```csharp var client = new DashScopeClient("your-api-key"); -var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt); -// 也可以直接输入模型名称进行调用 -// var completion = await client.GetQWenCompletionAsync("qwen-max", prompt); -Console.WriteLine(completion.Output.Text); +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) ``` ### ASP.NET Core 应用 @@ -58,8 +69,21 @@ public class YourService(IDashScopeClient client) { public async Task CompletePromptAsync(string prompt) { - var completion = await client.GetQWenCompletionAsync(QWenLlm.QWenMax, prompt); - return completion.Output.Text; + 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" } + }); + return completion.Output.Choices![0].Message.Content } } ```