做了快两年AI应用开发,我越来越确定一件事:决定一个AI工具好用不好用的,往往不是模型选得有多新,而是你往模型里塞的上下文有多准。大模型本身没有记忆,每次调用都得把背景信息带上去;可哪些信息该带、哪些该丢、按什么顺序排,就成了一个容易被忽略但影响极大的工程问题。context-mode,也就是上下文模式,就是为解决这个问题而生的设计思路。
它不是某个开源库的名字,而是一类做法:把上下文的选择、组装、切换做成显式、可配置、可观测的模式,而不是在代码里硬编码一套固定逻辑。只要你正在做AI辅助编程、智能客服、文档问答、个人知识库这类应用,或者你只是想给自己写的AI小工具加点“记忆力”,这篇文章都值得看完。我会把设计思路、数据模型、核心实现、参数推导和实测踩坑一次讲清楚。
1. 内容整体设计与思路拆解
1.1 为什么需要 context-mode
先讲一个我在实际项目里遇到的场景。我们做一个面向研发团队的代码问答助手,第一版特别简单:把用户的问题和整个仓库的文件列表往模型里一塞,让它回答。结果可想而知,token消耗大得惊人,一次请求经常要付几万token的钱,回答质量却很糟糕。模型被大量无关代码淹没,反而抓不住真正相关的那几个文件。
这里的问题不是模型不行,而是上下文的选择策略不行。大模型的注意力虽然强,但也是有上限的,信息越多,关键信号被稀释得越厉害。就像让一个专家在堆满杂物的房间里找一份合同,他能找到,但速度慢、容易分心。如果在进门之前就先递给他一份“这个房间里跟合同相关的物品清单”,效率完全不一样。
context-mode就是这份清单的生产系统。它的核心任务是回答三个问题:
- 这次请求需要携带哪些上下文?
- 这些上下文按什么优先级组织?
- 当上下文超过模型窗口时,先牺牲谁?
把这三个问题固化成不同的模式,就能适应不同场景。比如代码问答需要“当前文件+相关引用+项目结构”,客服机器人需要“用户历史订单+FAQ+会话摘要”,个人知识库需要“检索到的Top-K文档片段”。如果只用一种策略去应对所有场景,一定会有人觉得它又慢又笨。
1.2 方案选型:上下文模式的三个核心维度
设计上下文模式之前,我建议先想清楚三个维度,因为几乎所有模式都可以拆成这三个维度的组合。
时间维度:上下文是单轮的、多轮的,还是跨会话的?单轮模式只携带当前问题相关的最新信息,适合一次性问答;多轮模式会带上历史对话的摘要,适合需要持续交互的场景;跨会话模式则要引入长期记忆,比如用户偏好、历史偏好声明,甚至上周问过但没解决的问题。
空间维度:上下文来自哪个物理范围?在代码工具里是当前文件、相关文件、整个模块还是整个仓库;在客服场景里是当前工单、用户历史会话、全量用户信息还是产品文档。这个维度决定了采集器需要访问多少数据源。
语义维度:上下文是直接相关的还是间接相关的?直接相关可以通过关键词匹配、路径匹配拿到,间接相关往往要靠向量检索、知识图谱甚至模型自身去挖掘。语义维度决定了检索策略和排序策略的复杂度。
我建议把这三个维度当成坐标系,给每个候选模式打点。比如:
| 模式 | 时间维度 | 空间维度 | 语义维度 | 典型场景 |
|---|---|---|---|---|
| 聚焦模式 | 单轮 | 当前文件 | 关键词 | 解释选中代码 |
| 推理模式 | 多轮 | 相关文件链 | 向量召回 | 跨文件排查Bug |
| 全局模式 | 跨会话 | 整个项目+记忆 | 图谱+向量 | 新需求方案设计 |
这样做的好处是,当你要给产品加新模式时,不用从零设计,而是先在坐标系里找到空缺位置,再补对应的采集器和排序策略。
1.3 模式划分的原则与边界
模式不是越多越好,我见过把上下文模式做成七八个选项的产品,用户根本不知道怎么选,最后全用默认模式。模式划分要遵循几个原则。
第一,每个模式必须有清晰语义,用户看到名字就能猜到它干什么。compact就是更省token,balanced就是兼顾质量与成本,full就是尽量带全信息,这种命名比mode1、mode2强一百倍。
第二,模式之间应该可以降级。不能说用户选了full模式,结果上下文超长就只能报错,而应该设计一条降级链:full超限降级到balanced,balanced超限降级到compact。用户的可预期性比绝对能力更重要。
第三,模式必须能被观测。每次请求是哪个模式、用了多少token、哪些上下文因为超限被丢掉,这些都要有日志。否则用户反馈“回答变差了”,你连复盘的抓手都没有。
第四,模式要支持用户手动覆盖。自动检测再聪明,也会有判断错的时候,给用户一个强制切换的开关,比任何智能优化都能提升信任感。后面我会单独讲自动切换的坑。
2. 核心细节解析与实操要点
2.1 上下文数据模型:别只存字符串
很多新手第一次实现context-mode时,会用一个字符串把所有拼接好,然后直接塞给模型。短期能跑,但很快就发现问题:不知道每段内容来自哪里,不知道哪段内容让模型产生了幻觉,不知道缓存如何失效。所以第一件事就是把上下文建模成结构化对象。
我在项目里用的是这样的数据模型:
from dataclasses import dataclass, field from typing import Any @dataclass class ContextItem: source: str # 来源标识,如 file://src/main.py#L42 content: str # 文本片段 score: float # 当前模式下的相关度 token_count: int # 预估token数 metadata: dict[str, Any] = field(default_factory=dict) # 语言、修改时间、权限等 @dataclass class ContextBundle: mode: str # 当前上下文模式 query: str # 用户输入或任务目标 items: list[ContextItem] # 实际注入的上下文列表 total_tokens: int # 累计token数 truncated: list[str] # 被裁剪掉的source列表 created_at: float # 构建时间为什么一定要带source和truncated?因为后面排错几乎全靠它们。比如模型回答了一个不存在的函数名,你翻日志就能看到这个函数名来自哪个文件;被裁剪掉的source有记录,你就能判断裁剪策略是否太激进。
token_count也很关键。不要到调用模型前才估算token,那样你没法做分层裁剪。我习惯在采集阶段就估算,虽然不可能百分百准,但可以用 OpenAI 的 tiktoken 或本地 Tokenizer 做预切分,误差控制在10%以内足够用了。
2.2 上下文来源采集:从哪里拿,怎么拿
上下文来源五花八门,但采集方式有共性。以代码辅助工具为例,我常用的采集器有五种。
当前活动文件采集器,读取编辑器光标所在文件内容,再加上光标附近的展开区域。这个最常见,也最容易实现。
最近编辑文件采集器,维护一个按修改时间排序的文件列表,取前N个。它能捕捉用户刚刚改过但没保存到当前文件的关联信息。
结构化信息采集器,读取项目里的依赖清单、配置文件、目录结构,让模型知道这个项目的技术栈和组织方式。比如用户问“这个项目怎么跑起来”,模型需要知道package.json里的脚本和Python版本。
语义检索采集器,把用户问题向量化,在项目文件的知识库中做相似度检索。这是full模式和balanced模式的主要区别点,检索质量直接决定回答上限。
用户手动挂载采集器,允许用户手动pin一个文件或一段文本作为“必带上下文”。它的权重应该是最高的,因为用户显式指定的信息一定是最重要的。
采集阶段要注意去重。同一个文件可能同时被“当前活动文件”和“语义检索”命中,如果不去重,上下文里会出现两遍相同内容,既浪费token又干扰模型。我常用的去重键是source + 内容哈希,简单有效。
2.3 权重打分与裁剪策略:决定谁进窗口
采集到的候选上下文可能非常多,但模型窗口有限,所以必须有打分和裁剪机制。打分的目的是给所有候选上下文排一个优先级,裁剪的目的是在token预算内留下优先级最高的那些。
我用的打分公式是加权求和:
score = 0.25 * lexical_similarity + 0.15 * recency_score + 0.30 * semantic_similarity + 0.20 * explicit_weight + 0.10 * structural_importancelexical_similarity是关键词覆盖率,拿用户问题里的关键词去匹配上下文文本,简单粗暴;recency_score是时间衰减因子,比如最近10分钟内的文件记1分,之后按指数衰减;semantic_similarity是向量相似度,由向量检索给出;explicit_weight是用户手动置顶或历史偏好记录的加分;structural_importance是结构权重,比如入口文件、核心类定义比随机配置文件重要。
权重不是拍脑袋定的,我建议先手动标注一批典型问题,然后用网格搜索调参。不要追求理论最优,够用就好。比如我调过一轮后发现,代码问答场景里 structural_importance 太高会把项目入口文件反复带进来,反而压低了真正的答案文件优先级,后来把它的权重从0.2降到0.1。
裁剪分两层。第一层是硬裁剪,按token预算从高到低依次保留,直到预算用尽。第二层是软裁剪,如果某个上下文片段非常大(比如一个几千行的文件),我会先截取其关键部分,而不是整段丢弃。比如只取光标所在函数、类的定义区,以及顶部注释区,而不是整个文件。
2.4 模式切换与用户感知:别让用户陷入黑盒
context-mode最容易翻车的地方就是自动切换。刚开始我觉得全自动最聪明,根据用户当前行为自动匹配模式,结果用户完全不知道该模式下发生了什么。有人问“为什么刚才回答很快现在很慢”,有人问“为什么这个文件你没看到”。问题不在判断逻辑,而在用户感知缺失。
我的做法是:把模式名、当前token用量、上下文来源列表直接暴露给用户。界面里不用放技术词汇,展示“当前模式:聚焦”“已使用上下文量:3.2k/8k”“已载入文件:3个”就好。用户一看就知道发生了什么。
自动切换的触发信号我用三类:
- 行为信号:鼠标/光标停留时间、最近打开文件、复制粘贴行为
- 查询信号:问题长度、关键词类型、是否包含文件名
- 环境信号:当前窗口大小、项目规模、模型服务端的可用上下文窗口
切换时要柔和。不要突然从紧凑模式跳到全局模式,让用户莫名其妙。我后来加了一条规则:只有用户问题中包含文件名或明显跨文件意图时,才自动从聚焦模式升级到推理模式,否则保持当前模式不变。多数误判问题都被这条规则挡掉了。
3. 实操过程与核心环节实现
3.1 最小闭环:一个可用的 context-mode 引擎
理论讲再多,不如看一段能跑起来的代码。下面是一个简化版引擎,核心流程是:根据模式找到采集器、采集候选、打分排序、按token预算裁剪、组装成上下文。
import time class ContextModeEngine: def __init__(self, mode_configs, token_budget=8192): self.mode_configs = mode_configs self.token_budget = token_budget def collect_candidates(self, mode, signal): collector_configs = self.mode_configs[mode]["collectors"] candidates = [] for conf in collector_configs: collector = conf["collector"] items = collector(signal) candidates.extend(items) return candidates def score_candidates(self, candidates, query, mode, user_explicit): scored = [] for item in candidates: score = self._compute_score(item, query, mode, user_explicit) scored.append((score, item)) scored.sort(key=lambda x: x[0], reverse=True) return scored def truncate(self, scored, budget): kept = [] truncated_sources = [] used = 0 for score, item in scored: if used + item.token_count <= budget: kept.append(item) used += item.token_count else: truncated_sources.append(item.source) return kept, truncated_sources def build(self, mode, query, signal, user_explicit=None): candidates = self.collect_candidates(mode, signal) scored = self.score_candidates(candidates, query, mode, user_explicit or {}) kept, truncated = self.truncate(scored, self.token_budget) bundle = ContextBundle( mode=mode, query=query, items=kept, total_tokens=sum(x.token_count for x in kept), truncated=truncated, created_at=time.time(), ) return bundle这个引擎做了基本的职责分离。如果你想加新模式,只需在模式配置里指定新的采集器组合,复用打分和裁剪逻辑,不需要改动主流程。这也是context-mode设计的核心价值:上下文策略可插拔,而不是所有逻辑都堆在AI调用函数里。
注意我在这里没有写_compute_score的具体实现,因为它是纯函数,你完全可以根据自己的场景替换。我在2.3节给出的权重公式可以直接落地,只要把它翻译成代码即可。
3.2 关键参数怎么定:token预算的推导过程
token预算是最容易拍脑袋的参数,但拍错了代价很大。预算太高,请求慢、费用高;预算太低,模型缺信息、回答差。我建议按以下步骤推导,而不是直接设一个固定的“8192”。
可用窗口 = 模型上下文窗口(例如 128k) 输出预留 = 模型回答的最大长度(例如 4k) 系统提示与示例 = 固定开销(例如 2k) 安全余量 = 总窗口的 20%(应对超长输出和token估算误差) 上下文预算 = 可用窗口 - 输出预留 - 系统提示与示例 - 安全余量 = 128k - 4k - 2k - 25.6k = 96.4k但这里有一个容易被忽略的问题:很多模型在长上下文下质量会下降,不是因为窗口不够,而是因为注意力分散。所以我实际上线时,会把计算出的预算再乘一个经验系数,常见的是0.5到0.75。上面的例子折中取0.75,最终上下文预算约72k;再按模式区分,从聚焦模式的8k到全局模式的72k分三档。
另外,token估算误差不可忽略。tiktoken估算比较准,但中文和代码的误差率不同。保险起见,我在代码里统一把估算结果向上取整5%,并在裁剪逻辑中留下最后200 token的缓冲。这样即使有小规模乱码或换行差异,也不会导致最终拼接后超过实际上下文窗口。
3.3 接入大模型接口的结构化封装
上下文拼接不是简单把字符串加起来。如果不做结构化封装,模型可能会把上下文当成用户指令的一部分,导致它把文件内容里的注释误认成命令。我在所有上下文片段外面套上显式的标记,让模型明确知道这块是参考资料。
你是智能助手。以下是本次任务需要用到的上下文资料,全部标记在<context>标签内。 <context> <file path="src/main.py" language="python"> ...代码片段... </file> <file path="src/utils.py" language="python"> ...代码片段... </file> </context> 请根据上述上下文回答用户问题。如果上下文不足,请明确说明。 <user_query> {query} </user_query>这里有三点经验。
第一,source信息必须透传给模型。模型看到file path="src/utils.py"后,回答时会自然使用“utils.py里实现了xxx”这种表达,幻觉率明显降低。
第二,系统提示里的语气要中性。不要用“必须严格按照上下文回答”,否则模型会变得过于保守,明明能推理出答案也不敢答。我试过几种措辞,效果最好的是“请优先引用上下文信息,若缺失则说明”。
第三,调用模型前再检查一次拼接结果。把整个prompt打出来,人工扫一眼是否有残缺标签、重复片段、乱码。这一步看起来原始,但能拦住大多数线上事故。
4. 常见问题与排查技巧实录
4.1 上下文超长被硬截断
我遇到过最频繁的问题是上下文超长。表象是模型API直接报错,或者回答到一半被截断。排查时要先区分是请求前截断还是请求中截断。
请求前截断,通常是token估算不准,特别是代码文件里的中文注释和长字符串会低估。我把估算逻辑从“按字符数乘系数”改成“按内容类型区分”:代码部分用tokenizer精确切分,注释部分按中文特有字符密集度加权,问题才缓解。
请求中截断,往往是因为动态输出太长。解决方法是预留输出空间,并设置max_tokens,同时开启流式输出。如果遇到模型在长回答中途截断,可以把它设计成先给结论再给详情的格式,这样截断时至少拿到了核心答案。
4.2 上下文过时导致答非所问
项目文件一直在变,如果上下文缓存没有及时失效,模型会拿着旧代码回答问题。用户问“我刚改的函数为什么没生效”,模型根据旧代码解释了五分钟,越解释越错。
我的解决办法是给每个文件建立内容哈希,采集器返回上下文时附带哈希值。引擎比较哈希与缓存版本,不一致就直接重新采集,而不是复用缓存。对于特别大的项目,还可以用文件系统事件监听器(比如watchdog)在文件保存后几秒内主动失效该文件的缓存。
另外,用户手动置顶的上下文也要有过期概念。不能因为用户昨天pin了一个文件,今天就一直带着。我在显式上下文上加了时间戳,超过设定时限后自动降权,用户也能看到“该文件已被置顶超过30分钟”的提示。
4.3 自动模式误判与用户冲突
自动模式误判最经典的场景有两种。一种是用户明明在看前端页面,却在聊天框问后端接口设计,自动聚焦模式只带了前端文件,答案自然不对。另一种是用户问“这个项目有几个模块”,自动模式判断为聚焦模式,结果只带了当前目录结构,缺失全局信息。
我处理误判的方式是“先降级,提醒,再升级”。也就是说,当模型回答置信度低,或者检测到当前模式上下文里没有用户问题中的关键词时,不是立刻切换模式,而是追加一条提醒:“当前模式下可能缺少项目全局信息,建议切换到推理模式或全局模式”。把主动权交给用户。
还有一个容易被忽略的问题:用户手动切换的模式与自动模式冲突。如果用户手动切到聚焦模式,而我后台又自动升级到推理模式,用户会觉得系统在跟他对着干。后来我定了规则:用户手动选择优先,自动切换只在用户未手动指定时才生效,且自动切换后必须留下可恢复入口。
4.4 实测数据与避坑经验
最后分享一组我自己项目里的前后对比。最早只有全量模式时,平均每次请求消耗约2.3万token,P95延迟6.8秒,用户对“回答太慢”的抱怨很多。改成聚焦/推理/全局三档后,默认模式设为聚焦,平均消耗降到8千token左右,P95延迟降到3.1秒,回答准确率主观评价提高了约15%。
这个结果说明问题了吗?说明降低上下文量并不一定降低质量,反而因为信息更聚焦而提升了效果。但也带来一个新问题:聚焦模式下,用户问跨文件问题时,模型经常回答“缺少上下文”。所以我又加了“检测到跨文件意图时自动升级为推理模式”的规则,通过问题里的文件名、模块名关键词触发。
避坑经验有几条:
- 不要追求模式数量,先做两档:紧凑和完整;上线一周后再看日志决定是否加第三档。
- 每次请求的上下文日志必须留,包含模式名、source列表、token用量、裁剪源。没有日志,优化就是盲人摸象。
- 权重参数可以从一个基准配置开始,但一定要设计成配置文件而不是硬编码,这样在不同项目间切换时不用改代码。
- 测试用例要包含“上下文不足”场景。直接问模型“信息不够时你会怎么说”,确保它在缺失信息时不胡编。
我个人在实际操作中的体会是,context-mode最值钱的不是那一堆代码,而是你对“哪些上下文真正有用”的判断。判断来自日志、来自失误、来自一次次的对比实验。如果你准备给自己的AI工具加这个能力,先花一周时间记录用户真实提问,把每个问题“应该带哪些文件”都标出来,再回头设计模式,会顺利得多。最后再分享一个小技巧:把模式切换的快捷键放在用户最容易按到的地方,用户越容易控制上下文模式,就越信任你的AI工具。