AIFreeAPI Logo

DeepSeek V4 本地编码 Agent 不调用工具:DSML、解析器、量化还是 Harness?

A
8 分钟阅读开发者指南

能对话不等于能执行工具。保存同一轮的原始输出、结构化响应、客户端事件和下一次请求,先找出结构在哪一层丢失。

DeepSeek V4 工具调用经过模型、解析器、运行时和 Agent harness 的故障边界

本地 DeepSeek V4 能解释代码,却不肯读文件、跑命令或提交补丁,未必是模型“变笨了”。同一个界面症状,可能发生在完全不同的位置:模型没生成正确 DSML,服务端没有把 DSML 转成 tool_calls,量化或推理路径改变了结构 token,客户端没有派发工具,或者第二轮请求丢了必要的 assistant 状态。

排障的关键不是先换模型,而是固定一条最小工具请求,找到结构第一次出错的边界。一次改变多个变量,只会得到另一个无法解释的结果。

先保存一轮完整证据

选择一个无副作用、参数简单的工具,例如读取一个临时文本文件或返回固定日期。为同一次请求保存以下六项,并在分享前删除密钥、真实路径、工具参数和仓库内容:

  1. 发给模型的最终 prompt 或渲染后的消息摘要;
  2. parser 处理前的原始 completion text;
  3. OpenAI 兼容接口返回的完整 message;
  4. Agent 客户端归一化后的工具事件;
  5. 实际派发的工具名、call ID 和结果;
  6. 带着工具结果发出的下一次请求。

这六项能把“没有调用工具”拆成可验证的问题:

第一个异常位置看到的现象优先检查
模型原始输出没有 invoke,或 DSML 标签、工具名、参数不完整prompt 渲染、上下文长度、采样、模型 artifact
服务端 parser原始输出有完整 invoke,API 却只有普通 contentV4 tokenizer/parser 配置与 runtime 版本
量化/推理运行时某 artifact 无法加载,或只有某一推理路径产生坏标签保持其他条件不变做 artifact 或特性 A/B
Agent harnessAPI 已有 tool_calls,客户端仍不执行事件归一化、权限、参数校验、派发逻辑
消息回放第一轮执行成功,第二轮 400、重复调用或失忆assistant message、call ID、tool result 的完整性

原始 DSML 能先排除一半猜测

DeepSeek 的 V4 官方模型卡 明确说明,这一代没有随模型提供普通 Jinja chat template,而是通过专用 encoding 实现编码 OpenAI 风格消息、解析 completion。工具意图在模型文本里使用 DSML 表达,结构大致如下:

text
<|DSML|tool_calls> <|DSML|invoke name="read_file"> <|DSML|parameter name="path" string="true">src/app.ts</|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls>

这不是让编码 Agent 自己解析的公共格式。正常链路应由服务端把它转换为结构化 tool_calls。因此,判断方法非常直接:

  • 原始文本就没有 invoke:先查模型生成和 prompt rendering;
  • 原始文本含完整 invoke,但 API message 没有 tool_calls:先查 parser;
  • API 已有正确 tool_calls,客户端没动作:转向 harness;
  • 工具执行后下一轮才坏:检查历史与协议回放。

vLLM issue #48931 给出了一个不需要 GPU 的 parser 级复现:当完整 invoke 缺少外层开始标记时,相关路径会把整段 DSML 当成普通内容,没有生成 tool_calls。这同时揭示两个不同问题——模型或渲染可能漏了 wrapper,而 parser 是否应该保守恢复是另一个决定。不能把两者合并成“量化毁了工具调用”。

另一份 #51914 报告在 V4-Flash-0731、vLLM 0.27.1 和 DSpark 环境里看到开始 wrapper 拼写异常。报告者明确写明 DSpark 因果尚未成立,正确动作是保持输入不变做 ON/OFF 对照,而不是把 speculative decoding 直接定罪。

Parser 排查可以不经过编码 Agent

先用 stream=false 请求一次,再打开 streaming。对确实需要工具的同一 prompt,再比较 tool_choice="required"tool_choice="auto"。vLLM 的 #40801 曾在特定环境中报告 auto + stream 更容易泄漏 DSML,而 required 或非流式明显改善。该 issue 已关闭,版本也在变化;这些开关适合定位分支,不应永久写成“最佳配置”。

