会猜下一个 token 只是「能说话」,能干活还得再加四样:上下文、工具、仓库、反馈。 四样里缺哪一样,它就会在那一环上开始编——而且它不会告诉你它缺了。 上下文是工作台(多宽决定看多远),仓库是唯一的真相(文件写错了它就跟着错)。
上一篇讲「它凭什么能写代码」。这一篇讲「它凭什么能干完一个任务」。 这两件事差得很远。 能写代码是「会说这门语言」,能干完任务是「知道该说什么、说完了自己验一遍」。 后面这四样,才是 Codex 这类工具真正的骨架。 四样分别是:上下文窗口、工具、仓库、反馈回路。 教材 2.2 把它们叫「完成任务时依赖的东西」,我按「缺了会怎样」重排了一遍。 这一篇的 Demo 能量化其中两样: 上下文(窗口开多大,答对几个)和反馈(有测试没测试,差别在哪)。 另外两样在沙箱里没法真跑,只能用数字说明差别在哪。
一、上下文窗口是工作台
上下文窗口就是「它一次能看见多少东西」。
这个量是有上限的——不管多大,都是有限的。
而一个真实项目的代码量,几乎总是超过这个上限。
这就带来一个根本性的问题:它每次干活,都只能看见项目的一部分。
看见哪一部分,直接决定它能答对什么。
这个 Demo 量过:同一个问题(六个函数里哪几个会写日志),窗口 60 行时答对 0 个,
240 行时答对 3 个,全看见才答对 6 个。
这里最要命的一点是:它不会告诉你「我没看见」。
你问它「这个项目里有几个地方调用了 send_mail」,如果那个文件不在窗口里,
它会回答「只有一个地方」——语气和答对的时候一模一样。
所以「上下文」这件事的正确用法是:不是你问得多好,是你给得够不够。
要它改哪个文件,就把那个文件给它;要它理解调用关系,就把调用方也带上。
指望它自己去找,前提是它有工具。
二、工具调用是手
没有工具的模型只有嘴:你贴什么它看什么,它说什么你贴什么。 有工具的模型有手:能读文件、能写文件、能跑命令、能看命令输出、能搜代码。
| 工具 | 它因此能做到 | 没有的话 |
|---|---|---|
| 读文件 | 自己去找那个函数在哪 | 只能靠你贴 |
| 写文件 | 直接改到仓库里 | 给你一段代码,你自己贴回去 |
| 跑命令 | 跑测试、看报错、再改 | 你代跑,它盲改 |
| 搜索 | 全仓库找引用 | 漏改 |
这四样里,最重要的是「跑命令」。 因为它是唯一能让模型「自己发现自己错了」的工具。 没有它,模型只能靠「像不像」判断对错——而「像」正是它最擅长的事,所以它会一直觉得自己对。 「搜索」这一项容易被低估。 改一个函数名要改几处,靠人记是记不全的——必须真去搜。 这一步在补全时代是靠人 grep 的,现在可以交给它, 但前提是它真的去搜了,而不是凭印象说「应该只有两处」。
三、仓库里的文件是唯一的真相
这一条听起来像废话,但它是整套协作的地基。
当「你说的」和「文件里写的」不一致时,以文件为准。
因为文件能被验证,话不能。
反过来说更要紧:文件写错了,它就跟着错,而且错得理直气壮。
README 里写着「调用 init() 即可」,而代码里 init() 早就改名了——
它会照着 README 写,然后跑不通。
这不是它不看代码,是它两份都看了,然后信了写得更像人话的那份。
所以「文档过期」这件事,在有了 AI 之后成本变高了。
以前过期文档只误导人(人还会试着跑一下);现在它同时误导人和机器,而机器不会怀疑。
一条可操作的规矩:文档里写了命令,就要保证那条命令能跑。
四、反馈回路决定成败
反馈回路的意思是:它做一个动作,环境给它一个结果,它根据结果调整。 这个回路转得起来,它就能自己收敛到正确答案;转不起来,它就只能靠猜。 回路要成立,需要三个条件: ① 动作有明确的结果(命令的退出码、测试的通过与否);② 结果能读懂(报错信息说得清哪里错); ③ 它能根据结果再动一次(还有时间、还有额度)。 三个条件里最容易被破坏的是第二个。 测试报错只写「AssertionError」不写期望值和实际值—— 它读到了错误,但读不出「差在哪」,于是只能瞎改。 所以「把报错写清楚」这件事,在 AI 协作里不是给人看的,是给回路用的。 第三个条件也常被忽略。 一次任务跑了几十轮还没收敛,多半不是它笨,是「结果读不懂」——它在同一个错上绕圈。 这时候人该做的不是「再试一次」,是「把报错改清楚再试」。
五、动手 Demo:上下文被截断之后,它还能答对几成
这个 Demo 造一个三百多行的模块,六个函数分散在里面,其中三个函数体里有日志、三个没有。
任务是:这六个函数里,哪几个会写日志?
然后把模块按不同长度截断(60 行、120 行、240 行、全部),模拟「上下文窗口只有这么大」,
看每次能答对几个。
这个实验里的「它」是一个正则匹配器——
它只会读给它的那部分文本,跟真正的模型在这一点上完全一样。
后面还补了另外三个依赖的说明,其中「反馈」那一条真跑了一遍:一段看着没问题的求和代码,
真跑起来把字符串拼成了 12。
# 造一个 376 行的模块:六段填充 + 六个函数
for i in range(1, 7):
for j in range(58):
parts.append("# 填充行 %d-%d" % (i, j))
body = [" log.info(\"enter %d\")" % i, " return %d" % (i * 10)]
if i % 2 == 0:
body = body[1:] # 偶数号不写日志
parts.append("def f%d(x):" % i)
parts.extend(body)
# 只给它前 N 行,再问「哪几个写日志」
for win in (60, 120, 240, n_lines):
seen = "\n".join(text.split("\n")[:win])
# 在 seen 里逐个找函数体,找不到就是「没看到」
实跑输出:
--------------------------------------------------------------------
上下文被截断之后,它还能答对几成
--------------------------------------------------------------------
① 造一个三百多行的模块,六个函数分散在里面
--------------------------------------------------------------------
mod.py 共 376 行,六个函数 f1~f6 分散在其中。
其中 f1、f3、f5 的函数体里有 log.info,f2、f4、f6 没有。
② 任务:这六个函数里,哪几个会写日志?
--------------------------------------------------------------------
正确答案是 f1、f3、f5。要把六个函数体都看到,才能答全。
③ 只给它前 N 行,看能答对几个
--------------------------------------------------------------------
| 窗口大小 | 六个函数的判断(f1~f6) | 答对 |
|---:|---|---|
| 60 行 | 没看到 / 没看到 / 没看到 / 没看到 / 没看到 / 没看到 | 0 / 6 |
| 120 行 | 写 / 没看到 / 没看到 / 没看到 / 没看到 / 没看到 | 1 / 6 |
| 240 行 | 写 / 不写 / 写 / 没看到 / 没看到 / 没看到 | 3 / 6 |
| 376 行 | 写 / 不写 / 写 / 不写 / 写 / 不写 | 6 / 6 |
窗口开多大,就直接决定能答对几个——这跟「它聪不聪明」没有关系,是「看不看得见」。
! 上下文窗口是硬边界:窗口外的内容对它等于不存在。它不会告诉你「我没看到」,它会照着看到的编。
④ 另外三个依赖,各试一次
--------------------------------------------------------------------
依赖二:工具(能不能伸手去读)
--------------------------------------------------------------------
没有工具的时候,它只能看「你贴给它的那一段」。有工具的时候,它可以自己去找文件、自己 grep。
这一条在沙箱里没法真跑(没有 agent 本体),但它的差别可以用一个数说明:本例 376 行,
贴给它的部分是 240 行,剩下 136 行只能靠工具去读。
依赖三:仓库(真相在文件里,不在对话里)
--------------------------------------------------------------------
同一份代码,你说「这个函数返回列表」,而文件里写的是返回字典——以文件为准。
它读文件,所以文件写错了它就跟着错。
依赖四:反馈(跑不跑得起来)
--------------------------------------------------------------------
这一条能真跑,下面单独演。
⑤ 有反馈和没反馈,差多少
--------------------------------------------------------------------
有反馈(真跑一遍):退出码 1,最后一行 TypeError: unsupported operand type(s) for +: 'int' and
字符串相加得到 '12',不是 3——因为列表里是字符串。这个错只有跑一遍才看得见。
没反馈的时候,这段代码「读起来是对的」:求和嘛,谁看得出错?
! 四个依赖里,反馈是唯一能让它自己发现错误的那一个。没有反馈,它就只能靠「像不像」判断对错。
结果是一条很干净的阶梯:60 行答对 0,120 行答对 1,240 行答对 3,全看见答对 6。
这条阶梯说明的不是「它越来越聪明」,是「它看得见的部分越来越多」。
注意 120 行那一档:它答对 1 个,答错的不是「判断错了」,是「没看到」。
但在真实场景里,这个区别你看不出来——
它只会说「只有 f1 写日志」,不会补一句「后面五个我没看到」。
「反馈」那一段也值得看:total(["1", "2"]) 这段代码,读起来毫无问题——求和嘛。
真跑一遍输出 12,因为列表里是字符串,加号变成了拼接。
这个错只有跑才看得见。
四个依赖里,反馈是唯一能让它自己发现错误的那一个。
另外三个决定「它能不能做对」,反馈决定「它知不知道自己没做对」。
后者才是「能不能放手」的分界。
30 秒实验
- 数一数你的项目有多大:核心模块几千行? 如果超过上下文窗口,那你每一次提问,它都只看见了一部分—— 这时候「给它什么」比「问它什么」更决定结果。
- 翻一条你的测试失败信息,问自己:这个报错,我能读懂吗? 读不懂的报错,它也读不懂。 改报错信息是给回路做维护,不是给人看的。
- 检查你的 README:里面的命令还能跑吗? 跑不了的那一条,就是下一次它照着做会踩的坑。
常见误区
- 「窗口大到能装下整个项目,就没这个问题了。」 装得下也有别的限制:装得越多,越容易「中间那段被忽略」;而且项目会长大。 真正稳的做法是「按需给它」,不是「一次全塞」。
- 「它没说话,说明它都看到了。」 它不会报告「我没看到」。 沉默不代表知情,答得流利也不代表知情。 要确认它看到了,就问一个只有看见某处才能答的问题。
- 「有测试就够了,反馈自然有。」
测试得有「能读懂的报错」才算反馈。
只写
assert x == y不给消息,出错了就是一句 AssertionError—— 人还能去翻代码,机器只能瞎猜。
自测三问(点开对答案)
1. 上下文、工具、仓库、反馈,哪一样是「能不能放手」的分界? —— **反馈。** 另外三样决定它能不能做对,**反馈决定它知不知道自己没做对**。 **没有反馈,它就只能靠「像不像」判断,而「像」恰恰是它最擅长的。** 2. 为什么说「它会自己去找文件」这件事很重要? —— **因为上下文窗口是有限的,而项目通常超过窗口。** **没有工具,它只能看见你贴的那一段,看不见的部分它会当成不存在。** **有工具,它才能伸手去够。** 3. 「文件写错了它就跟着错」这句话,对写文档的人有什么要求? —— **文档里写了命令,就要保证那条命令能跑。** **以前过期文档只误导人(人还会试着跑一下);现在它同时误导机器,而机器不会怀疑。**下一篇 2.3 讲第一条边界:就算四样都齐了,信息还是永远不完整——而它不会停下来问,它会猜。
- ☐ (本篇完,下一篇 2.3 第一条边界:信息永远不完整)