上下文工程与代码库理解
18

上下文工程:为什么不是塞越多代码越好

很多程序员第一次用 AI 改真实项目,会自然想到一个办法:多给它代码。

把相关目录贴进去,把文档贴进去,把报错贴进去,把历史聊天也贴进去。看起来越完整,AI 越应该懂项目。

实际经常相反。

上下文越多,模型能看到的信息越多,但噪音、旧结论、冲突规则、相似但无关的代码也会一起进来。Agent 不是把所有材料变成可靠事实,它会根据当前上下文里的线索生成下一步判断。你给它一堆材料,如果没有优先级和边界,它就可能被最显眼、最新、最相似但错误的线索带偏。

上下文工程不是“喂更多”,而是“给对、给少、给得有结构”。

上下文选择漏斗

为什么更多上下文会变差

真实项目里,错误上下文主要有五种。

第一种,相似但无关的文件。

比如你要修线上订单提交,仓库里同时有:

src/order/submit.ts
src/order/submit.legacy.ts
src/demo/order-submit.ts
docs/order-submit.md
test/order-submit.test.ts

如果 Agent 先读到 demo,它可能按 demo 逻辑改真实代码;如果先读到 legacy,它可能误以为旧接口仍是主路径;如果只读 docs,可能忽略当前实现已经偏离文档。

第二种,过期文档。

文档很有用,但文档会过期。尤其是接口、状态机、部署脚本、环境变量说明。Agent 如果把过期文档当作当前事实,修改方向会很危险。

第三种,冲突规则。

用户说“快速修好”,项目规则说“不要改公共接口”,历史对话里又有人说“可以重构”。这些指令混在一起,Agent 可能抓住最显眼的那条执行。

第四种,失败假设。

排障过程中,前几轮猜测如果没有被清理,会一直污染后续判断。比如一开始怀疑是缓存,后来日志证明不是,但对话里保留了大量缓存讨论,Agent 之后仍可能继续沿着缓存方向改。

第五种,无验证结论。

“应该是前端问题”“看起来是后端返回慢”“可能是 token 过期”,这些话如果没证据,就不能进入实现上下文。它们最多是待验证假设。

上下文工程的第一条原则是:不要把未验证判断和已确认事实混在一起。

Agent 需要的不是完整项目,而是任务视野

一次具体任务需要的上下文通常分成六层。

层级 内容 作用
任务目标 要改变的用户可见行为或代码行为 防止实现跑偏
硬约束 不能改的接口、目录、依赖、兼容规则 防止越界
入口文件 请求入口、页面入口、命令入口、模块入口 确定从哪里开始看
关键实现 业务逻辑、状态流转、核心函数 决定怎么改
验证材料 测试、复现步骤、日志、截图、命令 证明改动有效
项目规则 命令、目录、代码风格、完成报告 保持工程一致性

这六层比“把相关目录都给它”更有用。

例如修 session 过期后的首次提交失败,给 Agent 这些材料就足够开始:

## Objective

session 过期后重新登录,第一次提交资料表单应该成功。

## Constraints

- 不改变登录接口协议。
- 不改变 token 存储格式。
- 不吞掉非 401 错误。

## Candidate Files

- `src/auth/request.ts`
- `src/forms/ProfileForm.tsx`
- `test/auth-refresh.test.ts`

## Evidence

- 错误日志:`401 refresh token missing`
- 复现路径:登录 -> 等待 session 过期 -> 刷新 -> 提交表单

## Verification

- `npm test -- auth-refresh`
- 浏览器复现路径

如果 Agent 读完这些后发现缺上下文,再让它搜索和补充。不要一开始就把整个 src/ 目录塞进去。

上下文要有优先级

不是所有材料地位相同。

一个实用的优先级可以这样排:

1. 当前任务目标和硬约束
2. 当前仓库真实源码
3. 当前测试和命令输出
4. 项目规则文件
5. 官方文档和接口契约
6. 历史 issue、PR、聊天记录
7. 个人猜测和经验判断

如果源码和文档冲突,不能直接相信文档;要确认当前实现和测试。 如果用户猜测和日志冲突,不能按猜测改;要回到日志。 如果历史 issue 和当前规则冲突,不能直接照历史 issue;要看它是否仍适用。

你可以把优先级写进提示:

如果资料之间冲突,请按下面顺序判断:
1. 当前任务硬约束
2. 当前源码和测试
3. 实际命令输出
4. 项目规则
5. 历史文档和讨论

发现冲突时先停止并说明,不要自行选择。

这段话能减少 Agent 自己“合理化”的空间。

一个上下文选择流程

每次任务开始前,可以按下面流程选文件。

