The primary reader changed
The primary reader changed
How agents actually read code, why re-derivation cost has a unit, and why this doesn’t go away when context windows grow.
AI 代理(Agent)究竟是如何阅读代码的?为什么代码的“重推导成本”会有明确的度量单位?为什么即便上下文窗口不断扩大,这个问题依然无法消失?
Series: Grounded Code. Second of five articles in the Grounded Code series. The first one showed the cost. This one shows the mechanism underneath.
系列文章:Grounded Code(扎根代码)。这是该系列五篇文章中的第二篇。第一篇展示了成本,而这一篇将揭示其背后的机制。
I. The receipt, and what’s underneath it
I. 账单及其背后的真相
In the manifesto I showed a number: 7.5x more tokens for the same feature, with the same final verification on both sides. That’s the receipt. This article is about what produced it. The short version is that code has a new primary reader, and the new reader works with a completely different set of tools, a completely different memory model, and a completely different way of recognizing patterns. The patterns we adopted over twenty years answered the old reader’s needs. The cost in the new reader’s mode of reading wasn’t on anyone’s chart, because there was no reader to charge it to. There is now.
在宣言中,我展示了一个数字:实现相同的功能,在双方最终验证一致的情况下,AI 代理消耗的 Token 是人类的 7.5 倍。这就是那张“账单”。本文旨在探讨产生这一成本的原因。简而言之,代码迎来了一位新的“主要读者”,这位新读者使用着完全不同的工具集、完全不同的记忆模型,以及完全不同的模式识别方式。我们过去二十年所采用的代码模式,都是为了满足旧读者的需求。而在新读者的阅读模式下,这些成本从未被纳入任何人的考量,因为过去并没有需要为此买单的“读者”。但现在有了。
II. How a human reads code
II. 人类如何阅读代码
For a moment, ignore the agent. Look at what your IDE has been doing for you. When you open a feature in a clean codebase, you carry roughly five to ten files in working memory at any time. Your tooling lets you jump between them in a fraction of a second. Cmd+P, fuzzy match, you’re in the file. Cmd+click on a symbol, you’re at its definition. The IDE follows path aliases, computes type inference, resolves imports, highlights related symbols, all silently.
暂时忽略 AI 代理,看看你的 IDE 一直在为你做什么。当你在一个整洁的代码库中开发功能时,你的工作记忆中大约会同时处理五到十个文件。你的工具让你能在几分之一秒内跳转于这些文件之间:Cmd+P,模糊匹配,你就进入了文件;Cmd+点击某个符号,你就跳转到了它的定义处。IDE 会自动处理路径别名、计算类型推断、解析导入、高亮相关符号,这一切都在后台静默完成。
You recognize patterns by similarity. Two functions that “look like they do the same thing” register as related even if they share no literal substring. A PaymentService and a UserService register as parallel because of conceptual symmetry, not because of any string they share. You carry the project’s mental model across days. The feature you worked on Tuesday is still in your head Thursday morning. You don’t need to re-derive what the system does every time you open the editor.
你通过相似性来识别模式。两个“看起来功能相似”的函数,即使没有任何字面上的重合,你也会认为它们是相关的。PaymentService 和 UserService 被视为平行结构,是因为概念上的对称性,而不是因为它们共享任何字符串。你会将项目的思维模型跨天保留——周二处理的功能,周四早上依然在你的脑海中。你不需要每次打开编辑器时都重新推导系统是如何运作的。
Domain language helps you bridge files. When you read Aggregate in one file and EntityRoot in another, you know they belong to the same DDD vocabulary even if neither imports the other. The vocabulary is in your head, not in the code. Clean Code, DDD, Clean Architecture, hexagonal: every pattern in that family was tuned to this reader. The trade-offs they made (some indirection, some boilerplate, some abstraction) were paid in exchange for a real benefit: this reader’s mental load went down. The reader is still you. The reader is still real. But on the codebases that ship features regularly with an agent in the loop, this reader is not doing most of the work anymore.
领域语言帮助你跨越文件边界。当你在一个文件中读到 Aggregate,在另一个文件中读到 EntityRoot 时,即使它们互不引用,你也知道它们属于同一个 DDD(领域驱动设计)词汇表。这些词汇存在于你的脑海中,而非代码里。《代码整洁之道》、DDD、整洁架构、六边形架构:这一家族中的每一种模式都是为这位读者量身定制的。它们所做的权衡(一定的间接性、样板代码、抽象化)都是为了换取一个实际的好处:降低读者的认知负荷。这位读者依然是你,依然是真实存在的。但在那些通过 AI 代理持续交付功能的代码库中,这位读者已经不再承担大部分工作了。
III. How an agent reads code
III. AI 代理如何阅读代码
An agent reading code does not scroll. It does not Cmd+P. It does not Cmd+click. It has none of the silent IDE machinery you take for granted. What it has is three primitives: grep, to find files containing a literal pattern; glob, to find files matching a path pattern; read with offset and limit, to load a slice of a file into context. That’s the whole toolset for navigating a codebase.
AI 代理阅读代码时不会滚动屏幕,不会使用 Cmd+P,也不会使用 Cmd+点击。它没有你习以为常的那些静默 IDE 机制。它拥有的只有三个原语:grep(查找包含特定字面模式的文件)、glob(查找匹配路径模式的文件)、以及带偏移量和限制的 read(将文件片段加载到上下文中)。这就是它导航代码库的全部工具集。
Every operation that an IDE does for you in milliseconds becomes a tool call. Every tool call costs tokens (for the call, for the result, for the model to parse the result). Every file the agent reads stays in context until something pushes it out. There is no persistent project memory. The session is the memory. This changes pattern recognition completely.
IDE 在几毫秒内为你完成的每一个操作,对代理来说都变成了一次工具调用。每一次工具调用都会消耗 Token(用于调用本身、结果返回以及模型解析结果)。代理读取的每一个文件都会保留在上下文中,直到被其他内容挤出。它没有持久的项目记忆,会话即记忆。这彻底改变了模式识别的方式。
The agent doesn’t see similarity the way you do. It recognizes patterns by co-located names and exact shapes. If two files have parallel filenames (user.ts and user.test.ts) and parallel structure inside, the agent will pattern-match them. If one is models/user.ts and the other is tests/user-tests.ts, the relationship is invisible until something explicit ties them together. Domain language doesn’t bridge files for an agent the way it does for you. If Aggregate is mentioned in domain/order/aggregate.ts and EntityRoot is mentioned in infra/repository.ts, the agent has no automatic understanding that these terms are part of the same vocabulary. It might learn the connection by reading a glossary if one exists and is in context. But the vocabulary doesn’t live in the agent’s head between sessions, because there is no head.
代理看待相似性的方式与你不同。它通过名称的共存和精确的结构来识别模式。如果两个文件有平行的文件名(如 user.ts 和 user.test.ts)且内部结构平行,代理就能匹配它们。但如果一个是 models/user.ts,另一个是 tests/user-tests.ts,除非有明确的关联,否则这种关系对它来说是不可见的。领域语言无法像对你那样为代理架起文件间的桥梁。如果 Aggregate 出现在 domain/order/aggregate.ts 中,而 EntityRoot 出现在 infra/repository.ts 中,代理无法自动理解这些术语属于同一个词汇表。它或许可以通过阅读上下文中的术语表来学习这种联系,但这些词汇不会在会话间留存在代理的“脑海”中,因为它根本没有大脑。
Each turn, the agent rebuilds enough of the project model to do the next action. The model is rebuilt from what’s currently in context, plus what it fetches with its three primitives. Everything not in context has to be re-fetched, which means re-paid in tokens. This is the new primary reader. None of this is a deficiency of the model. It is the structural reality of how a stateless reasoning system, equipped with grep, navigates a codebase.
每一轮,代理都会重建足以执行下一步操作的项目模型。该模型由当前上下文中的内容,加上它通过三个原语获取的内容共同构成。所有不在上下文中的内容都必须重新获取,这意味着必须重新支付 Token。这就是新的“主要读者”。这并非模型的缺陷,而是无状态推理系统在配备 grep 等工具时导航代码库的结构性现实。
IV. Three places the cost shows up
IV. 成本显现的三个地方
Let me make this concrete with three patterns from the original article and show what they actually cost. 让我用原文中的三个模式来具体说明它们到底消耗了什么。
A. Path aliases A. 路径别名
// services/user.ts
import { User } from '@models/user'
export async function getUser(id: string): Promise<User> {
// ...
}
Human path: You Cmd+click on @models/user. Your IDE knows the alias config from tsconfig.json and lands you in src/models/user.ts in one keystroke. 人类路径:你 Cmd+点击 @models/user。你的 IDE 从 tsconfig.json 中读取别名配置,只需一次按键就能带你跳转到 src/models/user.ts。
Agent path: The agent reads services/user.ts and sees @models/user. It does not know what that resolves to. It reads tsconfig.json. The tsconfig.json may extend a base config, so it reads that too. It identifies the paths entry that maps @models/* to src/models/. Now it can glob for src/models/user.ts. Then it reads it. Three file reads where the IDE did one keystroke. Multiply by every import in every file the agent has to navigate during a task. 代理路径:代理读取 services/user.ts 并看到 @models/user。它不知道这指向哪里。它读取 tsconfig.json。由于 tsconfig.json 可能继承了基础配置,它还得读取那个基础配置。它识别出将 @models/ 映射到 src/models/* 的路径条目。现在它才能 glob 找到 src/models/user.ts,然后读取它。IDE 只需一次按键,代理却要读取三个文件。将此成本乘以代理在任务期间导航的每个文件中的每个导入,成本便显而易见了。
The fix is one line of change per import: 解决方法是每个导入修改一行:
// services/user.ts
import { User } from '../models/user.ts'
Now the path is the path. No tsconfig hop. The agent reads services/user.ts, sees ../models/user.ts, globs it, reads it. Two operations instead of three or four. 现在路径就是路径,无需跳转 tsconfig。代理读取 services/user.ts,看到 ../models/user.ts,glob 找到它,然后读取。操作次数从三四次减少到了两次。
B. Deep dependency injection B. 深度依赖注入
function process(deps: ProcessDeps) {
const user = deps.db.users.findById(deps.context.userId)
deps.logger.info('processed', { user })
deps.metrics.increment('process.success')
return user
}
(Human…) (人类…)