🛠️ 第9章 场景一:接手陌生代码库
📄 一句话概括

串行读的代价不是慢,是「读完之后上下文里没地方干活了」。 分区的边界按职责切,不按目录层级切,也不按文件数量切。 子 Agent 只回摘要,不回原文——这是峰值能降下来的唯一原因。

📝 本章和前面几章的关系

前三部分是打地基,第三部分开始是实战场。 四个场景按难度排:接手陌生代码库、修 bug 补测试、跨前后端开发、PR 与 CI 自动化。 为什么第一个场景是「接手陌生代码库」? 因为它是唯一一个「你还没开始改,就已经被信息量压住」的场景。 后面三个场景都能靠改小范围绕开,这个绕不开——你不读完,就不知道能改哪儿。 Demo 造了一个 22 个文件的仓库,串行读和分九区读各跑一遍,量两个数:主上下文占多少、 墙钟多少。 结论有点反直觉:分区读省了十七倍上下文,但一点时间都没省。

一、串行阅读的问题不在速度

先说一个容易搞错的地方:读文件本身很快。 Demo 里 22 个文件、四百多行,读完只要两毫秒。 所以「串行读慢」这个说法是不准的——它不快也不慢,它就是读了。 真正的问题在读完以后。 那三千多 token 会一直占着上下文窗口,一直到任务结束。 你后面还要写代码、还要看报错、还要读需求——这些都要挤在同一块地方。 上下文窗口是上限,不是账单。 这句话的意思是:占满了不是「多花了钱」,是「后面装不下了」。 装不下的表现很具体:Agent 开始忘掉前面的约定、重复读同一个文件、把你已经否掉的方案又提一遍。 所以分区读买的不是速度,是「后面还有地方干活」。 这也解释了为什么 Demo 里分区版的墙钟反而更长——它本来就不是为了快。

二、并行分区的切法

切法有三个层次,效果差很远。

切法 怎么切 问题
按文件数 每五个文件一组 切断了同一条调用链
按目录层级 一级目录一组 app 那格可能占一半
按职责 能独立回答一个问题的一组 要人先看一眼

第一行最常见,也最糟。 五个文件一组,很可能把「接口层」和「存储层」切开——两边都得再看一眼对面那半,等于读了两遍。 Demo 用的是第三行:先按目录,再把 app 拆成 routes、models、services。 理由不是「目录更细」,是「这三块各自能回答一个问题」—— routes 回答「对外长什么样」,models 回答「数据怎么存」, services 回答「跟外部系统怎么打交道」。 切完是九个扇区,大小很不均:最大的一百多个 token,最小的二十几。 这不要紧。 扇区大小不均只影响谁先读完,不影响结论对不对。 反过来,为了「平均」把 app 硬切成一样大,一定切在职责中间。 一个自查办法:把每个扇区单独给一个人,问他能不能只凭这些下结论。 能,切对了;他还要问「那另一块呢」,切错了。

三、子 Agent 只回摘要,不回原文

这一条比切法更重要。 如果你的子 Agent 读完把原文带回来,主上下文照样满——等于白拆。 Demo 里定死的输出是六个字段:文件数、行数、token 数、最大的文件、依赖了谁、扇区名。 九份加起来不到两百 token。 这就是三千三压到两百的来路:不是压缩了原文,是根本没把原文带回来。 主 Agent 从头到尾没见过那 22 个文件的正文。 代价是:主 Agent 之后要改代码,还得自己去读那个文件。 这是对的。 它读的是「要改的那一个」,不是「全部二十二个」。 分区读的意义就是把「一次读全部」换成「按需读一个」。

四、汇总成一张地图

九份摘要拼起来,就是一张表:区、职责、先看哪个、被谁依赖。 四个字段各有各的用处。 「职责」告诉你这个区管什么,一句话就够; 「先看哪个」是给下一个人的入口,省掉他自己翻一遍; 「被谁依赖」是改动前的必查项——你要动的那个区,被三个区依赖,那就得三个都过一遍。 这张表最值钱的地方是「能被验证」。 「被谁依赖」不是猜的,是扫 import 扫出来的;「先看哪个」不是挑的,是按行数排出来的。 每条都有据可查,所以下一个人敢信。 反过来,一份「这个模块负责订单业务的核心逻辑」的地图没人敢信—— 它说得对,但它不能被验证,也就不能被用来做决定。

五、动手 Demo:串行读和分区并行,量一次上下文占用