上下文污染和清理机制

第一步,写目标。

目标必须是行为,不是愿望。

差:

优化登录体验。

好:

session 过期后重新登录,第一次提交资料表单应该成功,不应出现重复错误 toast。

第二步,写硬约束。

硬约束比背景更重要。

不改变登录接口协议。
不改变 token 存储格式。
不新增依赖。
不吞掉非 401 错误。

第三步,找入口。

用 rg 先找候选入口:

rg -n "refresh token|refreshToken|401|session expired|submit" src test
rg --files | rg "auth|session|form|submit|test|spec"

不要让 Agent 只凭文件名判断。候选入口要读过才能确认。

第四步,读测试。

很多人先读实现,后读测试。AI Coding 里更建议尽早读测试。测试会告诉你当前行为边界、已有命名、断言风格和可运行命令。

第五步,标记不该改的文件。

上下文包里不仅要写“可能相关”,还要写“看起来相关但暂时不该改”。例如:

## Do Not Edit Initially

- `src/auth/login.ts`: 登录接口协议不在本次范围。
- `src/auth/storage.ts`: token 存储格式不改。
- `src/demo/`: 示例页面,不是生产路径。

这能避免 Agent 把相似文件都纳入改动。

第六步,缺什么再补什么。

如果计划阶段发现缺调用链,再让 Agent 搜索;如果测试失败需要环境变量,再补 .env.example;如果浏览器路径不清楚,再补路由文件。上下文是按证据逐步扩展,不是一次性塞满。

让 Agent 做上下文侦察,而不是自己整理材料

这里容易产生误解。

上下文工程不是让程序员手工复制一堆文件、手工筛选几十条搜索结果、手工写一份漂亮文档。现在的 Agent 本来就能搜索仓库、读文件、运行命令。程序员要做的是定义侦察目标和验收格式。

一个可用的上下文侦察提示可以这样写:

先不要改代码。

任务:
session 过期后重新登录,第一次提交资料表单失败。

请做上下文侦察,并输出 Context Recon Report。

要求:
1. 先列出搜索关键词,不要直接实现。
2. 搜索入口、实现、测试、配置和项目规则。
3. 每个候选文件都说明为什么相关。
4. 区分 production、test、demo/example、legacy、generated。
5. 标出看起来相关但暂时不应该修改的文件。
6. 给出缺失上下文和下一步最小验证动作。
7. 不要编辑任何文件。

Agent 的输出应该长这样:

# Context Recon Report

## Objective

session 过期后重新登录,第一次提交资料表单应该成功。

## Search Terms

- session, refreshToken, 401, unauthorized, submit, ProfileForm

## Files Read

- `src/auth/request.ts`
  - reason: HTTP 401 handling and retry behavior.
- `src/forms/ProfileForm.tsx`
  - reason: user-visible submit entry.
- `test/auth-refresh.test.ts`
  - reason: existing refresh behavior tests.
- `AGENTS.md`
  - reason: project rules and verification commands.

## Excluded For Now

- `src/generated/api-client.ts`
  - generated file, do not edit directly.
- `src/demo/ProfileForm.tsx`
  - demo route, not production path.
- `docs/auth-old.md`
  - appears outdated; conflicts with current request wrapper.

## Missing Context

- Need to confirm whether original request should retry exactly once.
- Need browser path for expired session reproduction.

## Next Action

- Add or locate a failing regression test for expired-session first submit.

这个报告比“我读完了项目”有用得多。你能直接检查它有没有漏测试、有没有把 demo 当生产、有没有把 generated 放进修改范围。

配套代码仓库 DavidDingXu/learn-ai-coding 的工具 module 里的 context-audit 可以作为第一层自动检查:

cd learn-ai-coding/ai-coding-workflow-kit
node src/cli.js context-audit --root .

它只做启发式扫描,不证明真实调用路径。它的价值是提醒 Agent 和人:先看哪些文件,哪些路径要谨慎,哪些结论必须复核。

生产项目里最常见的上下文过载

上下文过载不一定是 token 超限。更常见的是材料太多,但优先级不清楚。

第一类,旧 PR 和当前代码混在一起。

很多项目会把历史 PR、issue、设计文档贴给 Agent。它们可能解释了当时为什么这么做,但不一定代表当前系统事实。尤其是经历过多次重构的模块,旧 PR 里的入口、命名、命令可能都变了。

处理方式很简单:历史材料只能作为线索,不能直接作为实现依据。让 Agent 回到当前源码、测试和命令输出确认。

下面的 issue 只能作为历史线索。
请以当前源码、测试和命令输出为准。
如果 issue 与当前实现冲突,列出冲突点,不要直接按 issue 修改。

