🐎 第10章 炉火纯青:工程化实战
📄 一句话概括

大仓库的题不是「读得慢」,是「读进来就占着窗口,而且后面每一轮都带着」。 实测两百个文件:全量读 21797 token,搜索后读 1872,子代理只回 38。 省的不是时间,是「带上」。

📝 本章和前面几章的关系

前三节讲钱、讲查、讲守,这一节讲规模——窗口就那么大,代码有几百万行。 这一节有真跑:现造一个两百文件的仓库,三种读法各量一次进主窗口多少。

一、三种读法

全量读:把相关目录全读一遍。最保险,也最先撞窗口。 搜索后读:先按关键词搜,只读命中的那几个。快,但可能漏掉间接相关的。 子代理:派一个去读,它读多少都行,只把结论回给主线。

三种不是「新旧」的关系,是三种不同的取舍:全量的代价是窗口,搜索的代价是召回, 子代理的代价是「你看不见中间过程」。

二、真跑:两百个文件找一个函数

造一个目录结构像真项目的仓库:src 下二十个模块各八个文件,tests 下四十个,合计两百个文件。 其中十三个文件里定义了同一个函数,散在不同模块里——按目录名猜不出它在哪儿。 全量读要 21797 token,占两百 k 窗口的百分之十一。这还只是代码,没算指令、没算工具结果。

搜索后读只要 1872 token,是全量的百分之九。因为命中那十三个文件本来就只占仓库的一小角。 子代理读的是同一批文件,但只回三行结论,主窗口只增 38 token。 三个数差三个数量级,而它们做的是同一件事。

三、演示里的仓库太小了

这里的每个文件只有三十来行、八百字符。真实项目一个文件几百行,两百个就是几百万字符。 那时候全量读不是「占一成窗口」,是「根本进不去」——光代码就是上限的好几倍。 所以这三个数要当趋势看,不要当比例记:全量是基准,搜索省到一成,子代理再省到五十分之一。

也正因为真实项目进不去,才需要「引导搜索」而不是「让它自己找」——你告诉它去哪个目录、搜哪个词, 比它自己扫一遍省得多。

四、子代理省的不是读,是「带上」

子代理也要把那十三个文件读完,读的时候一样吃窗口——吃的是它自己的窗口。 省下来的关键是:那块窗口用完就销毁,主上下文只留一份结论。 所以判断要不要派子代理,不是看任务大不大,是看「中间过程需不需要留在主线上」。

查一个模块的用法、跑一次批量搜索、审一遍日志——这些都属于「过程不必留」。 反过来,需要反复回看原文的活儿(比如逐行改一个文件),就别派出去:结论回来了,原文还得再读一遍。

五、分层指令解决的是另一半

根目录一份指令写全局规矩,子目录各一份写本模块的约定。 这样 agent 走到哪个模块,只读那一份,不用把全项目的约定都背着。 这一条和「搜索后读」是配套的:搜索决定读哪些文件,分层指令决定读哪些规矩。

还有一个常被忽略的点:压缩。 窗口快满时会自动压缩,而压缩会重排前缀,缓存当场失效(10.1 节那笔账)。 所以少读不只是省钱,也是让压缩晚点发生。

六、动手 Demo:两百个文件,三种读法的窗口账

现造一个两百文件的仓库,同一件事用三种读法各做一次,只量一件事:进了主窗口多少。 时间不是重点,占住的窗口才是。

hit_chars = sum(len(get(tmp, os.path.relpath(p, tmp).replace(u"\\", u"/")))
                for p in hits)
# 全量读:tokens(u"x" * n_chars)
# 子代理:tokens(u"x" * 150)   —— 只回三行结论

实跑输出:

--------------------------------------------------------------------
两百个文件,三种读法的窗口账
--------------------------------------------------------------------

  大仓库的题不是「读得慢」,是「读进来就占着窗口,而且后面每一轮都带着」。
  这里真造一个两百文件的仓库,三种读法各量一次。

  ① 三种读法
  --------------------------------------------------------------------
