Spring AI 2.0reasoning_content丢失排查报告背景Jiang I-Agent 使用 Spring AI 2.0 DeepSeek 模型通过硅基流动 SiliconFlow 代理需要获取模型的思考过程reasoning_content在前端展示。时间线第一阶段发现丢失约 2 周前现象使用 Spring AIChatClient流式调用时前端收不到reasoning_content同步调用却能收到。当时诊断怀疑ChunkMerger.mergeDeltas()在拼接流式 chunk 时丢弃了Delta.additionalProperties中的reasoning_content。当时决策绕过 Spring AI用java.net.http.HttpClient直接调硅基流动 API手动解析 SSE 响应。项目运行正常至今。第二阶段质疑诊断今日Spring AI 官方文档文档明确指出Spring AI maps thereasoning_contentfield from the JSON response into theAssistantMessagemetadata under the keyreasoningContent.正确用法ChatResponseresponsechatModel.call(newPrompt(...));AssistantMessagemessageresponse.getResult().getOutput();Stringreasoningmessage.getMetadata().get(reasoningContent);用户质疑既然 Spring AI 官方支持reasoningContent为什么我们会丢掉是不是我们用错了 API 层级ChatClient.stream().content()只拿 String而ChatModel.stream()才拿完整ChatResponse第三阶段三层测试验证今日为彻底搞清楚设计了三层递进测试测试 1原始 HTTP 层用java.net.http.HttpClient直接调硅基流动 API检查原始 JSON 响应中是否包含reasoning_content。被测模型deepseek-ai/DeepSeek-R1原生推理始终输出 reasoning_contentdeepseek-ai/DeepSeek-V3.2enable_thinking: true结果模型方式结果R1流式delta.reasoning_content存在于 333/345 chunks总长 613 字符R1同步message.reasoning_content存在总长 1004 字符V3.2thinking流式delta.reasoning_content存在于 46 chunks总长 93 字符结论硅基流动 API 完好无损地返回了reasoning_content。测试 2Spring AI 2.0 ChatModel 层用 Spring Boot 测试上下文加载真实的OpenAiChatModelBean排除了 MySQL/Redis 循环依赖问题后调用chatModel.stream()打印每个 chunk 的完整 metadata。被测模型deepseek-ai/DeepSeek-R1结果Chunk #1 | metadata keys[role, messageType, finishReason, refusal, annotations, index, id, reasoningContent] metadata[reasoningContent] ← 始终为空 metadata[finishReason] _UNKNOWN metadata[index] 0 Chunk #2: metadata[reasoningContent] ← 仍然为空 Chunk #5: metadata[reasoningContent] ← 始终为空结论reasoningContentkey存在但值始终是空字符串。测试 3根因定位Spring AI 2.0 内部使用官方 OpenAI Java SDKcom.openai.client.OpenAIClientokhttp4 实现来发送 HTTP 请求和解析 SSE 响应。这个 SDK 是 OpenAI 官方维护的它的数据模型只包含 OpenAI 标准字段不认reasoning_content这个非标准字段。数据流硅基流动 API 响应 ↓ 包含 delta.reasoning_content ✅ OpenAI Java SDK (com.openai.client) ↓ 解析 SSE映射到内部 ChatCompletion 对象 ↓ ❌ SDK 对象模型不支持 reasoning_content丢弃 Spring AI 2.0 OpenAiChatModel ↓ 从 SDK 对象构建 AssistantMessage ↓ metadata[reasoningContent] SDK 对象中的空值 应用代码 ↓ metadata.get(reasoningContent) → 官方文档说的metadata.get(reasoningContent)能拿到值的前提是使用spring-ai-starter-model-deepseek适配器DeepSeek 官方 API而不是通过硅基流动 OpenAI 适配器。第四阶段附带发现——循环依赖 Bug在测试过程中触发了ToolRegistry的循环依赖chatController → chatService → toolRegistry → ApplicationContextAware.setApplicationContext() → ctx.getBean(name) 遍历所有 bean → 触发 chatController 创建尚未完成 → BeanCurrentlyInCreationException修复将ToolRegistry从ApplicationContextAware改为EventListener(ApplicationReadyEvent.class)延迟到应用完全就绪后再扫描工具。最终结论谁丢了 reasoning_content层级reasoning_content 状态硅基流动 API 原始 JSON✅ 完整存在Spring AI 官方文档✅ 声称支持走 DeepSeek 官方适配器时OpenAI Java SDK (com.openai.client)❌ 对象模型不支持此非标准字段Spring AI 2.0 OpenAI 适配器❌ 映射后为空字符串我们自己的 HttpClient 直连✅ 手动解析完整保留我们的决策是对的吗是的。不用 Spring AI 直连的决策是正确的。不是因为 Spring AI 框架有 bug而是因为我们走的是硅基流动代理 OpenAI 适配器底层 SDK 是 OpenAI 官方的 Java 客户端reasoning_content不是 OpenAI 标准字段OpenAI 官方 SDK 不支持它除非换用 DeepSeek 官方 API spring-ai-starter-model-deepseek否则reasoningContent永远为空可以切回 Spring AI 吗如果要切换回去享受自动 Tool 发现等功能有两个选择换 DeepSeek 官方 APIapi.deepseek.comspring-ai-starter-model-deepseek——但这会失去硅基流动的代理优势继续用 HttpClient 直连——当前方案工作正常Tool 体系也已自建完成附测试代码位置src/test/java/com/jiang/ReasoningContentRawTest.java— 原始 HTTP 层测试可复现src/test/java/com/jiang/SpringAIReasoningTest.java— Spring AI ChatModel 层测试可复现需 MySQL/Redissrc/test/java/com/jiang/ReasoningContentTest.java— 最初尝试已废弃依赖 Spring 上下文加载失败
Spring AI 2.0 `reasoning_content` 丢失排查报告
发布时间:2026/6/26 10:14:02
Spring AI 2.0reasoning_content丢失排查报告背景Jiang I-Agent 使用 Spring AI 2.0 DeepSeek 模型通过硅基流动 SiliconFlow 代理需要获取模型的思考过程reasoning_content在前端展示。时间线第一阶段发现丢失约 2 周前现象使用 Spring AIChatClient流式调用时前端收不到reasoning_content同步调用却能收到。当时诊断怀疑ChunkMerger.mergeDeltas()在拼接流式 chunk 时丢弃了Delta.additionalProperties中的reasoning_content。当时决策绕过 Spring AI用java.net.http.HttpClient直接调硅基流动 API手动解析 SSE 响应。项目运行正常至今。第二阶段质疑诊断今日Spring AI 官方文档文档明确指出Spring AI maps thereasoning_contentfield from the JSON response into theAssistantMessagemetadata under the keyreasoningContent.正确用法ChatResponseresponsechatModel.call(newPrompt(...));AssistantMessagemessageresponse.getResult().getOutput();Stringreasoningmessage.getMetadata().get(reasoningContent);用户质疑既然 Spring AI 官方支持reasoningContent为什么我们会丢掉是不是我们用错了 API 层级ChatClient.stream().content()只拿 String而ChatModel.stream()才拿完整ChatResponse第三阶段三层测试验证今日为彻底搞清楚设计了三层递进测试测试 1原始 HTTP 层用java.net.http.HttpClient直接调硅基流动 API检查原始 JSON 响应中是否包含reasoning_content。被测模型deepseek-ai/DeepSeek-R1原生推理始终输出 reasoning_contentdeepseek-ai/DeepSeek-V3.2enable_thinking: true结果模型方式结果R1流式delta.reasoning_content存在于 333/345 chunks总长 613 字符R1同步message.reasoning_content存在总长 1004 字符V3.2thinking流式delta.reasoning_content存在于 46 chunks总长 93 字符结论硅基流动 API 完好无损地返回了reasoning_content。测试 2Spring AI 2.0 ChatModel 层用 Spring Boot 测试上下文加载真实的OpenAiChatModelBean排除了 MySQL/Redis 循环依赖问题后调用chatModel.stream()打印每个 chunk 的完整 metadata。被测模型deepseek-ai/DeepSeek-R1结果Chunk #1 | metadata keys[role, messageType, finishReason, refusal, annotations, index, id, reasoningContent] metadata[reasoningContent] ← 始终为空 metadata[finishReason] _UNKNOWN metadata[index] 0 Chunk #2: metadata[reasoningContent] ← 仍然为空 Chunk #5: metadata[reasoningContent] ← 始终为空结论reasoningContentkey存在但值始终是空字符串。测试 3根因定位Spring AI 2.0 内部使用官方 OpenAI Java SDKcom.openai.client.OpenAIClientokhttp4 实现来发送 HTTP 请求和解析 SSE 响应。这个 SDK 是 OpenAI 官方维护的它的数据模型只包含 OpenAI 标准字段不认reasoning_content这个非标准字段。数据流硅基流动 API 响应 ↓ 包含 delta.reasoning_content ✅ OpenAI Java SDK (com.openai.client) ↓ 解析 SSE映射到内部 ChatCompletion 对象 ↓ ❌ SDK 对象模型不支持 reasoning_content丢弃 Spring AI 2.0 OpenAiChatModel ↓ 从 SDK 对象构建 AssistantMessage ↓ metadata[reasoningContent] SDK 对象中的空值 应用代码 ↓ metadata.get(reasoningContent) → 官方文档说的metadata.get(reasoningContent)能拿到值的前提是使用spring-ai-starter-model-deepseek适配器DeepSeek 官方 API而不是通过硅基流动 OpenAI 适配器。第四阶段附带发现——循环依赖 Bug在测试过程中触发了ToolRegistry的循环依赖chatController → chatService → toolRegistry → ApplicationContextAware.setApplicationContext() → ctx.getBean(name) 遍历所有 bean → 触发 chatController 创建尚未完成 → BeanCurrentlyInCreationException修复将ToolRegistry从ApplicationContextAware改为EventListener(ApplicationReadyEvent.class)延迟到应用完全就绪后再扫描工具。最终结论谁丢了 reasoning_content层级reasoning_content 状态硅基流动 API 原始 JSON✅ 完整存在Spring AI 官方文档✅ 声称支持走 DeepSeek 官方适配器时OpenAI Java SDK (com.openai.client)❌ 对象模型不支持此非标准字段Spring AI 2.0 OpenAI 适配器❌ 映射后为空字符串我们自己的 HttpClient 直连✅ 手动解析完整保留我们的决策是对的吗是的。不用 Spring AI 直连的决策是正确的。不是因为 Spring AI 框架有 bug而是因为我们走的是硅基流动代理 OpenAI 适配器底层 SDK 是 OpenAI 官方的 Java 客户端reasoning_content不是 OpenAI 标准字段OpenAI 官方 SDK 不支持它除非换用 DeepSeek 官方 API spring-ai-starter-model-deepseek否则reasoningContent永远为空可以切回 Spring AI 吗如果要切换回去享受自动 Tool 发现等功能有两个选择换 DeepSeek 官方 APIapi.deepseek.comspring-ai-starter-model-deepseek——但这会失去硅基流动的代理优势继续用 HttpClient 直连——当前方案工作正常Tool 体系也已自建完成附测试代码位置src/test/java/com/jiang/ReasoningContentRawTest.java— 原始 HTTP 层测试可复现src/test/java/com/jiang/SpringAIReasoningTest.java— Spring AI ChatModel 层测试可复现需 MySQL/Redissrc/test/java/com/jiang/ReasoningContentTest.java— 最初尝试已废弃依赖 Spring 上下文加载失败