无人值守AI内容矩阵实战(2):我给 Agent 装了个「声称检测器」,专治它谎报已发布

9 阅读5分钟

上一篇我拆了整套发布流水线的 4 个组件,今天聚焦一个差点让我翻车的隐蔽问题:模型 0 次真实工具调用,却能输出一篇格式完美、链接齐全的「执行成功汇报」。这篇把检测器的实现思路、代码结构和踩坑过程都摊开讲。

事故:连续两天,日报上写着「零发布」

先交代背景。我的流水线是定时任务驱动:选题 → 生成正文 → 调「拟人化改写」打分 → 调「发布」推到平台 → 调「登记」写台账 → 汇总汇报。每一步对应一个真实的 capability(tool / function call)。

某天早上我打开日报,傻眼了:连续两天,发布数是 0。

但翻 Agent 运行日志,它写得清清楚楚:

文章已生成,拟人化评分 82 分,已发布至平台,链接 example.com/a/123,已登记到台…

有链接、有分数、有登记号,格式完全正确,连域名结构都对。

我第一反应是发布器挂了。查浏览器自动化那侧——没有任何执行记录。再查台账文件——空的。

也就是说,这一整段话,模型一个字都没干,全是编的。

为什么会「假装调用工具」

复现了几次后规律很明显:换到某些走「自动路由」的聚合模型时,它不输出标准的 tool_call 结构,而是把工具调用写成散文。

我的理解是:模型在训练语料里见过海量「人类写的工作汇报」,那些文本满是「已完成」「已提交」「链接如下」。当它当前对工具调用格式不够确信时,就退化到最熟悉的模式——用自然语言描述一个「本该发生」的结果。

真正危险的是它编得很像:

  • URL 的域名、路径都对,只有 ID 是假的
  • 评分是 70~90 之间的合理数字
  • 登记号格式和真的一模一样

如果它编得离谱,你一眼就看穿了;正因为它编得合理,你才会信。 而且 Agent 循环的终止条件通常是「模型不再请求工具调用」,模型把调用写成散文后,循环就认为「它没活要干了」,于是静默正常退出。退出码 0,日志无异常,监控一片绿。

解法:声称 vs 事实的比对

思路很朴素——不要相信模型说了什么,只信系统真的执行了什么。

我在 Agent 循环里加了一个「声称检测器」,核心代码大致长这样:

# 维护本次会话中真实执行成功的 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  # 重跑本轮

几个关键设计:

  1. called_caps 是唯一事实来源,只在真实的 tool_call 成功返回后往里加。
  2. 声称检测用正则,扫描模型输出的文本层,识别「发布成功/已登记/已写入/评分为N/链接」这类完成类声称。
  3. 比对:如果模型声称做了 X,但 called_caps 里没有对应项,判定幻觉。
  4. 命中后不直接失败,先注入纠偏提示重跑本轮,上限 2 次。
  5. 超限则整轮判「执行作废」,而不是静默成功——这是最关键的一步,宁可显式失败,也不能让假结果混进台账。

上线第一天的实战拦截

防护上线后第一天就拦到一次:模型又开始编「已发布」,被规则命中,注入纠偏提示后它老老实实发出了真实的 tool_call,那一轮任务最终跑了 13 轮工具调用,正常完成。

这次拦截让我确认了几件事:

一、Agent 的可观测性,核心不是日志,是「事实台账」。 日志记录模型说了什么,台账记录系统做了什么,两者差集就是幻觉。

二、终止条件要挂在业务状态上,而不是模型行为上。 「模型不再调用工具」≠「任务完成」。我的终止条件最终改成了「台账里有没有这条记录」。

三、防护必须通用,不能针对某个模型写死。 今天遇到 A 模型,明天换供应商可能是 B 模型。规则要基于「声称-事实」这个抽象,而不是某个模型的怪癖。

四、纠偏优于直接失败。 直接抛异常更安全,但会浪费一次完整上下文。先给一次改正机会,成本更低,实测成功率也不差。

一点提醒

如果你也在做 Agent 编排,建议现在就回去看一眼:你的系统里,「模型说做完了」和「系统确认做完了」是同一件事吗? 如果是,那可能只是你还没遇到那个会撒谎的模型。

下一篇我会聊聊这个「声称检测器」在真实发布流水线里怎么跟多平台发布、登记台账串起来,欢迎关注。