三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

dotnet 对接 DeepSeek 模型工具调用时 400 错误

dotnet 对接 DeepSeek 模型工具调用时 400 错误

本文内容由 AI 辅助编写

最近我尝试用 OpenAI 官方 SDK 以兼容模式对接 DeepSeek 模型,开启深度思考模式之后,如果调用了自定义工具,就会在工具调用完成后发起下一轮请求的时候抛出 HTTP 400 错误,错误内容为 The \reasoning_content` in the thinking mode must be passed back to the API`

翻查 DeepSeek 官方文档 https://api-docs.deepseek.com/zh-cn/guides/thinking_mode#工具调用 可知,问题出在上下文回传的逻辑上:DeepSeek 的思考模式下,模型返回的 reasoning_content 思考内容,在后续的上下文请求里必须完整带回,包括工具调用完成后的续传请求。但是官方的 OpenAI .NET SDK 并没有适配 reasoning_content 这个扩展字段,在拼接上下文时会直接丢弃这部分内容,就触发了 API 的参数校验错误

为了解决这个问题,我采用了 walterlv 封装的 DeepSeekChatClient 实现,它完全兼容 Microsoft.Extensions.AI 的 IChatClient 接口,原生支持了 DeepSeek 的思考模式和 reasoning_content 的上下文回传逻辑,完美解决了这个问题。我还专门将其封装为 Microsoft.Extensions.AI.DeepSeek 库,可以直接在项目中引入使用。

项目配置

Microsoft.Extensions.AI.DeepSeek 库支持 net6.0、net8.0 和 net10.0 框架,依赖 Microsoft.Extensions.AI 10.5.2 版本,项目的 csproj 配置如下:

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFrameworks>net10.0;net6.0;net8.0</TargetFrameworks><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable><LangVersion>latest</LangVersion></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Extensions.AI" Version="10.5.2" /></ItemGroup>
</Project>

你只需要将这个项目添加到你的解决方案中,或者打包为 NuGet 包引入即可使用。

核心实现原理

DeepSeekChatClient 解决问题的核心逻辑是在构建请求消息的时候,会将上下文里的 TextReasoningContent 内容序列化为 reasoning_content 字段回传给 API,不会丢失思考内容。对应的实现代码如下:

