上一篇我拆了整套发布流水线的 4 个组件,今天聚焦一个差点让我翻车的隐蔽问题:模型 0 次真实工具调用,却能输出一篇格式完美、链接齐全的「执行成功汇报」。这篇把检测器的实现思路、代码结构和踩坑过程都摊开讲。
事故:连续两天,日报上写着「零发布」
先交代背景。我的流水线是定时任务驱动:选题 → 生成正文 → 调「拟人化改写」打分 → 调「发布」推到平台 → 调「登记」写台账 → 汇总汇报。每一步对应一个真实的 capability(tool / function call)。
某天早上我打开日报,傻眼了:连续两天,发布数是 0。
但翻 Agent 运行日志,它写得清清楚楚:
文章已生成,拟人化评分 82 分,已发布至平台,链接 example.com/a/123,已登记到台...
有链接、有分数、有登记号,格式完全正确,连域名结构都对。
我第一反应是发布器挂了。查浏览器自动化那侧------没有任何执行记录。再查台账文件------空的。
也就是说,这一整段话,模型一个字都没干,全是编的。
为什么会「假装调用工具」
复现了几次后规律很明显:换到某些走「自动路由」的聚合模型时,它不输出标准的 tool_call 结构,而是把工具调用写成散文。
我的理解是:模型在训练语料里见过海量「人类写的工作汇报」,那些文本满是「已完成」「已提交」「链接如下」。当它当前对工具调用格式不够确信时,就退化到最熟悉的模式------用自然语言描述一个「本该发生」的结果。
真正危险的是它编得很像:
- URL 的域名、路径都对,只有 ID 是假的
- 评分是 70~90 之间的合理数字
- 登记号格式和真的一模一样
如果它编得离谱,你一眼就看穿了;正因为它编得合理,你才会信。 而且 Agent 循环的终止条件通常是「模型不再请求工具调用」,模型把调用写成散文后,循环就认为「它没活要干了」,于是静默正常退出。退出码 0,日志无异常,监控一片绿。
解法:声称 vs 事实的比对
思路很朴素------不要相信模型说了什么,只信系统真的执行了什么。
我在 Agent 循环里加了一个「声称检测器」,核心代码大致长这样:
python
# 维护本次会话中真实执行成功的 capability 集合(唯一事实来源)
called_caps: set[str] = set()
# 用正则识别「完成类声称」
_CLAIM_RULES = [
r"发布(成功|完成|到\S+平台)",
r"已(登记|写入|提交|推送)",
r"评分为?\s*\d+",
r"链接[::]?\s*https?://",
]
_HALLUCINATION_NUDGE = (
"你声称完成了工具调用,但系统并未收到对应 tool_call。"
"请用标准 tool_call 结构重新真实调用,不要用文字描述结果。"
)
def check_claims(text: str, called: set[str]) -> list[str]:
hits = []
for rule in _CLAIM_RULES:
if re.search(rule, text):
hits.append(rule)
return hits # 命中即视为「声称做了某类动作」
# 每轮模型输出后
claims = check_claims(model_output, called_caps)
if claims and not called_caps:
# 声称了动作,但真实调用集合为空 → 判定幻觉
inject(_HALLUCINATION_NUDGE)
retry += 1
if retry > 2:
mark_failed("execution-voided") # 整轮判执行作废
continue
continue # 重跑本轮
几个关键设计:
called_caps是唯一事实来源,只在真实的 tool_call 成功返回后往里加。- 声称检测用正则,扫描模型输出的文本层,识别「发布成功/已登记/已写入/评分为N/链接」这类完成类声称。
- 比对 :如果模型声称做了 X,但
called_caps里没有对应项,判定幻觉。 - 命中后不直接失败,先注入纠偏提示重跑本轮,上限 2 次。
- 超限则整轮判「执行作废」,而不是静默成功------这是最关键的一步,宁可显式失败,也不能让假结果混进台账。
上线第一天的实战拦截
防护上线后第一天就拦到一次:模型又开始编「已发布」,被规则命中,注入纠偏提示后它老老实实发出了真实的 tool_call,那一轮任务最终跑了 13 轮工具调用,正常完成。
这次拦截让我确认了几件事:
一、Agent 的可观测性,核心不是日志,是「事实台账」。 日志记录模型说了什么,台账记录系统做了什么,两者差集就是幻觉。
二、终止条件要挂在业务状态上,而不是模型行为上。 「模型不再调用工具」≠「任务完成」。我的终止条件最终改成了「台账里有没有这条记录」。
三、防护必须通用,不能针对某个模型写死。 今天遇到 A 模型,明天换供应商可能是 B 模型。规则要基于「声称-事实」这个抽象,而不是某个模型的怪癖。
四、纠偏优于直接失败。 直接抛异常更安全,但会浪费一次完整上下文。先给一次改正机会,成本更低,实测成功率也不差。
一点提醒
如果你也在做 Agent 编排,建议现在就回去看一眼:你的系统里,「模型说做完了」和「系统确认做完了」是同一件事吗? 如果是,那可能只是你还没遇到那个会撒谎的模型。
下一篇我会聊聊这个「声称检测器」在真实发布流水线里怎么跟多平台发布、登记台账串起来,欢迎关注。