Spring AI 2.0 reasoning_content 丢失排查报告

背景

Jiang I-Agent 使用 Spring AI 2.0 + DeepSeek 模型(通过硅基流动 SiliconFlow 代理),需要获取模型的思考过程(reasoning_content)在前端展示。

时间线

第一阶段:发现丢失(约 2 周前)

现象:使用 Spring AI ChatClient 流式调用时,前端收不到 reasoning_content,同步调用却能收到。

当时诊断:怀疑 ChunkMerger.mergeDeltas() 在拼接流式 chunk 时丢弃了 Delta.additionalProperties 中的 reasoning_content

当时决策:绕过 Spring AI,用 java.net.http.HttpClient 直接调硅基流动 API,手动解析 SSE 响应。项目运行正常至今。

第二阶段:质疑诊断(今日)

Spring AI 官方文档,文档明确指出:

Spring AI maps the reasoning_content field from the JSON response into the AssistantMessage metadata under the key reasoningContent.

正确用法:

ChatResponse response = chatModel.call(new Prompt("..."));
AssistantMessage message = response.getResult().getOutput();
String reasoning = message.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_content)
  • deepseek-ai/DeepSeek-V3.2 + enable_thinking: true

结果

模型方式结果
R1流式🟢 delta.reasoning_content 存在于 333/345 chunks,总长 613 字符
R1同步🟢 message.reasoning_content 存在,总长 1004 字符
V3.2+thinking流式🟢 delta.reasoning_content 存在于 46 chunks,总长 93 字符

结论:硅基流动 API 完好无损地返回了 reasoning_content

测试 2:Spring AI 2.0 ChatModel 层

用 Spring Boot 测试上下文加载真实的 OpenAiChatModel Bean(排除了 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"] = ""     ← 🔴 始终为空!

结论reasoningContent key 存在,但值始终是空字符串

测试 3:根因定位

Spring AI 2.0 内部使用官方 OpenAI Java SDKcom.openai.client.OpenAIClient,okhttp4 实现)来发送 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

修复:将 ToolRegistryApplicationContextAware 改为 @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,而是因为:

  1. 我们走的是硅基流动代理 + OpenAI 适配器,底层 SDK 是 OpenAI 官方的 Java 客户端
  2. reasoning_content 不是 OpenAI 标准字段,OpenAI 官方 SDK 不支持它
  3. 除非换用 DeepSeek 官方 API + spring-ai-starter-model-deepseek,否则 reasoningContent 永远为空

可以切回 Spring AI 吗?

如果要切换回去享受自动 Tool 发现等功能,有两个选择:

  1. 换 DeepSeek 官方 APIapi.deepseek.com)+ spring-ai-starter-model-deepseek——但这会失去硅基流动的代理优势
  2. 继续用 HttpClient 直连——当前方案工作正常,Tool 体系也已自建完成

附:测试代码位置

  • src/test/java/com/jiang/ReasoningContentRawTest.java — 原始 HTTP 层测试(可复现)
  • src/test/java/com/jiang/SpringAIReasoningTest.java — Spring AI ChatModel 层测试(可复现,需 MySQL/Redis)
  • src/test/java/com/jiang/ReasoningContentTest.java — 最初尝试(已废弃,依赖 Spring 上下文加载失败)
Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