造一个 22 个文件的订单服务,串行读一遍,再分九个扇区读一遍。 两边都量两个数:主上下文占多少 token、墙钟多少毫秒。 扇区读是真起子进程的——九个独立的 Python 进程,各读各的那几个文件,各回一行 JSON。 不是我模拟的。

# 每个子进程只做一件事:读自己那几个文件,回一行 JSON
rows, deps = [], set()
for p in sys.argv[1:]:
    s = open(p, encoding="utf-8").read()
    lines = len([x for x in s.split("\n") if x.strip()])
    cjk = len(re.findall(u"[\u4e00-\u9fff]", s))
    rows.append({"f": rel, "l": lines,
                 "t": int(cjk * 1.6 + (len(s) - cjk) / 4 + 0.5)})

print(json.dumps({"n": len(rows), "l": sum(r["l"] for r in rows),
                  "t": sum(r["t"] for r in rows),
                  "big": max(rows, key=lambda r: r["l"])["f"],
                  "deps": sorted(deps)}, ensure_ascii=False))

实跑输出:

--------------------------------------------------------------------
串行阅读和分区并行,量一次上下文占用
--------------------------------------------------------------------

  造一个「没见过」的仓库:22 个文件、9 个区,标准库实现的订单服务。

  ① 先看它长什么样
  --------------------------------------------------------------------
  ├── README.md
  ├── app/
  │   ├── __init__.py
  │   ├── config.py
  │   ├── main.py
  │   ├── models/
  │   │   ├── __init__.py
  │   │   ├── item.py
  │   │   ├── order.py
  │   │   └── user.py
  │   ├── routes/
  │   │   ├── __init__.py
  │   │   ├── order.py
  │   │   ├── pay.py
  │   │   └── user.py
  │   └── services/
  │       ├── __init__.py
  │       ├── notify.py
  │       ├── pay.py
  │       └── stock.py
  ├── core/
  │   ├── __init__.py
  │   ├── cache.py
  │   ├── db.py
  │   ├── log.py
  │   └── tasks/
  │       ├── __init__.py
  │       ├── clean.py
  │       └── sync.py
  ├── data/
  │   └── app.db
  ├── tests/
  │   ├── __init__.py
  │   ├── test_order.py
  │   └── test_pay.py
  ├── tools/
  │   ├── __init__.py
  │   ├── migrate.py
  │   └── seed.py
  └── web/
      ├── api.js
      └── index.js
  非空行 453 行,粗算 3,333 token。这个体量的仓库,人也要读一个下午。

  ② 串行读:一个上下文装下全部
  --------------------------------------------------------------------
  一个 Agent 从头读到尾,全部内容都留在自己的上下文里。
  读完 22 个文件,花了 1.9 毫秒——读文件本身很快。
  ! 慢的从来不是读,是「读完之后上下文里还剩多少地方干活」。

  ③ 分区读:九个扇区各读一片,只交摘要
  --------------------------------------------------------------------
| 扇区 | 文件 | 行 | token | 最大文件 |
|---|---:|---:|---:|---|
| app/ | 2 | 29 | 241 | main.py |
| app/models/ | 3 | 79 | 528 | order.py |
| app/routes/ | 3 | 79 | 620 | order.py |
| app/services/ | 3 | 72 | 582 | pay.py |
| core/ | 3 | 72 | 512 | db.py |
| core/tasks/ | 2 | 41 | 298 | sync.py |
| tests/ | 2 | 16 | 128 | test_order.py |
| tools/ | 2 | 41 | 275 | migrate.py |
| web/ | 2 | 24 | 149 | index.js |

  九个扇区交回来的摘要合计 199 token。

  ④ 两种读法的账
  --------------------------------------------------------------------
| 指标 | 串行一个上下文 | 九扇区并行 |
|---|---:|---:|
| 主上下文看到的内容 | 22 个文件全文 | 9 份 JSON 摘要 |
| 主上下文占用 | 3,333 token | 199 token |
| 读完的墙钟(本进程) | 1.9 毫秒 | — |
| 墙钟·顺序起子进程 | — | 4,014 毫秒 |
| 墙钟·并发起子进程 | — | 896 毫秒 |

  ! 上下文这一栏是 3,333 比 199,压缩了 17 倍。这是分区读真正买到的东西。
  ! 时间这一栏反过来:并发的 896 毫秒比串行读的 1.9 毫秒还慢。
  二十二个小文件,读它们本来就不费时间,起九个进程的开销全摊在墙钟上。
  所以别指望「并行读文件」提速——它省的是上下文,不是时间。
  顺带量了一下:一个什么都不干的子进程要 421 毫秒。
  读文件只要 1.9 毫秒,起进程要 421 毫秒——顺序九次那 4,014 毫秒,基本都是这笔开销的叠加。
  什么时候时间也省?几千个文件、每个还要跑分析的时候。
  那时单个扇区的耗时远大于进程启动,并发才划得来。

  ⑤ 汇总成一张地图
  --------------------------------------------------------------------
