第三季系列文章第 4 篇总第 61 篇- DeepSeek API · DSML 标记泄漏 · Unicode hex 分析 · 流式过滤 · API 契约缺陷 专栏信息《从零到一构建跨平台 AI 助手WeClaw 实战指南》专栏 ·第三季专栏定位面向开发者和技术决策者的实战专栏用真实案例和完整代码带你理解如何构建生产级 AI 应用本文是模块五·问题诊断实战的最新一篇深入剖析 DeepSeek 模型在流式 API 中将内部工具调用协议DSML泄漏到用户可见文本字段的系统性 Bug以及我们如何通过 hex 级字节分析 多层防御过滤器彻底修复。‍ 作者与项目作者简介翁勇刚 WENG YONGGANG新概念龙虾-WeClaw 开发团队负责人一群专注于跨平台 AI 应用的实践者理念“再复杂的技术也能用代码讲清楚” 项目地址https://github.com/wyg5208/weclaw.git 官网地址https://weclaw.link 作者 CSDNhttps://blog.csdn.net/yweng18⭐ 欢迎 Star⭐、Fork、贡献代码 摘要本文结构概览从一个看似简单的 Bug——AI 回复中出现了‖DSML‖tool_calls这样的乱码——出发逐步拆解 OpenAI 兼容 API 的双通道架构delta.contentvsdelta.tool_calls揭示 DeepSeek 模型违反 API 契约将内部协议泄漏到用户可见字段的根本原因并通过 hex 字节级分析证明肉眼不可信的 Unicode 陷阱最终给出多层防御的过滤方案。背景WeClaw 使用 DeepSeek 模型作为核心推理引擎。用户反馈在语音对话场景中AI 的文本回复中突然出现了原始的工具调用标记DSML这些标记不仅显示在聊天区还被 TTS 朗读出来——用户听到的是左竖线竖线 DSML 竖线竖线 tool calls 右尖括号这样的乱码。核心问题为什么本应只在delta.tool_calls中流转的工具调用指令会污染到delta.content用户可见文本上一轮修复v5.25.x声称解决了此问题为什么重启后仍然出现解决方案通过 hex 字节级分析发现上一轮修复添加的标记使用单竖线(UFF5C × 1)而 DeepSeek 模型实际输出双竖线(UFF5C × 2) DSML关键字格式。新增‖DSML‖前缀标记 合并重复正则定义实现覆盖所有已知格式变体的多层防御。关键成果双竖线格式 DSML 标记过滤率0% → 100%单向字符差异导致的过滤器失效被彻底修复10/10 测试用例全部通过含真实会话历史样本揭示了 DeepSeek API 的一个系统性缺陷适合读者使用 DeepSeek API 的开发者、对流式 LLM 输出处理有需求的工程师、对 Unicode 编码陷阱感兴趣的开发者阅读时长约 15 分钟关键词DeepSeek、DSML、Function Calling、content 污染、流式过滤、Unicode hex 分析、API 契约一、问题现场 —— 当 AI 开始说代码1.1 用户看到的诡异现象2026 年 6 月 13 日晚上用户在 WeClaw 中对 AI 说“西溪公主回来了你唱一首童话诗歌给她吧。”AI 的回复令人困惑好的我先写一首小诗再念给西溪公主听 ‖DSML‖tool_calls ‖DSML‖invoke namevoice_output_speak ‖DSML‖parameter nametext stringtrue西溪公主回城堡.../‖DSML‖parameter /‖DSML‖invoke /‖DSML‖tool_calls这些‖DSML‖...标签直接显示在了聊天区。更糟糕的是——因为 WeClaw 支持 TTS 流式朗读——这些标记被语音引擎读了出来。1.2 为什么 v5.26.0 之后特别明显这并非巧合。v5.26.0 新增了voice_output工具决策树使 LLM 更频繁地调用语音工具。但 DSML 过滤器本身的缺陷在更早版本就已存在——只是之前工具调用频率低用户偶尔才碰到v5.26.0 之后几乎每次语音对话都会触发。关键洞察性能优化和新功能上线往往会暴露已有的隐蔽 Bug而不是引入它们。二、架构解读 —— 为什么工具调用会进入用户可见通道2.1 OpenAI 兼容 API 的双通道设计OpenAI 兼容的流式 API 中每个deltachunk 包含两个关键字段deltachunk.choices[0].delta delta.content# ← 通道1用户可见的文本内容delta.tool_calls# ← 通道2工具调用的结构化指令按照 API 规范delta.content只应包含给用户看的自然语言文本delta.tool_calls只应包含结构化的函数调用 JSON这两个通道在 WeClaw 的代码中是完全分离的# 通道1文本内容 → 过滤 DSML → yield 给用户delta_contentgetattr(delta,content,None)orifdelta_contentandnot_dsml_started:# ... DSML 过滤逻辑 ...yielddelta_content# → 发送到聊天区和 TTS# 通道2工具调用 → 解析 → 执行delta_tool_callsgetattr(delta,tool_calls,None)ifdelta_tool_calls:fordtcindelta_tool_calls:# 收集 function name arguments# → 调用 tool_registry.call_function()2.2 DeepSeek 的漏水行为问题出在 DeepSeek 模型的实现上。在标准的 API 规范下工具调用只应该出现在delta.tool_calls中。但 DeepSeek 模型在生成响应时会同时往delta.content中输出其原生的 DSMLDeepSeek Markup Language标记┌─ delta.content → 好的...‖DSML‖tool_calls... ← 泄漏 └─ delta.tool_calls → [{function: {name: voice_output_speak, ...}}] ← 正确这本质上是一个 API 契约违反上游服务没有正确剥离内部协议标记导致它们泄漏到了用户可见的文本通道。类比来说你去餐厅点菜服务员递给你一张菜单content同时也递了一张内部厨房订单tool_calls。但是菜单上居然也印着后厨3号灶台、少盐、大火快炒——这是厨房内部信息不该给顾客看的。三、诊断过程 —— hex 分析的威力3.1 第一轮修复为什么失效v5.25.x 期间我们已经添加了 DSML 过滤器包含以下标记_DSML_MARKERS(DSML,# 单竖线 DSML 格式toolcalls,# 单竖线无 DSML 格式invoke,# 调用开始parameter,# 参数开始# ... 共 9 个标记)这些标记在文本编辑器中看起来和用户报告的格式完全一样。但它们真的匹配吗3.2 hex 分析揭示的真相关键的突破来自于对会话历史 JSONL 文件的 hex 级别分析。我们编写脚本直接检查实际模型输出的字节序列# 从会话历史中提取的实际 DSML 片段Hex:3cefbd9cefbd9c44534d4cefbd9cefbd9c746f6f6c5f63616c6c733e# 逐字节解码3c →(U003C)efbd9c →(UFF5C)← 第一个竖线 efbd9c →(UFF5C)← 第二个竖线双竖线44534d 4c →DSMLefbd9c →(UFF5C)efbd9c →(UFF5C)← 又是双竖线746f 6f 6c 5f63616c 6c73→tool_calls3e →发现模型实际输出的是‖DSML‖tool_calls——使用双竖线DSML关键字。而我们的过滤器标记是toolcalls——使用单竖线 无DSML关键字。3.3 为什么肉眼看不出来用户看到的文本 toolcalls 和 ‖DSML‖tool_calls 过滤器的标记 toolcalls ← 匹配第一个 模型实际输出 ‖DSML‖tool_calls ← 完全不匹配(UFF5C FULLWIDTH VERTICAL LINE) 和两个并列在屏幕上几乎无法区分。Unicode 字符的视觉相似性构成了一个完美的陷阱——你以为修好了实际上过滤器的核心匹配逻辑从未生效。验证脚本的结果测试1: 当前标记 vs 实际模型输出双竖线格式 样本1: ‖DSML‖tool_calls → ❌ 无匹配! 样本2: ‖DSML‖invoke name... → ❌ 无匹配! 样本3: ‖DSML‖parameter name... → ❌ 无匹配!9 个标记对 5 个真实样本的匹配率0/5。过滤器从未真正工作过。四、修复方案 —— 多层防御策略4.1 方案设计修复的核心思路是前缀匹配不尝试枚举所有可能的标签组合而是匹配 DSML 格式的特征前缀。DeepSeek DSML 标签结构 ‖DSML‖tool_calls ← 所有标签都以 ‖DSML‖ 开头 /‖DSML‖tool_calls ← 闭合标签以 /‖DSML‖ 开头 ‖DSML‖invoke name... ‖DSML‖parameter name... stringtrue只需要两个前缀标记即可覆盖所有变体# ★ 模型最常输出的双竖线 DSML 格式UFF5C × 2含 DSML 关键字‖DSML‖,# 匹配所有 DSML 开始标签/‖DSML‖,# 匹配所有 DSML 闭合标签4.2 流式过滤的完整逻辑# 每个 stream chunk 的处理逻辑delta_contentgetattr(delta,content,None)orifdelta_contentandnot_dsml_started:dsml_pos-1# 方式1: 精确匹配已知标记O(n) 高效for_markerin_DSML_MARKERS:_posdelta_content.find(_marker)if_pos0and(dsml_pos0or_posdsml_pos):dsml_pos_pos# 方式2: 正则兜底捕获未知格式变体ifdsml_pos0:_m_DSML_PATTERN.search(delta_content)if_m:dsml_pos_m.start()ifdsml_pos0:# 截断至标记前的正常文本后续 content 不再 yielddelta_contentdelta_content[:dsml_pos].rstrip()_dsml_startedTrue# 一旦检测到 DSML后续 chunk 全部屏蔽ifdelta_content:yielddelta_content# 只发送过滤后的纯净文本4.3 完整的标记清单修复后_DSML_MARKERS(# 旧格式兼容DSML,tool▁calls▁begin,|DSML|,|tool_calls_begin|,# 单竖线无 DSML 格式DeepSeek 少数情况toolcalls,/toolcalls,invoke,/invoke,parameter,# ★ 双竖线 DSML 格式DeepSeek 最常见情况★‖DSML‖,# ← 本次修复新增/‖DSML‖,# ← 本次修复新增)# 正则兜底覆盖所有竖线数量变体_DSML_PATTERNre.compile(r/?[|]{1,2}# 或 / 1-2 竖线r(?:DSML[|]{1,2}# DSML 1-2 竖线r|)# 或无 DSMLr(?:tool_calls|tool[|]{1,2}callsr|invoker|parameterr))4.4 非流式路径的同步修复除了流式路径WeClaw 还有非流式调用路径当模型返回完整响应时。两个路径必须同步修复# 非流式路径的标记同样新增双竖线标记_DSML_MARKERS_NONSTREAM(DSML,tool▁calls▁begin,|DSML|,|tool_calls_begin|,toolcalls,/toolcalls,invoke,/invoke,parameter,‖DSML‖,/‖DSML‖,# ← 本次修复新增)五、验证 —— 10/10 全过5.1 测试用例设计从真实会话历史中提取 5 个样本同时构造 5 个综合场景样本内容期望S1‖DSML‖tool_calls匹配 ✅S2‖DSML‖invoke namevoice_output_speak匹配 ✅S3‖DSML‖parameter nametext stringtrue匹配 ✅S4完整工具调用块含嵌套闭合标签匹配 ✅S5toolcalls单竖线变体匹配 ✅场景输入期望输出TC1好的...\n\n‖DSML‖tool_calls...好的...TC2纯正常文本原样输出TC3纯 DSML 标记空字符串TC4单竖线变体 DSML空字符串TC5用户实际场景“西溪公主”好的我先写一首小诗...结果10/10 全部通过。5.2 验证脚本的核心逻辑deffilter_dsml(content:str)-tuple[str,bool]:dsml_pos-1# 方式1: 精确匹配formarkerinall_markers:poscontent.find(marker)ifpos0and(dsml_pos0orposdsml_pos):dsml_pospos# 方式2: 正则兜底ifdsml_pos0:mdsml_pattern.search(content)ifm:dsml_posm.start()ifdsml_pos0:returncontent[:dsml_pos].rstrip(),Truereturncontent,False六、深层反思 —— API 契约的边界6.1 谁的责任这个问题揭示了 AI API 生态系统中一个责任边界模糊的地带层级责任本次实际情况模型训练不应在文本流中输出控制标记❌ DeepSeek 训练时使用了 DSML 内部格式API 网关应剥离内部标记只返回标准字段❌ API 未完全剥离content中的 DSML应用层依赖 API 契约不应处理协议细节⚠️ 被迫增加防御性过滤理想情况是 DeepSeek API 在返回数据前将content中的 DSML 标记完全剥离。但现实是我们必须在应用层增加过滤器来弥补这个缺口。6.2 流式场景的特殊挑战在流式streaming场景下DSML 过滤还有一个额外的复杂性chunk 边界可能切分标记。例如Chunk 1: 好的...\n ← 只有 未触发过滤 Chunk 2: ‖DSML‖tool_calls... ← 过滤器触发但 已泄漏这种情况在实际中极少发生因为 DSML 标记通常与前面的文本在不同 chunk但如果要100% 彻底消除泄漏需要实现一个状态机解析器来缓冲跨 chunk 的部分标记。6.3 Unicode 的视觉欺骗本次排查最大的教训是在 Unicode 问题上永远不要相信肉眼。# 这两种格式在屏幕上几乎一模一样format_atoolcalls# 用户报告中看到的format_b‖DSML‖tool_calls# 模型实际输出的# 但字节级完全不同assertformat_a!format_basserttoolcallsnotin‖DSML‖tool_calls在涉及非 ASCII 字符的字符串匹配时必须用 hex dump 确认实际字节用代码验证匹配结果assert marker in sample不依赖文本编辑器的显示七、对社区的启示7.1 如果你也在使用 DeepSeek API如果你正在使用 DeepSeek 的 Function Calling 功能建议检查你的应用中是否存在类似的 DSML 泄漏。简单的检测方法# 在流式输出处理中添加检查ifDSMLindelta_contentortool_callsindelta_content:logger.warning(fPossible DSML leak in content:{delta_content[:100]})7.2 通用的防御策略对于任何使用流式 LLM API 的应用建议采用以下多层防御已知模式过滤str.find最快针对已知格式正则兜底re.search覆盖未知变体状态机解析可选处理跨 chunk 边界的情况日志记录每次过滤触发时记录便于监控和调试7.3 给 API 供应商的建议如果你是 API 供应商建议在 API 网关层剥离内部协议标记不要让它们污染content字段提供明确的文档说明 Function Calling 的输出格式考虑提供一个配置选项让用户选择是否需要在content中看到工具调用标记八、总结这次排查从用户的一个反馈出发经历了现象观察DSML 标记出现在聊天区和 TTS 输出假设提出v5.26.0 引入了 Bug假设推翻v5.26.0 只是暴露了已有 Bughex 分析发现单竖线 vs 双竖线的字节差异根本原因过滤器标记与实际输出格式不匹配修复实施新增双竖线前缀标记 合并正则验证确认10/10 测试全部通过核心教训信任 hex不信任肉眼——Unicode 的视觉陷阱极其隐蔽️API 契约是脆弱的——应用层必须有自己的防御新功能会暴露旧 Bug——性能优化和功能迭代可能改变 Bug 的触发频率️多层防御是必需的——精确匹配 正则兜底 状态机 完整性保障 相关文章WeClaw_07_流式响应转发实战LLM Token 流的实时推送技术WeClaw_24_工具注册系统演进从手动映射到配置驱动自动发现的架构之路WeClaw_29_LLM Function Calling的Schema陷阱与纯语言输出双重保障本文是 WeClaw 专栏的第 61 篇。如果这篇文章对你有帮助欢迎给项目点个 Star ⭐