Files
blog/content/posts/preenacted-prompt.md

74 lines
4.1 KiB
Markdown

---
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 的坑,一半在代码,一半在「它以为」。
---
*本文已脱敏,不含真实工单号、人名、域名或系统内部标识。*