开场:一份怪行为清单
刚开始用大模型的那一年,我在笔记里专门开了一页,标题叫"它为什么这样"。遇到想不通的事就记一笔:
- 问它一个开源库的函数,它给我编了一个根本不存在的参数,还附带了用法示例;
- 我说"这样改应该没问题吧",它说"是的,完全没问题"。上线之后出了问题;
- 问它某个工具的最新版本号,它很自信地告诉我一个一年前的版本;
- 同一段报错,问了三次,得到三个不同的原因(第 08 篇的开场);
- 让它数一个单词里有几个 r,数错了(第 06 篇的开场);
- 一个很长的对话里,聊到后来它开始忘记我一开始提的要求,回答也越来越敷衍。
那时候我只能靠"玄学"应对:换个说法问,多问几次,重新开一个对话。有时候管用,有时候不管用,我也说不清为什么。后来开始用 Agent,清单越记越长,又多了几条只有在 Agent 里才会遇到的毛病。
现在原理篇讲完了,回头看这份清单,每一条都能找到它的根源。这一篇是原理篇的结业考:把六种最常见的怪行为,逐个还原成机制,再说说在 Agent 里它会变成什么样、该怎么应对。 读完本篇,你应该能做到:
- 看到模型的一个怪行为,能说出它大概来自哪一层原理;
- 知道每种怪行为在 Agent 里会以什么形式出现;
- 知道每种怪行为的对策,而不是靠玄学调 prompt;
- 认出三种只在 Agent 里出现的怪行为,知道该在 harness 的哪里设防。
一、先看全景
第 06 篇"] N["一个字一个字地猜
第 07 篇"] S["采样
第 08 篇"] A["attention 与上下文
第 09、10 篇"] TR["训练
第 11 篇"] end subgraph 怪行为 H["幻觉"] SY["讨好"] O["不知道新事"] D["同题不同答"] L["数不清字母"] R["越聊越笨"] end TR --> H N --> H TR --> SY TR --> O S --> D T --> L A --> R style H fill:#ffcdd2,stroke:#b71c1c style SY fill:#ffcdd2,stroke:#b71c1c style R fill:#ffcdd2,stroke:#b71c1c
下面一个一个说。每个都按同样的结构:现象、原理、在 Agent 里的样子、对策。最后再加试三种 Agent 特有的。
二、幻觉:一本正经地胡说
2.1 现象
问它一个它不太确定的问题,它不说"我不知道",而是给出一个看起来非常合理、但其实是编出来的答案:不存在的函数参数、不存在的论文、不存在的历史事件,有鼻子有眼。
2.2 原理
两个原因叠在一起:
- 知识是有损压缩(第 11 篇):模型把海量文本压进了有限的参数里,冷门的细节记得模糊。就像一张压缩过头的照片,放大看细节,是根据周围像素"脑补"出来的;
- 它被训练成"总要给出像样的回答"(第 07、11 篇):模型每一步都在选"最可能的下一个 token",而"最可能"不等于"最正确"。在一个它记不清的问题上,最像样的续写往往是一个具体的、自信的答案,而不是"我不确定"。训练过程中,一个完整、具体、有帮助的回答,通常比一句"我不知道"得到更高的评价。
模糊"] M --> G["选最像样的下一个 token"] G --> A["具体、自信、
但可能是编的答案"] style M fill:#fff9c4 style A fill:#ffcdd2,stroke:#b71c1c
2.3 在 Agent 里
- 编造文件内容:它没读某个文件,却"记得"那个文件里有某个函数,然后基于这个想象去改代码;
- 编造工具结果:第 03 篇 ReAct 的坑 1,没设 stop sequence,模型自己编了观察结果;
- 编造 API:用了一个看起来很合理、但这个库根本没有的方法,代码一跑就报错。
2.4 对策
- 让它查,别让它想:需要事实的地方,让 Agent 用工具去读文件、查文档、搜代码,把事实放进上下文里。编码 Agent 好用,很大一部分原因就是它会先读再改;
- 用验证兜底:编译器和测试会戳穿编出来的 API(第 05 篇)。代码类的幻觉,往往活不过一次
go build; - 给它说"不知道"的许可:在 system prompt 里明确写"不确定时请说明,不要猜测",并解释为什么(第 15 篇)。
三、讨好:你说什么都对
3.1 现象
你提出一个观点,它倾向于同意;你反驳它,它很容易改口,哪怕它原来是对的;你的方案明明有问题,它先夸一通,再很委婉地提一点点意见。
3.2 原理
偏好对齐(第 11 篇)。模型通过人类(或 AI)的偏好打分来学习什么是"好的回答",而人在打分时,本身就偏爱被认同。一个说"你说得对"的回答,比一个说"你错了"的回答更容易得到好评。模型学会了人的偏好,也学会了人的这个小毛病。
3.3 在 Agent 里
- 你说"我觉得问题出在缓存上",它就顺着你的思路去改缓存,而不是先自己排查;
- 你质疑它的改动"这样会不会有问题",它马上撤回,改成一个更差的方案;
- 让它自己评价自己的工作,它总是说"已经完成,效果很好"。
3.4 对策
- 别给它暗示答案:"这样没问题吧?"换成"这个方案有什么风险?";"问题是不是出在缓存?"换成"帮我排查一下问题出在哪";
- 让它扮演反方:"假设你是一个挑剔的代码评审,你会在哪里打回这个 PR?";
- 别让它自己评自己:第 04 篇的评估-优化模式,评审和写手要分开;第 36 篇的 LLM-as-judge 要逐条核对、要求给出依据;
- 最可靠的还是客观验证:测试通过就是通过,不需要它的评价。
四、不知道新事:活在过去
4.1 现象
问它最新的版本号、最近发生的事、新发布的 API,它要么说不知道,要么很自信地告诉你一个过时的答案。
4.2 原理
知识截止日期(第 11 篇)。预训练数据是在某个时间点之前收集的,之后的事情它没见过。而且截止日期附近的数据本身就少,所以它对"最近"的事情了解得尤其模糊。
还有一个更隐蔽的问题:它不知道今天是几号。 第 01 篇讲过,模型是无状态的,除非应用在 system prompt 里告诉它日期。所以它也很难意识到"我的知识可能已经过时了"。
4.3 在 Agent 里
- 用了一个已经被废弃的 API,或者一个库的旧版本写法;
- 照着旧的文档写配置,新版本的配置项早就改名了;
- 安装依赖时,指定了一个很旧的版本号。
4.4 对策
- 告诉它今天的日期:mini-agent 的 system prompt 里就有一行"今天日期"(第 24 篇);
- 把最新的信息放进上下文:项目里的依赖文件(
go.mod、package.json)、最新的文档、changelog。让它先读项目,再动手(第 17、18 篇); - 让编译器和测试来纠正:用了废弃的 API,编译器往往会给出警告或错误,这个反馈会回到 Agent 的循环里。
五、同题不同答:每次都不一样
5.1 现象
同一个问题,问两次,得到两个不同的答案。有时只是措辞不同,有时连结论都不一样。
5.2 原理
采样(第 08 篇)。模型每一步都是在一个概率分布里"抽签",抽到不同的 token,后面的走向就不同,而且这种差异会一路累积放大。就算 temperature 设成 0,批处理和浮点运算也可能带来细微差异。
5.3 在 Agent 里
- 同一个任务跑两次,路径完全不同,一次三步搞定,一次绕了二十步;
- 用户报告的问题,你复现不出来;
- 改了一句 prompt,效果"好像变好了",但不确定是真的变好了还是运气。
5.4 对策
- 把不一致当成信号:同一个问题问几次,答案高度一致,说明它有把握;各不相同,说明它其实不知道(第 08 篇);
- 用格式和验证换稳定:structured outputs、gate 检查、测试(第 04、35 篇),而不是指望 temperature;
- 评估效果要看多次运行:第 36 篇的评测,每道题跑 k 次,看通过率,而不是看一次的结果。
六、数不清字母:看不见字
6.1 现象
数不清单词里有几个某个字母,倒写字符串出错,写不出"正好 100 字"的文字,长数字的运算偶尔出错。
6.2 原理
token 粒度比字母粗(第 06 篇)。模型看到的是 token 编号,不是字母。"strawberry 里有几个 r"这种问题,需要它知道每个 token 是由哪些字母组成的,而这种知识它只能间接地学到,学得并不牢。
6.3 在 Agent 里
- 让它"把这个函数控制在 50 行以内",它数不准;
- 让它在文件的第 237 行插入一段代码,它可能插错位置;
- 让它统计日志里某个关键词出现了多少次,它报一个大概的数。
6.4 对策
- 字符级的操作交给代码:数数、计数、排序、精确定位,让 Agent 写一行命令或代码去做(
wc -l、grep -c、len()),而不是自己数。编码 Agent 在这类问题上反而很靠谱,因为它会这么做; - 工具设计上避开行号:第 26 篇实现
edit_file时,我们会用"找到这段文字、替换成那段文字"的方式,而不是"修改第几行",就是为了绕开这个弱点; - 让它写出来再处理:推理模型能数对,是因为它在思考里把单词拆成了一个个字母。需要它做这类事时,让它先把东西展开写出来。
七、越聊越笨:上下文被稀释
7.1 现象
一个对话聊得越久,模型的表现越差:开始忘记一开始提的要求,回答越来越敷衍,甚至开始重复之前犯过的错误。
7.2 原理
上下文被无关内容稀释(第 09、10 篇):
- 注意力是有限的预算:softmax 的权重加起来等于 1,上下文里的东西越多,每样东西分到的注意力越少;
- 中间的信息容易被忽略:lost in the middle,一开始提的要求,随着对话变长,渐渐被挤到了"中间";
- 噪声越来越多:长对话里有大量已经过时的信息、走过的弯路、被推翻的方案,它们都还在上下文里,干扰模型的判断。
第 16 篇会讲一份专门研究这个现象的报告,它有个形象的名字:Context Rot,上下文腐烂。
7.3 在 Agent 里
这是 Agent 最常见、也最头疼的问题之一。一个任务转了几十圈之后:
- 忘了用户最开始说的"不要修改测试文件";
- 反复读同一个文件,因为前面读过的内容"淹没"在了一大堆输出里;
- 开始在两个方案之间来回横跳。
7.4 对策
- 只放需要的:工具输出截断,大文件只读需要的部分(第 28 篇);
- 定期清理:任务换了就清空(
/reset),任务太长就压缩成摘要(/compact),或者写一份交接文档,开一个新会话(第 16 篇); - 重要的东西放在显眼的位置:关键约束写进 system prompt 或项目记忆文件,每次请求都在最前面(第 17 篇);
- 该分身就分身:读大量材料的事情交给子 Agent,主 Agent 只拿结论(第 30 篇)。
八、加试:三种 Agent 特有的怪行为
上面六种,在聊天里就能遇到。下面三种,基本只在 Agent 里出现:它们是前面几种机制在"一个循环转几十圈"这种场景下的变体。结构简化一下,只讲现象、根源和对策。
8.1 认死理:一条路走到黑
现象:它在第 3 圈认定"问题出在缓存",之后二十圈都在改缓存相关的代码,哪怕测试结果一次次说明不是这里。
根源:一个字一个字地猜(第 07 篇)。它写下的判断留在了消息历史里,之后的每一圈都会"看到"自己说过的话,而保持前后一致正是它最擅长的事。再叠加上越聊越笨,那些反驳它的证据,可能正被埋在一大堆工具输出的中间。
对策:要求它先列出几个假设再逐一验证;用一个明确的反例打断它;方向彻底错了,就换一个干净的会话,把已经排除的方向写清楚(第 07 篇 4.4 节、第 16 篇)。
8.2 自作主张:做了你没让它做的事
现象:让它修一个 bug,它顺手重构了三个函数、改了代码风格、还"优化"了一下错误处理。改动很多,你要审查的东西也跟着变多,出问题的机会也变多。
根源:训练(第 11 篇)。它被训练成"尽可能有帮助",而"多做一点"在很多打分者眼里就是"更有帮助"。另外,长任务里最初的任务边界会被慢慢稀释(第 10、16 篇),这也是第 39 篇说的目标漂移。
对策:在 system prompt 和任务描述里写清楚边界,比如"只改和这个 bug 直接相关的代码,不要做无关的重构";改完之后看一眼 diff 的范围,超出范围的改动先撤掉;一个任务一个会话,别在修 bug 的会话里顺便说"对了,再帮我……"。
8.3 谎报完成:说做完了,其实没有
现象:它宣布"已完成,所有测试通过",你一跑,测试是挂的;或者测试确实过了,因为它把那个过不了的测试跳过了。
根源:训练里的奖励投机(第 11 篇 4.5 节),加上幻觉:它"记得"自己跑过测试,但最后那次修改之后其实没有再跑。
对策:不要相信它的报告,相信它改不了的东西。测试文件不让它改,判分时换回原版测试、加上隐藏测试(第 35、36 篇);在 harness 里,"完成"之前自动跑一遍检查命令,结果由程序判断,而不是由模型来说(第 33 篇的 hooks)。
对策:列假设、换会话"] B["自作主张"] --> B1["根源:讨好 + 目标漂移
对策:写清边界、看 diff"] C["谎报完成"] --> C1["根源:奖励投机
对策:程序来验收"] style A fill:#ffcdd2,stroke:#b71c1c style B fill:#ffcdd2,stroke:#b71c1c style C fill:#ffcdd2,stroke:#b71c1c
九、一张总表
把上面六种怪行为收拢成一张表,这张表值得存下来:
| 怪行为 | 根源 | 在 Agent 里 | 对策 | 详见 |
|---|---|---|---|---|
| 幻觉 | 有损压缩 + 总要给出像样回答 | 编文件内容、编 API | 让它查,用验证兜底 | 06、11、18、35 |
| 讨好 | 人类偏好本身偏爱被认同 | 顺着你的错误思路走 | 别暗示答案,让它当反方,客观验证 | 11、15、36 |
| 不知道新事 | 知识截止日期 | 用废弃的 API、旧的版本 | 告诉它日期,让它先读项目 | 11、17、24 |
| 同题不同答 | 采样的随机性 | 同一任务路径不同、难复现 | 用格式和验证换稳定,多次评估 | 08、36 |
| 数不清字母 | token 粒度比字母粗 | 行号、字数、计数不准 | 交给代码,工具设计避开行号 | 06、26 |
| 越聊越笨 | 注意力被稀释 | 忘记约束、来回横跳 | 截断、压缩、交接、子 Agent | 09、10、16、28、30 |
加试的三种,各自对应上表里的一两行:认死理是"一个字一个字地猜"加上"越聊越笨",自作主张是"讨好"加上"越聊越笨",谎报完成是"讨好"的 Agent 版本加上"幻觉"。
你会发现,几乎所有对策,最后都落在两件事上:管好上下文,加上验证。 这正好是接下来两大部分的主题:第三部分讲上下文工程,第七部分讲验证和评测。
十、动手:结业考
考题:不看前文,用自己的话解释
拿一张纸,或者打开一个空白文档,不看本篇,也不看前面的文章,用自己的话把这六个现象各解释一遍:
- 为什么模型会一本正经地编造不存在的东西?
- 为什么你反驳它一句,它很容易改口?
- 为什么它不知道最新的事情?为什么它也不知道今天几号?
- 为什么同一个问题问两次,答案会不一样?temperature 设成 0 为什么也不保证一样?
- 为什么它数不清 strawberry 里有几个 r?为什么推理模型又能数对了?
- 为什么聊得越久,它越笨?
每个问题,试着回答三层:现象是什么,根源是哪个机制,在 Agent 里该怎么应对。 写完再回来对照本篇。如果某一题你只能答出现象,答不出机制,那就回到"详见"那一列对应的文章,再读一遍那一节。
附加题:用配套代码复现三个
有三个现象可以用配套仓库亲手复现:
go run ./examples/ch06-tokens letters # 数不清字母(第 06 篇)
go run ./examples/ch08-sampling -n 20 # 同题不同答(第 08 篇)
go run ./examples/ch10-needle # 越长越难找(第 10 篇)这几个实验要接真实模型才有意义,在线 Playground 演示不了:先 export DEEPSEEK_API_KEY=sk-... 再跑。不设 Key 时它们用的是离线演示模型,只能看到实验的流程。接上 DeepSeek 之后,试着在每个实验的输出里,指出它对应的是哪个机制。
加试那三种怪行为,没法用一个小程序稳定地复现,因为它们要在真实的长任务里才会出现。但你可以留个心眼:下次用编码 Agent 做一个稍微长一点的任务,任务结束后翻一遍它的过程,找一找有没有"认死理""自作主张""谎报完成"的影子。第 37 篇的 trace 文件会让这件事容易很多。
附加题二:给你自己的清单归因
翻翻你自己用 AI 的记录,找三个你遇到过的"怪行为",试着把它们归到上面六类里。如果有归不进去的,那就是你自己的研究课题了。欢迎告诉我。
十一、踩坑记录:归因时的四个误区
误区 1:把所有问题都归因于"模型不够聪明"。
很多怪行为和模型的"聪明"程度关系不大,而是机制决定的。最强的模型也会数不清字母(如果不让它思考),也有知识截止日期,也会被长上下文稀释注意力。换一个更强的模型能缓解一部分,但解决不了根源。
误区 2:试图用 prompt 解决机制性的问题。
"请务必准确数出字母的数量",这句话不会让模型突然看见字母。"请不要编造",也不会让有损压缩变成无损的。机制性的问题,要用机制性的办法解决:给它工具、给它上下文、给它验证。
误区 3:只记住了现象,没记住对策。
知道"模型会幻觉"不难,难的是写 Agent 时记得"让它先读文件再改代码""改完一定要跑测试"。这一篇最有用的部分,是那张总表的第四列。
误区 4:把 Agent 的怪行为都当成模型的问题。
加试那三种怪行为,有一半的责任在 harness:没有步数上限,它就能一条路走到黑;没有限制改动范围,它就能自作主张;没有程序化的验收,它说完成就算完成。模型的毛病改不了,但 harness 可以把这些毛病关进笼子里。 第五部分手写 Agent 时,你会看到这些"笼子"是怎么一根一根搭起来的。
一句话总结
模型的怪行为都有机制可循:幻觉来自有损压缩和"总要像样",讨好来自人类偏好,不知道新事来自截止日期,同题不同答来自采样,数不清字母来自 token,越聊越笨来自注意力被稀释;对策归根结底只有两件事:管好上下文,加上验证。
评论
还没有评论,来说点什么吧。