这个场景值得先认清楚,所以就从它说起。您刚写完一个游戏,重启后分数就归零了,于是您问智能体:

为什么重启之后分数就没了?

回答来得又冷静又流畅:

项目里已经有保存分数的功能了,只是写入功能哪里都没有打开。
只要改一个设置,结果就能在重启后保留下来。

您去检查了一下。项目里既没有这个设置,也从来没有分数保存的功能 - 一行都没有,连影子都没有。这个回答听起来像是分析了您的代码,其实只是对这类项目通常怎么搭建的一个猜测。

而且回答之前既没有停顿,也没加任何保留说明。语调和之前所有正确答案完全一样。工具本身没坏,模型也没有比昨天变差。

原因在于答案到底是怎么来的。模型并不是从存储里取出一个现成的事实,而是一块一块地为您的文本挑出一个概率高的续写。而"概率高"和"正确"是两回事,尤其是涉及您的项目时 - 模型根本没见过它。这类编造出来但看着合理的细节,通常叫做幻觉。

一张桌子从中间分成两半:左边在黑暗中摆着占卜卡牌和骰子,右边在一束光下放着一本夹着书签、配着放大镜的翻开的书

接下来看看陷阱都设在哪里,为什么错误总是带着冷静的语气,以及哪三个检查能覆盖大部分这类情况。

模型在您的请求里到底看到了什么

这里的陷阱是所有陷阱里最简单的一个:感觉上模型是在看您的项目。实际上它只看别人递给它的东西,而递给它的是这些:

  • 内置知识。 训练时沉淀在模型参数里的规律。不是带着事实和出处的卡片,而是一堆数字,所以哪怕是熟悉的事实,模型也可能复述得不准。而且它有时间边界:对于后来才发布的库,模型知道多少,完全取决于给它看了多少。
  • 当前对话。 您的请求、对话历史、智能体读过的文件、它运行过的命令的输出。
  • 应用的记忆。 保存在单独文件里、并在下一次会话开始时被放进对话的内容。在 Claude Code 里,承担这个作用的是由您来写的 CLAUDE.md,以及智能体自己记笔记的 auto memory。
  • 外部来源。 文档、搜索、数据库、项目文件 - 所有训练数据里可能没有、或者后来发生了变化的东西。

后三种都走同一条路:要想影响答案,它们必须先进入当前对话。模型本身不会主动去看项目。

由此可以推出一点,值得在脑子里彻底转过这个弯。没人打开过的文件,对模型来说就是不存在的。在它的内容进入对话之前,"我们的登录验证在哪处理"这个问题,更诚实的读法其实是"这类项目通常把这种文件叫什么"。两种情况下您都会得到一个答案,而且看起来一模一样。

旁边还有一个更小、但更让人恼火的陷阱。读过的文件会以被读取时的样子留在对话里。您在编辑器里改了一行,对话历史里留着的却还是旧版本。Claude Code 会提示文件已经改过,但智能体会不会重新读取,要由模型来决定。如果是在对话中途手动改的文件,更稳妥的做法是直接请智能体重新读一遍。

在 Claude Code 里该看什么

Claude Code 会先读项目、跑命令,然后才回答。回答里通常能看出它打开了哪些文件、运行了什么。如果关于您代码的回答在智能体读任何东西之前就出现了,那您看到的是对一个典型项目的猜测,而不是对您项目的分析。

为什么措辞会改变结果

在所有进入对话的东西里,只有您的请求是您自己写的。所以里面的细节才这么关键:调换两个词的顺序、加一句说明,答案就跑到另一个方向去了。这不是工具在耍脾气。

这里容易搞混,所以单独说清楚。同一个请求问两次,得到的答案也会不一样:生成出来的不一定是最可能的那个续写,生成过程本身有设置,决定了排名靠后的选项被选中的概率有多大。这就会有个疑问:既然答案本来就会波动,我的措辞又有什么关系。区别在于波动的是什么。同一个请求两次的差异,是围绕同一个选择产生的噪声:措辞会不同,方向不会变。而请求里的用词动的是方向本身,而且是稳定地动。

模糊的请求效果最差。它把选择权留给了模型,而模型选的是总体上最可能的那个 - 是它见过的所有东西的平均值。就拿简单的"把这段文字缩短"来说。缩短多少 - 百分之几、三分之一,还是一半?哪些内容能删,哪些不能碰?这些问题请求里都没有答案,所以只能由模型来回答,而它形式上并没有错。相反,"把这段描述缩短 30%"或者"把描述压到 300 字以内"就没给它留下选择的空间 - 该由您决定的地方,它没得选。