| 区 | 职责 | 先看哪个 | 被谁依赖 |
|---|---|---|---|
| app/ | 入口:装配路由、连库、启动 | main.py | app/services |
| app/models/ | 订单表读写 | order.py | app/routes、app/services |
| app/routes/ | 订单接口:建单、查单、取消 | order.py | app、tests |
| app/services/ | 对接支付网关:下单、查询、退款、对账 | pay.py | app/routes |
| core/ | 连接池与事务封装 | db.py | app/models、core/tasks、tools |
| core/tasks/ | 定时同步外部订单 | sync.py | — |
| tests/ | 订单接口测试 | test_order.py | — |
| tools/ | 建表与升版 | migrate.py | — |
| web/ | 前端首页脚本 | index.js | — |

  ! 这张表就是「俯瞰」的产物:九个区、每个区一句话职责、一个入口、一份依赖。
  主上下文从头到尾没见过那 22 个文件的正文。
  地图的价值不在好看,在于它能被验证——「被谁依赖」这一列,下一个人改代码前一定会查。

先说那个反直觉的数:分区读的墙钟比串行读长了两千多倍。 串行读两毫秒,并发起九个进程九百多毫秒。 这不是实现问题——顺带量了一下,一个什么都不干的子进程要四百多毫秒,九个串起来就是四千毫秒。 所以「用子 Agent 提速」这个说法,在小仓库上是错的。 它提速的前提是「单个扇区的活足够重」—— 几千个文件、每个还要跑分析的时候,四百毫秒的启动开销才被摊薄。 二十二个小文件,摊不薄。 第二个数才是重点:三千三百多 token 压到两百以内,十七倍。 省下来的不是钱,是「后面还能装多少东西」。 一个装满三千 token 原文的上下文,你后面写计划、看报错、读需求都得在这剩下的一点点地方里挤。 最后那张地图要看「被谁依赖」那一列。 core 被 app/models、core/tasks、tools 三方依赖,core/tasks 一个都不被依赖。 这两个信息决定了改代码时先看谁——而这恰恰是主 Agent 没读原文也能给出的判断。

30 秒实验

  1. 挑你手边一个不熟的仓库,数一下它有几个一级目录,每个目录大致管什么。 数到「说不清」的那个,就是该单独切一个扇区的地方。
  2. 把「被谁依赖」这一列手写一遍:你打算改的模块,谁 import 它。 写不出来就说明还没准备好动手。
  3. 量一下你自己的上下文:这次任务里,你让 Agent 读了多少个文件? 有没有哪几个是「读了但没用到」的?

常见误区

  1. 「分区并行能提速。」 小仓库上不能,反而更慢。 它省的是上下文,不是时间。 要提速,得单个扇区的活足够重。
  2. 「按文件数平均分最公平。」 平均分一定切在职责中间。 切法的判据是「能不能独立下结论」,不是「大小一样」。
  3. 「子 Agent 把读到的内容带回来,主 Agent 才好判断。」 带回来就白拆了。 子 Agent 的价值在于它把原文消化掉,只交结论。
自测三问(点开对答案) 1. 串行读的问题到底是什么? —— **不是慢,是占地方。** **读文件本身两毫秒,但那三千 token 一直占着窗口,后面写代码、看报错都得挤。** 2. 为什么按文件数平均分是坏切法? —— **因为它会切在职责中间。** **切开的每一半都得回头看一眼另一半,等于读了两遍。** 3. 子 Agent 的输出为什么要提前定死格式? —— **因为主 Agent 要能「拼」而不是「读」。** **格式定死,汇总就是拼九个 JSON;不定死,主 Agent 又得读一遍内容。**

下一篇 9.2 讲地图之前的那一步:先给这个陌生仓库写一份 AGENTS.md,而且只写能被验证的话。

相关笔记