| 做法 | 怎么读 | 代价 |
|---|---|---|
| 全量读 | 把所有文件读一遍 | 最保险,也最先撞窗口 |
| 搜索后读 | 先搜关键词,只读命中的 | 快,但可能漏掉间接相关的 |
| 子代理 | 派一个去读,只回结论 | 主窗口只增结论那几行 |


  ② 真跑:造一个两百文件的仓库
  --------------------------------------------------------------------
  造好了:src 下 20 个模块各 8 个文件,tests 下 40 个,合计 200 个、87188 字符。
  其中 13 个文件里定义了 format_date——f0.py、f5.py、f2.py,散在不同模块里。
  所以「找它」这件事本身就得翻一遍:文件按模块分目录,按名字猜不出它在哪儿。
  顺带说一句:这 13 个文件总共才 7488 字符,只占仓库的 9%。
  所以「全量读」浪费的不是一点点,是九成。

  ③ 三种读法各读进来多少
  --------------------------------------------------------------------
| 做法 | 读了几处 | 读进来多少 | 进主窗口 |
|---|:---:|:---:|:---:|
| 全量读 | 200 个 | 87188 字符 | 21797 token |
| 搜索后读 | 13 个 | 7488 字符 | 1872 token |
| 子代理 | 13 个 | 约 150 字符 | 38 token |

  全量读要 21797 token,占两百 k 窗口的 11%——这还只是代码,没算指令、没算工具结果。
  搜索后读只要 1872 token,是全量的 9%——命中那 13 个文件本来就只占仓库的一小角。
  子代理读的是同一批文件,但只回三行结论,主窗口只增 38 token。

  ④ 演示里的仓库太小了
  --------------------------------------------------------------------
  这里每个文件只有三十来行、八百字符。真实项目一个文件几百行,两百个就是几百万字符。
  那时候全量读不是「占一成窗口」,是「根本进不去」——光代码就是上限的好几倍。
  所以这里的比例要当「趋势」看:全量是基准,搜索省到一成,子代理再省到五十分之一。

  ⑤ 子代理省的不是读,是「带上」
  --------------------------------------------------------------------
  子代理也要把那 13 个文件读完,它读的时候一样吃窗口——吃的是它自己的窗口。
  省下来的关键是:那块窗口用完就销毁,主上下文只留一份结论。
  所以子代理的判据不是「任务大不大」,是「中间过程需不需要留在主线上」。
  查一个模块的用法、跑一次批量搜索、审一遍日志——这些都属于「过程不必留」。

  ⑥ 分层 CLAUDE.md 解决的是另一半
  --------------------------------------------------------------------
  根目录一份写全局规矩,子目录各一份写本模块的约定。
  这样 agent 走到哪个模块,只读那一份,不用把全项目的约定都背着。
  ! 仓库是本沙箱现造的,比例按字符数实算;token 按英文四字符一个估。

①三种读法:全量读(代价是窗口)、搜索后读(代价是召回)、子代理(代价是看不见过程)。 ②真造两百个文件:全量 21797 token、搜索后 1872、子代理 38。 占两百 k 窗口是百分之十一、不到百分之一、可以忽略。 ③演示里每个文件只有三十来行。真实项目一个文件几百行,全量读不是占一成,是根本进不去。 ④子代理省的不是读,是「带上」——它读的时候一样吃窗口,只是吃完就销毁。 判据是「中间过程要不要留在主线上」。

30 秒实验

  1. 把仓库从两百个文件加到四百个(每个文件不变),重跑一遍。三行数字各涨多少倍?哪一行几乎没变?
  2. 让搜索命中率从十三分之一降到三十分之一,重跑一遍。搜索后读的成本怎么变? 漏掉的那部分代价怎么算?
  3. 给子代理的结论从三行扩到三十行,重跑一遍。它相对全量读还剩多少优势?

常见误区

  1. 让它自己扫一遍。真实项目扫不动,而且扫进来的内容后面每一轮都带着。
  2. 以为子代理「读得快」。它不快,它只是读完就把窗口扔了。
  3. 什么事都派子代理。需要反复回看原文的活儿,结论回来了还得再读一遍。
  4. 只靠目录名猜位置。函数散在不同模块时,猜不出来——得靠搜索。
  5. 忽略压缩的代价。窗口一满就压缩,压缩重排前缀,缓存当场失效。
自测三问(点开对答案) 1. 大仓库真正的难题是什么? —— 不是读得慢,是读进来就占着窗口,而且后面每一轮都要带着它。 2. 子代理到底省了什么? —— 省的是「带上」。它读的时候一样吃窗口,但吃完就销毁,主上下文只留结论。 3. 什么时候不该派子代理? —— 需要反复回看原文的活儿。结论回来了,原文还得再读一遍,等于白派。

读得少了,下一步是让它读对——指令怎么写才不是「希望」。

相关笔记