编码Agent如何阅读代码的,以及如何写代码让Agent更好阅读
本文探讨了AI编码Agent如何通过字符串搜索(如grep/ripgrep)来导航代码库,并提出了让代码对代理更“可发现”的实践建议:使用描述性命名、精准类型、注释放在定义处、统一拼写、标记遗留代码等。作者通过两个实验验证了这些方法的效果:在生成的TypeScript库中,遵循建议后代理读取代码的token消耗平均降低6%~66%,错误答案减少;在真实Python项目Odysseus中,重构使代理查找效率提升,bug检测率从23/32提高到32/32。文章强调,代理没有人类的理解背景,代码中的文本就是它们唯一的线索,因此为代理编写的代码本质上也会让人类受益。
为智能体编写代码 系列第一篇:智能体如何通过字符串搜索在代码库中导航,以及如何编写它们能真正找到、解析并信任的代码。
当我们在 2025 年创立 Modem 时,我们相信 AI 编码是真实的,它会快速加速软件开发,而传统产品流程将成为执行的新瓶颈。为了拥抱那个未来,我们从第一天起就使用 AI 代码生成工具来构建 Modem——当时 Sonnet 3.7 还是领先的编码模型。一年后,Modem 成为了一个拥有付费客户的真实产品:36 万行 TypeScript 应用代码和另外 32 万行测试代码,其中 99.9% 由 LLM 生成。
让 Modem 成为可用的代码库并不容易。它需要实验、试错和持续投入,才能有效地与智能体代码生成协作。在这篇博客系列 为智能体编写代码 中,我们将分享一些我们在过程中学到的技巧。
第一篇(即本文)将涵盖如何编写代码,使智能体能更有效地导航、解析和阅读:你选择的名称、定义的类型以及放置解释的位置。结果并非假设;在我们的测试中,遵循本指南中的技巧导致花在代码检索上的 token 更少、智能体交互轮次更少,缺陷检测率更高,自信的错误答案更少。
但为了理解这如何可能,我们首先需要了解:智能体究竟是如何搜索代码的?
智能体如何搜索代码(剧透:是文本搜索)
你的编码智能体会频繁搜索你的代码库。例如,想要重命名一个函数?Claude Code、Codex、OpenCode 等都会搜索你的代码,找到该函数所有可能需要更新的引用。
https://modem.dev/videos/how-coding-agents-claude-code-search.mp4
Claude Code 会话界面,智能体正在搜索代码库。
在底层,大多数编码智能体只是在运行 grep——或者更确切地说,是其更快的变体 ripgrep,即 rg——来遍历你的代码并找到匹配的文本引用。智能体读取返回的结果,如果上下文不够,它会重复扫描代码直到获得所需内容。
$ rg -l 'formatDuration' --type ts
packages/common/src/duration.ts
packages/common/src/duration.test.ts
apps/dashboard/src/lib/utils/dates.ts
apps/slack/src/lib/rate-limit.ts
...
这是 Claude Code(使用 Fable 5)在 Modem 代码库中查找 formatDuration 引用的真实例子。
在文件中查找文本字符串并不是智能体唯一的搜索方式:它们也会按文件名搜索。例如,如果你问编码智能体“会话代理是如何工作的?”,智能体可能会扫描你的代码库查找类似的内容:
$ rg --files | rg -i 'session-broker|sessionBroker|session_broker'
apps/api/src/session-broker/index.ts
apps/api/src/session-broker/handler.ts
apps/api/src/session-broker/handler.test.ts
所以路径也是搜索词。一个名为 session-broker/ 的目录在智能体读取一行代码之前就已经被命中。
注意,所有这些都只是字符串。没有编译器向智能体提供依赖图,也没有语言服务器解析符号(通常如此,更多细节见下文)。Claude Code 团队早期尝试过嵌入和向量数据库,但抛弃了它们,因为纯文本搜索效果更好。学术界的智能体也得出了相同结论;SWE-agent 通过给模型提供专门构建的关键词搜索工具(没有更花哨的东西)取得了成果。

