AIAgent

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

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

先看结论:获胜者是逐跳比对,不是盲目重试

只差 1 个历史字段,就可能让首轮工具调用成功、第二轮请求返回 400。DeepSeek 官方文档明确说明:思考模式下,如果 assistant 轮次产生了工具调用,后续请求必须完整回传 reasoning_content,否则可能收到 400。(api-docs.deepseek.com)

因此,排障优先级应是:模型响应 → 内存消息 → 数据库记录 → 队列消费者 → 最终请求。不要先重试、扩容,或把 reasoning_contentreasoning 全局复制到所有消息里。若同一业务链路同时连接 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,因为其中同时包含 contentreasoning_contenttool_calls。如果业务代码只保存 contenttool_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 返回对象被转换成只包含 contenttool_calls 的自定义类;
  • 数据库 schema 没有推理字段,写入时被静默丢弃;
  • 网关只允许固定字段,未知字段被白名单过滤;
  • 流式响应合并时只拼接 content,没有累积推理增量。

DeepSeek 官方接口定义将 reasoning_content 放在 assistant 消息层级,并与 contenttool_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_contentreasoning 是否存在;
  • 流式模式下的累计长度。

某些 SDK 对未知字段不会报错,而是直接在对象转换时丢失。此时只看 Python 对象的 repr 不够,应使用序列化后的字典检查。

第 2 跳:网关出站与入站

网关重点检查 3 类规则:

  • JSON schema 是否只允许 rolecontenttool_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 次:

  1. 直连 DeepSeek 官方 API;
  2. 直连目标 vLLM 版本;
  3. 经过完整业务链路,包括网关、数据库、队列和最终适配器。

每次回放记录:

  • 端点类型;
  • 模型标识;
  • vLLM 部署版本或官方 API 路由;
  • assistant 消息字段集合;
  • tool_call ID;
  • 最终请求与修复前的差异;
  • 服务端错误消息。

修复应落在明确的序列化或端点适配层。全局同时塞入 reasoning_contentreasoning,只能暂时掩盖边界问题,还可能把内部推理字段发送到不接受它的端点。

如果团队需要固定环境保存失败会话,可以先参考 ProxyMac 的帮助与远程环境使用说明,把端点版本、回放脚本和日志脱敏规则固定下来。对于需要 macOS 客户端、Xcode 自动化或长时间在线回归的项目,再单独评估ProxyMac 的 Mac 方案,不要把云端 Mac 当成 API 字段问题的替代修复。

两类方案的排查边界

方案 适合确认的问题 优点 局限
直连 DeepSeek 官方 API reasoning_content 是否符合官方工具调用契约 端点行为清晰,适合验证 400 条件 无法覆盖业务网关和数据库问题
直连 vLLM 当前部署版本返回什么 reasoning 字段 可验证实际协议与解析器配置 不同版本兼容行为不能互相推定
完整业务链路回放 字段在哪一跳丢失或改名 最接近生产,能暴露序列化问题 环境变化会影响复现稳定性
本地临时拼接两个字段 快速验证字段是否相关 改动小 ❌ 容易掩盖端点边界,不能作为长期方案

首轮成功、第二轮失败时,最可靠的证据不是“重试后偶尔成功”,而是同一会话在不同节点的字段集合差异。只要能指出 reasoning_contentreasoningtool_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 适配层分开,先用固定会话验证修复,再决定是否长期保留。

用独享远程 Mac,快速复现并定位第二轮 400

ProxyMac 提供独享 Mac mini M4 云主机,适合搭建稳定、可重复的请求回放与日志排查环境。
支持 SSH、VNC 和浏览器直连,无需等待本地设备配置,付款后通常 1–5 分钟即可开通。