如果你问我在设计 context-mode 之前踩过最大的坑是什么我会说是给模型喂太多上下文。当时我维护团队内部的 AI 编码辅助工具 ctxctl所有人包括我自己都默认一个朴素逻辑模型很聪明上下文越多就越懂项目。于是每次请求都塞进十几个文件、一整套架构说明结果模型反而越来越笨改 A 文件时删掉 B 文件的无关代码回答前言不搭后语甚至凭空造一个不存在的函数。被折磨了一整周之后我决定把上下文的组织方式当成产品来设计于是有了这套 context-mode 机制。这篇文章是我对这大半年的完整复盘包括三种模式的定义、落地细节、实测数据和一次惨烈的线上事故排查。1. 为什么上下文越全越好是个陷阱1.1 最初的做法把整个仓库都塞给模型ctxctl 刚上线的时候设计思路特别简单粗暴。既然模型的上下文窗口足够大那就把能收集到的信息全部拼进 prompt仓库 README、目录树、核心模块代码、CI 配置、甚至一些过期的设计文档。第一次跑通时效果确实惊艳它能够准确说出项目里模块之间的依赖关系还能给出一份像模像样的重构方案。团队里的同事都很兴奋觉得以后技术解答、代码审查都可以交给它了。但这种兴奋只维持了几天。随着接入的成员变多问题开始集中爆发。最直观的是费用和速度一次请求经常要消耗几万 token响应时间动辄半分钟以上几乎没法在日常开发流程里顺畅使用。更让人头疼的是质量问题。明明喂了比之前多几倍的材料回答反而频繁偏离需求甚至开始出现非常基础的错误比如把一个正在使用的接口判断成废弃接口或者在一个纯后端项目里推荐前端状态管理库。我一开始以为是模型能力不够换过好几个版本问题依旧。后来静下心来对比不同 prompt 的输出才意识到问题不在模型而在我自己的输入组织方式。1.2 三个结构性原因注意力稀释、信息打架、信噪比太低后来我总结这种退化是结构性的不是错觉主要有三个原因。第一注意力被稀释。长上下文模型不是对每个位置都一视同仁大量无关代码混进来之后真正重要的函数会被淹没在中间地段。学术界有所谓 lost in the middle 的观测我在业务里体验得更直接模型记得清楚 README 开头写了什么却忽略了紧挨着 bug 的那个关键文件。第二信息互相打架。仓库里废弃接口和新接口经常并存全量注入等于把所有矛盾都摆到模型面前。它看到两个同名但实现完全不同的函数时只能靠猜而猜的结果往往是最糟糕的那个。有一次它甚至把两个版本的代码各取一半拼在一起产出了一个既不兼容旧版、也不符合新版的中间态。第三信噪比太低。一次代码修改真正依赖的信息往往只有几百行其余都是背景噪声。噪声越多模型下判断的置信度反而越低。这就像开会时一屋子人同时说话你筛不出哪句话跟当前议题有关。我整理过一组在项目里记录的近似数据。同样是修复一个 JSON 解析相关的 bug把全仓库关键文件都塞进去和只注入 git diff、当前文件、直接依赖文件对比差异非常明显维度全量上下文精准上下文输入 token 数约 15000约 3200平均响应时间约 35 秒约 9 秒首次修改正确率约 50%约 80%是否经常误删无关代码经常很少这组数据不是严谨的对照实验但趋势在各个任务里都稳定复现。用一句话总结上下文不是免费的它有成本而且成本不只是钱还有模型的注意力、回答的稳定性以及我们反复返工的时间。所以我在 ctxctl 里加了一个叫 context-mode 的模块本质就是一套上下文路由策略任何一次调用模型前先明确这类任务需要什么信息再按这个判断去收集、过滤、压缩、组装上下文。它要做的事情是主动地、可配置地管理边界而不是把一切都丢给模型自己判断。2. context-mode 的三种核心形态全局、局部、混合在设计 context-mode 时我没有一上来就搞复杂的规则而是先按开发场景把任务分成三类需要理解全貌的方案类任务、需要精准改动的局部任务、以及二者兼顾的跨文件任务。对应下来就自然有了三个档位。2.1 global mode先看全局再谈具体global mode 的目标是让模型看到一个项目的骨架。适合什么时候用比如拿到一个新需求需要先知道当前技术栈、模块划分、已有约定然后输出一份技术方案再比如你对一个老项目完全陌生想让模型先讲讲架构。这种任务的信息需求很宽泛精确到某个文件反而没用。在这个模式下我会注入四类内容项目目录树、技术栈清单、架构文档摘要、编码规范要点。代码文件一般不直接塞全文只保留关键模块的职责说明。原因是方案类任务需要的是结构和关系不需要把每个实现细节都暴露出来如果连实现细节都给了模型就容易陷入具体代码里反而忽略了方案层面的取舍。配置长这样modes: global: inject: - PROJECT_TREE - TECH_STACK - ARCH_SUMMARY - CODING_STANDARDS max_tokens: 6000注意 max_tokens 一定要卡住不能无限扩展。global mode 很容易膨胀不设上限的话各种文档摘要迟早会把请求撑爆最后又回到前面说的全量上下文老路。2.2 local mode把上下文锁死在一个小范围local mode 是我日常使用频率最高的档位适合修 bug、加个小功能、补测试这类改动范围明确的任务。它的核心思路是不要试图让模型了解整个项目只给它和这次改动事实相关的文件。什么算事实相关当前文件、当前文件的 git diff、直接 import 进来并且被改动影响到的文件、以及跑一遍就能确认结果的单测文件。这里有一个容易误解的地方local 不等于只给一个文件。如果改 A 函数会影响 B 模块的调用方却不把 B 的接口签名放进去模型同样会瞎猜。local 的关键是沿着依赖关系走一层或两层而不是粗暴地砍到只剩一个文件。具体走几层取决于仓库耦合度我在 ctxctl 里默认走一层复杂项目会走到两层。local mode 的配置modes: local: scope: git-diff include: [**/*.go] exclude: [**/*_test.go] dependency_depth: 1 max_tokens: 3000我特意把测试文件从主上下文里排除因为修 bug 场景下测试结论可以单独用一条工具调用去获取不需要整段塞给模型如果直接把整组测试代码塞进去prompt 又容易膨胀反而稀释了主要矛盾。2.3 hybrid mode全局摘要配局部全文还有一类任务最尴尬既要懂全局又要动手改局部。典型的就是代码审查和跨文件重构不看整体架构就没法判断这次改动是否合理不看具体代码又提不出行级别的意见。这种场景我把它切到 hybrid mode。hybrid mode 的思路是两个层次分开处理全局信息全部压成摘要只留目录树、模块职责、被改动文件涉及的公共接口局部信息保留全文包括本次改动的文件、diff、以及新增测试。这样模型左手握着地图右手拿着放大镜既知道自己在项目里的坐标又看得清地面的纹理。我给一个简化的注入结构{ global_context: { project_tree: ..., module_responsibilities: {}, changed_files: [pkg/api/handler.go] }, local_context: { diff: ..., full_files: [pkg/api/handler.go], related_tests: [pkg/api/handler_test.go] } }这组设计和 prompt 模板是配套的不是简单把内容拼起来就行。关键是要让模型明确知道哪些是背景哪些是事实依据哪些是它必须专注处理的改动。最后用一张表总结三种模式的差异模式适用任务注入重点典型 token 量global方案设计、架构梳理目录、技术栈、文档摘要6000-9000local修 bug、小需求diff、当前文件、直接依赖2000-4000hybridreview、重构、跨文件改动全局摘要 局部全文4000-70003. 落地一套最小可用的 context-mode 构建管线模式定义只是第一步真正难的是把它落地成一条可靠的构建管线。ctxctl 里这段逻辑我重构了好几版最终结构可以总结为一句话先收集、再过滤、再分级、再压缩、最后注入。下面按这个顺序拆开讲。3.1 先用一份 YAML 把模式定下来配置最终沉淀成了一份 YAML每个模式有独立的规则块。选 YAML 而不是直接写在代码里是为了让非 Python 背景的团队成员也能改。尤其是前端同事看 JSON 容易纠结嵌套看 YAML 反而觉得像写样式表。modes: global: inject: [PROJECT_TREE, TECH_STACK, ARCH_SUMMARY, CODING_STANDARDS] max_tokens: 6000 local: scope: git-diff include: [**/*.{ts,tsx}] exclude: [**/*.test.{ts,tsx}, **/node_modules/**] dependency_depth: 1 max_tokens: 3000 hybrid: scope: git-diff include: [**/*.{ts,tsx}] exclude: [**/node_modules/**] dependency_depth: 2 global_summary: true max_tokens: 6000include 和 exclude 用的是 gitignore 风格的 glob选这种方案纯粹是因为大家熟没有学习成本。scope 字段决定收集文件的起点git-diff 表示只关注本次变更涉及的文件。dependency_depth 决定依赖分析向上游走几层数值越大信息越全但上下文也会指数级膨胀默认值一般不要超过 2。3.2 上下文构建的五步管线整个构建管线是纯函数式的输入是仓库当前状态和 mode 配置输出是一个结构化的 prompt 对象。这样设计最容易测试也方便在请求日志里定位问题。第一步收集候选文件。local 和 hybrid 模式会先执行 git diff拿到变更文件清单。然后对每个变更文件做一次依赖解析把 import 关系往外扩一到两层。global 模式不跑 diff直接读仓库的目录树和文档索引。第二步过滤。这里不只是过滤 node_modules、dist 这类目录还要过滤敏感信息。ctxctl 内置了一个规则库像 .env 文件、包含密钥字样的配置、内网地址段都会直接挡掉。技术性过滤只需要处理 glob 规则安全过滤会更严格下面第六节还会专门展开。第三步分级排序。我会给每个候选文件打一个优先级标签P0 是本次直接改动的文件P1 是依赖分析后认为强相关的文件P2 是只提供背景信息的文件。排序的作用是当 max_tokens 不够时可以按优先级从低到高逐个丢弃而不是随机截断。第四步压缩。这是最花心思的环节。P0 文件基本保留关键代码段P1 文件如果很长我会提取函数签名和导出符号列表而不是塞全文P2 文件只保留摘要。全局摘要也不是临时调一次模型就完事而是通过目录树加模块的 docstring 自动生成然后离线缓存起来。第五步注入。把压缩后的内容按模式模板拼成 prompt。这里我坚持结构化和非结构化混合文件内容保持代码块原样但文件之间的关系、改动范围这些元信息用一个 YAML 块放在 prompt 顶部。模型对这种格式的解析稳定度比纯散文高得多实测下来可以让它更准确地区分背景信息和待处理对象。3.3 缓存失效必须够狠管线里最容易出 bug 的地方是缓存。ctxctl 会对文件摘要、目录树、模块职责说明做离线缓存目的就是省掉重复调用模型生成摘要的成本。第一次实现时我偷懒只用文件修改时间判断缓存是否失效后来被坑得很惨。实际情况是git checkout、IDE 自动恢复、CI 机器上的残留目录都可能让文件内容大变而 mtime 保持不变。一旦摘要缓存没失效模型拿到的就是旧版本的事实自然会一本正经地胡说八道。这个问题值得专门写一段因为后面要讲的那次线上事故根子就出在这里。现在的实现是内容哈希和版本号双保险内容哈希用于单文件缓存判断版本号则记录当前仓库的 commit所有注入 prompt 的内容都会打上版本标签。如果 commit 没变内容理论上就不该变如果变了但哈希没变日志里会立刻暴露出数据源不一致的问题。4. 实测对比模式切对了效果提升立竿见影4.1 测试是怎么做的配置和管线都稳定之后我做了一轮比较系统的内部实测。目的不是发论文而是想用可复现的数据说服团队context-mode 不是多此一举。任务集从真实研发流程里挑了 30 个任务修 bug、新功能开发、代码审查各 10 个。模型统一用同一个版本temperature 固定为 0评价指标只有三个首次通过率、平均输入 token 数、平均响应时间。所谓首次通过率就是第一次让模型产出结果后不经人工修改就能合入或者提出的 review 意见被采纳的比例。这个指标很苛刻但能比较好地反映上下文质量。每个任务在 global、local、hybrid 三种模式下各跑一遍任务顺序打乱避免前后任务互相影响。4.2 实测结果结果比我预想的还要极端。下表是 30 个任务的平均值数字是近似值但趋势非常稳定任务类型global 首次通过local 首次通过hybrid 首次通过local 平均 tokenhybrid 平均 token修 bug5/108/107/10约 3100约 4400新功能开发7/106/108/10约 6500约 5200代码审查5/104/108/10约 5800约 4700global 模式在任何一个任务类型上都不是最优的这是个挺反直觉的结论。它输给 local 和 hybrid不是因为模型能力下降而是因为无关信息把关键信号盖住了。团队里原来坚持喂得越多越聪明的同事看到这组数据之后就不再坚持了。4.3 数据背后的选择逻辑仔细看会发现local 模式在新功能开发上的首次通过率只有 6/10低于 global 的 7/10。原因不复杂新功能开发往往需要理解现有模块的扩展点如果只盯着当前文件模型看不到全局设计约束容易写出风格不一致的代码。这种情况下 hybrid 的 8/10 反而是最稳的。修 bug 场景则正好相反绝大多数 bug 的根因就藏在几个函数里local 的高信噪比能让模型集中火力切到 global 之后模型反而容易被仓库里相似的函数名带偏。所以我在 ctxctl 里做了一个很小的默认路由逻辑根据命令类型自动选择模式fix 类任务默认 localplan 类任务默认 globalreview 类任务默认 hybrid。用户也可以手动覆盖但绝大多数情况下默认值是靠谱的。另外延迟上的差异同样可观。local 平均响应 9 秒左右global 普遍超过 25 秒。对命令行工具来说这直接影响了大家愿不愿意在 CI 里跑 AI 检查。我见过太多功能很好的工具因为慢最后被人从流程里关掉。5. 一次模型突然变傻的排查全记录5.1 现象模型开始删除完全不相关的代码有一天上午前端同事跑过来说工具疯了它只让模型改一个 JSON 序列化函数的错误处理逻辑结果产出里把另一个模块的一百多行业务代码全删了还加了一段莫名其妙的注释。我第一反应是模型抽风换了更强的模型重跑居然还有类似倾向。这时才意识到问题大概率出在上下文上。我打开请求日志发现那次请求使用的是 local 模式但 prompt 里的内容大小异常足足有 8000 多 token远超 local 模式 3000 的上限。这说明管线里的分级排序或缓存出问题了把不该注入的内容放进了 prompt。5.2 一步步定位到旧缓存污染排查链路我尽量还原方便你遇到类似问题时对照排查。第一个动作是导出完整请求上下文把 prompt 原样打印出来。我看见 local 模式里注入了一个叫 parser_v1.go 的文件但当时仓库里根本不应该有这个名字的活跃文件。再细看内容函数签名和当前代码风格明显不匹配几乎可以断定是旧版本的内容。第二个动作是查缓存失效逻辑。ctxctl 的摘要缓存记录里有 parser_v1.go 的条目缓存 key 是文件路径加修改时间。我跑到 CI 机器上检查发现工作目录虽然是从 git 重新 checkout 出来的但缓存目录是复用的里面残留了上次构建的旧摘要。文件路径没变新的 checkout 又把 mtime 重置成一样的时间于是缓存判定有效旧摘要就被塞进了 prompt。第三个动作是重新审视 diff 的收集范围。那个前端任务在 git diff 里根本不涉及 parser_v1.go但它被依赖分析带了出来。依赖分析从当前文件出发import 链里确实引用过这个旧文件而旧文件的内容摘要已经是过期的。5.3 修复方案与复盘结论修复分成两层。第一层是缓存失效策略改成内容哈希为主、mtime 为辅并且每次构建 context 前先确认 git commitcommit 不同就整组缓存全部作废。第二层是 prompt 内容增加版本标注所有注入的文件片段都带上它在仓库里的 commit 号模型读到不一致的信息时至少有机会意识到这里描述的不是当前代码。这个事故给我的教训是context-mode 这个系统里最危险的不是模型而是数据源。模型的幻觉还能靠提示词纠正数据源的旧版本一旦混进去模型只是在理性地基于错误事实进行推导输出看起来无比合理实际上全是错的。所以我把单一可信来源写进了 ctxctl 的设计文档此后所有注入内容都必须能追溯到具体的 commit 或者运行时计算的结果不能依赖任何无版本的缓存。6. 让 context-mode 更聪明的几个补充机制6.1 按需加载让模型自己决定看哪个文件管线跑顺之后我意识到 context-mode 还可以更激进一点。在超大仓库里即使 local 模式只走两层依赖文件数量也可能爆炸。于是我在部分命令里实现了按需加载第一轮只注入变更文件和一个候选文件清单让模型自己判断还需要看哪些文件第二轮根据它的选择只注入这些文件。这等于把上下文路由的判断权分了一部分给模型。实测下来这种方式在大型 monorepo 里特别有效token 消耗常常能再降一半。代价是多一轮交互延迟变大所以它没有被设成默认而是作为一个手动参数。如果你也在做类似工具建议先别把它做成全自动否则排查问题时会多一个变量。6.2 结构化注入JSON 比散文更不容易读歪我在 hybrid mode 里用 JSON 结构组织全局和局部信息这不是炫技。同样是描述本次改动涉及某模块和某接口散文容易让模型把注意力放在修饰词上而结构化字段能让它直接按 key 取用。简单说JSON 给了模型一个清晰的文件柜让它知道什么东西放在哪个抽屉而不是把一堆文件散落在桌面上。当然结构化也不是越深越好。我试过三层嵌套的复杂 JSON模型反而开始困惑。目前比较稳的是两层外层是角色分类内层是具体内容。超过两层就建议拆成多个工具调用或多次请求而不是强行塞进一个 prompt。6.3 检索增强把文件清单变成召回结果硬编码 include 规则在快速变化的大型项目里会过时。后来我在 ctxctl 里接了一个轻量级检索层先用关键词和简单的 embedding 召回与任务描述最相关的 top-k 文件再把这些文件作为候选输入交给构建管线。召回结果和 git diff 结果是两个独立来源做一个简单的求并集再由分级排序决定谁进 P0、谁进 P1。检索增强不是替代 context-mode而是它的上游。mode 决定的是最终给模型看什么的形态检索决定的是候选集合从哪里来。把这两个职责分开代码思路会清晰很多。硬耦合在一起后面想改其中任何一边都会牵一发动全身。6.4 安全过滤必须前置最后说一个容易被忽视的点。context-mode 的过滤步骤我建议把安全规则放在所有模式的最前面而不是等每个模式自己处理。比如密钥、内网地址、个人信息这些内容在任何 mode 下都不应该进入 prompt。ctxctl 的做法是构建管线第一步就做全局脱敏命中规则的文本直接替换成占位符并且保留审计日志。这一层的前置能省掉很多麻烦。曾经有个同事在本地配置里放了云厂商的密钥如果当时安全过滤放到了 local mode 的可选开关里这个密钥大概率就跟着请求发出去了。现在强制前置就算配置写错也会在源头被挡住。做 context-mode 这大半年我最大的感触是上下文管理不是给模型省 token 的小技巧而是一种工程纪律。它逼着你去想清楚每个任务真正依赖什么信息哪些是噪音哪些必须保证最新。如果你也在搭类似的东西我的建议是从 local 模式起步把 git diff 和依赖分析做好再慢慢加摘要、检索这些进阶能力缓存和版本标注从一开始就要做否则迟早会在数据源上踩坑。希望我的这些记录能让你少走几段弯路。