diff --git a/content/posts/preenacted-prompt.md b/content/posts/preenacted-prompt.md new file mode 100644 index 0000000..d4e2ee9 --- /dev/null +++ b/content/posts/preenacted-prompt.md @@ -0,0 +1,73 @@ +--- +title: "它预演了一个不存在的提示" +description: "执行 Agent 给用户弹了一个格式完全正确的「二选一」确认框,用户认真选了 A,结果工单静默派去了寄修组。排查发现:那个提示从头到尾没被任何脚本生成过——是模型自己脑补的。" +date: 2026-09-09T18:50:00+08:00 +draft: false +tags: + - AI Agent + - 事故复盘 + - 幻觉 +--- + +## 事故:一个认真的选择,落进了静默兜底 + +一张工单,地址在「某县级市」。执行 Agent 弹出一个二选一确认框,格式完全正确,让用户选派单方向。用户认真选了 A。 + +结果工单**静默派去了寄修组**——A 选项的映射根本没生效,异地兜底逻辑接管了。 + +排查时先问了一个更奇怪的问题:**这个二选一提示,是谁生成的?** + +## 破案:提示是模型脑补的 + +三份证据,指向同一个结论: + +1. 那条提示对应的对话记录,是**纯模型输出**——没有任何工具调用 +2. 操作日志里,**没有**这次拦截的记录 +3. 待确认队列里,**也没有**这条工单 + +也就是说:**脚本从头到尾没触发过二选一拦截。** 那个格式完美的确认框,是模型自己生成的。 + +它看到了建单输出里的地址「某县级市新开河路」,又记得记忆里有一条「某县级市是二选一城市」的规则,于是**自己脑补了一个拦截提示**——格式、措辞、选项,全都对,唯独不是真的。 + +## 第二层:就算真触发,也匹配不上 + +继续往下挖,发现就算模型没脑补,真跑脚本也会出问题: + +- 二选一城市的匹配,**只查工单的 `city` 字段** +- 但 工单系统 工单里,`city` 是「某地级市」,`region`(区县)才是「某县级市」 +- 配置里的键是「某县级市」→ 查 `city=某地级市` 永远命中不了 → 拦截从未真实触发 + +于是用户选的 A,落到 `--no-choice` 映射时**找不到对应项**,静默走了异地兜底寄修组。 + +**两层错误叠加**:模型预演了一个假提示(认知层),代码的区县匹配又漏了(代码层)。用户从头到尾都在跟一个不存在的对话框互动。 + +## 修复:代码 + 铁律 + +**代码层**:把二选一判断和选 A 映射的查找链,从「只查 city」扩成四级——`city → city_norm → region → region_norm`。顺手扫了同型隐患(第三方平台分组也有同样的「某县级市键」问题),一并修了。 + +**认知层**:给执行 Agent 立了一条铁律—— + +> 执行结果必须来自真实输出。只有工具结果里真实出现了拦截文本,才转述;否则先跑脚本看真实输出,**严禁凭记忆预演**。 + +这条和之前「输入原文传递」的铁律配成一对:**输入不改字,输出不预演。** + +## 为什么这比普通 bug 更危险 + +普通 bug 是「代码错了」,日志里能查到、能复现、能定位。 + +模型预演是「**它看起来完全正确,但从未发生**」。用户无从分辨——提示的格式、措辞、选项全都对,谁会怀疑它是假的? + +更危险的是它的镜像:**假成功**。如果模型没派单却回复「已派单」,那就是漏单——比预演提示更隐蔽,因为连一个「看起来不对」的痕迹都没有。 + +## 总结 + +1. **Agent 的输出要能溯源**。每一条「执行结果」的表述,都应该对应某一次真实的工具调用结果。凭空生成的、格式再漂亮,也是幻觉 +2. **「看起来对」不等于「真的发生了」**。排查时先问:这件事有没有在日志/队列/工具结果里留下痕迹 +3. **配置的键和数据的字段要对齐**。`city` 和 `region` 是两个字段,配置里写了「某县级市」却只查 `city`,等于白写 +4. **软约束的边界要诚实**。铁律写在 AGENTS.md 里,依赖模型遵循——如果还出现预演,就得考虑脚本侧预检这种硬约束 + +> 上一回它忘了自己是谁,这一回它预演了没发生的事。AI Agent 的坑,一半在代码,一半在「它以为」。 + +--- + +*本文已脱敏,不含真实工单号、人名、域名或系统内部标识。*