DeepSeek V4 迁移过程中,如果 Agent 使用 thinking mode + 多轮 Tool-Calling,一个比较隐蔽的问题是 reasoning_content 在轮次之间丢失,最终触发 400。
典型错误类似:
text
The reasoning_content in the thinking mode must be passed back to the API
这个问题不一定是业务 tool 写错了,也不一定是某个 tool result 有问题。很多时候,它发生在 多轮 messages 传递过程 中:上游框架或 agent loop 把 DeepSeek provider-specific 字段丢掉了。
本文从消息链路出发,梳理这个问题的原因、排查方法,以及一种迁移期可用的本地中继缓解方案。
1. 问题背景
在普通 Chat Completion 场景里,请求和响应通常围绕这些字段展开:
json
{
"role": "assistant",
"content": "..."
}
而在 Tool-Calling 场景里,assistant message 可能包含:
json
{
"role": "assistant",
"tool_calls": [
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
}
]
}
到了 DeepSeek thinking mode,assistant message 里还可能出现 provider-specific 字段:
json
{
"role": "assistant",
"reasoning_content": "...",
"tool_calls": [
{
"id": "call_xxx",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Shanghai\"}"
}
}
]
}
问题就出在这里:很多上游框架会把 message 规范化成通用 OpenAI-compatible 结构,在下一轮请求里只保留 role、content、tool_calls 等字段,而丢掉 reasoning_content。
2. 最小问题链路
可以把问题简化成下面这个链路:
text
Turn 1 response:
assistant message has:
- tool_calls
- reasoning_content
Turn 2 request:
assistant message keeps:
- tool_calls
assistant message drops:
- reasoning_content
DeepSeek API:
HTTP 400
也就是说,tool call 本身可能是对的,tool result 也可能是对的,但 assistant message 的 provider-specific 状态在多轮传递时丢了。
这类问题的难点在于,它不是一个单点错误,而是跨轮次状态错误。
3. 排查清单
遇到这个错误时,可以按下面顺序排查。
3.1 找上一轮 assistant response
先找到失败前一轮模型返回的 assistant message,看它是否同时包含:
tool_callsreasoning_content
如果上一轮 response 本身没有 reasoning_content,那就要先确认当前模型模式、请求参数和 DeepSeek thinking mode 是否符合预期。
3.2 找下一轮 request
再看下一轮发送给 DeepSeek 的 request。
重点检查同一个 assistant tool-call message 是否还保留:
- 原来的
tool_calls - 原来的
reasoning_content
如果 tool_calls 还在,但 reasoning_content 没了,就高度可疑。
3.3 对 tool_call id
检查 tool_call_id 是否能对应上:
text
assistant.tool_calls[0].id == tool.tool_call_id
如果 id 对不上,可能是另一个 Tool-Calling 链路问题,不一定是 reasoning_content 丢失。
3.4 检查 strict schema
DeepSeek strict schema 也容易被误判。
常见要求:
- object 的所有 properties 应该进入
required - object 应该设置
additionalProperties: false minLength、maxLength、minItems、maxItems这类限制不适合直接放进 strict schema
同时不要把支持项误报成不支持。例如:
patternformatminimummaximummultipleOf
这些字段在 JSON Schema 工具链里很常见,如果 linter 写得不准,很容易把排查方向带偏。
4. 长期修复方案
长期看,最合理的修复位置是上游框架或 agent loop。
框架在多轮 Tool-Calling 中应该保证:
- assistant message 的 provider-specific 字段不被无意丢弃
tool_calls和后续toolmessage 的tool_call_id稳定对应- message role 顺序符合模型 API 要求
- strict schema 生成器不会输出 DeepSeek 不支持的字段
如果问题出现在 LangChain、LlamaIndex、OpenClaw、Hermes Agent 等框架里,比较好的方式是整理一个最小可复现 fixture,然后给上游提 issue。
长期方案应该是上游正确保留状态,而不是业务侧一直维护兼容补丁。
5. 迁移期缓解:本地 OpenAI-compatible 中继
但在迁移期,很多团队需要先做三件事:
- 快速判断问题是不是
reasoning_content丢失 - 尽快恢复本地调试链路
- 生成可复现材料给上游框架提 issue
这时可以考虑在本地加一层 OpenAI-compatible 中继:
text
OpenAI-compatible client -> local relay -> DeepSeek API
已有代码通常只需要改 baseURL:
text
http://127.0.0.1:8787/v1
中继可以做这些事:
- 第一轮 response 经过中继时,记录 tool_call id 对应的
reasoning_content - 后续 request 经过中继时,检查 assistant tool-call message 是否丢失
reasoning_content - 如果上下文完整、tool_call id 能对应上,在转发给 DeepSeek 前补回
- 输出诊断日志,指出哪一轮 message 被修复
这种方式的好处是侵入性低:对 OpenAI-compatible client 来说,只需要改 baseURL。
6. 必须强调的边界
本地中继不是万能修复。
它能工作有一个前提:
text
相关请求和响应必须从对话开始就经过中继
如果 reasoning_content 在进入中继之前就已经丢了,中继无法凭空恢复,只能诊断。
另外,本地中继更适合:
- 本地调试
- 迁移期止血
- 复现问题
- 生成 issue fixture
不应该在没有会话隔离、持久化、鉴权、审计的情况下,当作长期生产 LLM Gateway 直接使用。
7. 可复现参考实现
下面这个开源仓库把上述思路整理成了一个参考实现:
GitHub: github.com/xiaoshuo198...
npm: www.npmjs.com/package/dee...
它包含:
diagnose:检查 JSONL run 中是否存在reasoning_content丢失lint-schema:检查 DeepSeek strict schema 常见问题proxy:启动本地 OpenAI-compatible 中继- mock upstream demo:不需要 DeepSeek API key 就能复现修复链路
- OpenAI JS 多轮 Tool-Calling demo:模拟第二轮丢失
reasoning_content,验证中继补回
无 key 复现:
bash
git clone https://github.com/xiaoshuo1988130/deepseek-compat-kit.git
cd deepseek-compat-kit
npm run demo:mock
OpenAI JS SDK 多轮 Tool-Calling 复现:
bash
npm run demo:openai-js-tool-calls
终端里会看到类似:
text
WARN DSK_REASONING_003 messages[1]: injected cached reasoning_content for 1 tool call(s).
[openai-js-tool-calls] final: mock upstream received repaired reasoning_content
如果只是想启动本地中继:
bash
DEEPSEEK_API_KEY=sk-... npx deepseek-compat-kit proxy --port 8787
然后把 OpenAI-compatible client 的 baseURL 指到:
text
http://127.0.0.1:8787/v1
8. 生态接入现状
当前比较适合直接验证的是 OpenAI-compatible 链路。
参考实现里目前有这些材料:
- OpenAI JS SDK:有可运行 mock demo
- OpenClaw:有 baseURL 配置指南,live e2e 待验证
- Hermes Agent:有 baseURL 配置指南,live e2e 待验证
- Claude Code:目前不是 OpenAI-compatible 直连路径,只适合作为兼容性边界说明
这里尤其要注意 Claude Code。它通常走 Anthropic-compatible 配置,而不是 OpenAI-compatible /v1/chat/completions。不能简单把 ANTHROPIC_BASE_URL 指到 OpenAI-compatible 中继地址。
9. 迁移窗口
DeepSeek 官方 changelog 提到 deepseek-chat 和 deepseek-reasoner 会在 2026-07-24 停用。
如果 Agent 依赖多轮 Tool-Calling,建议提前验证 V4 路径:
- 多轮 messages 是否保留 provider-specific 字段
- strict schema 是否符合 DeepSeek 要求
- 失败时能否导出脱敏 replay fixture
- 上游框架是否已经修复对应问题
总结
reasoning_content 400 的核心不是"某个 tool 写错了",而是多轮 Tool-Calling 里 assistant message 的 provider-specific 状态在传递过程中丢失。
排查时要看跨轮次 messages,而不是只看最后一次 API 请求。
长期方案应该是框架正确保留 DeepSeek 字段;迁移期可以用本地 OpenAI-compatible 中继做诊断和临时缓解。但中继必须从第一轮对话开始看到完整上下文,否则无法恢复已经丢失的内容。