DS2API Tool Calling 终极指南:DSML 与 Canonical XML 工具块格式全解析
【免费下载链接】ds2apiDeepSeek-Compatible Middleware Interface: A technical exploration project in Go, focusing on high-concurrency protocol adaptation. It serves as a reference implementation for converting diverse web protocols into standardized formats.项目地址: https://gitcode.com/GitHub_Trending/ds/ds2api
DS2API 是一个用 Go 编写的 DeepSeek 兼容中间件,核心价值在于高并发协议适配:它把 OpenAI Chat、Responses、Claude、Gemini 等 API 请求统一转换成标准化文本上下文,再把模型返回的工具调用(Tool Calling)解析回各协议的原生结构。其中最受关注的机制,就是本文要完整拆解的DSML / canonical XML 工具块格式——理解它,你就能明白 DS2API 如何稳定、防泄漏地执行工具调用。
1️⃣ 为什么用 XML 工具块,而不是 JSON?
传统 OpenAI 式tools调用依赖模型输出严格的 JSONtool_calls字段,但纯文本网页对话渠道没有结构化输出通道。DS2API 的解法是:让模型直接输出约定格式的 XML 文本块,由兼容层负责识别和解析。
这样做有三个好处:
- 🎯协议无关:不管客户端用哪种 API 格式,模型侧只有一套工具输出语法
- 🛡️防误触发:代码块、行内代码里的示例 XML 会被明确忽略,不会误执行
- 🔧强容错:对模型常见的标签拼写漂移、全角符号、漏写开标签等失误做了窄修复
语义细节的权威描述见项目文档 docs/toolcall-semantics.md,它是 Go 与 Node 两套解析实现的统一行为说明。
2️⃣ 两种可执行工具块格式
推荐格式:半角管道符 DSML 外壳
这是 Prompt 中要求模型输出的首选格式:
<|DSML|tool_calls> <|DSML|invoke name="read_file"> <|DSML|parameter name="path"><![CDATA[README.MD]]></|DSML|parameter> </|DSML|invoke> </|DSML|tool_calls><|DSML|tool_calls>:最外层 wrapper,一次响应可包含多个工具调用<|DSML|invoke name="...">:每次工具调用,工具名必须放在name属性<|DSML|parameter name="...">:参数节点,字符串值统一用<![CDATA[...]]>包裹
兼容格式:旧式 canonical XML
兼容层仍接受不带协议前缀的写法(参考测试夹具 tests/compat/fixtures/toolcalls/canonical_tool_call.json):
<tool_calls> <invoke name="read_file"> <parameter name="path"><![CDATA[README.MD]]></parameter> </invoke> </tool_calls>⚠️ 注意:DSML 只是"外壳别名",进入解析器前会归一化成本地标签名
tool_calls/invoke/parameter,内部始终按现有 XML 解析语义处理,并不是原生 DSML 全链路实现。
3️⃣ 工具块格式规则清单(新手速查表)
| 规则 | 说明 |
|---|---|
| 必须有 wrapper | 外层<\|DSML\|tool_calls>或<tool_calls>缺一不可 |
| 调用放 invoke 内 | 每个调用必须在invoke标签内,工具名放name属性 |
| 参数用 parameter | 每个顶层参数一个parameter name="..."节点 |
| 字符串用 CDATA | 代码、路径、prompt 等一律<![CDATA[...]]>包裹 |
| 数字/布尔保持纯文本 | 123、true、null直接写,会自动还原为对应 JSON 类型 |
| 数组用重复 item | <item>...</item>重复子节点会被还原为数组 |
| 禁止混用标签 | 同一工具块内不要 DSML 与旧式 XML 混搭 |
| 不要空参数 | 缺参数应询问用户,而非输出空占位 |
完整指令模板由 internal/toolcall/tool_prompt.go 中的BuildToolCallInstructions生成,OpenAI / Claude / Gemini 三种适配器共用同一份规则、反例与正例。
4️⃣ 流式防泄漏:为什么工具块不会被"泄漏"到正文
DS2API 对流式(SSE)场景做了专门的"筛分器"(stream sieve)设计,源码见 internal/toolstream/(Go)与 internal/js/helpers/stream-tool-sieve/(Node),核心行为:
- ✅ 已识别成功的工具调用不会回流为普通文本,客户端收到的
delta.tool_calls是结构化增量 - ✅ fenced 代码块(
```与~~~)、Markdown 行内代码中的 XML 示例始终按普通文本处理,不会误执行 - ✅ 支持嵌套围栏(4 反引号嵌套 3 反引号)与 CDATA 内围栏保护
- ✅ 长文本参数(如
command/content)内的 CDATA 即使包含</parameter>这类片段,也不会被误判为外层结束 - ✅ 若 wrapper 完整但内部形态不合法(如用了
<param>),整块作为普通文本释放——不吞、不半漏
这一层设计直接解决了社区常见的"工具调用输出成文本、没被执行"的痛点。
5️⃣ 容错修复:模型写"歪了"怎么办
解析链路(internal/toolcall/toolcalls_parse.go)内置了一组窄修复,专门应对真实模型的失误:
- 漏写开标签:只有 closing wrapper 存在且结构证据充分时,才补回缺失的 opening wrapper
- 符号漂移:全角感叹号
!、顿号、、CJK 尖括号〈〉、弯引号、重复的<、Unicode 空白等,在固定标签名上会被折回 ASCII 语义 - 尾部分隔符:
<|DSML|tool_calls|这类标签后多出的非结构性分隔符会被归一化 - 未闭合 CDATA:流式阶段保守缓冲,收尾阶段再做窄修复
但修复是有边界的:参数正文、普通聊天文本、非工具壳 XML不会被广义 Unicode 归一化;tool_calls_extra这类相似但非固定标签名仍按普通文本透传。
6️⃣ 解析结果与参数类型还原
ParseToolCallsDetailed返回结构(internal/toolcall/toolcalls_parse.go):
calls:解析出的工具调用列表(name+input)sawToolCallSyntax:检测到工具块语法或命中可修复形态时为true- 显式空字符串参数会保留,是否拒绝由工具执行侧 / 客户端 schema 校验决定
参数值还原规则:
| 参数写法 | 还原结果 |
|---|---|
<parameter name="n">123</parameter> | 数字123 |
<parameter name="arr"><![CDATA[[1,2]]]></parameter> | 数组[1,2](合法 JSON 字面量) |
多个<item>子节点 | JSON 数组 |
| CDATA 内完整 XML 结构 | object / array(content/command等原文字段受保护) |
单个行内标签如<b>urgent</b> | 保留原始字符串 |
7️⃣ 常见问题排查清单
- "工具调用输出成文本、未执行"→ 先检查模型输出是否为推荐的 DSML 外壳,或兼容的 canonical XML;旧式 `
【免费下载链接】ds2apiDeepSeek-Compatible Middleware Interface: A technical exploration project in Go, focusing on high-concurrency protocol adaptation. It serves as a reference implementation for converting diverse web protocols into standardized formats.项目地址: https://gitcode.com/GitHub_Trending/ds/ds2api
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考