用一对关于同一件事的请求最能看清楚。"把排序修一下"几乎什么都没说清楚:哪个列表,按哪个字段,到底什么算坏了。而第二种说法几乎没留下任何空白:

订单列表里的日期是从旧到新排的,需要反过来。
先给我看设置排序方式的文件和那一行,然后再提出修改方案。

它更长,长出来的部分正好就是第一种说法留给猜测的部分。而且它顺带替您完成了一半的检查:智能体在您开口问之前,就已经把代码里的位置带过来了。

结论很简单:请求里的自由度越小,答案里的猜测就越少。这不是什么咒语,就是一件很平常的事 - 要么由您来划定边界,要么边界由别人替您选。

自信的语气不等于事实

这才是最主要的陷阱。模型没有专门给"这个我确实知道"用的语气,也没有专门给"这是我现编的"用的语气:两种句子都是用同一种方式拼出来的,听起来一样冷静。语气和真假之间也不能说完全没有关系,但没什么用处:模型的自信程度一直是偏高的,所以光靠语气分不出哪个是查证过的,哪个是编出来的。

在普通文字里,一句奇怪的话会让人下意识停下来。可在代码里,这道防线不起作用:名字看起来都对,写法也眼熟,一个真实的库方法和按类比拼出来的方法,光用眼睛看根本分不出来,除非您对这个库了如指掌。所以这类失误往往不是在阅读时暴露,而是在运行时才浮出水面。

而且名字也很少是凭空编出来的。有一份对生成代码的分析专门找那些指向不存在的库的引用,结果发现三分之一以上的这类名字,都是两个真实名字拼在一起的。这也是为什么会有可信的感觉:名字的各部分您确实在别处见过,只是没见过这种组合。

只要知道该找什么,下面这四种情况一眼就能认出来:

看起来是什么样实际发生了什么用什么能查出来
说命令有某个标志,其实没有模型按熟悉的格式拼出了这一行,所以标志看着像真的命令自带的帮助或它的文档
说出了项目里没有的文件、函数或配置字段名字看起来很像这类项目该有的,但没人读过这个项目在项目里搜索,或者要求给出那一行
自信地讲述了您的项目是怎么搭的对话里根本没有任何一个项目文件,所以描述的是一个典型项目问它回答之前打开过哪些文件
说"完成了",但命令根本没跑过报告是从任务文本拼出来的,不是从运行结果里来的自己跑一遍,看输出

这四种情况有一个共同点:回答听起来像是工作的结果,但背后其实没有做过工作。

最后一种情况值得单独说一下,因为它看着像是撒谎,其实不是。智能体只要觉得工作看起来完成了,就会停下来。如果它没什么可运行的,"看起来完成了"就是它唯一能拿到的信号,而检查这一环就落到了您头上。

这件事的代价是可以量化的。在一份对智能体日志的分析里,虚假的"完成了"占了将近一半的失败案例。而在那些独立于智能体自身报告去检查状态的地方,这类情况几乎消失了。这正是下一节第二个检查的用意:用您亲眼能看到的东西,替换掉那份报告。

还剩一个问题:模型为什么宁愿编也不愿承认不知道。这和它是怎么训练、怎么打分的有关。设想一道选择题,空着不填保证得零分,随便选一个还有机会得分。在这种体系下,沉默永远不划算,遇到任何不确定都回答才是理性的做法。人后来会在真实任务中改过来,因为答错的代价比诚实说"不知道"更高。模型却没有这样改过来的机会。

请模型在不确定的时候如实提醒您,确实有帮助 - 但这句话本身也是同一套机制拼出来的文本。当作额外信号可以,当作检查的替代品不行。

工具能缩小盲区

这里最好把两个经常被混在一起说的词分开来看。模型能做的事只有一件:接收一段对话,输出一段续写。智能体是围着模型搭的一个程序,它拥有工具:读文件、跑命令、改项目。

接下来这个细节决定了错误具体会出在哪一环。选工具的不是程序。模型在回答里请求打开某个文件或者跑某条命令,程序负责检查这个请求、执行它,再把结果送回对话里。做决定的是模型,动手的是程序。也就是说,出错的可能是决定本身:不是这个文件、不是这条命令、不是这个测试。