第二类,多个相似模块一起进入上下文。

比如 order、order-v2、legacy-order、demo-order 同时存在。Agent 很容易选择最短、最整洁、最像标准答案的那条路径,但生产路径往往不是最漂亮的那条。

这时要让它做路径分类:

## Path Classification

- production path:
- compatibility path:
- demo/example path:
- generated path:
- test fixture:

分类不清楚,就不能实现。

第三类,失败尝试污染当前判断。

调试时,前两轮假设会留下很多文本。比如先怀疑缓存,再怀疑后端,再怀疑 token。即使后面日志排除了缓存,缓存讨论仍然在上下文里,Agent 可能继续沿着旧假设修改。

处理方式是把已排除假设单独列出来:

## Excluded Hypotheses

- Not cache: reproduced after clearing local storage and disabling cache.
- Not backend 500: submit API returns 401 before backend handler.

这比简单说“继续”更能保住当前证据链。

第四类,工具输出未经整理。

一大段 rg 输出、测试日志、浏览器 console,很容易把关键行淹没。Agent 需要原始输出时可以读,但进入实现前应该先整理成摘要。

摘要必须保留证据,不要只留结论:

## Evidence Summary

- `test/auth-refresh.test.ts:42` covers refresh success but not original submit retry.
- `src/auth/request.ts:88` handles 401 and refresh.
- No test found for duplicate submit after refresh.

第五类,官方文档被当成项目事实。

官方文档能说明工具或框架当前支持什么,但你的项目可能锁在旧版本,也可能有历史约定。写法应该是:

先确认当前依赖版本和项目配置。
只有在版本和配置匹配时,才按官方文档建议修改。

这些场景都不是理论问题。真实生产里,AI 很少因为“完全没材料”犯错,更多是因为材料太多、层级不清、旧结论没被排除。

给 Agent 的上下文包模板

可以直接复用这个模板:

# Task Context Package

## Objective

用一句话说明完成后的行为。

## Constraints

- 不允许改变什么。
- 不允许新增什么。
- 必须保留什么兼容行为。

## Known Evidence

- 复现步骤:
- 错误日志:
- 截图或接口响应:

## Files To Inspect First

- `path/to/entry`
- `path/to/implementation`
- `path/to/test`

## Files Not To Edit Initially

- `path/to/generated`
- `path/to/legacy`

## Verification

- focused test:
- broader check:
- browser/API path:

## Open Questions

- 需要人确认的问题。

这个模板不是为了好看,而是为了把上下文分层。

配套工程的工具 module 里可以生成基础任务上下文:

cd learn-ai-coding/ai-coding-workflow-kit
node src/cli.js task-context \
  --title "修复 session 过期后首次提交失败" \
  --files "src/auth/request.ts,src/forms/ProfileForm.tsx,test/auth-refresh.test.ts"

生成后要复核 constraints、evidence、not-to-edit 和 verification。不要把生成结果当最终判断。

代码地图是上下文工程的入口

陌生仓库里,先别让 Agent 直接改。

先让它读代码地图,或者生成一份轻量代码地图:

cd learn-ai-coding/ai-coding-workflow-kit
node src/cli.js codemap --root .

代码地图应该帮它回答:

  • 运行入口在哪里。
  • 测试目录在哪里。
  • 配置文件在哪里。
  • 生成文件和构建产物在哪里。
  • 哪些目录不应该直接改。
  • 常用验证命令是什么。

代码地图不是全量架构文档。它是一张导航图。

一个老项目里,Agent 最容易误改的地方往往不是业务核心,而是旁边那些看起来相关的文件:旧入口、示例目录、生成代码、测试夹具、历史迁移脚本。代码地图要把这些地方标出来。

上下文污染的典型症状

你怎么知道上下文已经污染?

看这些信号。

第一,Agent 引用已经被否定的假设。

比如日志已经证明不是缓存问题,它仍然说“我们可以清理缓存”。这说明旧上下文还在影响判断。

第二,Agent 修改了相似但无关文件。

比如任务是生产订单提交,它改了 demo/order-submit.ts。这说明文件优先级没讲清。

第三,Agent 反复扩大范围。

一开始修 bug,后来变成重构 auth,再后来变成统一错误处理。上下文里可能混入了“顺便优化”的目标。

第四,Agent 无法解释为什么改某个文件。

如果它只能说“这个文件相关”,而不能说明入口、调用关系和验证方式,就说明上下文选择不够干净。

第五,完成报告没有“未验证风险”。

这通常意味着它把可见材料当成完整事实。

遇到这些情况,不要继续补更多材料。先清理上下文。

