让 Agent 修一个函数,它能径直扎进你五年没敢动的祖传模块;让它排查一个偶发 bug,它 grep 两下就锁定病灶。这类体验的背后是同一个问题:**百万行代码库里,这一轮该让模型看到哪几百行?**代码是上下文工程最苛刻的场景——内容量级远超窗口,相关性判断千头万绪,还多出一个自然语料没有的变量:代码有语法结构。
这是《上下文工程》系列第 12 篇。预算(第 4 篇)、检索(第 10 篇)、隔离(第 9 篇)这些原则在代码场景全部生效,而 Claude Code 把它们捏合成了一件开发者每天在用的工具。本文先算难题的账,再拆开 Claude Code 的策略地图,然后是检索式供给的 token 数学、代码地图模式、增量上下文维护、与 IDE 补全的对比,最后给一套可直接落地的实战流程。
1、代码上下文的独特难题
代码上下文难在三件事叠在一起。
**量级。**一行代码平均折合 8~12 个 token,200k token 的窗口折算下来约 2 万行。一个 30 万行的中型项目要 15 个满窗口才装得下,近 3000 万行的 Linux 内核需要 1400 个。结论只有一个:任何"把代码库装进上下文"的念头都不成立,能做的只有让模型按需看到正确的局部。
**相关性。**哪些文件与"修复登录偶发超时"相关?直觉答案是文件名里带 login 的——然后你会发现 handler.go 这类名字在仓库里有几十个。Sourcegraph 的上下文工程指南对代码场景下朴素 RAG(Retrieval-Augmented Generation,检索增强生成)的判断很直接:切块会把调用点和定义切开,常见文件名制造噪声,向量相似度分不清一个函数的定义和它的测试 mock——三刀都砍在要害上。
**结构。**代码与自然语言最大的不同:它有确定性的语法结构。抽象语法树(AST)里定义在哪、谁引用了它、调用链怎么走,这些不是"语义相近"的问题,是编译器能精确回答的问题。Sourcegraph 给出的组合是:有结构的地方用编译器背书的符号导航(跳定义、查引用),没结构的地方(注释、文档、commit message)才交给语义检索。他们的 CodeScaleBench 基准(370 个任务、40 多个仓库、9 种语言)还给出一个规模阈值:只靠本地 grep/glob/read 的 Agent 在仓库超过约 40 万行后开始力不从心,结构化工具的收益在那个区间才拉开。
2、Claude Code 的策略地图
把 Claude Code 每轮实际进入上下文的东西摊开,是五层结构:
任务:修复登录接口偶发超时
│
├─ 常驻层 每轮都在、字节稳定、整段吃缓存
│ system prompt + CLAUDE.md(命令、规范、踩坑记录)
│
├─ 按需层 用到才进窗口
│ Glob/Grep 先定位 → Read 局部读取(分页 + 行号)
│ @文件路径 手动引用 → 整文件一次性注入
│
├─ 治理层 进了窗口也要瘦身
│ Read 分页截断 · Bash 输出限长 · Edit 只回显变更块
│
├─ 隔离层 大探索不进主窗口
│ Task 派子代理独立探索,只回摘要
│
└─ 兜底层 窗口快满时
/compact 手动压缩 + 自动压缩兜底
逐层看设计意图。
**常驻层小而精是刻意的。**CLAUDE.md 里每一行都是"税"——每一轮对话都为它付 token,还占注意力。官方记忆文档因此强调它放高频、稳定、全局的信息(构建命令、代码规范、已知坑),而不是架构文档全文。想临时注入某个完整文件,用 @文件路径 引用,这是使用者手里的手动按需阀门。
按需层的顺序有讲究:先定位后读取。Grep 默认回命中文件列表或行级片段,几百 token;Read 才把正文拉进窗口,还带分页与行号。连工具本身也是按需的——MCP(Model Context Protocol,模型上下文协议)服务器的完整工具定义与 skill 说明,都是用到时才加载。治理层与隔离层分别对应本系列第 8 篇(工具结果治理)与第 9 篇(子代理隔离),机制相同不再展开;/compact 就是第 5 篇的 compaction。官方把这套机制做成了交互式窗口模拟,能看到一次会话里各层怎么把窗口填满。
3、检索式供给的 token 账
用同一任务算两种供给方式的账。病灶在 RetryInterceptor.kt 的一个 40 行函数加 20 行配置,真正需要的上下文约 600 token。
方案 A,凭直觉塞文件:挑 6 个"感觉相关"的文件整个读入,每个 500 行,共 3000 行、约 3 万 token 进窗口,其中与病灶相关的不足 2%。
方案 B,检索式供给:Grep 关键词 timeout 回 3 个命中文件约 300 token;Read 其中 2 个文件的相关段落 120 行约 1200 token。合计约 1500 token,相关内容占比四成。
差距是 20 倍 token,但真正贵的不是钱。本系列第 2 篇讲过上下文腐烂:无关 token 稀释模型对关键信息的注意力,Sourcegraph 的结论更狠——无关上下文让输出变差而不是变慢。方案 A 那 98% 的无关代码,每一行都是错误关联的素材:模型可能把另一个文件里的降级逻辑错牵进这次修复。
检索式供给对代码格外有效,有个自然语言没有的原因:代码里的标识符是全局唯一锚点。函数名、类名、错误字符串、配置键——grep 一个精确标识符的命中率天然远高于 grep 一段自然语言描述。CodeScaleBench 里有个扎眼的数据:即便提供了语义检索工具,Agent 仍做了 4813 次关键词搜索、只有 587 次自然语言查询——模型自己也在用脚投票。
grep 的盲区是语义问题:"用户会话过期在哪处理"没有任何稳定标识符可 grep。这正是下一节代码地图要补的洞。
4、代码地图模式
思路:与其让模型每轮从零摸索仓库结构,不如预先给它一张地图——目录树加每个文件一句话职责,一两千 token,常驻或按需引用。模型的探索从"盲 glob"变成"按图索骥",第一跳就落对目录。
地图放哪要按仓库大小算账。一两百个文件的小仓库,地图直接进 CLAUDE.md 当常驻——它的定位收益大于 token 税;更大的仓库把地图存成独立文件,CLAUDE.md 只留一行指针,模型需要时再 @引用,税就变成了按需支出。
生成地图的骨架脚本只做机械工作:走目录树、过滤噪声目录、尝试取每个文件的首行注释。代码如下:
import os, re
SKIP = {".git", "node_modules", "build", "dist", "__pycache__"}
FIRST = re.compile(r'^\s*(?:#|//|/\*|<!--|\*)\s*(.{6,60})$')
CODE_EXT = (".ts", ".tsx", ".py", ".kt", ".go", ".rs", ".java")
def describe(path):
"""首行注释当一句话职责,取不到留空"""
with open(path, encoding="utf-8", errors="ignore") as f:
for line in f:
m = FIRST.match(line)
if m:
return m.group(1).rstrip("*/ ")
if line.strip():
return "" # 首个非注释行出现即放弃
return ""
def skeleton(root, max_depth=3): # max_depth 层目录树
lines = []
for dirpath, dirs, files in os.walk(root):
dirs[:] = sorted(d for d in dirs if d not in SKIP)
rel = os.path.relpath(dirpath, root)
if rel.count(os.sep) >= max_depth:
dirs[:] = [] # 深层目录不再下钻
continue
lines.append(f"{rel}/ ({len(files)})")
lines += [f" {os.path.join(rel, f)} # {describe(os.path.join(dirpath, f))}"
for f in sorted(files) if f.endswith(CODE_EXT)]
return "\n".join(lines)
print(skeleton("src"))
解读:骨架脚本的职责是穷举而不是理解——describe 只在文件恰好有首行注释时有产出,多数文件的职责栏是空的。这一步故意留给模型,下一节实战里补。
5、增量上下文维护
编辑会话还有第二重账:改了 10 个文件之后,模型上下文里应该是什么?
Claude Code 的答案是变更块而不是全文件。Edit 工具的返回只有替换成功与否的确认与变更处的短摘录,不把整个文件重发一遍;连续改同一个文件 5 次,窗口里是 5 个几十 token 的变更块,而不是 5 份几万 token 的完整快照。自己写编码 Agent 时,同一个模式可以用一个会话状态类实现:
class EditSession:
"""维护变更块增量视图,不重读全文件"""
def __init__(self):
self.blob = {} # 文件 -> 模型已见过的版本
self.patch = {} # 文件 -> [(old, new), ...]
def edit(self, path, old, new):
assert old in self.blob[path] # 锚点必须在已见版本中
self.patch.setdefault(path, []).append((old, new))
self.blob[path] = self.blob[path].replace(old, new, 1)
def view(self, path):
"""回给模型的轻量指针:文件 + 变更数"""
n = len(self.patch.get(path, []))
return f"{path}(已变更 {n} 处,以变更块为准)"
三个要点。锚点校验保证编辑始终建立在模型真实见过的内容上,挡住幻觉漂移;replace(…, 1) 限定单点替换,同名代码块只改第一处;view 回的是指针而不是全文,模型需要时会自己重新 Read 对齐。进的少、留的准——这与本系列第 8 篇的工具结果治理是同一原则在编辑场景的落点。
6、与 IDE 补全的对比
Copilot 式补全与 Claude Code 式 Agent 是两种上下文哲学。
补全的上下文围绕光标:当前文件、打开的标签页、少量路径线索,策略是就近拼装——它假设你正在写的那一行与你正看的代码最相关。这个假设在写样板代码时成立,在跨文件改动时不成立:光标周围没有那个三天前刚改过签名的函数。
Agent 的上下文围绕任务:它不假设你在看什么,而是自己去查该看什么——grep 定位、read 拉取、必要时翻地图。代价是工具调用的多轮往返,换来跨文件一致性:改了函数签名,它能 grep 全部调用方逐一修掉。
| 维度 | IDE 补全 | 编码 Agent |
|---|---|---|
| 上下文中心 | 光标位置 | 任务目标 |
| 供给策略 | 就近拼装、被动跟随 | 主动检索、按需拉取 |
| 典型尺度 | 数百~数千行 | 数千~数万行跨文件 |
| 一致性保障 | 靠人复核 | 靠 grep、编译、测试闭环 |
| 失效模式 | 看不见调用方、风格漂移 | 检索漏文件、上下文过期 |
| 适合任务 | 样板代码、局部续写 | 跨文件重构、排查、脚手架 |
两种策略不可互换:给补全塞任务级上下文会拖慢每一次按键,给 Agent 只看光标周围等于废掉它的手脚。工具选择本身就是上下文策略选择——写新页面的样板代码用补全快,动老模块的重构交给 Agent。
7、实战生成项目代码地图
给自己的项目配一张地图,完整流程四步:
- 生成骨架:脚本机械穷举目录树
- 补职责:让 Claude 为每个文件写一句话
- 挂引用:CLAUDE.md 加一行指针
- 防过期:定期重跑并标日期
第一步跑第 4 节的骨架脚本,输出存为 tmp/map-skeleton.txt。第二步把机械穷举的产出交给模型做语义概括,一条命令完成:
claude -p "读取 tmp/map-skeleton.txt,为每个文件补一句不超过 20 字的
职责描述,输出到 docs/codebase-map.md,格式:路径 — 职责。没把握的标 TODO"
-p 是无交互的 print 模式,跑完即退出,适合这类一次性批处理。第三步在 CLAUDE.md 里挂一行引用:
仓库地图见 docs/codebase-map.md,动手改代码前先读地图再检索。
第四步防过期。地图是快照不是实时视图:
注意:结构演进后不更新的地图比没有地图更糟——模型会按旧图自信地走进死胡同。
两个廉价保险:把前两步塞进 CI 或 git 钩子定期重跑;地图头部标注生成日期与对应 commit。Sourcegraph 指南把记忆失效当作受追踪的工程属性而不是假设——地图同理。
8、总结
代码上下文的三个难题是量级、相关性与结构;Claude Code 的答案是五层策略:CLAUDE.md 小而精的常驻层、Grep/Glob 先定位后 Read 的按需层、工具结果瘦身治理、子代理隔离大探索、compaction 兜底。检索式供给用 20 倍的 token 差距证明"grep 定位加读片段"胜过"塞整个文件";代码地图给模型第一跳的方向感;编辑会话维护变更块而非全文件快照;补全与 Agent 是围绕光标与围绕任务两种不可互换的上下文哲学。
对于正在用或准备引入编码 Agent 的团队,建议本周做两件事:审计项目的 CLAUDE.md,删掉与任务无关的段落;跑一遍第 7 节的流程给仓库配上地图。前者减税,后者指路,都是一小时内的投入。
练习:找一个你超过三个月没动过的项目,先让 Claude Code 只凭现有 CLAUDE.md 回答"登录态在哪里维护",记录它的检索轮数与最终命中率;再按第 7 节生成代码地图挂进 CLAUDE.md,新开会话问同样的问题。对比两次的轮数、token 消耗(/cost 可查)与答案准确度,体会一张地图对第一跳的影响。