还要核对实际容器版本,而不是只看启动命令。当前 vLLM 把 DeepSeek V4 的 reasoning 和 DSML 放进专用 parser 状态机,早期路径则经历过类型转换、arguments wrapper、流结束缓冲等修复,#41240 记录了这些边界。给旧镜像复制新 flag,并不会把新 parser 代码复制进去。

可以在进入复杂客户端前放一个极小的观测门:

python
def first_bad_boundary(raw_text, api_message): raw_has_invoke = "<|DSML|invoke" in raw_text calls = api_message.get("tool_calls") or [] if raw_has_invoke and not calls: return "server parser" if calls: return "inspect client dispatch and replay" return "inspect generation and prompt rendering"

它不是容错 DSML parser,也不要放进生产链路“修复”模型输出;它只帮助记录第一个断点。

对照原始 DSML、结构化 tool_calls 与后续检查动作的分层诊断表
对照原始 DSML、结构化 tool_calls 与后续检查动作的分层诊断表

什么时候才轮到量化

官方模型卡列出的 V4-Flash 本身就是混合精度:MoE expert 使用 FP4,多数其他参数使用 FP8。因此,“出现量化”不等于“使用了不正确的第三方版本”。真正的问题是 artifact、tokenizer/encoding revision 与 runtime 是否匹配。

量化相关现象至少分两类:

  • 加载兼容性:vLLM #41604 报告某些非 canonical V4 quant 缺少 scale_fmt metadata,初始化时直接失败。此时尚未进入工具循环。
  • 生成差异:某个 artifact 在相同输入下更容易漏掉 DSML delimiter。这必须比较 parser 前的原始输出,不能从 Q4、AWQ、GGUF 等名字推断。

做量化 A/B 时,固定模型 revision、encoding/tokenizer、runtime build、parser flag、采样、上下文、tool schema、prompt,以及支持时的 seed。只替换 artifact。若两个 artifact 都产生相同的有效 DSML,只有其中一次 API message 丢了 tool_calls,最早异常仍在 parser 或其运行路径,而不是权重。

Harness 要到第二轮成功后才能过关

服务端返回 tool_calls 只证明结构到达客户端。继续检查:客户端是否接受 finish reason,工具别名是否被改写,arguments 是否被二次包裹,call ID 是否保持,权限是否阻止执行,tool result 是否匹配原调用。

如果第一轮工具执行成功、下一轮才返回 400,还要分清托管 API 和本地 OpenAI 兼容服务的合同。DeepSeek 的托管 thinking mode 文档要求:含工具调用的 assistant turn 需要在后续请求中完整回传 reasoning_content,漏掉可能得到 HTTP 400。本地服务不一定执行完全相同的校验。正确做法不是盲目补字段,而是逐字段比较服务端返回的 assistant message 与 harness 实际追加的历史。

一个最小 SDK 循环能完成两轮、完整编码 Agent 却失败时,继续换 quant 的信息价值已经很低。差异通常落在客户端事件适配、工具权限、派发或 history reconstruction。

从模型原始输出到工具执行和下一轮请求的一次完整受控链路
从模型原始输出到工具执行和下一轮请求的一次完整受控链路

用小矩阵得到可行动的结论

建议按下列顺序做单变量测试:非流式/流式、required/auto、短/长上下文、并发 1/生产并发、官方 artifact/候选 quant、最小客户端/完整 Agent。每一轮都保存原始文本与解析后 message。

并发也不能省略。vLLM #48089 在一个 0.24.0 环境中报告顺序基线正常、并发下出现结构损坏,而且非流式也有异常。这只能证明该环境存在负载相关性,不能把报告中的比例当成你的故障率;它提醒你先把并发降到 1,再判断 streaming 是否真是根因。

最终 bug report 至少包含模型仓库与 revision、quant 文件及 hash、encoding/tokenizer revision、runtime 版本、启动 flag、脱敏请求、原始 completion、解析后响应和下一轮历史。只有“Claude Code 不调用工具”无法区分任何一层。

如果你要核对模型 ID、托管 API 和 thinking 合同,转到 DeepSeek V4 Pro 接入指南;如果问题其实是有限显存下该选哪种本地 Agent 模型,参考本地 Agent 编码模型选择。当前故障的完成标准更窄:一次工具调用从模型文本到执行,再到下一轮,结构始终没有丢失。