智能体导航循环:使用 rg 搜索,读取最佳命中周围的内容,如果需要则用更好的查询循环,然后开始编辑。
即使是前沿模型,在拥有合适工具的情况下,也通常使用本质上相当于 grep 的方式来导航你的代码。注意,这个循环中有一个最重要的输入是由代码作者控制的:代码中的单词和文件上的名称是否能成为好的搜索词。
语言服务器(LSP)呢? 编辑器多年前就通过语言服务器协议解决了代码导航问题,编码智能体可以使用 LSP 服务器来解析代码符号(有些原生支持,有些通过 MCP)。但这需要在运行智能体的每个地方进行额外配置:每种语言一个 LSP 服务器,在每台机器和每个检出上。而且即使你配置了,结果是否更好也不清楚。
grep/rg是大多数编码智能体开箱即用的工具,这也是本博客文章的重点。
并非所有代码的发现性都相同
让我们考虑三个假设的函数。它们都做同样的事情:创建一个向外部服务(例如 Stripe)发送 HTTP 请求的 API 客户端对象。唯一的区别是它们的命名方式:
export function create(apiKey: string) {
// ...
}
export function createClient(apiKey: string) {
// ...
}
export function createStripeClient(apiKey: string) {
// ...
}
下面是这些函数在代码库其他地方可能被调用的方式:
import { create } from '../api.ts';
const client = create(...);
client.getUsers(...);
// const client = createClient();
// const client = createStripeClient();
假设你正在使用智能体处理这段代码。你想修改 API 客户端,比如添加一个调用新 HTTP API 端点的方法。智能体会自然地尝试发现这个对象被使用的所有位置。如我们之前所了解的,它会使用 grep/ripgrep。
这是在我们的单体仓库(约 2900 个 TypeScript 文件)中,grep 对这些字符串返回的匹配数量,以及使用 ripgrep (rg) 在 M5 级 MacBook 上每次搜索所需的时间。
| 搜索词 | 匹配行数 | 文件数 | 时间 |
|---|---|---|---|
create |
1,585 | 459 | ~50ms |
createClient |
466 | 23 | ~47ms |
createStripeClient |
43 | 19 | ~47ms |
create 的 1585 个结果跨越了数百个不相关的东西:测试夹具、数据库插入、工作流注册。createStripeClient 的 43 个结果是定义、调用点和测试——全都关于同一个客户端。
在你的仓库中尝试 运行 ripgrep 搜索你的代码库中的符号:
rg -w "yourFunctionName" --type ts -c | wc -l
你可能也注意到 ripgrep 很快。搜索 create(50ms)和 createStripeClient(47ms)之间没有明显的时间差异。但问题不在这里。问题在于这些结果如何影响你的智能体的上下文。
代码发现性差会消耗上下文
Grep 返回的是匹配行,而不是答案。像 const client = create(config) 这样的结果并不能告诉智能体这是你关心的客户端还是数百个其他名为 create 的东西之一。为了弄清楚,智能体读取命中行周围的内容,并且经常展开或打开额外的部分。这可能加起来从几十行到数百行甚至数千行代码。
$ rg -c 'create' --type ts | wc -l # 459 个文件包含匹配
459
## 智能体打开看似相关的文件并完整读取每个文件:
## apps/ingest/src/jobs/register.ts (~220 行) → 工作流注册,不是它
## packages/database/src/seed.ts (~110 行) → 数据库插入,不是它
## apps/dashboard/src/hooks/use-form.ts (~70 行) → 表单工厂,不是它
## ...更多文件,直到找到真正的那个
以每行大约十个 token 计算,每个文件读取需要数百到数千个 token 来排除一个错误的候选者。折腾十几个这样的文件,你就会在编写实际任务的一行代码之前,消耗数万 token 来分离信号和噪音。
而搜索特定名称则跳过了筛选:
$ rg -l 'createStripeClient' --type ts
apps/payments/src/stripe-client.ts
apps/payments/src/checkout.ts
apps/payments/src/stripe-client.test.ts
## 只需要读取 3 个文件,而不是 459 个
三个文件,全都关于同一个客户端。而且节省是累积的。上下文是有限的,所以每花费一个 token 来排除 create 测试夹具,智能体就少了一个 token 用于你的更改。更糟的是,当它的上下文充满了近似匹配时,模型在使用其中实际内容时会变得明显不可靠。一个能一次 grep 解析的名称能将智能体的注意力和预算都集中在任务上。
名称的影响不止于检索 研究人员已经证明,仅通过重命名标识符(不修改逻辑),就可以改变代码模型的预测结果。另一个基准测试发现,误导性的名称会导致模型在代码推理任务上的准确率下降约 23 个百分点,而思维链推理只能恢复部分损失。 模型信任代码所声称的功能。人类也是如此(一项旧研究发现,当标识符使用完整单词时,开发人员定位缺陷的速度要快 19%),但一个人最终会对代码库中的谎言产生免疫力。而智能体每次会话都是全新的,会再次相信它们。
模块无法拯救通用名称
到目前为止,我们一直在谈论纯符号查找。但导入和模块呢?
让我们回到之前的 client TypeScript 示例:
import { create } from '../api.ts';
const client = create(...);
client.getUsers(...);
在这个文件的顶部,有一个相当清晰的线索指向 client 函数定义的位置。当我们可以直接查找文件时,为什么还要 grep 整个代码库呢?
如果目标严格来说是找到定义,那么你是对的。导入是一个前向指针:给定调用点,它告诉你定义在哪里。
但是开启这篇文章的任务是一次重命名——这是反向方向:给定定义,找到所有调用者——而纯文本搜索不提供反向索引。api.ts 中没有任何东西记录谁导入了它。(有些语言在这方面做得更好;更多细节见下文。)
反向符号查找
假设 create 有三个调用点,每个都使用普通的导入风格:
// 相对路径
import { create } from '../../../packages/common/src/api';
// 包说明符,通过 barrel 文件
import { create } from '@modem/common';
// 重命名导入
import { create as createApiClient } from '@modem/common';
现在尝试通过文本来找到它们:
$ rg -l "from '.*api'" # 3 个中的 1 个——包说明符不包含路径
$ rg -n '\bcreate\(' # 3 个中的 2 个——未匹配重命名,外加所有不相关的 create()
以上两个查询都无法干净地找到所有三个——除非名称足够独特,使得 grep 只返回实际用法。导入图存在于你的代码中,但它是不可 grep 的逆向查找。对于以 grep 为首要搜索手段的智能体来说,一个独特的符号名称是最可移植的反向查找。
Barrel 文件和包说明符
即使是前向方向——从调用点到定义——在单体仓库中也会退化。文件顶部的导入说 create 来自 @modem/common。但 @modem/common 不是一个路径;它是一个包说明符。要跟踪它,智能体首先必须找到该包的位置,这意味着打开 package.json 或 grep 包名。
这会让它落到 barrel 文件上:
// packages/common/src/index.ts
export * from './api';
export * from './auth';
export * from './duration';
export * from './validation';
这些重新导出中哪个有 create?Barrel 文件没有说明。export * 抹掉了它转发的名称,所以智能体又回到了在包内部 grep 来寻找实际定义。这和不开始就已经可以进行的搜索是一样的,只不过现在多了 3 步。
对象上的方法
方法从来就没有导入。看看我们一直在处理的调用:
const client = create(...);
client.getUsers(...);
在 getUsers 的任何地方都没有导入语句;导入处理的是模块及其顶层导出,而方法两者都不是。一个以 grep 为首要搜索手段的智能体找到 getUsers 定义在哪、或者谁调用了它的唯一方式就是搜索这个名称。与上面两个失败案例不同,这并非 TypeScript 的过错:client.getUsers() 在 Python、Go、Ruby 和 Java 中留下的是同样不存在的线索。没有语义工具,方法名通常是智能体拥有的最好文本Handle。
关于 grep 与依赖图的研究 你可能认为解析过的依赖图会胜过简单的文本搜索。但一项近期研究发现并非如此。GrepRAG(2026 年 1 月)在仓库级代码补全基准测试中,将 LLM 驱动的 ripgrep 与基于真实导入图的检索系统(如 GraphCoder)进行了比较。Grep 与之相当甚至更优。在 grep 确实失败的地方,论文的失败分析指向了通用名称:搜索像
init这样的词会返回数百个包含该关键词但不含答案的匹配。听起来熟悉吗?
模块只指向一个方向,在 barrel 文件和包说明符后面变得模糊,并且对你的对象上的方法只字不提。名称仍然承担着繁重的工作。
类型强制更好的发现性
名称帮助智能体找到你的代码;类型告诉它如何使用这段代码。而且不像注释或 README,类型是强制性的。
换句话说:智能体可能跳过你的文档。但当它运行编译器时,类型错误提供了快速、具体的反馈。无效的调用会产生错误,智能体可以利用这个错误来纠正它的方法——通常在一轮交互内。
不过,在编译器捕获任何东西之前,类型已经在为自己赚取价值了,因为一个精确的签名通常可以回答智能体的第一个问题,而不需要它读取实现。考虑这个例子:
function enrichUser(user: User): EnrichedUser
智能体在 grep 结果中看到这个,然后继续前进。输入什么(User),输出什么(EnrichedUser)。取决于提出的问题,可能没有理由打开函数体。现在是没有类型的版本:
function enrichUser(data) {
// 或
function enrichUser(data: any) {
要了解 data 是什么,智能体很可能必须读取实现。如果实现将 data 传递给其他东西,它也会读取那个,可能还有一两个调用者。一个缺失的注解可能把一次性读取变成多次文件读取。
注意 TypeScript 的 any 在 看起来 像类型的同时造成了同样的损害;每个 any 都增加了智能体需要读取实现的可能性。
类型质量很重要
所以仅仅类型的出现就可以减少智能体的读取。但类型的描述性也很重要。
考虑这个函数:
function transferOwnership(userId: string, orgId: string, projectId: string) {
// ...
}
// 意外的参数交换
transferOwnership(orgId, projectId, userId);
由于每个参数的类型都是 string,编译器不会捕获意外的参数交换。但如果这些 ID 有独特的类型(TypeScript 中的品牌类型,Rust 中的 newtype)呢?
function transferOwnership(userId: UserId, orgId: OrgId, projectId: ProjectId) {
// ...
}
现在参数交换变成了构建错误:
类型 `OrgId` 的参数不能赋值给类型 `UserId` 的参数。
智能体遇到这个错误,修复它,然后继续前进,通常在一轮交互内。研究人员已经观察到了这个循环的工作:一项关于编译器反馈的研究看到模型在经过几次错误和重试轮次后,成功率大约提高了两倍。
这正是它和上面所有内容联系起来的地方。那个错误命名了一个类型——OrgId——而 OrgId 几乎肯定在另一个文件中定义。为了处理这个错误,智能体必须去读取那个定义,它通过它知道的唯一方式到达那里:搜索这个名称。
类型名就像函数名一样是一个搜索词,并且遵循相同的规则。OrgId 或 UserEnrichmentResult 会 grep 到一个定义;而 Result、Data、Config 则会把智能体丢回之前的多结果大海捞针中。编译器告诉它去哪里看;名称决定了寻找需要多长时间。
这就是反对 any 的完整理由:编译器无话可说,智能体也无处可搜。一个坏名字至少还留下一条线索。
独特性的最佳点在哪? 我们在 Modem 的 TypeScript 仓库中测量了这个——7922 个导出的名称,检查 grep 每个名称是否落在一个单一定义上:
单词数 示例(说明性) 唯一的概率 1 get████████████░░░░░░░░ 61% 2 getUser██████████████████░░ 88% 3 getUserProfile███████████████████░ 96% 4+ getUserProfileById████████████████████ 98% 三个单词大致是名称不再模糊、开始充当地址的临界点。给导出的东西用 2 到 3 个单词,其中一个可以是领域词——例如
diffUserObjects,而不是diff。 注意这些阈值来自一个扁平的 TypeScript 导出命名空间。在像 Go 这样的限定调用语言中,包名算作一个单词——例如stripe.NewClient已经是一个三词 token。
同一思想的更小应用
至此,模式已经很清楚了:让你代码中的单词成为好的搜索词。剩下的大部分是同一个思想在更小地方的应用。
将注释放在定义上。智能体通过搜索一个名称到达你的代码,而搜索会解析到该名称定义的地方——所以定义是你唯一可以指望它读取的位置。这使得紧接在定义上方的一行注释成为你能编写的最具成本效益的文档:它正好位于搜索落地的位置。写一句话说明代码本身无法表达的事情。
每个概念选择一种拼写。尝试为不同的概念唯一命名。例如,不要在一个地方叫它 organizations,在另一个地方叫它 customers——即使使用本地导入别名。代码库中的同义词也可能导致混淆(例如 orgId 和 organizationId),但智能体似乎更成功地导航它们。
根据测试覆盖的源文件来命名测试。我们有意识地根据测试覆盖的代码来命名测试文件(例如 stripe.test.ts 测试 stripe.ts)。我们观察到,如果没有明显的名称,智能体会花额外的轮次来找到匹配。
标记遗留路径为 @deprecated。如果你因为某种原因保留遗留代码,确保将其标记为已弃用(例如使用 TypeScript 的 @deprecated 标签)。否则智能体会发现并使用你不希望它们使用的代码。更好的是,努力完全移除这段代码。
编写约定文件。你可能已经有了 AGENTS.md 或 CLAUDE.md。确保记录任何不明显的内容,比如命名约定、源代码位置等。这是确保智能体有效导航你代码的最高杠杆习惯。
这一切只是感觉吗?让我们看数据
到目前为止,我们已经在理论上进行了论证。但它在实践中是否产生影响呢?
为了尝试回答这个问题,我们让 Fable 5 将这篇博客文章提炼成一个 write-discoverable-code 技能文件——一组单页规则,当智能体编写代码时加载到其上下文中。然后我们在一组孤立的实验中,比较了其他智能体如何检索和推理使用和不使用这些规则产生的代码。
在我们深入结果之前,有一个注意事项:这出奇地难以干净地测试。智能体导航代码库的难易程度取决于代码本身、任务、模型和框架,以及其他许多因素。此外,这篇文章中的建议(捕获在技能文件中)同时改变了多个方面:名称、文件、类型、结构等。变量太多了。
这些并不是决定性的基准测试。它们是更小、更有范围的实验,问一个更简单的问题:当任务和周围仓库大致保持不变时,为发现性编写的代码是否更容易被搜索驱动的智能体导航?
实验 1:生成库(TypeScript)
这个实验的目标是看看是否使用我们的技能文件生成的、全新的智能体代码能带来更好的智能体检索结果,与不使用相比。
两个作者智能体分别编写两次库——一次没有辅助,一次带有技能文件——产生四个代码变体。每个变体都被埋藏在同一个 500 文件的仓库中。十四个不同的读取器配置随后搜索并读取这些仓库;读取器从未收到技能文件。

两个作者智能体生成四个代码变体;十四个独立的读取器配置随后搜索每个版本。读取器从未看到技能文件。
为此,我们从一个小型通知管道库的中性规范开始,包含重试、速率限制、负载签名等特性。然后我们将其交给两个不同的作者智能体:Haiku 4.5(在 Claude Code 中)和 GPT-5.6 Sol(在 Codex CLI 中)。每个作者生成库两次:一次没有辅助,一次加载了 write-discoverable-code.md 技能。这给了我们同一个库的四个版本:
- Haiku 4.5(Claude Code)- 无辅助
- Haiku 4.5(Claude Code)- 加载技能文件
- GPT-5.6 Sol(Codex CLI)- 无辅助
- GPT-5.6 Sol(Codex CLI)- 加载技能文件
我们选择 Haiku 4.5 作为一个故意较小的作者模型,它的无辅助版本正好产生了本文警告的那类代码。加载技能后,差异立即在名称中显现:
| Haiku 4.5 - 无辅助 | Haiku 4.5 - 技能辅助 |
|---|---|
signer.ts |
hmac-payload-signer.ts |
queue.ts |
channel-router.ts |
validateConfig() |
validateNotificationDeliveryConfig() |
另一方面,GPT-5.6 Sol 的无辅助代码已经为发现性而塑造:calculateBackoffDelay 在 backoff.ts 中,TokenBucket 在 token-bucket.ts 中。在这次运行中,Sol 在没有额外指导的情况下就产生了具体、可发现的名称。
加载技能后,Sol 更进一步——例如 backoff.ts 变成了 notification-retry-backoff.ts——但它的两个版本之间的差距明显比 Haiku 窄。(就个人品味而言,我觉得这段代码可能过于详细了。)
接下来,我们测试了其他智能体如何读取四个库版本。我们将每个版本埋藏在同一个 500 个文件的真实代码(来自我们的单体仓库)中(为了看到在我们现有代码中的假设影响)。然后我们启动全新的智能体 CLI 会话,限制为搜索和读取工具,并提问如:
下一次重试尝试之前的等待时间在哪里计算?
在交付之前,计算的 HMAC 签名在哪里附加到传出负载?
在哪里可以查找给定通知 ID 的所有过去交付尝试?
...
十四个模型/框架组合针对每个版本回答 10 个问题三次:总共 1680 次运行。每次运行,我们记录 token 消耗以及答案是否正确。
作者 1:Haiku 4.5(Claude Code)
| 模型 | 无辅助 → 技能辅助 的 token/问题 | 变化 | 错误答案 |
|---|---|---|---|
| DeepSeek v4 pro · OpenCode | 70,336→24,142 | ████████████████████−65.7% | 4→0 |
| GLM 5.2 · OpenCode | 42,657→17,526 | ██████████████████░░−58.9% | 1→0 |
| Composer 2.5 · Cursor CLI | 164,952→89,125 | ██████████████░░░░░░−46.0% | 0→0 |
| GPT-5.6 Sol · pi | 49,684→28,218 | █████████████░░░░░░░−43.2% | 0→0 |
| Grok 4.5 · OpenCode | 33,008→19,431 | ████████████░░░░░░░░−41.1% | 0→0 |
| Haiku 4.5 · Claude Code | 153,299→102,215 | ██████████░░░░░░░░░░−33.3% | 0→0 |
| GPT-5.6 Sol · Codex CLI | 78,346→52,816 | ██████████░░░░░░░░░░−32.6% | 0→0 |
| GPT-5.6 Luna · Codex CLI | 82,429→57,051 | █████████░░░░░░░░░░░−30.8% | 1→0 |
| GPT-5.6 Luna · pi | 44,718→35,656 | ██████░░░░░░░░░░░░░░−20.3% | 0→0 |
| Sonnet 5 · Claude Code | 92,052→76,365 | █████░░░░░░░░░░░░░░░−17.0% | 1→0 |
| GPT-5.6 Terra · pi | 11,692→9,725 | █████░░░░░░░░░░░░░░░−16.8% | 0→0 |
| Fable 5 · Claude Code | 113,941→97,779 | ████░░░░░░░░░░░░░░░░−14.2% | 0→0 |
| GPT-5.6 Terra · Codex CLI | 55,939→51,275 | ██░░░░░░░░░░░░░░░░░░−8.3% | 0→0 |
| Opus 4.8 · Claude Code | 98,216→92,121 | ██░░░░░░░░░░░░░░░░░░−6.2% | 1→0 |
表 1:同一个库,由 Haiku 4.5 / Claude Code 在有无技能情况下编写。平均每个问题的 token,10 个问题 × 3 次运行,在 500 文件仓库中。
作者 2:GPT-5.6 Sol(Codex CLI)
| 模型 | 无辅助 → 技能辅助 的 token/问题 | 变化 | 错误答案 |
|---|---|---|---|
| Composer 2.5 · Cursor CLI | 166,449→90,555 | ██████████████░░░░░░−45.6% | 0→0 |
| GPT-5.6 Sol · pi | 45,398→28,052 | ████████████░░░░░░░░−38.2% | 0→0 |
| Haiku 4.5 · Claude Code | 148,395→111,215 | ████████░░░░░░░░░░░░−25.1% | 0→0 |
| GLM 5.2 · OpenCode | 20,055→16,455 | ██████░░░░░░░░░░░░░░−18.0% | 0→0 |
| Grok 4.5 · OpenCode | 23,966→19,825 | █████░░░░░░░░░░░░░░░−17.3% | 0→0 |
| Sonnet 5 · Claude Code | 138,981→118,797 | ████░░░░░░░░░░░░░░░░−14.5% | 0→0 |
| GPT-5.6 Sol · Codex CLI | 61,276→52,996 | ████░░░░░░░░░░░░░░░░−13.5% | 0→0 |
| GPT-5.6 Terra · Codex CLI | 58,975→52,932 | ███░░░░░░░░░░░░░░░░░−10.2% | 0→0 |
| GPT-5.6 Luna · Codex CLI | 60,901→55,637 | ███░░░░░░░░░░░░░░░░░−8.6% | 0→0 |
| Fable 5 · Claude Code | 110,109→100,636 | ███░░░░░░░░░░░░░░░░░−8.6% | 0→0 |
| GPT-5.6 Terra · pi | 11,694→10,767 | ██░░░░░░░░░░░░░░░░░░−7.9% | 0→0 |
| Opus 4.8 · Claude Code | 103,125→99,426 | █░░░░░░░░░░░░░░░░░░░−3.6% | 0→0 |
| DeepSeek v4 pro · OpenCode | 23,638→24,225 | █░░░░░░░░░░░░░░░░░░░+2.5% | 0→0 |
| GPT-5.6 Luna · pi | 37,263→40,379 | ███░░░░░░░░░░░░░░░░░+8.4% | 0→0 |
表 2:相同实验,由 GPT-5.6 Sol / Codex CLI 编写。更好的无辅助代码留下了更少的节省空间;技能仍然对几乎所有人有帮助。
一起阅读这些表格,在无辅助作者生成了更通用代码的情况下,效果更大。在这些运行中,Haiku 编写代码的中位数减少是 32%,Sol 编写代码是 12%。十四个 Sol 读取器配置中有十二个得到了改进;两个没有。在这些运行中,当作者留下更多改进空间时,技能起到了下限的作用,尽管实验没有将命名与技能引入的其他变化区分开。
读取器行为也很重要。有一行值得单独提一下:Sol 在读取它自己的无辅助代码时,当该代码在技能下编写时,仍然便宜了 13% 到 38%(取决于驱动它的框架)。编写代码的模型并没有特权记忆东西在哪。它和其他人运行同样的搜索。
同时,错误答案结果只出现在一个表中。在 Haiku 的通用代码上,无辅助会话返回了自信的错误答案——当被问到通知库的交付历史时,一个智能体指向了仓库中其他地方的 Slack 交付账本;被问到 HMAC 签名在哪里附加时,两个指向了我们的媒体代理的 URL 签名器。技能命名的代码产生了零个错误答案,因为近似匹配的代码不能穿上像 hmac-payload-signer.ts 这样的名称。在 Sol 的代码上,这种失败模式在两个版本中基本上消失了(840 次运行中 834 次正确,错误是偶发的,分布在两个版本中)。在这些运行中,自信的错误答案集中在 Haiku 的通用名称版本中。
实验 2:PewDiePie 的 Odysseus 仓库(Python)
合成代码库只能展示这么多,所以我们还用了一个困难的实际案例来测试这个想法:Odysseus 异常庞大的电子邮件子系统。Odysseus 是一个自托管的 AI 工作空间,在发布第一周就获得了数万个 GitHub star——对于一个自学成才的编码者来说,这是一个令人印象深刻的作品——而 routes/email_routes.py(在 GitHub 上查看)有 4,943 行,其中约 3,900 行是一个包含所有 81 个路由处理程序作为嵌套闭包的单一函数。一个函数,整个电子邮件客户端。
Odysseus,一个自托管的 AI 工作空间,发布第一周就积累了数万个 GitHub star。
我们使用 Claude Code + Sonnet 5 在技能指导下重构了仅仅一个文件(email_routes.py)——相同的行为,相同的路由,经过机械化验证——重新组织成按概念命名的模块:

然后向一组混合的智能体/框架询问了关于两个版本的相同六个问题:
“代码在哪里检查 SMTP 设置是否足以发送外发邮件?”
“在编写外发邮件时,Markdown 在哪里被转换为 HTML?”
“当用户偏好的邮件文件夹在 IMAP 服务器上不存在时,代码在哪里选择一个后备文件夹?”
...
以下是每个模型/智能体框架的表现。(这次模型/框架组合较少,因为我们没有无限的积分。)
| 模型 | 单体→重构的 token/问题 | 变化 |
|---|---|---|
| Haiku 4.5 · Claude Code | 290,324→190,811 | ██████████░░░░░░░░░░−34.3% |
| Grok 4.5 · OpenCode | 25,600→17,535 | ██████████░░░░░░░░░░−31.5% |
| GPT-5.6 Sol · Codex CLI | 72,072→67,843 | ██░░░░░░░░░░░░░░░░░░−5.9% |
| Sonnet 5 · Claude Code | 198,291→190,569 | █░░░░░░░░░░░░░░░░░░░−3.9% |
一次重构,由遵循技能的智能体生成一次;每个模型读取相同的两个代码库。每个问题平均 token,每个 3 次运行。同一行内的 token 数量可比较,不同框架间不可比较。
生成库表格中的模式再次出现:在单体中迷失的模型节省最多。Haiku 一直在 400,000 token 的会话中浏览一堵代码墙;重构后,它停止了。Grok 节省了三分之一。Sol 和 Sonnet 从未真正迷失,大致持平。
追踪表明文件名是一个主要贡献因素,尽管重构也改变了文件大小和模块边界。当智能体正在寻找电子邮件文件夹逻辑,而它存在于一个合理命名的文件如 email_folders.py 中时,智能体无需打开其他任何东西就能找到它。
但重构更大的影响在于最坏情况。几乎所有的灾难性运行——例如一次 400k token 的搜索最终没有答案——都发生在原始单体中。之所以说“几乎”,是因为 Haiku 在重构版本中也产生了一次,而位置很能说明问题。我们的实验只重构了一个文件,而 Haiku 在一个相关但未修改的 1,800 行 email_helpers.py 大杂烩中迷失了。重构提高了下限,新的下限变成了剩余的最差文件。
Grep 找不到不存在的代码 本实验中的第六个问题故意没有答案。它问的是收到的电子邮件 HTML 在哪里被清理,而这个功能根本不存在。这个问题在重构后变得更贵了——一个组织良好的代码库会不断提供看似合理的地方供下一步搜索,所以智能体会花掉全部预算才得出“这不存在”的结论。Grep 能找到代码;它找不到代码的缺失。如果你的代码库有意不做读者期望的事情,请把它写在他们会去寻找的地方。
实验 2.5:Odysseus 缺陷搜寻
最后一个测试:可读性是否改变了智能体能正确发现的事情,而不仅仅是他们花费了什么?我们在两个版本的电子邮件代码中植入了四个细微的缺陷——逐字节相同的编辑,所以每个版本包含完全相同的缺陷——然后要求四个模型在固定的八轮审查预算下验证相关需求。每个模型审查每个缺陷两次。
█ 捕获了植入的缺陷 █ 标记了错误的代码 █ 预算耗尽——无判断

相同的四个植入缺陷,相同的八轮预算。每个方块是一次审查。在单体中,Haiku 和 Sonnet 失败的审查大多是预算耗尽而没有结论;在重组代码中,它们抓住了全部八个。
我们测试的最新型号和最先进的前沿模型,Grok 4.5 和 GPT-5.6 Sol,在两个版本中都抓住了每个缺陷。八轮对于一位训练有素的搜索者来说,足以 grep 到任何一个缺陷,即使在 4,943 行的文件内部。
但 Haiku 和 Sonnet 无法完成同样的壮举:它们分别抓住了 4/8 和 3/8,而失败很少是错误答案——它们是审查耗尽了整个预算,在文件中翻页,从未达到任何结论。在重组代码中,两者都完美地达到了 8/8。
Sonnet 行也值得注意:在之前的 Odysseus 检索测试中,它几乎_没有_从重构中受益。而在审查预算下,它的准确性仍然从 3/8 提高到了 8/8。能够在单体中负担得起查找,并不等同于能够在其中得出结论。
在这些实验中,更容易的检索留下了更多预算让智能体做实际的工作。这表现为更便宜的查找、更少的错误答案,以及在轮次限制之前捕获更多缺陷。
那么,我应该直接使用这个技能文件吗? 这些实验是说明性的,而非决定性的:一个生成的库,一次实际的重构,以及一组有限的模型和框架。 我们确实认为它展示了这篇博客文章中的概念有实际价值。但你的具体情况可能会有所不同,我们不相信答案是盲目地向你的智能体扔一个代码重写技能文件并期望最好。 我们相信代码需要为人类和智能体工作,并且你需要为你、你的团队和你的代码做出正确的权衡。我们鼓励你尝试本文中的一些想法,看看它们是否对你有用。注意,我们的许多测试涉及我们的 Modem 代码仓库——我们想知道这些东西是否对我们有效,而不仅仅是在实验室中。
总结:是的,代码很重要
回顾一下:智能体通过字符串搜索导航你的代码库,它们信任你的名称告诉它们的信息,编译器是它们无法忽视的一个审查者,而它们首先读什么很大程度上由搜索落地的位置决定。当我们在一系列实验中对此进行测试时,结果站得住脚。
在我们使用较弱代码作者(Haiku 4.5)的合成生成测试中,所有十四个模型/框架组合读取技能编写的代码都使用了更少的 token——在 6% 到 66% 之间——自信的错误答案消失了。在更强作者(GPT-5.6 Sol)的代码上,14 个模型/框架组合中有 12 个表现更好,剩余两个没有改善。
在我们涉及现有“vibe 编码”生成仓库(Odysseus)的实验中,相同的重构为那些正在迷失的模型削减了大约三分之一的 token 消耗——而对那些没有迷失的模型则大致持平。在缺陷搜寻中,审查结果从单体中的 23/32 正确提升到重构中的 32/32,两个较弱模型从 7/16 提升到完美的 16/16,因为它们将预算花在了判断而不是寻找上。
我们不想过度推销这些数据。我们的仓库测量是观察性的,而受控实验只是一个规范加上几千次运行。框架也会继续演变——Cursor 已经发表了可观的数字表明嵌入在 grep 之上有帮助,所以纯文本搜索可能不是终点。但 grep、仓库映射和嵌入都依赖于相同的输入:你源文件中的单词。更好的名称能改善所有这一切。这在这个领域算是相当安全的赌注了。
有趣的是,这些建议几乎都不是新的。描述性的名称、精确的类型、解释为什么的注释——几十年来我们一直在互相告诉对方要做这些事,并且在我们没做的时候大多原谅自己,因为一个人总能问团队成员或深挖 git blame。但智能体不能。它只有你的文本、你的类型和一个搜索框。为那个读者写作,代码对所有人来说都会变得更好。
敬请期待《为智能体编写代码》第二篇:确定性的力量。
- 原文链接: modem.dev/blog/how-codin...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

