Codex Agent 在大仓库里失去上下文:原因排查 + scope 收敛方案

50 万 + 行的项目里 Codex 跑到一半就晕了。修法是先界定 working set、预喂目录摘要,并把约定放进 AGENTS.md,让它能扛过 compaction。

你把 Codex 指向一个 50 万行的 monorepo,让它”加一个 API endpoint、串通 service 和 DB、再加个测试”。两分钟后它读了 40 个随机文件、会话已经 compact 了两次、现在它在改错的 package——它忘了本来该动的是哪个 app 的路由层。输出看着像能跑又像跑不通,你也说不清它在哪儿走偏的。小仓库里 Codex 几乎不费劲;大仓库它就迷路。

最快的修法: 在 agent 开跑之前先把 working set 界定好。明确告诉它该改哪个子树、把那个子树的 tree 摘要贴给它让它跳过探索阶段、再把它必须遵守的约定放进 AGENTS.md(Codex 每个 turn 都会重新读它,而且它能扛过 compaction),而不是放进一条只发一次、会被 compaction 摘要掉的消息里。这三步能在你动用更大模型之前,就解决掉绝大多数”大仓库里迷路”的会话。

先纠正一个会影响整套策略的点:截至 2026 年 6 月,Codex CLI 跑的是 gpt-5.5(ChatGPT 登录时的默认模型),而 OpenAI 刻意把 Codex 这一层的上下文窗口封在 40 万 token,尽管 GPT-5.5 的 API 本身能到 1M。你是在 40 万的空间里干活,不是一百万。所以在大仓库上 scope 不是可选项——你会远在”整个仓库都进上下文”之前就把 40 万撑爆。

Codex 怎么管理上下文(理解了这些,修法才说得通)

两个机制最关键:

  • Compaction(压缩)。 会话接近窗口上限时,Codex 会把较早的 turn 摘要掉来腾空间(你也可以用 /compact 手动触发,或用 /clear 直接清空会话)。Compaction 会保留它判断重要的内容、丢掉其余的——这正是 12 步 plan 或某个结构锚点悄悄消失的原因。
  • AGENTS.md 每个 turn 都会被重新读取。 跟你只贴过一次的聊天消息不同,AGENTS.md 链在每个 turn 都会被重新注入,而且能完整扛过 compaction。这是你手上最有用的杠杆:任何你丢不起的东西都该写进 AGENTS.md,而不是放进一条性的 prompt 里。

查找链(每个会话构建一次)从 ~/.codex/AGENTS.md 往下走,沿着从 git 根目录到当前目录路径上的每一份 AGENTS.md。文件按根在前、叶在后拼接,所以更近的 AGENTS.md 在冲突时会覆盖根目录的。只有你路径上的文件会加载——你在 apps/web/ 干活时,apps/api/AGENTS.md 不会加载。整条链默认封顶 32 KiBproject_doc_max_bytes),所以每份文件都要精简,否则叶子层的规则会被臃肿的根文件挤掉。

常见原因

按大仓库失忆的命中率排序。

1. 没 scope —— agent 扫整个仓库

你说”找找认证在哪儿接的”。Agent 全 monorepo 跑 ripgrep,4000 个 match,读 50 个随机文件,原任务直接朝着 compaction 线滚过去。

如何判断: transcript 里出现了无关 package 的读(编辑 api-server/ 时却读 marketing-site/)。

2. Plan 列表被 compact 掉

会话一接近窗口上限,Codex 就会压缩早期 turn。原本 12 步 plan 变成”用户想加个 endpoint”,agent 忘了 6-12 步。

如何判断: 重 prompt”列出剩余 plan”——答案比你写的少,或者比原文更模糊。

3. 约定写在性消息里,而不是 AGENTS.md

你把项目约定贴进了开场消息里。到第 30 turn 会话已经 compact 过,只剩”follow conventions”幸存下来。Agent 现在在没有具体规则的状态下生成。(如果约定放在 AGENTS.md 里,它每个 turn 都会被重读、本来能扛过这一关。)

如何判断: 输出违反了你明确说过的约定。重 prompt”为这步引用相关规则”——agent 用同义改写或编造,就说明规则当初在一条被 compact 掉的消息里,而不在 AGENTS.md 里。

4. 冗长 tool 输出灌满上下文

pnpm tsc --noEmit 4000 个类型错误的 dump、含完整文件内容的 30 文件目录列表、1 万行测试日志——每一个一次 turn 就能烧光窗口。

如何判断: 某一次 tool 调用占了总 token 的 > 30%。事后用 wc -l 跑可疑命令的 stdout。

5. Agent 反复读同一个文件

在没有”我已经读过哪些”上下文缓存时,agent 会反复 re-issue 已经读过文件的 read。每次重读都耗窗口、零新信息,而且重读会在 compaction 之后陡增(读取历史被摘要掉了)。

如何判断: transcript 搜重复的 Read <path> 调用。同一个 path 读 3 次以上 = 严重浪费。

6. 跨 package 编辑但没有依赖图

