当模型将工具调用泄漏为文本:基于跨分块 DSML 状态机的流式恢复
内容提要
部分模型不通过标准 tool_calls 返回工具调用,而是将 DSML/XML 标记混入文本流,导致代理循环收不到工具事件、界面显示乱码。文章提出跨分块流式 DSML 恢复状态机:检测前缀后进入捕获模式,缓冲并拼接分块标签;识别用户示例时禁用恢复,原生调用与文本冲突时失败关闭;解析后校验工具名与 JSON Schema,合成标准工具调用事件,交由代理循环执行。
延伸解读
流式分块带来的解析挑战
文章指出,工具调用标记可能被拆分成多个网络分块到达,例如“<DSML<invoke”可能分散在四个数据包中。单分块正则匹配无法识别完整标签,因此状态机需要缓冲并拼接分块。同时,遇到不完整前缀时不能立即输出,但也不能无限期持有,正常文本必须在确认后立即释放,这对低延迟流式场景提出了更高要求。
用户示例与原生调用的冲突处理
当用户提示中明确讨论DSML语法时,状态机必须识别并禁用恢复,避免将引用的示例当作真实系统命令执行。此外,如果模型在同一轮中同时返回原生结构化工具调用和原始DSML文本,文章采用失败关闭策略,以防止重复执行。这些设计体现了对安全性和正确性的优先考虑。
校验与事件合成如何衔接代理循环
解析出工具名和参数后,状态机会验证工具名是否在注册表中,将非字符串参数解析为合法JSON值,并依据工具的JSON Schema进行校验。通过后合成标准的ToolCallStart、ToolInputDelta和ToolUse事件,使代理循环无法区分这是原生调用还是恢复的调用,从而直接进入正常工具执行流程。
传输层吸收异常以保持上层简洁
文章的核心观点是,面对多样化的模型端点,输出格式异常难以避免。将这类怪癖在传输适配层消化,可以避免上层代理状态机被污染,保持其干净可靠。这种分层思路不仅适用于DSML恢复,也为处理其他模型输出异常提供了参考模式。
Q&A
为什么有些模型不通过标准 tool_calls 返回工具调用,而是把 DSML/XML 标记混在文本流里?
文章指出,在集成多种模型用于编码代理时,传输层怪癖难以避免。某些推理模型(如 DeepSeek 衍生模型或代理网关)不会通过标准 tool_calls 载荷返回结构化函数调用,而是把原始 XML 标记直接倾倒进文本流(content 或 output_text.delta)中。
如果传输层直接把 DSML 标记当纯文本传给 UI,会有什么后果?
会导致两个问题:一是代理循环收不到任何 tool_call 事件,任务停滞;二是用户屏幕上会显示未解析的全角 XML 标签,造成乱码。
跨分块流式 DSML 恢复状态机是如何检测并捕获工具调用的?
状态机检测到前缀 <DSML< 后进入捕获模式,暂停向下游发送文本增量,同时用内部 capture_buffer 缓冲并拼接分块标签;当闭合标签解析完成后,提取工具名与参数对。
为什么不能简单用正则或 XML 解析器从流式文本中提取 DSML 标签?
因为代理需要低延迟流式文本,标签会跨网络包分片到达,例如 <DSML<invoke 可能被拆成 ["<", "DS", "ML<in", "voke"] 四个分片,单块正则匹配无效;同时部分前缀(如单独的 <)不能立即流式输出,也不能无限期持有。
状态机如何处理用户提示中讨论 DSML 语法的示例?
如果用户明确在讨论 DSML 语法(例如问“<DSML< 是什么意思?”),状态机必须识别这种情况并禁用恢复,而不是把引用的示例当作真实系统命令去执行。
当模型同时返回原生工具调用和 DSML 文本时,状态机怎么处理?
如果模型在同一轮中同时返回原生结构化工具调用和原始 DSML 文本,状态机会失败关闭(fail closed),以防止重复执行。
解析出 DSML 工具调用后,如何校验并合成标准事件交给代理循环?
解析闭合标签后提取 <invoke> 和 <parameter> 节点,验证工具名存在于当前注册表,将非字符串参数解析为合法 JSON 值,并依据工具注册的 JSON Schema 校验;然后合成原生 ToolCallStart、ToolInputDelta 和 ToolUse 事件,对代理循环而言与标准提供方工具调用无异,直接进入正常工具执行。
状态机用什么机制防止内存耗尽?
状态机使用 DSML_CAPTURE_LIMIT(256KB)作为上限,防止畸形输出导致内存耗尽。