如何清理上下文

清理上下文不是删除对话,而是重新整理事实。

可以让 Agent 停下来输出:

请停止修改,重新整理当前上下文。

只按证据分类:
1. 已确认事实。
2. 已排除假设。
3. 未验证假设。
4. 当前任务硬约束。
5. 下一步最小验证动作。

不要继续写代码。

一个好的整理结果应该像这样:

## 已确认事实

- 表单提交时第一跳请求返回 401。
- refresh token 存在。
- refresh 接口成功返回新 token。
- 原提交请求没有重试。

## 已排除假设

- 不是表单字段校验失败。
- 不是后端提交接口 500。

## 未验证假设

- 请求封装层是否禁止重复提交。
- 是否存在并发 refresh 问题。

## 硬约束

- 不改变登录接口协议。
- 不改变 token 存储格式。

## 下一步

- 补一个 refresh 成功后重试原请求的回归测试。

这份整理可以作为新对话或下一步实现的干净上下文。

如果对话已经很长,直接开新对话也可以。但新对话要带整理后的事实,而不是带原始长聊天。

什么时候应该补更多上下文

少给上下文不是目的。给够才是目的。

需要补上下文的信号:

  • Agent 无法确认真实入口。
  • 计划里出现“可能”“猜测”但没有文件证据。
  • 测试失败和当前 diff 对不上。
  • 实现涉及公共接口或共享模块。
  • 日志指向另一个服务或异步任务。
  • 浏览器行为和单元测试结果冲突。

补上下文时,要补具体材料:

不是:再看看整个项目。
而是:请读取 `src/auth/request.ts` 的调用方,并列出哪些路径会触发表单提交。

或者:

不是:看看测试为什么失败。
而是:请读取失败测试输出、对应测试文件和本次 diff,判断失败类型。

越具体,越能避免新噪音。

如何处理官方文档和项目源码冲突

AI Coding 经常会用到官方文档。官方文档优先级很高,但不是项目当前事实。

例如,官方文档说某个配置默认开启,但你的项目里显式关闭;官方建议某个 API 用法,但项目里因为兼容旧版本保留了不同写法;框架文档已经更新,但项目依赖还是旧版本。

这时应该让 Agent 这样处理:

请区分官方文档事实和当前项目事实。

如果二者冲突:
- 列出冲突点。
- 确认项目依赖版本和当前配置。
- 不要直接按官方最新写法修改。
- 先说明迁移成本和兼容风险。

工具文档、框架文档、社区经验,都不能绕过当前仓库。

常见错误写法

错误写法一:

我把整个 src 目录都给你,你帮我看看怎么改。

问题是没有目标、没有优先级、没有禁止修改范围。

更好的写法:

先围绕 session 过期后首次提交失败做上下文侦察。
只读 auth refresh、form submit 和相关测试。
列出读取文件和缺失上下文,不要改代码。

错误写法二:

你已经看过这个项目了,继续修。

问题是“看过”不等于当前任务上下文仍然有效。

更好的写法:

继续前先重新列出:
- 当前已确认事实
- 当前 diff
- 还没验证的假设
- 下一步最小动作

错误写法三:

这是之前的讨论记录,你全部参考。

问题是讨论记录里通常混有猜测、被推翻的假设、旧方案和无关信息。

更好的写法:

下面是整理后的事实和约束。
只把“已确认事实”和“硬约束”作为实现依据。
“未验证假设”只能用于设计验证动作。

让上下文进入仓库,而不是停在聊天里

好的上下文不要只停在对话窗口。

至少有三类内容值得落到仓库:

docs/ai-coding/code-map.md
docs/ai-coding/task-context-template.md
docs/ai-coding/verification-ledger-template.md

项目规则放 AGENTS.md 或对应工具支持的规则文件里。 任务上下文可以放 issue、PR、临时 markdown 或任务系统里。 验证记录可以放 PR 描述、变更记录或本地复盘文档里。

落到仓库的好处是:换工具、换对话、换 Agent,关键上下文仍然存在。

这会影响后面讲到的 Skill、OpenSpec 和 Superpowers。它们不是“多一套框架”,而是把上下文、规则和验证从聊天里搬到可复用资产里。

在不同项目里怎么落地

不同项目的上下文重点不一样。

前端项目里,最容易漏的是用户路径。

Agent 读了组件,不代表它理解页面从哪里进来。上下文里要有路由、状态管理、接口封装、关键组件和浏览器验证路径。

## Frontend Context

- Route: `src/routes/profile.tsx`
- Page: `src/pages/ProfilePage.tsx`
- Form: `src/forms/ProfileForm.tsx`
- API wrapper: `src/api/request.ts`
- State: `src/stores/session.ts`
- Browser path: login -> expire session -> refresh -> submit profile form