任务动 3 个 package,agent 不知道依赖图。它反复读 package.jsontsconfig.json 和 lockfile 自己摸索——每次摸索都吃窗口。

如何判断: transcript 里有大量来自不同目录的 Read package.json / Read tsconfig.json

对症诊断:你属于哪一类?

transcript 里的症状最可能的原因跳到
读了任务从没提过的 package没 scope(#1)Step 1
Agent 跳过 / 忘了你后面的 plan 步骤Plan 被 compact(#2)Step 6
输出违反了你明确说过的规则约定没放进 AGENTS.md(#3)Step 3
某一条命令占了 token 大头冗长 tool 输出(#4)Step 5
同一个文件读了 3 次以上没有读取缓存(#5)Step 4
反复读 package.json / tsconfig.json没有依赖图(#6)Step 2

动手前先确认

  • 大致摸一下仓库规模:tokeicloc 给基线(代码行数、文件数)。一个 10 万行的仓库按语言不同 tokenize 出来大概 30-50 万 token——已经超过 40 万的 Codex 窗口了,这就是为什么”全读”从来不是方案。
  • 确认任务实际需要的子树——用一句话写下来。
  • 检查你的 AGENTS.md 链长度;整条链封顶 32 KiB,所以如果根文件很大,你 package 级的规则可能正在被截断。

需要收集的信息

  • 仓库总文件数和 LoC(find . -type f -name "*.ts" | wc -lcloc .)。
  • 任务触及的具体子树。
  • 你模型的 Codex 上下文窗口:截至 2026 年 6 月,Codex 这一层的 gpt-5.540 万(1M 的 GPT-5.5 窗口只在 API 上有)。其他可选模型:gpt-5.4(20 万)、gpt-5.4-minigpt-5.3-codexgpt-5.3-codex-spark(仅 Pro)。
  • AGENTS.md 和根 README.md 的 token 长度(wc -w × 1.3 是粗略的 token 估算)。
  • 任务相关的内部术语 glossary(项目代号、包简称)。

最短修复路径

按收益从高到低。

Step 1:prompt 里界定 working scope

任何 plan 之前:

Working scope:
- 只编辑:packages/api-server/、packages/api-types/
- 只读参考:packages/db-client/(只读,不改)
- 不要碰:monorepo 里其他任何东西

如果某个问题需要在 scope 外做改动,停下来先问。

这条能把 agent 的搜索空间在大多数 monorepo 上砍 80-90%。想在工具层面强制它,就用 -C(工作目录)把 Codex 指向那个子树,再配 --sandbox workspace-write,让 workspace 外的写入都需要审批:

codex --cd packages/api-server --sandbox workspace-write

Step 2:预喂结构摘要

Agent 开始扫之前,先给它你生成好的目录树摘要:

tree -L 3 packages/api-server -I 'node_modules|dist|.next' > /tmp/tree.txt

然后 prompt 里:

仓库结构(读这个,不要再列):

[贴 tree 输出]

关键文件:
- packages/api-server/src/routes/index.ts — 路由注册
- packages/api-server/src/services/ — 业务逻辑
- packages/api-types/src/index.ts — 共享类型

Agent 直接跳过结构发现阶段进入正题。这同时也提前堵住了原因 #6:把依赖关系直接喂给它,它就不会为了重建这些关系而从五个目录反复读 package.json

Step 3:把约定放进 AGENTS.md,而不是 prompt

这是杠杆最大、也最多人搞错的一步。别把约定贴进开场消息然后指望它一直在——compaction 会把它摘要掉。把约定放进离子树最近的那份 AGENTS.md(Codex 每个 turn 都会重读,而且它扛得过 compaction):

# packages/api-server/AGENTS.md

约定:
- 路由通过 routes/index.ts 里的 registerRoute() 注册
- Services 通过 barrel 文件导出
- 所有 handler 返回 { ok: boolean, data?: T, error?: AppError }
- 完成前跑 `git diff --name-only`,确认每个文件都在 packages/api-server 或 packages/api-types 下

因为这条链是根在前、叶在后拼接,这份 package 级文件会覆盖仓库根 AGENTS.md 里更宽松的规则。保持精简:整条链封顶 32 KiB,臃肿的根文件会把这些规则挤掉。

Step 4:用目录级摘要代替文件读

探索阶段优先用摘要而不是完整内容:

跑:ls packages/api-server/src/services/
跑:head -1 packages/api-server/src/services/*.ts   (每个文件的第一行 / docstring)
锁定目标 service 之前不要读完整内容。

知道 30 个 service 名 + 各自一行 doc 大概 500 token,全读 30 个要 5 万 token。这也顺手干掉了原因 #5 的重读浪费——agent 挑出它唯一需要的那个文件,而不是一路乱啃。

Step 5:限制冗长 tool 输出

把吵的命令包一层,让单次 dump 炸不掉你的窗口:

pnpm tsc --noEmit 2>&1 | tee /tmp/tsc.log | head -100
echo "(完整输出在 /tmp/tsc.log)"

或者让 agent 只 grep 它要的:

grep -E "error TS|src/api-server/" /tmp/tsc.log | head -50

100 行而不是 4000 行。

Step 6:拆成 commit checkpoint 的子任务、各自全新上下文

长任务跑一串非交互调用,每次都是全新上下文。中间 commit,让 commit 过的代码——而不是对话——成为持久状态:

codex exec - < step1-add-types.md      # 在 packages/api-types 加新类型,然后 commit
codex exec - < step2-add-route.md      # 在 packages/api-server 加 route + handler,然后 commit
codex exec - < step3-add-test.md       # 加集成测试,然后 commit

codex exec(别名 codex e)是非交互命令;codex exec - < file 从文件读取 prompt。在交互会话里你也能拿到同样的效果:在干净的 checkpoint 上跑 /compact,或者每个子任务 commit 完之后用 /clear 开一段真正全新的对话。

Step 7:先调 compaction 阈值,再考虑动更大的模型

你没法挑一个”长上下文”的 Codex 模型——没有 gpt-5.5-long,而且 Codex 这一层无论如何都把 gpt-5.5 封在 40 万。所以杠杆不是更大的窗口,而是给 compaction 留更多余量。对那些必备上下文很大的仓库,把自动 compaction 触发点调低到容量的 80-85% 左右,让 compaction 之后的重读循环(系统 prompt 加 AGENTS.md 链)有地方落脚:

# ~/.codex/config.toml
model = "gpt-5.5"

要明白这里的取舍:更早 compact 能防硬溢出(原因 #1 和 #5),但对已经被压缩的 plan(原因 #2)或巨型 tool dump(原因 #4)毫无帮助。Scope 和 AGENTS.md 才是真正的修法。

怎么确认已经修好

  • 把同一任务重跑,看 transcript:只在 scope 内读、没有跨 package 乱跑。
  • 中途重 prompt”你在第几步”和”从 AGENTS.md 里为这步引用规则”——两个都应该精确而不是同义改写。如果规则被一字不差地引回来,说明它确实在上下文里。
  • 最后跑 git diff --name-only,确认每个改动文件都在你声明的 scope 内。出现 scope 外的路径,说明 Step 1 没守住,该把这条 diff 检查规则加进 AGENTS.md

长期预防

  • monorepo 上每个 agent 任务开头都有显式 Working scope: 段,启动时用 -C 指向子树。
  • 根目录维护一份 repo-map.md:目录树摘要 + 关键文件指针;让 agent 先读它。
  • Monorepo 每个 package 一份 AGENTS.md——冲突时最近的那份生效,单任务的 doc 量小很多。保持根文件精简,别超过 32 KiB 的链上限。
  • 探索阶段先用 lshead -1 摘要,再做完整文件读。
  • Shell tool 输出用 wrapper 限到 100 行,溢出导到 /tmp/*.log
  • 多 package 工作拆成 commit checkpoint 的子任务;3-package 改动绝不一个超长 prompt。

常见坑

  • 把约定贴一次就以为 200 turn 后还在——它不会在;放进 AGENTS.md,它每个 turn 都重读、扛得过 compaction。
  • 指望某个”长上下文”Codex 模型救你——没有这种东西;Codex 上的 gpt-5.5 是 40 万,而注意力质量在这个上限之前很早就掉了。
  • 把所有规则都塞进仓库根 AGENTS.md,直到它撑爆 32 KiB 的链上限、悄悄截断了你叶子 package 的规则。
  • tsc --noEmit 不加 head / tail 帽——一条坏命令能炸掉 40% 窗口。
  • 重 prompt”你读过哪些文件”——答案是不完整的,因为读取历史本身已经被 compact 了。

常见 FAQ

Q:我的仓库 20 万行,真的需要 scope 吗?

要。20 万行全读大概是 500 万 token 量级,对着 40 万的 Codex 窗口。你永远只能把整个 codebase 的一小部分放进上下文,早点 scope 让这”一小部分”是有意为之而不是随机的。

Q:怎么快速做结构摘要?

tree -L 3 -I 'node_modules|dist|.next|coverage' > repo-map.txt

两条命令就有一份 1-2k token 的图,胜过 50 次随机文件读。

Q:Agent 无视了我”不要碰其他 package”的规则,怎么办?

把那条规则从聊天消息搬进最近的 AGENTS.md(每个 turn 都重读),再加一行 verifier:“完成前跑 git diff --name-only,确认所有文件都在 scope 内”。Verifier 把静默违反变成可见错误。启动时用 -C--sandbox workspace-write 还能在上面再加一道硬护栏。

Q:GPT-5.5 不是有 1M 上下文窗口吗?我怎么还溢出?

那 1M 是 GPT-5.5 的 API 上限。截至 2026 年 6 月,Codex 出于吞吐和成本考虑,刻意把它这一层封在 40 万。在 Codex 里你只有 40 万,所以要按 40 万来规划,不是一百万。

Q:prompt caching 有用吗?

它让 scope 更便宜,但防不住溢出。Cache 加速重复使用相同内容(也降成本),但那部分内容依然占窗口。真正减少窗口占用的是 scope。

相关阅读

标签: #Codex #agent #排查 #context-window #monorepo