
如果你的 AI 编程助手时不时答非所问把三个月前的旧需求当成当前任务来“发挥”那大概率不是模型不行而是被塞进窗口里的上下文出了岔子。这个问题的核心就是 context-mode——上下文模式的选择与治理。我花了几周时间做了一个专门处理这件事的小项目核心就一句话让每一次请求都知道该带什么、不该带什么。这篇东西把完整的设计思路、实现细节和踩过的坑都整理出来对正在折腾 AI 辅助开发、或者在大模型应用里做上下文管理的人应该能少走不少弯路。1. 一次答非所问引发的项目context-mode 到底管什么1.1 表象是模型问题根子是上下文失焦先讲一个真实场景。我在改一个登录页的空指针报错让 AI 助手帮忙定位。它给的答复措辞很专业但内容完全跑偏——它把一周前我做活动页时讨论过的营销弹窗逻辑翻了出来还附赠了一段促销倒计时代码。我当时的第一反应是“模型不够聪明”但把完整 prompt 拉出来一看发现问题出在我这边我传给模型的上下文里同时包含了登录页代码、活动页代码、旧对话历史、还有一堆项目配置文件模型只是在那一堆 token 里做了它认为最合理的关联。大模型本身没有“当前任务”这个概念它的世界就是 prompt 里那串 token。给它什么它就基于什么回答。上下文里 70% 是无关内容时它答非所问才是正常发挥。这件事让我意识到与其纠结换哪个模型不如先把输入治理好。1.2 context-mode 的三个职责我做的这个项目名字就叫 context-mode它本质上是一个上下文治理模块专门负责三件事决定带什么从打开的文件、选中的代码、项目结构、历史对话里筛选出与当前任务真正相关的内容。决定带多少在模型窗口有限的前提下把 token 预算分配到最值得的地方。决定以什么顺序带核心内容往前放次要内容往后放防止中间位置被无关信息占据。用一个生活化类比解释你请人帮忙装修厨房不可能把整个杂物间的东西都搬到对方面前只会挑出眼下要用的扳手、瓷砖和图纸。context-mode 干的就是“挑东西”这件事只是它挑的对象是代码和文档。1.3 项目定位与适用范围我把它做成一个可以独立接入的中间件模块既能挂在 CLI 工具里也能被 IDE 插件调用还能嵌到服务端的请求链路中。适用对象主要有三类自己搭 AI 编码助手、被“乱塞上下文”问题困扰的开发者。做 Chat 类应用需要在多轮会话里管理历史上下文的人。做文档问答、代码仓库问答需要做检索增强但又不想让 token 成本失控的团队。后端接口设计得很薄输入是当前会话状态加上用户指令输出是一份已经排好序、去重、预算可控的 prompt 组装结果。如果你只想快速了解思路不写代码也行后面章节里的配置和规则可以直接借鉴。2. 三种模式的设计逻辑与适用边界context-mode 的核心不是某一个算法而是把“怎么选上下文”这件事拆成了三种可切换的模式manual、auto、agent。它们对应三种完全不同的使用心态用户全权指定、系统自动筛选、模型自主决策。2.1 manual把选择权完全交还用户manual 模式下系统不做任何推测。用户显式地通过 file 或 #selection 指定要携带的内容我只做组装、去重和预算控制。这个模式的适用场景非常明确用户已经知道问题出在哪个文件、哪个函数里只需要 AI 帮忙精读和修改。比如一个空指针异常报错栈指向UserService.java的getUserById你直接UserService.java就行没必要把整个 service 层都拖进来。manual 的风险在于它要求用户对自己的代码库结构足够熟悉。用得不好会出现两种情况一是漏带关键文件AI 只盯着你给的那一小段代码看不到调用方给出的方案驴唇不对马嘴二是带错文件把不相关的模块塞进去平白增加 token 消耗。2.2 auto按任务意图自动筛选相关上下文auto 是我日常用的最多的一种模式它的工作逻辑是以用户当前的操作信号打开的文件、选中的代码、最近的 git diff、输入的 prompt 文本为输入做一次多路召回把可能相关的文件和代码片段捞出来再按相关性排序和预算取舍。这里的核心是“相关性”怎么定义。代码场景里相关性不等于语义相似。userInfo和getUserInfo在 embedding 空间里距离可能很近但如果当前任务是修登录跳转逻辑真正相关的是调用链上的AuthController而不是那个长得像的userInfo工具类。所以我在召回路里同时使用了 BM25 关键词命中和 embedding 语义相似度两条路的结果做融合排序避免单一信号跑偏。auto 适合日常绝大部分开发场景改 bug、写单测、查日志、调样式。用户不需要精确指定系统给一个“足够好用”的上下文模型就能给出不错的答复。2.3 agent全量视角把决策交给模型agent 模式是另一种极端我把项目结构树、检索到的 top-k 片段、最近的 git 历史、对话摘要全部组装好塞进窗口让模型自己决定关注哪些内容。这种模式适合跨模块的重构、新需求设计、性能瓶颈分析这类任务。因为这类任务本身就不存在“某一个文件是正确答案”的情况用户往往自己也说不清需要哪些上下文不如把决策权交给模型。代价也很明显token 消耗是量级上涨响应延迟显著增加。如果每一条日常消息都走 agent 模式成本会非常难看而且模型在大量上下文里反而更容易“挑花眼”回答可能泛泛而谈。2.4 三种模式该怎么选我把三种模式的差异做成了一张对照表接入时可以直接参考模式输入范围典型首字延迟token 消耗量级适用任务主要风险manual用户显式指定的文件/选区低低约1k精修单文件 bug、定点代码审查用户漏带信息模型信息不足auto当前操作信号 多路召回中中约5k-8k日常编码、调试、单测召回不准关键文件被遗漏agent项目结构 全量检索 历史高高约20k跨模块重构、新需求设计token 成本高注意力被稀释一句话总结能用 manual 说清楚的就别让 auto 猜日常杂活用 auto真正要“上帝视角”的任务才开 agent。3. 核心实现token 预算、相关性排序与边界处理设计好模式之后真正让系统稳定跑起来的是几个底层实现细节。这块内容偏工程但我会尽量讲清楚每个设计背后的理由。3.1 token 预算怎么算我见过很多人用len(text)来估算文本长度这在做中文内容时误差极大。一个中文字符可能对应 1~2 个 token而一段代码里的回车、缩进、长变量名消耗的 token 数量跟字符数完全不成比例。所以第一步就是按模型对应的 tokenizer 做预估。以常见的 4k 窗口为例我的分配策略是这样的MAX_CONTEXT_WINDOW 4096 def build_prompt(system_text, history, retrieved_chunks, user_input): # 给模型生成预留的 token output_reserve 512 # 系统提示词固定占位 system_cost estimate_tokens(system_text) # 用户输入必带 input_cost estimate_tokens(user_input) remaining MAX_CONTEXT_WINDOW - output_reserve - system_cost - input_cost # 历史对话和检索内容竞争剩余预算 history_budget int(remaining * 0.3) retrieval_budget remaining - history_budget history_trimmed trim_by_token(history, history_budget) chunks_trimmed trim_ranked_chunks(retrieved_chunks, retrieval_budget) return assemble(system_text, history_trimmed, chunks_trimmed, user_input)这段逻辑的核心是给“历史对话”和“检索内容”显式划分预算。经验值历史对话占比 30%检索内容占比 70%。原因很简单检索内容是针对当前任务的直接证据历史对话只是提供背景优先级理应更低。3.2 相关性排序双路召回与分数融合auto 模式下的排序公式我用的是 BM25 分数和 embedding 余弦相似度的加权融合def fused_score(query, doc, bm25_scores, embedding_model): bm25 bm25_scores.get(doc.id, 0) emb cosine_similarity(embedding_model.encode(query), doc.embedding) # BM25 的命中要更“硬”权重给高一点 score 0.6 * normalize(bm25) 0.4 * emb # 精确修复合集的文件加分 if doc.path in test_related_files(query): score 0.15 return score为什么 BM25 权重比 embedding 高我踩过坑embedding 相似度经常被“高保真复制粘贴”骗到。一个文件如果到处引用某个公共变量名比如config.get(xxx)那么任何包含config的查询都可能把它召回来但实际上这个文件跟当前任务毫无关系。BM25 对关键词命中更严格能过滤掉这些“字数像但内容不像”的干扰项。3.3 三种模式共用的边界处理不管在哪个模式下有几个边界问题是共通的去重。同一个文件可能既被用户 了又在自动召回路中命中。如果不去重同一份内容会在 prompt 里出现两次token 翻倍模型还容易混淆。我的做法是对每个内容块计算 hash组装 prompt 时先过一遍指纹集合。定位信息。把代码块直接拼进 prompt 而不告诉模型它来自哪个文件等于让模型盲人摸象。每个代码片段前面我都会加上文件路径和函数名例如### File: src/auth/LoginController.java ### Function: handleLogin(HttpServletRequest req)这样模型至少能结合路径语义推断代码的使用场景回答时会更有针对性。保序截断。当检索内容超过预算时不能简单地从中间切一刀。代码块之间的顺序暗示着依赖关系我采用按排序分数从低到高丢弃的方式优先保证排序靠前的核心块完整保留。4. 实测三种模式在真实任务上的延迟与消耗对比光说设计逻辑不够我把自己项目里的一个中型代码库作为测试对象跑了三种模式各 50 次请求任务类型覆盖了单文件 bug 修复、跨模块功能开发、以及代码走查下面直接放结果。测试环境是一个约 8 万行代码的 Java 服务端项目模型接的是通用大模型 API记录指标包括首字延迟、总 token 消耗和一次答复是否被用户采纳我人工标注。模式平均首字延迟平均总 token单次任务平均对话轮数一次采纳率manual2.1s1.3k2.482%auto3.4s6.8k3.178%agent6.5s22.4k4.274%从数据里可以读出几个结论。第一manual 的采纳率最高这一点不意外。用户自己指定上下文模型拿到的信息最干净回答的自然最准确。它的问题在于“用户得知道要带哪些信息”这对新手不友好。第二auto 和 manual 的采纳率差距只有 4 个百分点但 token 消耗差了 5 倍。这意味着只要召回做得好auto 能以很小的可用性代价把用户从“手动整理上下文”这个负担里解放出来。日常开发里我用 auto 最多就是这个原因。第三agent 模式在简单任务上不仅慢还出现了一个有趣的现象模型会过度依赖项目结构里的某个看起来“高大上”的模块给出一些过度设计的方案。比如修一个空指针异常它居然建议引入缓存框架。上下文太多的时候模型容易“迷失在信息里”。这个测试结果让我坚定了原则模式切换一定要有感知不能一键 auto 用到底需要的时候得手动降级到 manual该上 agent 的任务也不要犹豫。5. 实际接入时最容易踩的四个坑这一章是我最想写的内容。设计文档里不会告诉你这些细节但它们直接决定系统能不能在实际项目里跑起来。5.1 坑一同一份文件被注入两次token 翻倍效果反而变差接入后的第三周我偶然看到一次请求的 token 统计高得离谱一份 3000 行的OrderServiceImpl.java在 prompt 里出现了两次。原因是用户用OrderServiceImpl.java手动指定了它同时自动召回路也把它命中了。排查链路是这样的先看 token 统计发现 6.8k 的消耗里有一份文件占了 3k再把完整 prompt 打印出来人肉比对发现文件内容重复最后定位到是 manual 指定和 auto 召回没有做去重联动。修复方案很直接组装 prompt 前全局维护一份内容指纹集合任何内容块进集合前先算 hash重复的直接丢弃。这个改动让我整体 token 消耗下降了近 20%。5.2 坑二窗口溢出被静默截断丢的偏偏是最有用的中间片段有一次用户反馈auto 模式下回答质量突然断崖式下跌。我一开始以为是模型抽风重试了好几次都一样。后来排查发现当检索内容特别多、超出窗口预算时我直接用了.slice(0, max_tokens)这种从头截断的写法于是 prompt 变成了系统提示 用户输入 最早注入的文件 被硬生生砍断的中间内容。最要命的是检索结果里相关性最高的几个片段全在中间位置被拦腰截断。模型拿到的核心证据是残缺的回答自然稀碎。正确做法是永远从低优先级的一侧开始裁剪。按照排序分数从低到高逐个淘汰保住头部核心块宁可让少的片段保持完整也不要用一半的高分内容。5.3 坑三长会话切换模式旧模式的历史污染新模式这个坑藏得很深。有一次用户开了 agent 模式分析了十分钟代码然后切到 manual 模式准备修一个小 bug只 了一个文件。按道理这是一次非常干净的上下文但模型给出的回答仍然在讨论 agent 模式下那个“全局设计方案”的术语完全没有聚焦到 bug 上。原因在于我对“模式切换”只重置了检索结果却没有重置历史对话缓冲区。agent 模式产出的那一大段分析文本还躺在历史里模型被它带偏了。修复方式有两种一是切换模式时清空历史只保留系统提示和用户当前输入二是把历史压缩成一段摘要再传入让模型知道“此前讨论过大方向现在转向具体修复”但不保留完整原文。我最终选了第二种用户体验更顺。5.4 坑四召回来的“相关文件”其实毫不相关auto 模式上线一段时间后我收到反馈说检索结果老是带上一批奇怪的文件。打开命中列表一看全是包含config、Util、Constants这类公共符号的文件。原因之前提过embedding 相似度太容易被高频符号刷分。最终方案是双路召回加权重调整。BM25 命中得分权重提到 0.6embedding 降到 0.4又加了一个惩罚项包含大量公共工具方法的文件工具类、常量类在打分时降低权重。这个改动上线后检索结果的精准度明显回升模型废话也变少了。6. 模式判定的小技巧什么时候切到什么模式context-mode 如果只靠用户手动切换使用门槛还是偏高。我加了一层轻量的自动判定模块基于规则和少量关键词信号就能在大多数场景下帮用户选对模式。6.1 从用户输入里读信号规则并不复杂用的是一些典型信号词用户输入特征判定模式理由包含“报错”、“异常”、“修复”、“改 bug”、“打印日志”manual用户目标明确需要精准定位包含“重构”、“梳理”、“设计”、“整体方案”、“依赖关系”agent需要全局视角单文件不够包含“写单测”、“测试用例”、“样式”、“文档”auto中等相关性自动召回效率最高默认且无法判定auto保守策略性价比最高这套规则我用 Python 实现只有几十行但已经能覆盖大部分情况。关键不是规则本身多聪明而是它能避免用户频繁手动切换减少使用摩擦。6.2 用数据微调判定阈值规则定了之后不要直接拍板上线。我在项目里加了埋点记录了每次请求的模式、用户是否手动切换了模式以及最终答案是“直接采纳”还是“被修改”。每周看一次这些数据就能知道规则是否误判。比如我发现“改样式”这个信号被规则划到了 auto但用户经常在 auto 结果里手动补充 文件说明这个信号的上下文范围还是不够精准后来我把样式类任务也归入了 manual反馈数据好了很多。6.3 一个可以直接抄走的配置样例如果你也想在自己项目里快速接入 context-mode 的思路可以先用下面这份 YAML 当起点context_mode: default_mode: auto modes: manual: enabled: true include_plain_path: true auto: enabled: true retrieval: bm25_weight: 0.6 embedding_weight: 0.4 top_k: 8 budget: retrieval_ratio: 0.7 history_ratio: 0.3 agent: enabled: true include_project_tree: true include_git_diff: true max_retrieval_top_k: 15 dedup: enabled: true mode_switch: reset_history: false condense_history: true这份配置跑了两周后平均 token 消耗下降了三成用户重复提问的次数也少了。不要觉得这些数字很小在模型按量计费的时代每一个 token 都值得省。我在实际接入中的体会是上下文治理永远比模型选型更值得先花时间。同一个模型在乱七八糟的上下文下可能只发挥三成功力但给它一份精挑细选的信息表现立刻上一个大台阶。context-mode 这个项目做下来最让我意外的收获不是 token 省了多少而是我把“AI 助手为什么不听话”这个问题从“模型不行”变成了“我给它塞了什么东西”——后一个才是真正能控制的部分。