光标停在一个一千多行的 Service 文件里你想让 AI 解释一下某个函数在极端输入下为什么会抛异常。你把问题发出去它却盯着文件头部的装饰器分析半天甚至建议你去改另一个模块的代码。这种答非所问我遇到过太多次一开始以为是模型不行后来才意识到问题不在模型在于 context-mode 没设对。context-mode直译是上下文模式在 AI 辅助编程和大模型应用开发里简单说就是决定模型在生成回复之前能看到哪些代码、以什么顺序和粒度看到它们的一套策略。同一段代码同一个 prompt用全文模式喂和用聚焦模式喂效果可以差出一个数量级。这篇文章不打算空谈概念我直接拿自己折腾的一套 context-mode 模块来讲它解决了什么、主流的模式有哪些、怎么从零实现以及我踩过的几个坑。适合正在做内部 AI 编程助手、代码问答机器人、或者单纯想提升日常 AI 用码效率的工程师参考。1. context-mode 到底在解决什么上下文管理与任务失配的痛点1.1 一个每天都在发生的灵异事件AI 改错了别的文件我最早天真地以为AI 编码助手的效果上限取决于模型本身后来发现真正拉胯的其实是上下文管理。举个例子我维护了一个中型 Python 服务120 个文件、3 万多行代码。某次我想让 AI 帮我重构订单状态机里的一个函数为了让信息足够全我把整个 services 层都粘进了对话。结果模型把订单状态的例子全部替换成了用户状态的注释还在另一个文件里凭空加了几个无关的钩子。问题出在哪它看到了太多东西注意力被无关代码稀释了。人也是一样让你在五百页源码里找一个 bug你也不会从目录页开始逐行扫。所以 context-mode 要解决的第一个问题是给模型一个与任务匹配的视野解释单个函数时只给函数体、调用方、被调用的依赖做跨文件重构时再给接口定义和调用图回答项目级问题时给的是结构树和检索索引。视野不对能力再强也发挥不出来。1.2 上下文窗口的物理边界128k 到底够不够我刚开始设计上下文方案时觉得 128k 窗口总够用了吧但一算账就傻眼。主流模型标称 128k tokens实际上能安全使用的远低于这个数系统提示一般吃 500 到 2000对话历史越积越多输出还得预留大几千到一万多真正留给代码的往往只剩一半。而代码的 token 消耗比想象中快得多平均一行代码约 5 到 15 个 token一个 1000 行的文件轻松破万。也就是说一个 128k 窗口在留足输出和系统提示后塞 10 到 20 个中等文件就到头了。真实项目的体量呢我那个 120 文件、3 万行的服务纯源代码折算下来就有 40 到 60 万 token。哪怕只把和 main 分支强相关的文件全部加载也动辄 5 万到 10 万。这不只是成本问题还直接影响体验token 数量翻倍首 token 响应时间肉眼可见地拉长工具一旦没了即时感人就再也不愿意用了。所以窗口够不够从来不是硬性指标而是策略问题——必须在有限的预算里做精确取舍。1.3 核心认知上下文质量远比数量重要很多人遇到窗口不够的第一反应是换更大的窗口但我大量测试后的结论是上下文质量远比数量重要。我做过一次对比实验同一个函数、同一个审查任务分三组投喂第一组只给目标文件约 8k tokens第二组加一个强相关依赖文件约 12k tokens第三组再加三个看起来相关但并不重要的文件约 20k tokens。结果第三组的正确率不升反降甚至还出现了两处虚构的 API 名称。这和注意力机制的研究结论一致无关信息会稀释 token 间的注意力权重让模型更倾向于生成看起来合理但实际错误的内容。所以我把 context-mode 理解成一次信息预算管理——预算有限必须花在刀刃上。没有银弹只能在模式和场景之间寻找匹配这也是下文要展开的核心。2. 五种常用 context-mode自动、全文、聚焦、滑窗与分层的取舍2.1 自动模式把判断交给信号与规则自动模式是我日常使用时的默认选项。它不依赖用户手动指定范围而是通过一组信号自动决定加载策略当前光标所在文件、git diff 里改过的文件、文件最近修改时间、项目的模块结构以及用户提问里出现的符号名称。简单实现甚至不需要模型参与推理纯规则就能跑如果问题里出现了具体函数名走聚焦模式如果带着 git diff就按 diff 构建上下文如果什么信号都没有退回滑窗模式。自动模式的好处是不打扰用户开发者在编辑器里自然地问问题就行。坏处是信号判断错误时整个回答一开始就跑偏而且用户很难看出哪里错了——界面上一片正常但结果完全不对。我见过很多团队做自动模式只做一半把自动等同于全塞进去那其实是偷懒。真正的自动模式应该是一套完整的信号优先级系统判断逻辑越透明出问题时才越好排查。2.2 全文模式门槛最低翻车也最多全文模式就是把整个仓库或整个模块的代码全部塞给模型。对一个小工具、只有几个文件的项目来说这是最简单可靠的方案信息完整不会出现模型看不到某个定义的抱怨。但项目一旦超过几万行问题就接踵而至。第一可用窗口塞不下被迫截断按行截断会把代码截成语法残废模型对着半个函数猜越猜越离谱。第二就算硬塞进去了模型推理速度变慢还容易在无关信息里迷失。我现在对全文模式立了一条规矩只对 30 个文件以内、总代码不超过 2 万行的项目使用。超过这个体量强制走聚焦、分层或者检索方案。为什么是 2 万行按每行平均 10 个 token 估算2 万行大约是 20 万 token即便在 200k 窗口里也占到了九成以上几乎没有给对话和输出留余地。超过这个线再玩全文纯属给自己找不痛快。2.3 聚焦模式围绕锚点的强相关筛选聚焦模式是我做代码审查和单点修改时的主力。思路是给定一个锚点比如当前文件、改动区域、目标函数只把强相关的代码单元拉进上下文。强相关的定义我列了一个优先级清单直接 import 的依赖、被当前函数调用的函数定义、调用当前函数的上游调用方、同一模块里的兄弟组件。这套逻辑依赖静态分析我通常用 tree-sitter 解析出符号表和调用关系条件允许就再接一层 code graph。聚焦模式最省 token回答也最贴题代价是静态分析在动态语言上容易漏。Python 的鸭子类型、Ruby 的 metaprogramming都会让调用关系分析产生误判。所以我坚持在聚焦模式后面再接一个文本检索的兜底静态分析给不出依赖时用 embedding 搜索相关代码块两套机制一起上才能覆盖大多数情况。2.4 滑窗模式动态跟随光标的小窗口滑窗模式最像 IDE 里我当前能看到的代码。它不加载整个文件而是以光标或当前函数为中心加载前后若干行通常控制在几百行内。优势是极省 token、响应快非常适合解释单行表达式、生成局部测试、补全函数实现这类贴近光标的轻量任务。劣势也明显当模型需要参考窗口之外的类型定义或函数入口时它只能靠猜。我在自己工具里做了一点改进管它叫多级滑窗第一级是光标所在的 ±150 行第二级是在这 300 行里做一次轻量扫描如果出现未定义的标识符自动把该标识符的定义页拉进来相当于按需扩窗。这个改进让滑窗模式的可用性提升不少代价是每次光标移动时都要做一次符号扫描对索引速度要求更高具体的优化手段放在第 3 章讲。2.5 分层模式先看地图再放大到街道分层模式是我最推荐用来处理跨文件重构的方案。它模仿人类工程师上手新项目的思路第一层给项目结构树包括目录层级、模块职责说明、README 摘要、git 状态第二层给当前正在编辑的文件或本次要改的 diff第三层再按需拉取被调用函数、接口定义、相关测试用例。这样模型先在心里建起一张地图再根据具体问题把某一个区域放大到街道级别既不容易漏掉关键依赖也不容易在细节里迷失。实现上需要一套按需展开的机制用户或规则指定下一步要看哪个节点系统才去加载对应文件内容。分层模式最灵活但也最复杂判断该展开哪一层本身就可能出错。不过对于跨文件重构、新项目答疑这类复杂任务多花这点复杂度非常值。2.6 五种模式横向对比模式核心策略优点缺点最适用场景自动信号规则动态分发省心、免配置判断错时难排查日常问答、通用场景全文全量代码注入信息完整、实现简单窗口压力大、易迷失小项目、单文件分析聚焦静态调用图强相关筛选精准、省 token动态语言易漏依赖代码审查、局部修改滑窗以光标为锚点动态加载响应快、开销小跨文件能力弱编辑器内补全、单行解释分层结构树按需展开全局与细节兼顾实现复杂、调试成本高跨文件重构、新项目答疑3. 从零实现一个 context-mode 模块索引、打分、预算与增量更新3.1 第一步把仓库切成标准上下文单元实现 context-mode 的第一件事是把整个仓库拆成可以单独调度和打分的上下文单元。我实践下来用两级粒度文件级和符号级。文件级就是整个文件理解简单、粒度粗适合对整体做介绍符号级是函数、类、宏定义这些 AST 节点粒度细能精确控制喂多少代码但需要额外维护符号与文件、行号的映射关系。切分时我强烈建议用 tree-sitter不要用正则。tree-sitter 能稳定识别各语言的函数和类边界即使代码格式不规整也能正确切分。如果没有 AST 能力退而求其次可以用空行和缩进做启发式切分对 Python 这类缩进敏感的语言效果尚可对 JS、Java 就差点意思。切完之后我会建立一个符号映射表符号名映射到所在文件、起止行号、依赖列表、被谁引用。这张表构建一次之后靠文件修改时间做增量更新。这一步是整个模块的地基地基没打好后面的相关性和预算分配全是空中楼阁。3.2 相关性打分静态调用图优先embedding 兜底判断哪些上下文单元该进窗口时我按优先级用三层方案。第一层是静态调用图从锚点出发沿 import 和函数调用做一到两跳的 BFS命中即强相关必定注入。第二层是文本检索把用户问题做 embedding在预建的 chunk 向量库里做相似度搜索找出语义相关的符号和文件用来兜底静态分析看不到的隐藏依赖。第三层是启发式信号git diff、文件最近修改时间、同目录文件的血缘关系用来微调权重。这里有一条关键经验千万别只依赖 embedding。embedding 在语义相近时很好用但它不感知控制流文件里所有代码在它看来都是平均地相关。而调用图能告诉你因果关系A 调用了 BB 出问题会影响 A。我遇到过不止一次某个函数名和一个不相关的工具函数相似度极高embedding 打分冲进前三结果把 AI 带偏。后来我把静态调用图的权重固定调高embedding 只作为补充误报率才明显下降。3.3 token 预算分配给响应留出保命空间预算分配看着简单坑却最多。我目前的分配比例是系统提示和任务描述占 10% 到 20%核心上下文占 50% 到 60%辅助上下文占 20% 到 30%输出预留至少 10% 到 20%。这里的输出预留最容易被忽略尤其是生成代码或长文档时一旦预算被上下文吃干模型会在生成中段被硬截断你拿到的不再是完整代码而是半截函数加一句以下省略。计算 token 别用字符长度估算字符数和 token 数差别很大。要么用 tiktoken 处理 OpenAI 系模型要么用模型的 tokenizer 处理本地模型。我在项目里封装了一个 BudgetAllocator输入任务类型和锚点输出每个上下文单元允许占用的 token 上限。任务类型是重构时给核心上下文多留任务类型是问答时给检索结果多留任务类型是写长文档时输出预留直接提到 30%。3.4 增量更新与缓存别让模式切换产生明显延迟没有增量更新和缓存context-mode 只能活在 Demo 里。我最早写过一版每次请求都全量重建索引的代码在 120 文件的项目里平均上下文构建耗时 200 毫秒以上加上模型推理整个工具慢到没人愿意用。后来的优化我拆成三层第一层是索引层文件保存时按 mtime 增量更新符号表和 embedding第二层是向量层embedding 按文件加版本号缓存到本地数据库版本没变就不重算第三层是上下文构建层把锚点加模式加预算这几个关键参数做哈希短时间内的重复请求直接复用上一次构建结果。做完这三层优化上下文构建阶段稳定降到了 30 毫秒以内工具终于有了顺手的感觉。这一步很容易被忽略但它决定你的工具是停留在玩具阶段还是真正能进生产环境。3.5 最小可运行示例与调用方式下面是一个简化但能跑通思路的骨架方便你复现整个调度流程import os from dataclasses import dataclass dataclass class ContextUnit: name: str content: str token_count: int 0 related_score: float 0.0 class RepoIndex: 文件级 符号级索引启动时构建mtime 增量更新 def __init__(self, repo_path: str): self.repo_path repo_path self.file_index {} # path - (mtime, token_count) self.symbol_map {} # symbol - ContextUnit self.build_initial_index() def build_initial_index(self): # 用 tree-sitter 解析所有源文件填充 symbol_map pass def is_dirty(self, path: str) - bool: mtime os.path.getmtime(path) old self.file_index.get(path, (0, 0)) return mtime old[0] class BudgetAllocator: def __init__(self, max_tokens: int): self.max_tokens max_tokens self.output_reserve int(max_tokens * 0.15) def trim(self, units: list[ContextUnit]) - list[str]: units.sort(keylambda x: -x.related_score) budget self.max_tokens - self.output_reserve result [] used 0 for unit in units: if used unit.token_count budget: continue result.append(unit.content) used unit.token_count return result class ContextBuilder: MODES (auto, full, focus, slide, layered) def __init__(self, repo_path: str, max_tokens: int 8000): self.repo_path repo_path self.max_tokens max_tokens self.index RepoIndex(repo_path) self.allocator BudgetAllocator(max_tokens) def build_context(self, mode: str, anchor: dict) - list[str]: if mode not in self.MODES: mode auto if mode full: units self._collect_full() elif mode focus: units self._focus_by_symbol(anchor.get(symbol)) elif mode slide: units self._slide_by_cursor(anchor.get(file), anchor.get(line)) elif mode layered: units self._layered_build(anchor) else: units self._auto_dispatch(anchor) return self.allocator.trim(units) def _focus_by_symbol(self, symbol: str): units [self.index.symbol_map[symbol]] # 沿调用图 BFS 一到两跳追加相关单元 return units def _slide_by_cursor(self, file: str, line: int): # 加载 file 中 line±150 行并按需拉取未定义符号的定义 return [] def _layered_build(self, anchor: dict): # 结构树 - 当前文件/diff - 按需展开 return [] def _auto_dispatch(self, anchor: dict): if anchor.get(diff): return self._focus_by_symbol(diff) if anchor.get(symbol): return self._focus_by_symbol(anchor[symbol]) return self._slide_by_cursor(anchor.get(file), anchor.get(line))骨架里的核心流转只有三步拿到索引、模式分发、预算裁剪。在实际工程里你还需要根据所用模型的 tokenizer 替换计数逻辑不同模型的输出预留比例也完全不同本地 7B 模型和 128k 商业模型在预算策略上的差异非常大。4. 场景与模式匹配本地工具里的三次调优记录4.1 场景一新项目答疑从塞不下到够精准我优先想聊的是新项目答疑这个场景。团队里新同事刚接手一个服务第一句话通常是帮我讲讲这个项目大概是怎么组织的。最初我用全文模式硬塞结果 3 万行代码根本进不了 32k 窗口频繁截断之后模型给出的项目介绍错得离谱连模块职责都说反了。后来我切成分层模式先给项目结构树和模块职责摘要占约 4k tokens再把新同事当前打开的文件加进去占 2k 左右最后如果他追问某个模块再用 embedding 检索相关代码块。实测下来模型对新项目的描述准确率明显提升新人也能更快定位到自己的改动范围。这个调优的核心是先给骨架再补血肉。4.2 场景二PR 代码审查按 diff 构建上下文代码审查是聚焦模式的主场。早期我让 AI 审查整个 PR把改动涉及的文件全部塞进去模型经常被十几个文件的改动量吓到只给出泛泛的注意测试覆盖这类废话。后来我改成按 diff 构建上下文只取 PR 中每个文件的 diff hunk再为每个 hunk 补齐被调用函数的定义和直接依赖总预算控制在 16k tokens 以内。效果非常明显误报率降低AI 给出的 review 意见开始落到具体行号和具体风险点上。我在流程里加了一个手动补充入口审查者觉得 AI 漏了什么文件可以一键把该文件追加进上下文。这个允许人为干预的设计很重要因为 context-mode 再智能也不可能永远猜对人脑中的关联关系。4.3 场景三跨文件重构调用图两跳内全量注入跨文件重构是目前最难处理的场景之一。我在重构一个老的支付模块时涉及三个文件里的二十多处调用用聚焦模式会漏上游调用方用全文模式又塞太多无关模块。最后我采用的是分层加调用图的组合第一层注入当前要改的文件和它的直接依赖第二层沿着调用图往上追溯两跳找出所有会受影响的上游调用方第三层只加载这些调用方中与当前改动相关的函数体。这样的上下文组织方式和重构任务的真实依赖关系是吻合的。AI 拿到的不再是三个文件的所有代码而是改动点加调用影响面的最小完备集。那次重构里AI 提前指出一个我漏掉的兼容性问题某个上游模块在异常分支里直接访问了旧字段名。这就是调用图两跳的价值。4.4 可复用的配置参数速查表场景推荐模式总预算关键参数新项目答疑分层16k-32k结构树 按需检索单文件修改聚焦8k-16k当前文件 直接依赖跨文件重构分层 滑窗32k 以上调用图两跳 diff hunkPR 代码审查聚焦16kdiff hunk 相关函数日常问答自动8k按符号/文件/diff 信号分发补充一句不要为了追求信息全把每个模式的预算都拉到窗口极限。我实测下来窗口利用率在 60% 到 70% 左右时回答质量和响应速度的平衡点最好。一旦超过 85%速度和稳定性都会明显下降收益却几乎为零。5. 容易翻车的四个地方相关性误判、快照过期、性能与输出挤占5.1 翻车之一文件名相似把 AI 带偏有一次我问 AI 改订单状态机的一个函数embedding 检索出来的结果里某个工具函数因为名字里带着order_status相关字样相似度打分冲得很高被当成了强相关文件注入上下文。结果 AI 在这个工具函数上做了大量无意义的改进浪费了一轮又一轮对话。教训是相关性排序不能只看语义相似度必须结合静态调用关系加权。名字像不代表逻辑相关真正的强相关是当前改动符号被它调用或者它调用了当前改动符号。我在评分公式里把调用图命中项的基础权重设成普通检索命中项的三倍以上这类误判才明显减少。如果你也在做 context-mode建议给相似度和调用关系分别打分不要混合成一个分数。5.2 翻车之二缓存快照与磁盘代码脱节上下文构建的速度优化依赖缓存但缓存有个致命问题快照过期。有一次 AI 基于旧版本的文件内容给了重构建议而磁盘上的代码早在我上一次提问后就被我改了。AI 建议的新增参数在最新代码里已经有了还建议把某一段已经删掉的代码重新加回来看着特别像模型在胡说。后来我在所有上下文单元上挂了版本号来源是文件的 mtime 或 git hash。文件一旦变化相关的符号级单元立即失效embedding 缓存也按版本号重新计算。这个机制尤其重要因为开发场景里文件是高频变化的不做版本绑定缓存带来的效率提升会反噬成正确性问题。5.3 翻车之三上下文构建吃掉太多响应时间我在做第一版工具时把索引构建和 embedding 计算全部放在请求路径里。在 120 文件的项目上一次聚焦模式的上下文构建要扫全量符号表和向量库平均耗时超过 200 毫秒。对聊天工具来说这个数字尚可接受但对编辑器内联问答来说等 200 毫秒再开始生成就已经等于卡了。我做的优化前面提过索引进程常驻文件变更走事件驱动更新embedding 做磁盘缓存上下文构建结果按参数哈希复用。优化后把构建耗时压到 30 毫秒以内。这里我还有一个体会把上下文构建耗时做成指标面板挂在调试页上比什么都管用。哪个模式慢、哪次检索慢一眼就能看到不用靠猜。5.4 翻车之四输出预算被挤压到截断最后一个坑是输出预算被挤压。有一回我让 AI 为一个模块生成完整的使用文档上下文里塞了大量示例代码token 预算占到了窗口上限的 95%。结果模型生成到一半被硬截断文档后半部分全丢还触发了一次报错。那次之后我在 BudgetAllocator 里把输出预留从固定比例改成了按任务类型动态调整代码生成、长文档类任务输出预留直接设到 30% 到 40%短问答和解释类任务再压缩到 10% 到 15%。另外我给上层接口加了一个字段如果任务类型没有显式指定默认按 20% 预留。这个数字来自我的实测经验可以当作起点再根据你的模型和使用习惯微调。最后说点个人体会折腾了大半年 context-mode我最深的感受是它本质上是把上下文窗口这个物理限制转化为一套可管理的工程策略。不是模型越大越好也不是窗口越长越好而是让模型在正确的时机看到正确范围的信息。我自己从最初的全文模式一路踩坑最后稳定在分层 聚焦为主自动模式为辅的组合上。最后分享一个实用小技巧给 context-mode 加一个开关面板调试时把本次任务喂了哪些文件、每个文件占了多少 token、相关性分数是多少打印到日志里。很多上下文问题根本不用猜日志一拉出来谁占了大头、谁不该进来一目了然。这个习惯帮我省下的排查时间比任何模型调参都多。