我是在一个很普通的周末开始较真 context-mode 这事的。那时候我在维护一个编辑器侧的 AI 辅助插件用户反馈最多的不是“模型不够聪明”而是“同一个问题换个文件问答案完全不一样”。这种情况只要你去做代码补全、智能问答、文档摘要都会遇到。问题不在大模型而在我们把它叫醒之前给了它什么样的上下文。后来我把这一整套处理机制封装出来内部代号就叫 context-mode。context-mode 说白了就干两件事决定哪些信息可以进入模型视野哪些信息必须拦在外面。这个决定比调模型参数更能影响回答质量。无论你是做代码辅助工具、智能客服还是内容总结只要想让 AI 输出贴合当前场景都绕不开这套机制。这篇东西我会按自己的真实项目路径来讲它解决什么问题、上下文怎么收集组织、落地步骤、踩过的坑、效果验证以及从单文件走向跨仓库时我做的调整。1. context-mode 到底在解决什么问题1.1 从“上下文缺失”这个高频失败场景说起在真实的编辑器工具里上下文缺失的失败现场有很多我归类之后发现主要是四类。第一类是改错同名对象。用户在文件 A 里说“帮我把这个函数改成异步”项目里有两个同名函数分布在 A 和 B模型拿到的上下文只包含 A 的片段它却把 B 里的同名函数改了。这不是模型笨是它根本没看到“文件 A 里这个函数”这个锚点。第二类是做跨文件重构时只盯着当前文件。让模型重构一个接口定义它把定义文件改了但所有调用方的文件不在上下文里结果就是全局编译崩溃。第三类是重复造轮子。模型建议写一个工具函数其实项目另一个模块里早就有了只是那段代码没有进入上下文。第四类最隐蔽同一个问题在文件 A 下问能得到正确答案切到文件 B 再问同样的话回答就完全跑偏。这说明模型每次都在“换一副眼镜”看问题这副眼镜就是上下文。这些现象背后其实有两种问题上下文缺失和上下文错误。缺失好理解就是该进的文件没进错误则是不该进的旧文件、废弃文件反而进去了。context-mode 作为一个独立模块来处理这些问题核心原则只有一条宁可少给不给错给宁可慢一点也要把最相关的片段放在最前面。1.2 它和“加大上下文长度”不是一回事有一个认知坑必须先拆掉。看到某个模型支持很长的上下文窗口就把整个项目全塞进去让模型自己去找。我实测下来这种做法的效果并不好。模型确实能接收这么多 token但它不会像人那样自动去翻查旧文档里的关键位置无关信息多了以后注意力被稀释结果往往是从长篇堆料里挑一个概率最高的错误答案。我当时做过一组对照实验。在一个重构任务里只加两个相关文件进上下文任务成功率能从接近零提升到 80% 左右再多塞二十个无关文件成功率又掉回 60% 附近。这个数据当然不严谨但它说明一个直觉是对的context-mode 的核心永远不是容量而是对候选内容做的筛选和排序。支持更长上下文的模型是“能装更多”而 context-mode 是“装得准”。前者是能力上限后者是使用策略两码事。1.3 context-mode 的五个关键决策点把概念落到工程上我在设计 context-mode 时把它拆成五个关键决策点每个点都能单独讨论很久。候选内容从哪些来源产生项目文件、对话历史、外部文档、依赖索引。用什么规则处理时效性和权威性文件改动时间、用户锁定标记、来源优先级。怎么表达“当前用户意图”与上下文的关联靠关键词、向量语义还是让用户显式声明范围。上下文达到预算上限后怎么办直接截断、摘要压缩还是分层存储按需展开。用什么指标证明这套机制确实起作用命中率、回答稳定性还是用户返工率。后面每一章基本都是在展开这五个问题。你会看到没有一个答案是唯一正确的能落地的答案都跟你的使用场景强相关。2. 上下文从哪里来静态收集与动态推断2.1 静态收集给对话装一份环境地图静态收集不调用大模型只靠代码分析和文件系统完成。我在项目里维护了一个叫 context-map 的结构化元数据内容包含仓库目录树、当前文件依赖关系、接口签名、最近修改时间和配置项说明。这份数据会在每次会话开始时生成相当于在模型面前铺开一张“环境地图”。context-map 的数据格式大概是这样的我用 JSON 存{ repo: content-service, updated_at: 2025-01-18T09:30:00Z, modules: [ { path: src/parser/parser.go, exports: [ParseConfig, LoadSource], dependencies: [src/scanner/scanner.go] } ], recent_changes: [ { file: src/parser/parser.go, reason: 调整错误处理分支 } ] }这份 context-map 会被压缩成一小段 JSON 放进系统提示词里。它不占太多 token但能帮助模型理解仓库结构和模块边界。更大的文件内容不会堆在这里而是走动态检索按需取用。静态收集的优势是稳定、可复现每次拿到结果一致劣势是实时性差文件改了但索引没刷新时容易带出旧信息。2.2 动态推断根据当前操作方向选择相关片段动态推断是 context-mode 真正有意思的部分。它采集的信号包括当前光标位置、当前打开的文件列表、最近一次保存记录、用户正在输入的关键词以及用户主动拖进“关注范围”的文件。采集到信号以后系统对候选片段做评分按分数决定顺序。评分函数一般分两步先做关键词匹配再用向量相似度做一轮排序。动态推断最容易踩的坑是误选。用户在文件 A 里问“这个函数怎么优化”系统却把同名但更复杂的 B 版本拉进来回答方向自然就偏了。为了缓解这个问题我加了“人工锚点”机制用户可以把某个文件固定住锁定后它的优先级直接提到最高不再受评分结果影响。这个改动很小但实际使用率很高因为它把控制权交还给了用户也给了 context-mode 一个明确的纠错入口。2.3 静态与动态如何配合静态和动态不能单独用。只做静态信息新鲜度跟不上只做动态响应慢且结果无法复现。我最终采用的组合是“静态索引打底动态信号负责调排名”。具体流程是静态侧从 context-map 生成一组基础切片这组切片内容相对稳定动态侧根据当前光标位置、最近文件变动和用户输入对基础切片重新排序再补入语义检索命中的新片段。我整理过一个简单的对照表给自己团队用纯静态模式可复现性最好实时性差适合文档问答和离线分析场景。纯动态模式实时性好可复现性差适合在线对话编辑但容易飘。混合模式两者折中既有稳定骨架又能感知新鲜信号推荐作为默认配置。混合模式的代价是多了一层缓存管理。改文件时要把基础切片、索引和动态排名同步刷新否则会出现“一边新一边旧”的撕裂状态表现就是模型拿到新旧两版代码互相打架。3. 把 context-mode 落到 AI 编码助手里的具体步骤3.1 上下文组装的基础链路上下文组装可以统一成一条链路收集候选 → 过滤噪声 → 相关性评分 → 按预算裁剪 → 拼接成最终 prompt。每个环节都能单独优化但顺序别乱。我项目里的实现大致是这样的伪代码def build_context(user_input, active_file, session): candidates rule_recall(user_input, active_file) semantic semantic_recall(session.embedding(), top_k8) merged merge_dedupe(candidates, semantic) ranked rerank(merged, active_file) context budget_allocator(ranked, session) return context我坚持用两步召回而不是只靠向量检索。第一步是规则召回根据当前文件路径、用户关键词、最近文件变动从索引里筛出候选文件。第二步是语义召回把最近一段对话向量化跟切片索引做相似度排序混入前一步候选集。两层结果去重归一化后再交给预算裁剪。为什么非要规则召回因为向量检索有时会给出语义相近但当前任务完全用不上的文件规则召回能保证稳定基线。比如用户只改了配置项关键词“配置”就能稳稳召回配置文件不需要语义模型参与。反过来语义召回能弥补规则之外的相关片段比如用户说“把这段改成流式处理”模型能从索引里找出工具库里的流式函数。两者结合是追稳妥的组合。3.2 窗口不够时的对策摘要、分层、压缩上下文窗口在大型项目里永远不够。常用策略有三个直接截断、摘要重写、分层存储。直接截断最简单但因为后文覆盖前文重要信息容易丢在最后。摘要重写会额外消耗 token 和时间但能保住逻辑主线。分层存储是我最推荐的做法把仓库按“仓库 - 模块 - 文件 - 函数”四个层级组织构建 prompt 时只动态展开与当前任务相关的路径或函数其他层停留在摘要里。我用在插件里的预算比例是系统提示与指令占 15%当前任务相关的代码片段占 60%会话历史压缩后占 15%剩下 10% 预留给模型输出。代码续写类任务会把代码片段提到 80%问答类任务会把会话历史调高。预算必须做成显式参数而不是把数字写死在逻辑里否则每次调优都要翻代码。3.3 检索增强让 context-mode 拥有外置记忆当项目大到连规则召回都覆盖不了的时候就该上外置记忆——本质是一个持续维护的索引。我把代码文件按函数、类、文档块切分每段控制在 100 到 300 行之间写入索引并保存元数据例如所在路径、修改时间、依赖关系。检索时把用户问题向量化与索引里的向量做相似度排序返回 top 结果。从小仓库起步时不需要复杂架构。最初的实现只需要一个目录级索引文件和一份向量库文件在文件保存时做增量更新。仓库变大后把索引按目录分片检索时先定位到相关目录再在分片内做向量排序避免全库扫描。索引构建不是一次性的。启动时做完整构建运行中监听文件变动事件对变更片段做单点更新。千万不要每次请求都重新扫全库那样会卡掉整个编辑器的响应。增量更新的细节值得做好文件保存时触发、校验和比对、过期段落标记这三件事缺一不可。4. 我踩过的三个坑以及对应的止损方案4.1 坑一上下文污染导致回答自相矛盾第一次把大量文档塞进上下文后模型开始自相矛盾。有一次用户反馈插件在改动一处配置时反复引用旧版配置无论怎么强调新配置都没用。排查链路是这样的先看请求日志发现旧配置的文档片段确实进入了上下文再查索引更新时间发现旧配置对应的索引一直没有被清理最后定位到是静态段更新策略的问题。静态收集基于目录树做全量快照旧配置文件在目录树中还存在但我们在代码移动后没有触发索引同步于是旧片段一直留在上下文里。止损方案分三层第一给每个上下文片段打上时间和来源标签在 prompt 里注明“这段来自 2025 年 1 月 15 日可能是过期内容”。第二组装时做冲突检测如果两个片段内容明显冲突且其中一个是过期时间偏向新片段或直接移除冲突片段。第三后台定期清理任务把已删除或失效的文件从索引中标记出来而不是一直保留。4.2 坑二陈旧上下文导致越改越错第二个坑和污染不太一样污染是内容互相打架陈旧是模型拿到的是用户改动前的旧版本。用户在编辑器中改了一个函数还没保存就来问“这个函数现在还会死循环吗”上下文里还是旧版本模型给出判断自然全错。这类问题的根因通常是文件监听没有覆盖所有变动类型。有些插件只监听磁盘变化不监听内存中的 buffer用户还没保存时对话拿到的就是磁盘上的旧内容。我们的应对是同时监听 buffer 和磁盘保存事件每次构建上下文前对“脏文件”用内存内容刷新对应片段。另外在每个片段旁记录校验和一旦一致性被破坏立即触发刷新。这样能保证上下文始终代表用户眼前最新的工作状态。4.3 坑三上下文增加带来不可控的成本上涨第三个坑很现实成本。上下文变大后 token 用量上升接口调用延迟也随之变高。刚开始我把候选文件全部填进上下文单次请求的 token 数直接起飞用了几周发现不行。后来把预算管理改成显式分层每个会话设一个总 token 上限。每个内容类型有单独配额代码片段、历史、指令不能互相侵占。配额不够时优先合并同类片段做摘要优化而不是简单截断。每次请求记录 token 消耗和候选命中数形成一条成本曲线。还有一个经验是区分定位和生成两阶段。定位阶段用轻量模型判断哪些片段值得进入重读范围生成阶段再用更重的模型。这种两级方案能省不少 token同时保证关键信息不丢。5. 效果验证与日常观测怎么知道 context-mode 有没有起效5.1 三层观测信号context-mode 做得好不好不能光靠“感觉回答聪明了”。我看三层信号质量层、成本层、交互层。质量层看回答是否命中、是否正确引用关键文件、是否出现前后矛盾。成本层看 token 消耗和平均延迟。交互层看用户是否会反复修改 prompt、是否频繁删除上下文、是否总在同一类问题上提问。三层信号合在一起才能判断某个失败案例是 context 的问题还是模型本身的问题。实现观测不需要很复杂。在请求入口给每次调用记录一份上下文快照包含候选文件列表、被选中片段、预算分配、最终 token 数。之后任何用户反馈都能回溯。这份快照会占一点存储但对排查问题非常值得。5.2 一组简单的回归测试集我在项目里维护了一组回归测试集规模不大但每种典型任务都会覆盖到。我把它限制在 15 个场景左右每个场景标注预期应该进入上下文的文件列表这样每次改 context-mode 的配置后只要跑一遍就能知道调整是不是正向的。表格大概是这样的任务类别预期命中文件实测命中任务成功率修改函数签名并同步调用方定义文件 调用方文件通过通过在 A 文件查找 B 文件的定义B 文件通过通过根据多个文件重构接口接口文件 实现文件 调用方遗漏调用方失败从调用链定位异常入口入口文件 中间层文件通过通过这份回归集的价值在于给后续优化一个锚点。没有回归集任何配置调整都是凭感觉有回归集才能量化说“这次调整提升了多少”。5.3 从观测结果反推配置有一个版本我把关键词命中权重调得非常高回归集里命中率明显上升但真实用户反馈回答质量反而下降。原因很典型太看重关键词把大量“名字相近但语义无关”的文件拉了进来。之后我改成“关键词召回候选 语义重排”的双通道。先用轻量关键词方法召回一批再用向量相关性对这一批做重排而不是完全看命中次数。这个经验适用于很多场景当某个中间指标上升而产品体验下降时很可能是这个中间指标本身定义错了。命中率不是目标回答质量才是。6. 再往前一步从单文件上下文走到跨仓库上下文6.1 跨仓库索引需要准备的扩展单仓库项目用前面那些步骤就够了。但一旦插件面向的是微服务或多仓库项目context-mode 的边界就被拉到了跨仓库场景。核心变化是“调用关系不在同一文件内甚至不在同一仓库里”。这时候我额外维护了两张表。第一张是“文件路径到所属仓库”的映射告诉模型某段代码属于哪个服务第二张是“符号到定义位置”的符号表当用户在某个仓库里对某函数提问时系统通过符号表找到真实定义所在的仓库和文件。跨仓库还会引入权限问题有些仓库对当前用户不可见。我的处理是把跨仓库信息分为“可见”和“摘要”两类摘要只包含接口签名和一句描述不包含实现内容。这样既能保护隔离也能让模型理解依赖关系。6.2 交互层也要跟着改跨仓库场景下自动收集的上下文可能太广用户会不信任。我在界面里加了一个“上下文参与集合”的展示当前对话究竟用了哪些文件、哪些仓库用户点开就能看也能手动移除某个片段。这本质上是在调整 context-mode 的评分把内部分数变成用户可理解、可干预的对象。另一个重要的交互调整是任务范围声明。允许用户在对话开头补一句“本次任务只在支付服务范围内”系统就把检索范围限制在对应仓库集合里。不要指望模型自己理解用户的“范围”概念检索范围先收窄它的回答自然会稳定。这个交互看起来只加了一句话但对跨仓库场景的效果提升非常明显。6.3 我下一版想继续实验的方向当前版本已经完成了从零到能用的阶段。下一版我准备在“结构化记忆”上投入更多时间把常见的代码变更模式保存为长期记忆让 context-mode 在多次重构时能依赖以前会话的结论而不是全量重读历史。这类方向有风险历史记忆很可能跟新代码不一致副作用比收益更明显。我的初步方案是给每条记忆加有效期字段在代码变更时对相关记忆项做失效处理。这个改动还在实验中不算稳定结论但如果你也在做类似的事可以从这个角度切入先把“记忆如何失效”想清楚再考虑“记忆如何存储”。如果你也在做类似的能力我的建议是先别急着调模型参数。把 context-mode 的配置和上下文快照打开把每一次失败当场看一遍绝大多数问题在进入模型之前就已经注定了。把上下文管好模型输出质量会自然跟上。