foreach (var content in message.Contents)
{switch (content){case TextContent textContent when !string.IsNullOrEmpty(textContent.Text):textBuilder.Append(textContent.Text);break;case TextReasoningContent reasoningContent when !string.IsNullOrEmpty(reasoningContent.Text):// 收集思考内容,后续回传给 APIreasoningBuilder.Append(reasoningContent.Text);break;case FunctionCallContent functionCall:toolCalls ??= [];toolCalls.Add(new JsonObject{["id"] = functionCall.CallId,["type"] = "function",["function"] = new JsonObject{["name"] = functionCall.Name,["arguments"] = JsonSerializer.Serialize(functionCall.Arguments, SerializerOptions),},});break;}
}
// 构建请求时带上 reasoning_content 字段
yield return new JsonObject
{["role"] = role,["content"] = text,["reasoning_content"] = reasoningBuilder.Length > 0 ? reasoningBuilder.ToString() : null,["tool_calls"] = toolCalls,
};

整个代码

namespace Microsoft.Extensions.AI.DeepSeek;using Microsoft.Extensions.AI;using System.Net.Http.Headers;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;public sealed partial class DeepSeekChatClient : IChatClient
{private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web){DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,};private readonly HttpClient _httpClient;private readonly bool _ownsHttpClient;private readonly Uri _baseUri;private readonly string _apiKey;private readonly string _defaultModelId;private readonly bool _enableThinkingMode;private readonly int _reasoningBudgetTokens;private readonly ChatClientMetadata _metadata;public DeepSeekChatClient(string apiKey,string modelId,string baseUrl = "https://api.deepseek.com/v1",bool enableThinkingMode = true,int reasoningBudgetTokens = 8000,HttpClient? httpClient = null){if (string.IsNullOrWhiteSpace(apiKey)){throw new ArgumentException("Value cannot be null or whitespace.", nameof(apiKey));}if (string.IsNullOrWhiteSpace(modelId)){throw new ArgumentException("Value cannot be null or whitespace.", nameof(modelId));}if (string.IsNullOrWhiteSpace(baseUrl)){throw new ArgumentException("Value cannot be null or whitespace.", nameof(baseUrl));}_httpClient = httpClient ?? new HttpClient();_ownsHttpClient = httpClient is null;_baseUri = new Uri($"{baseUrl.AsSpan().TrimEnd('/')}/", UriKind.Absolute);_apiKey = apiKey;_defaultModelId = modelId;_enableThinkingMode = enableThinkingMode;_reasoningBudgetTokens = reasoningBudgetTokens;_metadata = new ChatClientMetadata("DeepSeek", _baseUri, modelId);}public async Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages,ChatOptions? options = null,CancellationToken cancellationToken = default){ArgumentNullException.ThrowIfNull(messages);List<AIContent> responseContents = [];UsageDetails? usage = null;ChatFinishReason? finishReason = null;string? responseId = null;string? modelId = null;DateTimeOffset? createdAt = null;object? rawRepresentation = null;ChatRole role = ChatRole.Assistant;await foreach (var update in GetStreamingResponseAsync(messages, options, cancellationToken)){if (update.Role is { } updateRole){role = updateRole;}responseId ??= update.ResponseId;modelId ??= update.ModelId;createdAt ??= update.CreatedAt;finishReason = update.FinishReason ?? finishReason;rawRepresentation = update.RawRepresentation ?? rawRepresentation;foreach (var content in update.Contents){if (content is UsageContent usageContent){usage = usageContent.Details;continue;}responseContents.Add(content);}}var responseMessage = new ChatMessage(role, responseContents){CreatedAt = createdAt,MessageId = responseId,RawRepresentation = rawRepresentation,};return new ChatResponse(responseMessage){ResponseId = responseId,ModelId = modelId ?? options?.ModelId ?? _defaultModelId,CreatedAt = createdAt,FinishReason = finishReason,Usage = usage,RawRepresentation = rawRepresentation,};}public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> messages,ChatOptions? options = null,[EnumeratorCancellation] CancellationToken cancellationToken = default){ArgumentNullException.ThrowIfNull(messages);var requestJson = BuildRequest(messages, options);using var request = new HttpRequestMessage(HttpMethod.Post, new Uri(_baseUri, "chat/completions")){Content = new StringContent(requestJson.ToJsonString(SerializerOptions), Encoding.UTF8, "application/json"),};request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _apiKey);using var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);if (!response.IsSuccessStatusCode){var errorBody = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);throw new InvalidOperationException(MapErrorToMessage((int) response.StatusCode, errorBody));}using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);using var reader = new StreamReader(stream, Encoding.UTF8);string? responseId = null;string? modelId = null;DateTimeOffset? createdAt = null;ChatFinishReason? finishReason = null;UsageDetails? usage = null;var toolCallAccumulators = new Dictionary<int, ToolCallAccumulator>();await foreach (var chunk in ReadSseChunksAsync(reader, cancellationToken)){responseId ??= chunk.Id;modelId ??= chunk.Model;if (createdAt is null && chunk.Created is { } created){createdAt = DateTimeOffset.FromUnixTimeSeconds(created);}var choice = chunk.Choices?.FirstOrDefault();if (choice?.Delta is { } delta){List<AIContent> contents = [];if (!string.IsNullOrEmpty(delta.ReasoningContent)){contents.Add(new TextReasoningContent(delta.ReasoningContent){RawRepresentation = chunk,});}if (!string.IsNullOrEmpty(delta.Content)){contents.Add(new TextContent(delta.Content){RawRepresentation = chunk,});}if (delta.ToolCalls is { Length: > 0 }){foreach (var toolCall in delta.ToolCalls){if (!toolCallAccumulators.TryGetValue(toolCall.Index, out var accumulator)){accumulator = new ToolCallAccumulator();toolCallAccumulators[toolCall.Index] = accumulator;}accumulator.Apply(toolCall);}}if (contents.Count > 0){yield return new ChatResponseUpdate(ChatRole.Assistant, contents){ResponseId = responseId,ModelId = modelId,CreatedAt = createdAt,RawRepresentation = chunk,};}}if (!string.IsNullOrWhiteSpace(choice?.FinishReason)){finishReason = ParseFinishReason(choice.FinishReason);}if (chunk.Usage is { } chunkUsage){usage = CreateUsageDetails(chunkUsage);}}List<AIContent> finalContents = [];foreach (var toolCall in toolCallAccumulators.OrderBy(static pair => pair.Key).Select(static pair => pair.Value)){finalContents.Add(toolCall.ToFunctionCallContent());}if (usage is not null){finalContents.Add(new UsageContent(usage));}if (finalContents.Count > 0 || finishReason is not null){yield return new ChatResponseUpdate(ChatRole.Assistant, finalContents){ResponseId = responseId,ModelId = modelId ?? options?.ModelId ?? _defaultModelId,CreatedAt = createdAt,FinishReason = finishReason,};}}public object? GetService(Type serviceType, object? serviceKey = null){ArgumentNullException.ThrowIfNull(serviceType);if (serviceKey is not null){return null;}if (serviceType.IsInstanceOfType(this)){return this;}if (serviceType.IsInstanceOfType(_metadata)){return _metadata;}if (serviceType.IsInstanceOfType(_httpClient)){return _httpClient;}return null;}public void Dispose(){if (_ownsHttpClient){_httpClient.Dispose();}}private JsonObject BuildRequest(IEnumerable<ChatMessage> messages, ChatOptions? options){var request = new JsonObject{["model"] = options?.ModelId ?? _defaultModelId,["stream"] = true,["stream_options"] = new JsonObject { ["include_usage"] = true },["messages"] = BuildMessages(messages, options),};if (options?.Temperature is { } temperature){request["temperature"] = JsonValue.Create(temperature);}if (options?.TopP is { } topP){request["top_p"] = JsonValue.Create(topP);}if (options?.MaxOutputTokens is { } maxOutputTokens){request["max_tokens"] = JsonValue.Create(maxOutputTokens);}if (options?.StopSequences is { Count: > 0 } stopSequences){var stop = new JsonArray();foreach (var stopSequence in stopSequences){stop.Add(stopSequence);}request["stop"] = stop;}if (_enableThinkingMode){request["thinking"] = new JsonObject{["type"] = "enabled",["budget_tokens"] = _reasoningBudgetTokens,};}var tools = BuildTools(options?.Tools);if (tools.Count > 0){request["tools"] = tools;request["tool_choice"] = "auto";}return request;}private static JsonArray BuildMessages(IEnumerable<ChatMessage> messages, ChatOptions? options){JsonArray jsonMessages = [];if (!string.IsNullOrWhiteSpace(options?.Instructions)){jsonMessages.Add(new JsonObject{["role"] = "system",["content"] = options.Instructions,});}foreach (var message in messages){foreach (var jsonMessage in BuildMessages(message)){jsonMessages.Add(jsonMessage);}}return jsonMessages;}private static IEnumerable<JsonObject> BuildMessages(ChatMessage message){ArgumentNullException.ThrowIfNull(message);if (message.Role == ChatRole.Tool){foreach (var content in message.Contents){if (content is FunctionResultContent functionResult){yield return new JsonObject{["role"] = "tool",["tool_call_id"] = functionResult.CallId,["content"] = SerializeToolResult(functionResult.Result),};}}yield break;}string? text = null;string? reasoning = null;JsonArray? toolCalls = null;if (message.Contents.Count > 0){var textBuilder = new StringBuilder();var reasoningBuilder = new StringBuilder();foreach (var content in message.Contents){switch (content){case TextContent textContent when !string.IsNullOrEmpty(textContent.Text):textBuilder.Append(textContent.Text);break;case TextReasoningContent reasoningContent when !string.IsNullOrEmpty(reasoningContent.Text):reasoningBuilder.Append(reasoningContent.Text);break;case FunctionCallContent functionCall:toolCalls ??= [];toolCalls.Add(new JsonObject{["id"] = functionCall.CallId,["type"] = "function",["function"] = new JsonObject{["name"] = functionCall.Name,
#if NET8_0_OR_GREATER["arguments"] = JsonSerializer.Serialize(functionCall.Arguments, SourceGenerationContext.Default.Options),
#else["arguments"] = JsonSerializer.Serialize(functionCall.Arguments, SerializerOptions),
#endif},});break;}}text = textBuilder.Length > 0 ? textBuilder.ToString() : null;reasoning = reasoningBuilder.Length > 0 ? reasoningBuilder.ToString() : null;}else if (!string.IsNullOrWhiteSpace(message.Text)){text = message.Text;}var role = message.Role == ChatRole.System ? "system": message.Role == ChatRole.Assistant ? "assistant": "user";yield return new JsonObject{["role"] = role,["content"] = text,["reasoning_content"] = reasoning,["tool_calls"] = toolCalls,};}private static JsonArray BuildTools(IList<AITool>? tools){JsonArray jsonTools = [];if (tools is null){return jsonTools;}foreach (var tool in tools){if (tool is not AIFunctionDeclaration function){continue;}jsonTools.Add(new JsonObject{["type"] = "function",["function"] = new JsonObject{["name"] = function.Name,["description"] = function.Description,["parameters"] = ToJsonNode(function.JsonSchema) ?? new JsonObject(),},});}return jsonTools;}private static JsonNode? ToJsonNode(object? value){return value switch{null => null,JsonNode node => node.DeepClone(),JsonElement element => JsonNode.Parse(element.GetRawText()),JsonDocument document => JsonNode.Parse(document.RootElement.GetRawText()),string text when !string.IsNullOrWhiteSpace(text) => JsonNode.Parse(text),
#if NET8_0_OR_GREATER_ => JsonSerializer.SerializeToNode(value, SourceGenerationContext.Default.Options),
#else_ => JsonSerializer.SerializeToNode(value, SerializerOptions),
#endif};}private static string SerializeToolResult(object? result){return result switch{null => "null",string text => text,JsonElement element => element.GetRawText(),JsonDocument document => document.RootElement.GetRawText(),
#if NET8_0_OR_GREATER_ => JsonSerializer.Serialize(result, SourceGenerationContext.Default.Options),
#else_ => JsonSerializer.Serialize(result, SerializerOptions),
#endif};}private static async IAsyncEnumerable<DeepSeekChatChunk> ReadSseChunksAsync(StreamReader reader,[EnumeratorCancellation] CancellationToken cancellationToken){while (true){
#if NET8_0_OR_GREATERvar line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
#elsevar line = await reader.ReadLineAsync().ConfigureAwait(false);
#endifif (line is null){yield break;}if (!line.StartsWith("data: ", StringComparison.Ordinal)){continue;}var payload = line["data: ".Length..];if (payload == "[DONE]"){yield break;}if (string.IsNullOrWhiteSpace(payload)){continue;}DeepSeekChatChunk? chunk;try{
#if NET8_0_OR_GREATERchunk = JsonSerializer.Deserialize(payload, SourceGenerationContext.Default.DeepSeekChatChunk);
#elsechunk = JsonSerializer.Deserialize<DeepSeekChatChunk>(payload, SerializerOptions);
#endif}catch (JsonException){continue;}if (chunk is not null){yield return chunk;}}}private static UsageDetails CreateUsageDetails(DeepSeekUsage usage){var usageDetails = new UsageDetails{InputTokenCount = usage.PromptTokens,OutputTokenCount = usage.CompletionTokens,TotalTokenCount = usage.TotalTokens > 0 ? usage.TotalTokens : usage.PromptTokens + usage.CompletionTokens,CachedInputTokenCount = usage.PromptCacheHitTokens,ReasoningTokenCount = usage.CompletionTokensDetails?.ReasoningTokens,};if (usage.PromptCacheMissTokens > 0){usageDetails.AdditionalCounts = new AdditionalPropertiesDictionary<long>{["prompt_cache_miss_tokens"] = usage.PromptCacheMissTokens,};}return usageDetails;}private static ChatFinishReason? ParseFinishReason(string? finishReason){return finishReason switch{null => null,"stop" => ChatFinishReason.Stop,"length" => ChatFinishReason.Length,"tool_calls" => ChatFinishReason.ToolCalls,"content_filter" => ChatFinishReason.ContentFilter,_ => new ChatFinishReason(finishReason),};}private static string MapErrorToMessage(int statusCode, string responseBody){string? errorCode = null;string? errorMessage = null;try{
#if NET8_0_OR_GREATERvar error = JsonSerializer.Deserialize(responseBody, SourceGenerationContext.Default.DeepSeekErrorResponse);
#elsevar error = JsonSerializer.Deserialize<DeepSeekErrorResponse>(responseBody, SerializerOptions);
#endiferrorCode = error?.Error?.Code;errorMessage = error?.Error?.Message;}catch (JsonException){}var detail = errorCode switch{"insufficient_balance" => "账户余额不足,请充值后重试。","rate_limit_exceeded" => "请求速率超限,请稍后重试。","context_length_exceeded" => "输入内容超出模型上下文限制。","invalid_api_key" => "API Key 无效,请检查配置。","model_not_found" => "模型不存在,请检查模型名称。","server_error" => "服务端内部错误,请稍后重试。",_ => null,};if (detail is not null){return $"API 错误(HTTP {statusCode}, code={errorCode}):{detail}";}if (!string.IsNullOrWhiteSpace(errorMessage)){return $"API 错误(HTTP {statusCode}):{errorMessage}";}return $"API 请求失败,状态码 {statusCode}。";}#if NET8_0_OR_GREATER[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)][JsonSerializable(typeof(DeepSeekChatChunk))][JsonSerializable(typeof(DeepSeekErrorResponse))][JsonSerializable(typeof(Dictionary<string, JsonElement>))][JsonSerializable(typeof(IDictionary<string, object>), GenerationMode = JsonSourceGenerationMode.Serialization)]private partial class SourceGenerationContext : JsonSerializerContext{}
#endifprivate sealed class ToolCallAccumulator{private string? _id;private string? _name;private readonly StringBuilder _arguments = new();public void Apply(DeepSeekToolCallDelta delta){if (!string.IsNullOrWhiteSpace(delta.Id)){_id = delta.Id;}if (!string.IsNullOrWhiteSpace(delta.Function?.Name)){_name = delta.Function.Name;}if (!string.IsNullOrEmpty(delta.Function?.Arguments)){_arguments.Append(delta.Function.Arguments);}}public FunctionCallContent ToFunctionCallContent(){var argumentsJson = _arguments.Length > 0 ? _arguments.ToString() : "{}";return new FunctionCallContent(_id ?? Guid.NewGuid().ToString("N"),_name ?? string.Empty,ParseArguments(argumentsJson)){RawRepresentation = argumentsJson,};}private static Dictionary<string, object?> ParseArguments(string argumentsJson){try{
#if NET8_0_OR_GREATERvar json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(argumentsJson, SourceGenerationContext.Default.Options);
#elsevar json = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(argumentsJson, SerializerOptions);
#endifif (json is null){return [];}return json.ToDictionary(static pair => pair.Key, static pair => (object?) pair.Value.Clone(), StringComparer.Ordinal);}catch (JsonException){return [];}}}private sealed class DeepSeekChatChunk{[JsonPropertyName("id")]public string? Id { get; set; }[JsonPropertyName("model")]public string? Model { get; set; }[JsonPropertyName("created")]public long? Created { get; set; }[JsonPropertyName("choices")]public DeepSeekChoice[]? Choices { get; set; }[JsonPropertyName("usage")]public DeepSeekUsage? Usage { get; set; }}private sealed class DeepSeekChoice{[JsonPropertyName("delta")]public DeepSeekDelta? Delta { get; set; }[JsonPropertyName("finish_reason")]public string? FinishReason { get; set; }}private sealed class DeepSeekDelta{[JsonPropertyName("content")]public string? Content { get; set; }[JsonPropertyName("reasoning_content")]public string? ReasoningContent { get; set; }[JsonPropertyName("tool_calls")]public DeepSeekToolCallDelta[]? ToolCalls { get; set; }}private sealed class DeepSeekToolCallDelta{[JsonPropertyName("index")]public int Index { get; set; }[JsonPropertyName("id")]public string? Id { get; set; }[JsonPropertyName("function")]public DeepSeekFunctionDelta? Function { get; set; }}private sealed class DeepSeekFunctionDelta{[JsonPropertyName("name")]public string? Name { get; set; }[JsonPropertyName("arguments")]public string? Arguments { get; set; }}private sealed class DeepSeekUsage{[JsonPropertyName("prompt_tokens")]public int PromptTokens { get; set; }[JsonPropertyName("completion_tokens")]public int CompletionTokens { get; set; }[JsonPropertyName("total_tokens")]public int TotalTokens { get; set; }[JsonPropertyName("prompt_cache_hit_tokens")]public int PromptCacheHitTokens { get; set; }[JsonPropertyName("prompt_cache_miss_tokens")]public int PromptCacheMissTokens { get; set; }[JsonPropertyName("completion_tokens_details")]public DeepSeekCompletionTokensDetails? CompletionTokensDetails { get; set; }}private sealed class DeepSeekCompletionTokensDetails{[JsonPropertyName("reasoning_tokens")]public int? ReasoningTokens { get; set; }}private sealed class DeepSeekErrorResponse{[JsonPropertyName("error")]public DeepSeekError? Error { get; set; }}private sealed class DeepSeekError{[JsonPropertyName("code")]public string? Code { get; set; }[JsonPropertyName("message")]public string? Message { get; set; }}
}

同时这个实现还完整支持 DeepSeek 的所有扩展特性:流式响应输出、思考内容流式返回、工具调用完整流程、错误码友好转换、推理Token消耗统计等,甚至会把缓存命中、推理Token消耗这些扩展字段都映射到 UsageDetails 里,方便业务统计成本。

使用示例

初始化 DeepSeekChatClient 的方式非常简单,只需要传入你的 DeepSeek API Key 和要使用的模型ID即可,也可以根据业务需要配置思考模式开关、推理预算Token等参数。以下是完整的工具调用测试代码:

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.DeepSeek;
using OpenAI;
using System.ClientModel;// 读取你的 DeepSeek API Key,记得替换为你自己的 Key 文件路径
var keyFile = @"C:\lindexi\Work\deepseek.txt";
var key = File.ReadAllText(keyFile);// 初始化 DeepSeekChatClient,默认开启思考模式
using var chatClient = new DeepSeekChatClient(key, "deepseek-v4-pro");// 如果使用 OpenAIClient 对接,就会出现本文提到的错误
//var openAiClient = new OpenAIClient(new ApiKeyCredential(key), new OpenAIClientOptions()
//{
//    Endpoint = new Uri("https://api.deepseek.com/v1")
//});
//var openAiChatClient = openAiClient.GetChatClient("deepseek-v4-pro").AsIChatClient();// 构建带工具调用的 AI Agent,这里注册了查询天气的工具
ChatClientAgent agent = chatClient.AsBuilder().BuildAIAgent(tools: [AIFunctionFactory.Create(GetWeather)]);var session = await agent.CreateSessionAsync();
bool isThinking = false;// 流式获取响应内容,区分输出思考内容和最终回答
await foreach (var agentResponseUpdate in agent.RunStreamingAsync("北京天气怎样", session))
{foreach (var aiContent in agentResponseUpdate.Contents){if (aiContent is TextReasoningContent textReasoningContent){if (string.IsNullOrEmpty(textReasoningContent.Text)){continue;}isThinking = true;Console.Write(textReasoningContent.Text);}else if (aiContent is TextContent textContent){if (string.IsNullOrEmpty(textContent.Text)){continue;}if (isThinking){Console.WriteLine();}isThinking = false;Console.Write(textContent.Text);}}
}Console.WriteLine();
Console.WriteLine("Hello, World!");// 自定义的天气查询工具
string GetWeather(string city)
{return $"The weather in {city} is sunny.";
}

运行以上代码即可看到模型先输出思考过程,然后调用天气工具,最后返回最终回答,全程不会再出现 reasoning_content 相关的错误。

代码

本文代码放在 github 和 gitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin bc29ceb1611cb60c264143c3b13e408346ae14e3

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin bc29ceb1611cb60c264143c3b13e408346ae14e3

获取代码之后,进入 SemanticKernelSamples/Microsoft.Agents.AI.Extensions 文件夹,即可获取到源代码

更多技术博客,请参阅 博客导航

其他异常情况

由于 DeepSeek 对工具要求比较严格,如工具名只能符合 ^[a-zA-Z0-9_-]+$ 正则条件。如果传入不符合正则约束的工具名,将会遇到 HTTP 400 错误:

HTTP 400:Invalid 'tools[0].function.name': string does not match pattern. Expected a string that matches the pattern '^[a-zA-Z0-9_-]+$'.”

解决方法为修改工具名,让工具名符合规范

← 返回列表