
1. context-mode 这个需求是怎么冒出来的做 AI 辅助编程工具的人估计都遇到过同一个尴尬模型明明很强但喂给它的上下文不对回答就完全跑偏。我最初做的是一个基于大模型的代码补全插件用户选中一段代码插件把相关内容发给模型让模型给出解释或者续写建议。听起来很简单但一上线就翻车了——用户反馈说“模型根本不知道我在干什么”“我问它这个函数为什么报错它给我讲了一堆无关的变量”。排查半天发现问题不是模型笨而是我给的上下文太“死板”要么只传了用户选中的那几行要么把整个项目的文件一股脑塞进去。前者模型缺少周围代码的约束后者 token 直接爆炸还没等到模型回答请求就超时了。后来我意识到这类工具的核心不应该是“怎么调模型”而是“怎么管理给模型的上下文”。这个念头就是 context-mode 的起点。简单说context-mode 是一个上下文感知的输入处理机制它根据用户当前的操作场景、任务类型和代码范围动态决定应该把哪些信息组装进上下文窗口用哪种模式去约束模型的行为。它不是某个具体算法而是一套策略框架。这篇文章我会把 context-mode 从需求到实现、从踩坑到调优的完整过程写出来。适合谁看如果你在做 AI 编程助手、智能问答、代码解释器或者任何需要给大模型“喂”动态上下文的应用这篇能帮你少走不少弯路。如果你只是好奇大模型应用里“上下文工程”到底怎么做也能从里面看到一套可落地的思考方式。2. 三种模式的定义与边界global、focused、strict2.1 模式设计的第一原则范围决定质量我把 context-mode 设计成三档模式global、focused、strict。名字很直白但真正关键的不是名字而是每一档模式对“范围”的界定。范围决定了模型能看到什么也就决定了回答的上限。global全局模式收集整个项目根目录下的关键文件信息包括所有模块的概要、依赖关系、配置项适合做全局性的代码审查、架构问答、跨模块依赖分析。代价是 token 消耗大响应慢。focused焦点模式只围绕用户当前编辑文件及其直接关联的文件构建上下文比如一个 Python 文件里 import 进来的模块、同一个包内的兄弟文件、当前函数调用的上下游函数。适合绝大多数日常编码场景比如“这个函数为什么报错”“帮我重构这个方法”。strict严格模式把上下文窗口收敛到用户选中代码块、当前函数体、最近的若干次编辑记录上其他一切信息全部屏蔽。适合精确提问比如“这行正则表达式的含义是什么”“这段代码的时间复杂度是多少”。代价是模型缺少外部信息遇到需要底层依赖的问题时容易答错。这三种模式之间不是简单的“信息量从多到少”而是“关注点从全局到局部”的连续谱。实际使用中我建议不要把 strict 理解成“最省 token 的模式”而应该理解成“最聚焦当前任务的模式”。它的价值不是省钱而是减少干扰。2.2 边界条件什么场景用哪一档模式划分得再清晰用户不会手动去切也白搭。所以在设计初期我就定了一条边界模式可以手动切换但必须有自动推荐的逻辑。不然让用户自己选他根本不知道三档模式的区别选错了还会觉得是产品做得烂。自动推荐的规则我总结成了一张表用户行为推荐模式判断依据选中了代码块并提问strict明确的小范围任务不需要额外信息在某个函数内频繁编辑focused任务围绕当前函数和其调用关系展开提问内容涉及“为什么”“整个项目”“架构”global问题本身暗示了全局诉求刚打开项目没有选中代码focused用户大概率在浏览和定位问题连续报错反复修改同一个文件focused需要结合文件整体逻辑做判断报错信息里出现跨模块符号global错误可能源自依赖模块的定义差异这套规则不是拍脑袋定的而是我从用户行为日志里统计出来的。最开始我只有手动切换收集了两周数据后发现用户真正会去点模式按钮的比例不到 3%但他们的提问内容其实有明显的模式倾向。于是把推荐逻辑加进去才真正让 context-mode 成为一个“有感知”的模式系统。2.3 为什么不做无级调节有朋友问过我既然范围是个连续值为什么不做一个滑杆让用户自己拖上下文大小我的回答是滑杆让用户面对一个根本不知道怎么填的参数是一种把复杂性转嫁给用户的设计。三档模式虽然粗糙但它给用户提供了一个心理模型——我现在是要“全局思考”还是“局部操作”这符合直觉。更重要的是三档模式在工程实现上有明确的缓存策略。global 模式的项目索引可以小时级更新focused 模式的关联文件索引可以分钟级更新strict 模式则完全实时。如果做无级调节缓存策略就会非常尴尬每多一档范围就意味着多一种缓存粒度和失效策略实现复杂度和维护成本会指数级上升。3. 核心机制拆解上下文窗口构建、路由与状态管理3.1 上下文窗口不是简单拼接而是分优先级组装很多初做 AI 工具的人都以为上下文字符串用f-string拼一拼就行。真这么干过的人都知道模型输出质量完全不可控。context-mode 的核心机制之一是把上下文窗口拆成几个独立的分区每个分区设置不同的优先级和预算系统指令区system prompt固定占用说明当前模式的行为约束。比如 strict 模式下系统指令会写“只基于用户提供的代码片段回答问题不要推测外部定义”。核心代码区core用户当前操作的文件、选中的代码块、最近的 diff。这部分优先级最高token 预算最充裕。关联代码区related被当前文件 import 的模块、当前函数调用的上下游函数。在 focused 和 global 模式下启用。项目概要区project map文件树、关键符号表、README 摘要、依赖配置文件。只在 global 模式下启用。历史对话区memory最近几轮的问答摘要。严格模式往往只保留最后一轮避免旧对话干扰当前聚焦点。这个分区设计解决了一个很实际的问题模型对上下文不同位置的注意力并不均匀。放在最前面的指令对行为约束最强放在中间的代码细节对答案准确度贡献最大而放在最后的历史对话则容易产生“锚定效应”拉着模型重复旧观点。所以我不采用简单的“开头是系统指令、后面接大段代码”的常规写法而是把核心代码区紧贴在系统指令之后再放关联代码和项目概要。这样即使 token 预算紧张被强制截断优先保留的也是最关键的信息。3.2 路由逻辑怎么判断用户此刻最需要哪一档上下文构建解决的是“给什么”路由逻辑解决的是“给多少”。我在系统里实现了一个轻量级的ModeRouter它会结合三类信号决定最终的上下文模式信号一显式指令。用户输入里如果带了global、focused、strict这类前缀直接覆盖一切规则这就是手动优先。信号二操作特征。编辑器传来的事件类型。如果是 selection change 事件而且选中文本长度大于 50 个字符倾向 strict如果是文件保存事件倾向 focused如果用户打开了一个新文件且没有选中任何代码默认 focused。信号三问题意图分类。把用户的问题文本过一个轻量的分类器识别是不是“全局性问题”。这一步很省事因为全局性问题的关键词特征比较明显——“架构”“整个项目”“全局”“所有模块”“依赖关系”。命中这些词的直接升级到 global。路由器的输出不只是一个模式名还包括一个置信度分数。置信度高于 0.8 时系统自动切换模式在 0.5 到 0.8 之间系统在 UI 上给一个“推荐切换”的提示但不自动切低于 0.5 则保持当前模式不变。这个设计规避了“频繁切换打扰用户”的问题。3.3 有限状态机避免模式抖动实现上模式切换我用了一个简单的有限状态机而不是直接if/else改状态。原因很简单模式切换是有代价的——每次切换都意味着重新构建上下文窗口如果模型服务端有缓存切换还会让缓存失效。如果用户在 strict 和 focused 之间来回触发信号不做缓冲地反复切换系统会在短时间内重建好几次上下文浪费大量资源。状态机里专门设置了一个pending状态。路由器给出新模式建议后系统先进入 pending等待一个稳定信号确认比如用户停止输入 2 秒或再次触发相同模式的信号才真正完成切换。如果 pending 期间收到了不同的模式建议就取消当前切换重新评估。这个设计看起来多了一步实则在真实体验中大大减少了“上下文反复横跳”的问题。4. 落地实现从状态定义到最小可用系统4.1 代码结构模式枚举、窗口构建器、路由器三件套这里的代码我做了大幅精简保留了核心骨架。第一块是模式定义与状态机from enum import Enum from dataclasses import dataclass, field class ContextMode(Enum): GLOBAL global FOCUSED focused STRICT strict dataclass class ModeStateMachine: current: ContextMode ContextMode.FOCUSED pending: ContextMode | None None stable_signal_count: int 0 def suggest(self, new_mode: ContextMode, confidence: float): # 置信度不够不动作 if confidence 0.5: self.stable_signal_count 0 return # 置信度中等记录一次但不要立即切 if confidence 0.8: self.stable_signal_count 1 if self.stable_signal_count 2: self._apply(new_mode) return # 高置信度直接走 pending 再确认 self.pending new_mode self.stable_signal_count 1 if self.stable_signal_count 2: self._apply(new_mode) def _apply(self, new_mode: ContextMode): if new_mode ! self.current: print(f[context-mode] {self.current.value} - {new_mode.value}) self.current new_mode self.pending None self.stable_signal_count 0有一点要注意stable_signal_count的语义不是“连续几次建议”而是“在冻结期内收到的同方向建议次数”上一节说的 pending 缓冲就是在这里落地的。如果你在自己项目里实现建议再给状态机加一个时间戳比如 2 秒内的重复信号才累加超过时间窗口就清零防止用户很久之前的操作影响当前状态。4.2 上下文窗口构建器的实现要点窗口构建器是重头戏。它负责把“哪些文件内容该进上下文”这个问题变成具体的字符串。关键逻辑在build方法里dataclass class ContextWindow: system_prompt: str core_code: list[str] related_code: list[str] project_map: list[str] field(default_factorylist) history: list[str] field(default_factorylist) def render(self, max_tokens: int 8000) - str: sections [] budget max_tokens # 系统指令区先占用固定预算 sys_tokens estimate_tokens(self.system_prompt) sections.append(self.system_prompt) budget - sys_tokens # 核心代码区优先级最高但也要防爆 core_tokens min(estimate_tokens(.join(self.core_code)), int(budget * 0.5)) sections.append(truncate_text(.join(self.core_code), core_tokens)) budget - core_tokens # 关联代码区占用剩余预算的一半 if self.related_code: related_tokens min(estimate_tokens(.join(self.related_code)), int(budget * 0.5)) sections.append(truncate_text(.join(self.related_code), related_tokens)) budget - related_tokens # 项目概要和历史谁排在前面谁先得分 for section in self.project_map self.history: if budget 0: break sec_tokens estimate_tokens(section) if sec_tokens budget: sections.append(section) budget - sec_tokens return \n\n--- separator ---\n\n.join(sections)核心思想是每个分区都有独立预算而且render的顺序就是注意力的优先级顺序。有一个坑必须提醒如果你把 project_map 放在 core_code 前面模型很容易被文件清单带偏回答起来像在背目录反而忽略了你真正想问的代码细节。我一开始就是按“系统指令-项目概要-关联代码-核心代码”的顺序排结果模型老在解释项目结构根本不看当前函数。后来把 core_code 上调到第二位效果立竿见影。4.3 自动模式推荐的实现逻辑自动推荐依赖两个输入编辑器事件和问题文本。代码大致长这样def recommend_mode(editor_event: str, selected_text: str, query: str) - tuple[ContextMode, float]: # 显式指令优先 query_stripped query.strip() for prefix, mode in [(global, ContextMode.GLOBAL), (focused, ContextMode.FOCUSED), (strict, ContextMode.STRICT)]: if query_stripped.startswith(prefix): return mode, 1.0 # 全局性问题关键词检测 global_keywords [架构, 整个项目, 依赖关系, 全局, 所有模块, 模块划分] if any(kw in query for kw in global_keywords): return ContextMode.GLOBAL, 0.85 # 选中的是长代码块通常意味着聚焦 if len(selected_text) 200: return ContextMode.STRICT, 0.75 # 编辑事件推断 if editor_event save: return ContextMode.FOCUSED, 0.7 if editor_event open and not selected_text: return ContextMode.FOCUSED, 0.6 # 默认保持现状 return ContextMode.FOCUSED, 0.3这个推荐器写得比较粗但够用。因为它配合状态机之后单次低置信度建议不会产生实际动作只有连续同方向的信号才会改变模式。如果直接在推荐器里返回模式然后立刻切换交互会非常神经质用户会觉得系统在跟他抢键盘。5. 实测效果三种模式下的准确率与 token 消耗对比5.1 测试方法用同一批问题跑三种模式我在测试阶段准备了一个包含 23 个 Python 文件的模拟项目里面有清晰的模块划分、一个主入口、若干工具函数还故意埋了几个跨模块的 bug。测试问题分成三类局部性问题“这个函数的时间复杂度是多少”、关联性问题“这个类为什么导入失败”、全局性问题“整个项目的数据流是怎么设计的”。每个问题分别用 strict、focused、global 三种模式跑记录答案准确率和 token 消耗。准确率的判定标准不搞虚的答案中是否包含正确的结论或修复方向由两个有经验的开发者独立打分取平均。token 消耗则统计每次请求的输入 token 数。5.2 数据结果模式匹配场景价值巨大错配则翻车结果非常有意思问题类型strict 模式准确率focused 模式准确率global 模式准确率局部性问题91%84%62%关联性问题71%88%76%全局性问题43%69%87%看这张表我得先泼一盆冷水任何模式都不是万能的但“模式与任务匹配”确实能带来显著收益。局部性问题用 strict 比用 global 高了 29 个百分点全局性问题用 global 比用 strict 高了 44 个百分点。这说明什么说明给模型的信息多不一定好信息杂反而会带偏模型。再来看 token 消耗。同样一批问题strict 模式平均每次请求消耗 1.2k tokenfocused 平均 4.6kglobal 平均 13.8k。如果把整个测试跑完global 模式的总消耗是 strict 的 11 倍多。这意味着模式选对了不仅准确率高成本还能压到十分之一以下。5.3 一个反直觉的发现strict 模式也会导致全局性错误测试里有一个问题让我印象很深。我问“这个项目的数据库连接是在哪里初始化的”strict 模式下模型回答得理直气壮说某个工具函数里做得不对实际上那个函数跟数据库一点关系都没有。为什么因为 strict 模式下模型根本看不到那几个真正包含数据库初始化代码的文件它只能基于当前文件里的蛛丝马迹“硬猜”。这个案例提醒我strict 模式绝不能设计成“永远省 token 的默认选项”。它只适合用户明确聚焦在某段代码上的场景。一旦问题涉及“哪里”“为什么”“谁调用”这类检索式问题strict 就力不从心了。所以我在推荐规则里加了一条如果问题文本包含疑问代词且没有选中代码强制升到 focused不准落到 strict。6. 实战排坑两个最典型的故障复盘6.1 上下文污染旧文件内容阴魂不散第一个坑是上下文污染。现象是用户明明已经改了代码模型给出的回答还是基于旧版本。我一开始以为是缓存问题排查了很久才发现问题出在我的关联文件索引上。当用户编辑文件 A 时系统会预先加载 A 依赖的模块 B 的内容。但 B 的内容在第一次加载后就被缓存到了内存里用户后来又改了 B但索引没有及时失效导致每次构建上下文时用的都是 B 的旧版本。排查链路是这样的先复现问题发现只有在“编辑 A 之前已经浏览过 B”的情况下才出现然后我在构建上下文时打印了每个分区的来源文件 hash对比发现 B 的 hash 已经变了但上下文里用的还是旧 hash 对应的内容最后定位到是缓存的失效条件写错了——只监听了当前活跃文件的保存事件没有监听被索引文件的保存事件。修复方案倒也简单所有进入关联代码区的文件构建上下文前必须做一次 mtime 检查文件修改时间有变化就强制重新读取内容并刷新缓存。之后我再也没有遇到过“模型基于旧代码回答”的问题。6.2 模式抖动系统在跟用户抢键盘第二个坑是模式抖动。上线后收到用户反馈说“工具老是在切换模式界面上的模式标签闪个不停还没等我反应过来回答就已经生成了而且用到的是错误的模式”。我看了日志发现一个用户在 6 秒内触发了 7 次模式切换信号先是因为选中了一段代码被推荐 strict然后因为他敲了个空行触发了编辑事件被推荐 focused紧接着他自己又删掉了空行再次触发编辑信号系统又切回 strict。这类问题的本质是事件驱动的模式推荐天然有噪声而最初的实现里每一次推荐都会直接触发切换没有缓冲。我加了两道防线第一道是上一节提到的 pending 状态机只有信号稳定后才真正切换第二道是模式切换的最小时间间隔无论信号多强烈同一模式的切换动作 10 秒内只允许发生一次。实测下来用户感知到的“闪烁感”基本消失个别高频操作场景下偶尔还有一次切换但已经不影响阅读和操作了。6.3 关于排查方法的通用思路这两次排坑有一个共同的经验模式类问题不要一上来就怀疑模型先看“输入给模型的内容”和“模型实际收到的内容”是否一致。方法很简单在系统里加一个调试开关每次请求前把最终渲染出来的 context 字符串存一份到日志问题出现时直接对比日志里的 context 和用户当时的真实代码状态偏差出在哪一步就是问题在哪一步。这个习惯我一直保留到现在。AI 应用跟传统软件不同模型是个不可控的黑盒但我们能控制的是喂进去的内容。所有上下文工程的 bug最终都表现为“输入与预期不符”而不是“模型回答错误”。所以先确认输入再去纠结输出质量是最省时间的排查路径。7. 后续扩展方向把 context-mode 做成基础设施context-mode 目前在我这边已经不只是“给模型拼上下文”的模块了它还演化出了两个更有意思的能力。第一个是把模式元数据暴露给上层逻辑比如路由模块可以根据当前模式决定走哪个模型——strict 模式下直接调小模型速度快成本低global 模式下调大模型保证复杂推理的质量。第二个是模式感知的请求缓存strict 模式的上下文高度稳定很适合做缓存同一段代码的相同问题可以直接命中缓存连模型都不需要调。如果你也想在自己项目里做一个类似的机制我的建议是从最轻量的版本开始不要一上来就做三个模式先做 focused 和 strict 两档跑通之后再扩展。两档模式已经能覆盖 90% 的日常场景而且这两档的上下文构建逻辑很简单不会在前期投入太多精力。等用户的真实反馈告诉你“这两档不够用了”再上 global 也不会太迟。另外一个我自己很受益的思路是给模式切换做“记忆”。比如用户上次在某个文件里手动把模式从 focused 切到了 strict系统就把这个偏好记下来下次他再打开这个文件时自动沿用 strict。这套机制不复杂但很讨喜因为它传递了一个信号——系统能理解用户的工作习惯而不只是被动执行规则。precedent 设置比复杂的算法更能提升用户对工具的信任感。