文件上下文(File Context)
CC 用 readFileState 缓存 + attachment 流水线管理文件的进入/刷新/压缩后恢复;Codex 把文件视为 shell 输出流,@ 只是 TUI 补全,无状态追踪
文件上下文(File Context)
结论
CC 把「文件在上下文里」当作一等状态来管理:所有进入上下文的文件登记在 readFileState(LRU,100 条/25MB),每个 turn 的 attachment 流水线(getAttachments,30+ 种 attachment 类型)会做 mtime 比对——磁盘上变了的文件自动以 diff 片段形式重新注入(changed_files);@-mention 支持行号区间、引号路径、目录列举,并触发嵌套 CLAUDE.md 懒加载;压缩时 readFileState 清空,但最近 5 个文件在 5K token/文件、50K 总预算内自动恢复,且跳过已保留在尾部消息里的文件。Codex 则没有文件状态层:没有 Read 工具,文件内容经 shell 命令输出进入上下文,受 per-model truncation policy 截断;TUI 的 @ 只是模糊文件搜索补全,最终只是把路径文本插进消息;图片走 UserInput::LocalImage(序列化时转 base64);没有 mtime 比对、没有变更重注入、压缩后只重建 AGENTS.md/环境上下文而不恢复文件——一切刷新靠模型自己重新 cat。CC 的设计换来精准的去重与新鲜度,代价是巨大的工程面;Codex 的设计零状态、零特判,代价是过期文件内容留在上下文里没人提醒。
研究问题
- 文件通过哪些通道进入上下文?token 如何核算与限额?
- 文件变更后如何刷新(staleness)?重复内容如何去重?
- 压缩(compaction)后文件上下文如何恢复?
各 Agent 设计与实现
Claude Code
来源:重构源码
src_2026-03-31(utils/attachments.ts 3997 行、utils/fileStateCache.ts、services/compact/compact.ts)。[一手源码]
进入通道(五条主路径):
- Read 工具——结果写入 readFileState,是后续所有去重/刷新逻辑的登记入口;
- @-mention——
extractAtMentionedFiles(attachments.ts:2757-2790)用两条正则:/(^|\s)@"([^"]+)"/g(带空格路径)和/(^|\s)@([^\s]+)\b/g;支持@file.txt#L10-20行区间(attachments.ts:1905, 1953-1956);@ 到目录则注入目录列表(上限 1000 条目,attachments.ts:1917-1941); @server:uriMCP 资源(attachments.ts:2792-2800);- IDE 选区/打开文件(
ide_selection/ide_opened_file,仅主线程,attachments.ts:946-952); - 嵌套 CLAUDE.md——读到子目录文件时经
nestedMemoryAttachmentTriggers触发该目录记忆文件注入(attachments.ts:2167-2194)。
文件状态层。 FileStateCache(fileStateCache.ts:30-93):LRU,READ_FILE_STATE_CACHE_SIZE = 100 条、25MB 字节上限(按 Buffer.byteLength(content) 计),key 做路径归一化。FileState 记录 content / timestamp / offset / limit,外加 isPartialView 标记——CLAUDE.md 自动注入内容与磁盘不一致(剥离注释/截断)时置位,此时缓存仅用于去重和变更检测,Edit/Write 仍要求显式 Read(fileStateCache.ts:9-14 注释原文)。
新鲜度(refresh-on-change)。 每 turn 的 changed_files attachment(getChangedFiles,attachments.ts:2063-2161):遍历 readFileState 所有 key,mtime > fileState.timestamp 才重读;文本文件只注入变更片段(getSnippetForTwoFileDiff),touch 而未改的产出空 diff 直接跳过;图片重注入走 token 预算压缩。驱逐策略极克制:仅 ENOENT 驱逐——编辑器原子保存(tmp→rename)造成的瞬时 stat 失败不驱逐,否则下一次 Edit 会误报(attachments.ts:2146-2155 注释引 PR #18525 回归分析)。
压缩后的恢复。 压缩时 readFileState.clear()(compact.ts:518-521,先快照 preCompactReadFileState);随后 createPostCompactFileAttachments(compact.ts:1415-1465)按 timestamp 取最近 maxFiles=5 个文件经 FileReadTool 重读注入,限额 POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000、POST_COMPACT_TOKEN_BUDGET = 50_000(compact.ts:123-124);已作为 Read 结果保留在 post-compact 尾部消息里的文件跳过——注释直言重复注入"纯属浪费(每次压缩最多 25K token)"(compact.ts:1404-1408)。项目根 CLAUDE.md 的重注入由 resetGetMemoryFilesCache('compact') 驱动(claudemd.ts:1124-1130),嵌套 CLAUDE.md 不自动恢复(docs/context-window 的 What survives compaction,docs/memory.md:425-427 [一手文档])。
token 核算。 attachment 级用 roughTokenCountEstimation(jsonStringify(...)) 估算后对预算累加(compact.ts:1455-1463);缓存层面用字节数做 LRU sizing(sizeCalculation: value => Buffer.byteLength(value.content),fileStateCache.ts:37)。整条 attachment 流水线(attachments.ts:743-1003)有 1 秒超时保护,每个 attachment getter 经 maybe() 包裹独立容错;CLAUDE_CODE_DISABLE_ATTACHMENTS=1 或 --bare 可整体关闭(attachments.ts:752-761)。
权限闸门贯穿全链。 @-mention 与 changed_files 重注入都先过 isFileReadDenied(path, toolPermissionContext)(attachments.ts:1908-1912, 2082-2085)——settings 中 permissions.deny 的 Read 规则同样约束 harness 主动注入,不只是模型工具调用。这意味着「敏感文件不进上下文」在 CC 中可以策略化配置,而不是依赖模型自觉。
Codex CLI
来源:
codex-rs @ b89ce9a。[一手源码]
没有 Read 工具、没有文件状态。 文件内容经 shell/unified-exec 工具输出进入对话,输出量由 per-model truncation_policy(bytes 或 tokens 模式)控制,config tool_output_token_limit 可覆盖(models-manager/src/model_info.rs:41-53 with_config_overrides)。core 没有任何「哪些文件在上下文里」的登记簿——grep 不到 mtime 比对或 changed-files 类逻辑 [一手源码 + 推断]。
@ 引用是纯 TUI 层补全。 tui/src/file_search.rs:1-6 头注释:"ChatComposer publishes every change of the @token as AppEvent::StartFileSearch(query)"——@ 触发的是 codex-file-search 模糊搜索会话,选中后把路径作为文本插入消息;协议层用 TextElement(byte_range + placeholder)标记富文本 span 以便 UI 渲染与 resume 重放,但对模型就是普通文本(protocol/src/user_input.rs:16-24, 55-60)。读文件这步交给模型后续自己跑 cat/sed。
结构化输入项(protocol/src/user_input.rs:12-52):Text(单条上限 MAX_USER_INPUT_TEXT_CHARS = 1<<20,注释:"Conservative cap so one user message cannot monopolize a large context window")、Image(data URI)、LocalImage(路径,"converted to an Image variant (base64 data URL) during request serialization",user_input.rs:33-35)、Skill、Mention(app:// / plugin:// 结构化引用)。
项目文件的制度性注入:AGENTS.md 链(见 project-prompt 章)+ <environment_context> 的 cwd/workspace roots(core/src/context/environment_context.rs:525-580)是唯二的「harness 主动喂文件」通道。
压缩后:core/src/compact.rs:298-303 调 build_initial_context 把 AGENTS.md / 环境上下文 / permissions 等初始上下文重插到合适位置(insert_initial_context_before_last_real_user_or_summary,compact.rs:451)——不含任何用户文件的恢复;模型若还需要某文件,自己重新读 [一手源码]。staleness 的唯一防线是 apply_patch 在打补丁时直接对磁盘内容校验,上下文里的旧文件副本不构成写入依据(core/src/apply_patch.rs 的存在性即此架构的体现 [推断])。
ignore / 大库适配:Codex 没有 CC 式的 per-file 注入闸门;文件可达性由沙箱文件系统权限(<file_system> 在 environment_context 中渲染 readable/writable roots)整体圈定。@ 搜索底层的 codex-file-search 遵循忽略规则做候选过滤,但这只影响补全候选,不影响模型 shell 真正能读什么 [一手源码 + 推断]。
差异矩阵
| 维度 | Claude Code | Codex CLI |
|---|---|---|
| 读文件原语 | 专用 Read 工具(系统提示词强制优先于 cat) | shell 命令输出(无专用工具) |
| @ 引用语义 | harness 解析并注入文件内容 attachment(含 #L10-20、目录、MCP 资源) | TUI 补全插入路径文本,内容由模型自取 |
| 文件状态追踪 | readFileState LRU(100 条/25MB),记录内容+时间戳 | 无 |
| 变更刷新 | 每 turn mtime 比对,自动注入 diff 片段 | 无;靠模型重读 |
| 去重 | 缓存命中 + post-compact 跳过尾部已有 Read 结果 | 无机制 |
| 压缩后恢复 | 最近 5 文件重注入(5K/文件、50K 总预算)+ 根 CLAUDE.md 重读 | 仅重建 AGENTS.md/环境上下文,不恢复文件 |
| 图片 | Read 工具读图 + 粘贴;token 预算压缩(readImageWithTokenBudget) | LocalImage 路径 → 序列化期转 base64;-i 旗标附图 |
| 单消息体量限制 | attachment 各自预算 + 1s 流水线超时 | 文本 1MiB 硬上限;工具输出按模型 truncation policy 截断 |
| 写安全与 staleness | Edit 要求先 Read;isPartialView 强制重读 | apply_patch 对磁盘即时校验 |
| 关闭开关 | CLAUDE_CODE_DISABLE_ATTACHMENTS / --bare | 不适用(无此层) |
最小复现
# CC:@-mention 行区间 + 变更重注入
claude
# > explain @src/main.ts#L1-20 ← 文件片段作为 attachment 进入上下文
# (另开终端改动 src/main.ts 后回到会话再发一条消息)
# > continue ← 本 turn 自动注入 <system-reminder> 变更 diff
# CC:目录 mention
# > what's in @src/ ← 注入目录列表(≤1000 条目)
# Codex:@ 补全只插路径
codex
# 输入 @ 触发模糊搜索,选中后 composer 里只是文本路径;
# 提交后观察模型第一步是跑 shell 读该文件
# Codex:图片注入
codex -i screenshot.png "describe this"
# 或调试视角验证 LocalImage → base64 转换:
codex debug prompt-input -i screenshot.png "describe" | grep -o '"type": *"[a-z_]*"' | sort | uniq -c
# CC:验证 attachment 总开关
CLAUDE_CODE_DISABLE_ATTACHMENTS=1 claude -p "explain @README.md"
# @-mention 不再展开为文件内容,模型只看到字面文本Harness 接入建议(Yoda 实践)
- 「上下文里有哪些文件」面板:CC 侧不可直接读进程内 readFileState,但可从会话
.jsonl转写流重建——累计 Read/Edit/Write tool_use 与 attachment 事件即可得到等价集合(Yoda 已解析转写流,增量成本低)。Codex 侧没有状态可读,只能按 shell 命令启发式提取(cat/sed/rg 目标路径)[推断],UI 上应标注「推断值」。 - 喂文件给会话的正确姿势:CC 用 @-mention(带
#L区间最省 token),或直接把内容塞进首条消息;Codex 把「请先读 X 文件」写进 prompt 比预读内容贴进去更符合其原生路径(模型读到的内容会带正确的截断与新鲜度)。 - staleness 提醒是 Yoda 的补位机会:Codex 无变更重注入——Yoda 在检测到工作区文件被外部修改(如用户在编辑器里改了 agent 读过的文件)时,可主动注入一条提示消息,复刻 CC
changed_files的价值;对 CC 则无需重复造轮子。 - 压缩前后审计:CC 压缩日志中 post-compact 文件恢复有明确事件(
tengu_post_compact_file_restore_success/error,compact.ts:1444-1445);Yoda 的会话时间线应标出「压缩点 + 恢复了哪些文件」,帮用户理解为什么模型突然「忘了」某个没被恢复的文件。 - 大文件防线:CC 单文件恢复限 5K token、Codex 工具输出按模型 policy 截断——Yoda 投喂文件前自行做 token 预估并提示拆分,比事后发现被静默截断好。
失效条件
- CC 调整 readFileState 容量(100/25MB)或 post-compact 预算(5 文件/50K)——这些是裸常量,版本间易变
- CC attachment 流水线重构(attachments.ts 体量已近 4000 行,
relevant_memories已迁出至 prefetch,更多迁移可能发生) - Codex 引入专用文件读工具或文件状态层(若
tool_mode/ code-mode 演进改变「shell 即一切」假设) - Codex TUI mention 升级为 harness 侧内容注入(
UserInput::Mention已有app:///plugin://结构,扩到file://即语义反转)
参考资料
- CC 源码:
src_2026-03-31/utils/attachments.ts(getAttachments:743、processAtMentionedFiles:1894、getChangedFiles:2063、extractAtMentionedFiles:2757)、utils/fileStateCache.ts、services/compact/compact.ts(:123-124、:518-521、:1399-1465) - CC 文档:
claude-code-docs/docs/memory.md:425-427(压缩后指令存活)、docs/context-window.md - Codex 源码:
codex/codex-rs/protocol/src/user_input.rs、tui/src/file_search.rs、models-manager/src/model_info.rs:41-53、core/src/compact.rs:298-303、core/src/context/environment_context.rs