记录一下那天晚上的排查过程。用 deepseek-reasoner 做多轮排障对话,把上一轮 assistant 消息原样 append 回 messages,接口返回 400,原文 reasoning_content is not allowed in the request,只留 content 就正常,但模型的推理脉络也丢了。

刚开始盯着「从日志回放时把完整响应体直接塞进了历史」,折腾了半个多小时,排掉了。

排查方向其实有限,先把可能性排一排。reasoning_content 是输出专用字段,不能作为输入回传。也可能是SDK 的 message 对象会把所有字段一起序列化。

不容易想到的是,自建网关做格式转换时保留了未知字段。

先把实际发出的 messages 完整打印出来

python -c "import json;print(json.dumps(payload['messages'],ensure_ascii=False)[:500])"

然后用 jq 删掉历史里的该字段再重发

curl https://api.deepseek.com/chat/completions -H "Authorization: Bearer $DS_KEY" -H "Content-Type: application/json" -d "$(jq 'del(.messages[].reasoning_content)' payload.json)"

再往下在写历史的地方只保留 role 与 content

python -c "import json;d=json.load(open('payload.json'));[m.pop('reasoning_content',None) for m in d['messages']];json.dump(d,open('clean.json','w'),ensure_ascii=False)"

末了加一层字段白名单,防止以后再带进来

python -c "print(['role','content','tool_calls'])"

怎么算真的好了,看的是同一批现象。同样的三轮对话返回 200,messages 里不再出现该字段,模型仍能基于上一轮结论继续回答。

这里还有个坑。把推理内容写进日志是对的,但回放时必须过滤,历史里只留 role 和 content 这两类通用字段,换供应商时才不会踩同一个坑。线上能稳定复现的问题都不算难,难的是只在大流量下才出现的那种。

作者 9823189