DeepSeek V4 第二轮 400:请求日志怎么查?

先看结论:获胜者是逐跳比对,不是盲目重试
只差 1 个历史字段,就可能让首轮工具调用成功、第二轮请求返回 400。DeepSeek 官方文档明确说明:思考模式下,如果 assistant 轮次产生了工具调用,后续请求必须完整回传 reasoning_content,否则可能收到 400。(api-docs.deepseek.com)
因此,排障优先级应是:模型响应 → 内存消息 → 数据库记录 → 队列消费者 → 最终请求。不要先重试、扩容,或把 reasoning_content 和 reasoning 全局复制到所有消息里。若同一业务链路同时连接 DeepSeek 官方 API 与 vLLM,则必须在适配层按端点契约转换字段。
这篇文章适合以下读者:
- 维护多轮
function calling循环,发现第二次请求才报 400 的 Agent 开发者; - 负责 API 网关、会话数据库、SDK 封装,怀疑未知字段被过滤的后端工程师;
- 同时接入 DeepSeek 官方 API 与 vLLM,需要保留端点切换能力的推理平台团队。
说明:本文按 2026 年 8 月 15 日可核实的官方文档撰写。数据核实自 DeepSeek 思考模式文档、DeepSeek 工具调用文档、DeepSeek Chat Completion 接口定义 与 vLLM reasoning outputs 文档。如果 DeepSeek 或 vLLM 修改默认字段名、兼容策略或工具调用限制,应在变更后 24 小时内重新回放。
首轮成功,第二轮失败:故障范围已经缩小
假设日志呈现以下形态:
// 第 1 次请求
{
"messages": [
{"role": "user", "content": "查询订单状态"}
]
}
// 第 1 次响应,工具调用成功
{
"assistant": {
"role": "assistant",
"content": "",
"reasoning_content": "[已脱敏]",
"tool_calls": [
{
"id": "call_abc",
"type": "function",
"function": {
"name": "get_order",
"arguments": "{\"id\":\"[脱敏]\"}"
}
}
]
}
}
// 第 2 次请求,返回 400
{
"messages": [
{"role": "user", "content": "查询订单状态"},
{
"role": "assistant",
"content": "",
"tool_calls": [
{"id": "call_abc", "type": "function", "function": {"name": "get_order", "arguments": "{}"}}
]
},
{"role": "tool", "tool_call_id": "call_abc", "content": "[已脱敏]"}
]
}
这个故障签名首先排除两类问题:
✅ 工具名称、基础 JSON Schema 和首次请求至少能被服务端接受。
✅ API 密钥、基础 URL 和模型路由通常不是首要嫌疑。
⚠️ 历史 assistant 消息在第二次发送前被重建、过滤或字段映射错误,优先级更高。
DeepSeek 的工具调用示例把模型返回的 assistant 消息直接追加到 messages,因为其中同时包含 content、reasoning_content 和 tool_calls。如果业务代码只保存 content 与 tool_calls,第二次请求就不再等价于官方示例。(api-docs.deepseek.com)
这里不要只记录“第几次重试”。真正有价值的是同一 conversation_id 下的字段差异:
message[0] role=user fields=role,content
message[1] role=assistant fields=role,content,reasoning_content,tool_calls
message[2] role=tool fields=role,tool_call_id,content
失败请求中只要 message[1] 少了 reasoning_content,就应先沿序列化链路追踪,而不是继续增加重试次数。
没有推理字段:先查保存逻辑,再查模型本身
最终请求完全没有 reasoning_content 时,常见原因有 4 个:
- SDK 返回对象被转换成只包含
content、tool_calls的自定义类; - 数据库 schema 没有推理字段,写入时被静默丢弃;
- 网关只允许固定字段,未知字段被白名单过滤;
- 流式响应合并时只拼接
content,没有累积推理增量。
DeepSeek 官方接口定义将 reasoning_content 放在 assistant 消息层级,并与 content、tool_calls 并列。工具调用场景下,官方文档还特别要求后续请求继续携带该字段。(api-docs.deepseek.com)
排查时,先把“是否存在”与“内容是否完整”分开:
assistant_msg = {
"role": "assistant",
"content": model_msg.get("content"),
"reasoning_content": model_msg.get("reasoning_content"),
"tool_calls": model_msg.get("tool_calls"),
}
assert assistant_msg["role"] == "assistant"
assert assistant_msg["tool_calls"]
assert "reasoning_content" in assistant_msg
生产日志不要打印完整推理文本。建议记录:
{
"message_index": 1,
"role": "assistant",
"fields": ["role", "content", "reasoning_content", "tool_calls"],
"reasoning_present": true,
"reasoning_length": "[长度]",
"tool_call_ids": ["call_abc"]
}
如果使用流式返回,还要检查每个 chunk 的 reasoning_content 增量是否被单独累积。不能用“只要最终 content 非空”判断 assistant 消息完整,因为思考内容与最终内容是不同字段。
有推理内容但字段不匹配:DeepSeek 与 vLLM 必须分栏处理
这类问题最容易被“兼容 OpenAI 格式”掩盖。两个端点都可能使用类似的消息结构,但推理字段契约并不等价。
DeepSeek 官方思考模式工具调用链使用 reasoning_content。当前 vLLM reasoning 文档则以 reasoning 作为输出字段,并明确说明该字段曾经使用过 reasoning_content 这一旧名称。(github.com)
两类端点的判断可以先这样写:
| 检查项目 | DeepSeek 官方 API | vLLM 端点 |
|---|---|---|
| 工具调用时重点字段 | reasoning_content |
主要输出字段为 reasoning |
| 历史 assistant 消息 | 按官方文档完整回传 reasoning_content |
依据实际部署版本与协议定义处理 |
| 字段兼容性 | 以官方 API schema 为准 | 不能把旧字段兼容视为永久契约 |
| 出站策略 | 映射为 reasoning_content |
映射为目标版本要求的字段 |
| 主要风险 | 字段缺失导致后续 400 | 客户端仍硬编码旧字段,或把 vLLM 输出原样转发 |
安全的内部对象可以统一成业务字段,但不要把内部字段直接当成出站字段:
internal_message = {
"role": "assistant",
"content": msg.get("content"),
"reasoning": msg.get("reasoning") or msg.get("reasoning_content"),
"tool_calls": msg.get("tool_calls"),
}
def to_deepseek(message):
result = {
"role": "assistant",
"content": message.get("content"),
"tool_calls": message.get("tool_calls"),
}
if message.get("reasoning") is not None:
result["reasoning_content"] = message["reasoning"]
return result
def to_vllm(message):
result = {
"role": "assistant",
"content": message.get("content"),
"tool_calls": message.get("tool_calls"),
}
if message.get("reasoning") is not None:
result["reasoning"] = message["reasoning"]
return result
这段逻辑的重点不是“复制两个字段”,而是让端点适配层承担协议差异。vLLM 具体版本是否接受旧字段,必须对照部署版本的 protocol 定义和最小请求验证,不能依据某个旧版本经验下结论。vLLM 社区曾讨论过从 reasoning_content 迁移到 reasoning 的兼容策略,但这类 issue 只能作为版本线索,不能代表所有部署版本。(github.com)
字段在内存里存在,链路中途消失:按 4 跳留证
如果模型响应中有推理字段,但最终请求没有,故障就不在模型输出本身。建议按以下顺序采集脱敏快照。
第 1 跳:SDK 响应对象
记录:
- 返回对象的真实类型;
- assistant 消息的字段集合;
tool_calls数量与 ID;reasoning_content或reasoning是否存在;- 流式模式下的累计长度。
某些 SDK 对未知字段不会报错,而是直接在对象转换时丢失。此时只看 Python 对象的 repr 不够,应使用序列化后的字典检查。
第 2 跳:网关出站与入站
网关重点检查 3 类规则:
- JSON schema 是否只允许
role、content、tool_calls; - 清理空值时是否把推理字段删除;
- 对象重建时是否只复制“已知字段”。
应分别记录进入网关前和离开网关后的字段集合。不要记录密钥、完整推理文本和真实工具结果,只记录字段名、消息索引、长度和哈希。
第 3 跳:数据库与队列
数据库常见问题不是字段完全不存在,而是版本不一致:
- 新字段已写入,但旧消费者读取旧 schema;
- JSON 列保存完整对象,读取层却映射成固定 DTO;
- 队列消息经过压缩或重建,只保留可展示内容;
- 多个 assistant 消息按时间排序后,推理字段挂到了错误索引。
每个消息应保留稳定的 message_index 或内部 ID。只按时间戳重排,容易把 tool 消息插入错误位置。
第 4 跳:最终请求构造器
最终构造器必须做断言,而不是依赖日志观察:
for index, message in enumerate(messages):
if message["role"] == "assistant" and message.get("tool_calls"):
assert message.get("reasoning_content") is not None, index
for tool_message in [m for m in messages if m["role"] == "tool"]:
assert tool_message.get("tool_call_id")
如果目标端点是 vLLM,就把断言字段切换为当前版本实际要求的字段。断言应位于出站适配器之后,因为内部对象完整,不等于最终 payload 完整。
推理字段正确后:继续核对消息链与参数
字段已经存在,400 仍未消失时,不要继续扩大字段复制范围。下一组检查应集中在消息顺序、关联 ID 和思考模式参数。
标准工具调用链通常应保持:
user
assistant:tool_calls
tool:tool_call_id 对应 assistant 的调用 ID
assistant 或 user:继续请求
重点检查:
✅ tool_call_id 是否与上一条 assistant 的 tool_calls[].id 完全一致。
✅ tool 消息是否插在对应 assistant 工具调用之后。
✅ assistant 的 content 是否被框架改成了错误类型,或由空字符串变成不兼容的空值。
✅ 是否把多个工具结果合并成一条,却丢失了每个调用的独立 ID。
✅ 是否在思考模式下发送了目标端点不支持的额外参数。
DeepSeek 当前文档说明,思考模式对部分采样参数不提供实际支持;官方集成说明还指出,DeepSeek V4 思考模式可能拒绝 tool_choice 参数。遇到 400 时,应把字段错误、消息链错误和参数限制分别用最小请求验证,不能全部归因于 reasoning_content。(api-docs.deepseek.com)
一个合格的最小复现只保留:
{
"model": "[模型名]",
"messages": [
{"role": "user", "content": "[短问题]"},
{
"role": "assistant",
"content": "",
"reasoning_content": "[脱敏占位]",
"tool_calls": [
{
"id": "call_test",
"type": "function",
"function": {
"name": "test_tool",
"arguments": "{}"
}
}
]
},
{
"role": "tool",
"tool_call_id": "call_test",
"content": "[脱敏结果]"
}
]
}
如果这个请求直连官方 API 成功,而经过业务链路失败,问题就在网关、存储、队列或适配层。如果直连官方 API 失败,再去检查端点参数和消息契约。
按条件选择排障路径
- 若首轮响应已经没有推理字段,先检查模型模式、SDK 返回对象和流式合并逻辑;不要先查数据库。
- 若首轮响应有
reasoning_content,内存对象也有,最终请求没有,优先检查网关白名单、空值清理和出站序列化。 - 若 vLLM 输出只有
reasoning,目标是 DeepSeek 官方 API,在适配层映射为reasoning_content,不要原样转发。 - 若目标是 vLLM,且部署版本文档要求
reasoning,保留reasoning;旧字段是否可输入必须通过当前版本最小回放确认。 - 若字段存在、ID 正确但仍 400,移除可疑的
tool_choice和不适用参数,再核对消息顺序。 - 若直连两类端点都成功,完整链路失败,锁定网关、数据库、队列消费者或消息重建器。
- 若长期重负载任务无法稳定复现,回退到固定测试环境,保存失败会话和端点版本,不要用线上重试次数代替证据。
用同一会话回放确认修复层级
修复完成后,至少保留一份最小失败会话。相同的消息序列分别执行 3 次:
- 直连 DeepSeek 官方 API;
- 直连目标 vLLM 版本;
- 经过完整业务链路,包括网关、数据库、队列和最终适配器。
每次回放记录:
- 端点类型;
- 模型标识;
- vLLM 部署版本或官方 API 路由;
- assistant 消息字段集合;
- tool_call ID;
- 最终请求与修复前的差异;
- 服务端错误消息。
修复应落在明确的序列化或端点适配层。全局同时塞入 reasoning_content 和 reasoning,只能暂时掩盖边界问题,还可能把内部推理字段发送到不接受它的端点。
如果团队需要固定环境保存失败会话,可以先参考 ProxyMac 的帮助与远程环境使用说明,把端点版本、回放脚本和日志脱敏规则固定下来。对于需要 macOS 客户端、Xcode 自动化或长时间在线回归的项目,再单独评估ProxyMac 的 Mac 方案,不要把云端 Mac 当成 API 字段问题的替代修复。
两类方案的排查边界
| 方案 | 适合确认的问题 | 优点 | 局限 |
|---|---|---|---|
| 直连 DeepSeek 官方 API | reasoning_content 是否符合官方工具调用契约 |
端点行为清晰,适合验证 400 条件 | 无法覆盖业务网关和数据库问题 |
| 直连 vLLM | 当前部署版本返回什么 reasoning 字段 | 可验证实际协议与解析器配置 | 不同版本兼容行为不能互相推定 |
| 完整业务链路回放 | 字段在哪一跳丢失或改名 | 最接近生产,能暴露序列化问题 | 环境变化会影响复现稳定性 |
| 本地临时拼接两个字段 | 快速验证字段是否相关 | 改动小 | ❌ 容易掩盖端点边界,不能作为长期方案 |
首轮成功、第二轮失败时,最可靠的证据不是“重试后偶尔成功”,而是同一会话在不同节点的字段集合差异。只要能指出 reasoning_content、reasoning、tool_call_id 或消息索引在哪一跳发生变化,修复范围通常就能从整个 Agent 系统缩小到一个适配器或消费者。
常见问题
为什么第一次工具调用成功,第二次才报 400?
因为第一次请求只验证了工具定义和首次生成。第二次请求会把历史 assistant 工具调用消息重新发送给模型,服务端此时才检查推理字段、工具调用 ID 和消息排列。若历史消息被裁剪,首轮可以成功,后续仍会失败。
reasoning_content 已保存,为什么请求还是失败?
需要确认保存的是哪一份消息,以及最终出站请求是否读取了同一字段。数据库中存在字段,不代表队列消费者、SDK 序列化器或网关会保留它。应对比存储前、读取后和最终请求的字段集合。
vLLM 的 reasoning 能不能直接发给 DeepSeek?
不能把它当作无条件兼容。当前 vLLM 文档以 reasoning 为主字段,DeepSeek 官方工具调用链要求 reasoning_content。应在端点适配层转换,并针对实际 vLLM 版本验证输入行为。
最小日志需要记录完整推理内容吗?
不需要。记录字段是否存在、字段长度、消息索引、工具调用 ID 和脱敏哈希即可。完整推理内容可能包含敏感信息,也会增加日志体积。排障目标是定位字段丢失和消息错位,不是复盘模型全部内部过程。
结尾:把当前链路和 Mac 环境分开评估
如果问题已经定位到字段丢失层,继续更换模型、增加重试或扩容都不是优先解法。当前 API 链路的真实缺点是:网关字段白名单容易隐藏错误,数据库和队列会引入对象重建,官方 API 与 vLLM 的字段契约又不能直接混用。
对于只需要接口回放的项目,固定的脚本环境通常已经足够。若还要做 macOS 客户端联调、Xcode 自动化、桌面 Agent 回归或长期在线任务,临时本地机器往往会受到系统版本、登录状态和设备占用影响。此时,租赁 ProxyMac 的 Mac 环境可以把客户端运行环境与 API 适配层分开,先用固定会话验证修复,再决定是否长期保留。