后端项目里,最容易漏的是事务和异步路径。

Agent 读到 controller 不代表它理解 job、consumer、transaction、retry 和幂等。上下文要覆盖入口、service、repository、消息队列、事务边界和测试命令。

## Backend Context

- HTTP entry: `OrderController.cancel`
- Domain service: `OrderStatusService`
- Async callback: `PaymentCallbackConsumer`
- Transaction boundary: `OrderRepository.updateStatus`
- Focused test: `./mvnw test -Dtest=OrderStatusServiceTest`

老项目里,最容易漏的是隐式规则。

README 可能过期,测试可能只覆盖一部分,脚本可能只有老同事知道。上下文里要显式写“不知道什么”,不要假装完整。

## Legacy Context

- README test command is outdated.
- `legacy/` still handles old clients; inspect only unless task says otherwise.
- `db/migrations/` is production history; do not edit existing migration files.
- Payment callback has no automated test; manual verification path required.

多仓项目里,最容易漏的是合同边界。

前端仓库能看到请求,后端仓库才有业务规则;SDK 仓库有生成客户端,schema 仓库有接口定义。不要让 Agent 在当前仓库里硬猜另一个仓库的行为。

## Cross-Repo Boundary

- Current repo contains frontend submit path.
- Backend validation lives in `order-service`.
- API contract is generated from `api-schema`.
- Do not change frontend behavior until contract is confirmed.

这几类写法背后的原则一样:上下文不是材料堆积,而是把当前任务真正会用到的入口、约束和验证路径摆出来。

上下文质量怎么自检

任务开始前,可以用下面这张表检查上下文是否能进入实现。

检查项 合格 不合格
目标 写成可验证行为 只有“优化”“修一下”
入口 有真实入口和证据 只有相似文件名
测试 有测试或明确缺口 没提验证
禁止修改 写出 generated/legacy/API/依赖边界 只写“不要影响其他”
事实/假设 分开列 混在一段话里
冲突处理 说明冲突时停止 让 Agent 自己选
下一步 是最小验证或最小 diff “继续看看”

如果表里有两项不合格,就不要授权实现。先让 Agent 补上下文。

可以直接用这段:

请检查当前上下文是否满足实现条件。
按表格输出:
- Objective
- Entry evidence
- Test or verification
- Do-not-edit boundary
- Facts vs hypotheses
- Conflicts
- Next smallest action

不满足时只补上下文,不要写代码。

这段话适合放进 AGENTS.md 或个人 Skill 里,作为中高风险任务的默认门槛。

可以先实践的一件事

下次使用 AI 改代码前,不要先问它“帮我改”。先给它这段:

先不要改代码。

请基于当前任务建立上下文:
1. 任务目标是什么。
2. 硬约束是什么。
3. 需要先读哪些文件。
4. 哪些文件看起来相关但暂时不该改。
5. 需要哪些验证命令。
6. 还有哪些缺失上下文。

然后看它的回答。如果它说不清入口、测试和验证,就不要让它实现。

AI Coding 的上下文不是越大越好。有效上下文要能把 Agent 的下一步行动限定在正确文件、正确假设和正确验证上。

后面会讲代码地图:在陌生项目里,怎样让 AI 快速知道入口、模块、测试和禁止修改目录,而不是一上来就靠搜索结果乱猜。

常见问题

上下文给少了,AI 不会漏信息吗?

会。所以目标不是少给,而是按证据逐步给。第一轮给目标、约束、入口、测试和验证;如果 Agent 证明缺调用链、配置或外部合同,再补。不要一开始把所有材料混在一起。

Agent 自己能读仓库,还需要上下文工程吗?

需要。能读仓库不等于读对文件。上下文工程的价值是让它的搜索、读取、排除和下一步动作可检查。

官方文档和当前代码冲突,听谁的?

先听当前项目事实。官方文档说明工具或框架能力,但项目可能锁定旧版本、保留兼容写法或有特殊配置。冲突出现时,先列冲突点,再确认依赖版本和项目规则。

历史聊天记录要不要给 AI?

不要原样给。先整理成 Confirmed Facts、Excluded Hypotheses、Open Questions 和 Constraints。原始聊天里通常混有猜测和被推翻的方案。

上下文包要不要长期保存?

普通小任务不一定。高风险任务、跨天任务、多人 review 或以后可能复盘的任务,建议保存。保存时不要保留所有聊天,只保留目标、证据、约束、验证和剩余风险。

可复查资料

搜索全部 42 篇文章。

查看配图