但差别依然巨大。只要文件还没打开,关于它的回答就是靠对类似项目的记忆拼出来的。一旦文件被读过、测试跑过,对话里就会出现一个事实:这是内容,这是输出。这已经不是回忆,而是查证,之后回答的质量会明显不同。

由此有一个只需要在对话开头加一句话的做法:

先在项目里找到并给我看这个东西具体在哪里,然后再解释。

盲区会因此缩小,但不会消失。智能体可能打开了旁边的文件而不是需要的那个,可能按自己的理解解读命令输出,也可能停在第一个说得过去的解释上,没有找到真正的原因。这些我都见过,而且不止一次。所以外部信号 - 测试输出、文档内容、文件本身的内容 - 比毫无依据的、哪怕再自信的推理都更有分量。

三个值得花一分钟的检查

好消息是,抓住这类失误几乎不费什么力气。全部归结到一个岔路口。

检查答案的岔路口:如果答案里没有指向文件、命令或测试的依据,就把它当作一个假设,要求给出具体位置;如果有依据,就自己确认结果智能体的回答有没有指向文件、命令或测试的依据?当作假设要求给出具体位置自己确认打开、运行一下没有有
不管是哪个任务,岔路口都一样:先在答案里找依据,再决定是去验证还是去追问

这里要看的不是写得有多让人信服,而是答案里有没有什么能抓得住的东西:一个具体的文件、一行具体的代码、一条带着输出的命令。没有依据,就说明这还只是个假设,不是结果。很多回答一开始都是这样,问题只在于接下来您会怎么做。

接下来是三个检查。每个都花不到一分钟,而且很少需要三个一起用。

  1. 让它给出具体位置。 不是"再解释详细点",而是"给我看文件、行号和那段内容本身"。如果压根没有这个地方,马上就能看出来。
  2. 自己打开或运行一下结果。 浏览器里的页面、终端里的命令、项目里的测试。工作报告和工作本身不是一回事。
  3. 拿工具本身核对关于工具的说法。 命令有内置帮助,库有文档。说到的那个标志或配置项,要么真的存在,要么不存在,半分钟就能查清楚。库的方法则要更谨慎一些:不是所有东西都会写进文档,所以文档里没有它,只是个该追问的理由,不是定论。
能省时间的说法

与其和答案争论,不如直接要依据:

给我看这个是在哪个文件、哪一行里描述的。
如果项目里没有这个地方,就直接说这是个猜测。

这两句话值得随时备在手边。第一句让智能体去项目里找答案,而不是靠对类似情况的记忆来回答。第二句给了它一条退路:承认没有这个地方,代价比硬编出一个看似合理的答案更小。

对于"完成了"这样的报告,同样的请求也管用,只是改成问运行情况:"给我看你运行的命令,以及完整的输出"。

这不会变成一个保证:"这是个猜测"这句话,本身也是由同一套机制拼出来的。它能降低出错的概率,但代替不了检查。

顺带说一句,免得留下错误的印象。检查不代表不信任这个工具。自己写的 diff,提交前您也会重新看一遍,尽管是您自己写的。这里是同一个道理,只是理由稍微重一点。

这对使用 Claude Code 工作意味着什么

落到键盘前的实际操作上,能得出这些结论:

  • 带着文件、行号或命令输出的答案,几秒钟就能验证。没有这些的答案没什么可验证的,剩下的就只有重新读一遍然后同意;
  • 在提问之前,先让智能体读到需要的内容。这一步能省掉一整类失误;
  • 不能靠自信程度来判断:不管背后是查证过还是猜的,自信程度都是偏高的;
  • "听起来对"和"运行起来对"是两回事,只有运行一下才能分清楚;
  • 出错的代价越高,对话里就越应该早点出现一些外部的东西:测试、文档、真实的文件。小改动的话,看一眼 diff 就够了;
  • 如果智能体错了,用事实告诉它,而不是用评价。"项目里没有这个功能,这个文件夹里实际有的是这些"能推动工作继续下去,"你又在瞎编"什么都推动不了。还要记住一点:面对自信的反驳,智能体更可能顺着改,而不是坚持争论,哪怕原本是它对的。智能体表示同意,并不能证明您是对的。

一个偶尔会自信地犯错的机制,依然极其有用。了解它的边界,唯一的目的就是不把时间花在惊讶上,而是直接进入检查。