Skip to content

fix: 伪工具调用救回机制全面加固(误执行 0 / 漏检 0) - #853

Open
zhangPinkdolphin wants to merge 3 commits into
lsdefine:mainfrom
zhangPinkdolphin:fix/phantom-tool-call
Open

zhangPinkdolphin wants to merge 3 commits into
lsdefine:mainfrom
zhangPinkdolphin:fix/phantom-tool-call

Conversation

@zhangPinkdolphin

@zhangPinkdolphin zhangPinkdolphin commented Oct 26, 2026 •

Copy link
Copy Markdown

现象

模型偶尔不通过结构化 tool_calls,而是把工具调用写进正文:

<code_run><script>print(1)</script></code_run>
<code_run script="print(2)"></code_run>
<ask_user><question>选哪个?</question></ask_user>
<||DSML||invoke name="file_read"><||DSML||parameter name="path">/tmp/a</||DSML||parameter>

当前 main 对这类回复既不解析也不告警:该轮 tool_calls 为空,模型以为已执行,agent 空转/丢步,只能翻日志发现——静默失败。

方案:解析优先,全文级检测

本 PR 取代上一版(只认 <code_run> 家族 + 检测到即重试)的做法,改为能完整解析就直接执行,残缺才重试,避免白白浪费一轮。

llmcore.py

能力 说明
_strip_dsml_tags 容错 | | ‖ 单/双/三分隔符、破碎开闭标签、dsml_ 下划线式
DsmlStreamFilter 流式跨 chunk 剥离,chat_completions 与 responses 两条路径都接
_code_candidates / _xml_code_calls 属性式、体内参数式、压平单行式、体内 JSON 对象式
_bare_tag_calls / _bare_child_args 工具名 + 裸子标签参数;参数体必须是纯净标签对结构,正文里的 HTML 片段与裸脚本不会被误判
_looks_like_prose 体内是中文散文 → 判为"被讨论的标签" → 否决
_try_parse_tool_args arguments 字符串侧同样剥标记(#846-B 形态)
salvaged_call_problem 执行前校验:脚本为空 / Python 语法错 / 参数非 JSON → 不执行,转重试

ga.py

尾部 50 字检测 → 全文级检测;救回的调用经 dispatch 直接执行并把结果回流给模型;残缺则不执行、要求重生成。

frontends/tests/test_dsml_tool_markup.py

36 项验收用例,分 5 组:

  • A 危险例(正文里被讨论/引用的标签:中文讨论句、反引号、围栏代码块、函数名提及)→ 7/7 不执行
  • B 漏检例(行内真泄漏、调用后带说明、JSON 体、压平单行、裸子标签、错引号)→ 10/10 救回
  • C 标记变体(单/双/三分隔符、全角/ASCII、开/闭标签、跨 chunk、无标记不误伤)→ 7/7
  • D arguments 侧污染(剥标记后可解析、正常脚本含 < 不受影响、坏 JSON 退回 _raw 不抛)→ 3/3
  • E 类别与截断判定(讨论句不算泄漏/不算截断,残片算截断)→ 6/6

验证

项目 结果
验收用例 36 / 36 通过
真实泄漏日志逐块对拍(改前 vs 改后,50 个 content block) 50 / 50 行为一致,零退步
危险例误执行 0
已知泄漏形态漏检 0

对照同期两个 PR:

场景 期望 本 PR #838 上一版 #853
讨论句(正文提及标签) 不执行 ✅ ✅ ✅
反引号内裸标签 不执行 ✅ ❌ 误执行 ✅
行内真泄漏 执行 ✅ ✅ ✅
调用后带说明 执行 ✅ ❌ 漏检 ✅
裸子标签 ask_user 执行 ✅ ❌ 漏检 ✅
标签名后多引号 执行 ✅ ✅ ✅

风险与边界

  • 依赖工具名白名单(ga.py 用 do_* 动态推导),新增工具若命名不规范需同步
  • 中文散文判定基于统计特征,不是硬规则;已用真实日志 50/50 对拍确认无回归
  • 未包含本分支外的其他改动(如 opencode go 网关头),diff 保持在 649 行

- llmcore: 解析层将正文标记式调用(标签体/属性式/自闭合/invoke+parameter/DSML变体)转为真调用; 仅处理summary之后, 跳过代码围栏示例
- llmcore: find_phantom_tool_markup 检测无法挽救的残缺标记(仅尖括号标签形态, 尾部600字符窗口, 排除代码span)
- ga: do_no_tool 检测到伪标记时强提示重试, 成功后重置计数
上一版(lsdefine#853)只覆盖 <code_run> 家族且以"检测到即重试"为主,
实际运行中仍有两类问题:①反引号/围栏里被讲解的标签会被误判执行;
②<ask_user><question>..</question></ask_user> 这类裸子标签形态、以及
"调用后跟一段说明"的形态仍被漏检。

本次改为解析优先 + 全文级检测:

llmcore.py
- _strip_dsml_tags:容错 | | ‖ 单/双/三分隔符与破碎开闭标签
- DsmlStreamFilter:流式跨 chunk 剥离(chat_completions / responses 两条路径)
- _code_candidates/_xml_code_calls:属性式、体内参数式、压平单行式、JSON 体
- _bare_tag_calls/_bare_child_args:工具名+裸子标签参数;参数体必须纯净标签对,
  正文 HTML 片段与裸脚本不会被误判
- _looks_like_prose:中文散文判为"被讨论的标签"→否决
- _try_parse_tool_args:arguments 侧同样剥标记(上游 lsdefine#846-B)
- leaked_markup_kind/incomplete + salvaged_call_problem:可完整解析才执行,
  截断/语法错/空参数一律转重试

ga.py
- 尾部 50 字检测改为全文级:能解析就救回执行,残缺则不执行并要求重生成

实测:36 项验收用例全绿;50 条真实泄漏日志逐块对拍改前/改后行为完全一致(零退步);
对照 lsdefine#838 修复其两处误执行与两处漏检。
@zhangPinkdolphin zhangPinkdolphin changed the title fix: 不再静默丢弃 XML/DSML 风格伪工具调用(解析兜底 + 残留检测重试) fix: 伪工具调用救回机制全面加固(误执行 0 / 漏检 0) Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant