如果你最近在用各种 AI 助手写代码、做分析,一定遇到过这种状况:同一个问题,换个说法答案就完全不一样;或者明明把所有资料都贴上去了,它却抓住一段无关紧要的配置反复分析。多数人第一反应是模型不行,但我在实际项目中排查过几十个类似案例,真正的原因十有八九不是模型,而是上下文没管理好。context-mode——上下文模式——就是把“该让模型看到哪些信息、以什么顺序看到、以什么格式看到”这件事工程化的一套方法。这篇文章我会结合自己做 AI 应用开发的实战经历,把 context-mode 的组成、实现、踩坑和不同场景的适配思路一次讲透。无论你是在写智能客服、代码助手还是文档问答工具,这篇都值得收藏。
1. 从“答非所问”说起:context-mode 到底在解决什么问题
1.1 一个真实的翻车现场
前阵子我在调试一个数据清洗工具,当时图省事,把整个项目 20 多个源文件、几份设计文档和几轮讨论记录一股脑丢给 AI 助手,希望它帮我定位一个字段解析报错。结果它盯着工具配置文件分析了大半天,最后建议我改依赖版本。可我当时只想知道“date 字段为什么解析失败”,它却在看整个系统的依赖树。
后来我把上下文收窄到三样东西:原始报错信息、date 字段所在函数的完整代码、对应的测试用例。这次一分钟不到就定位了问题——时区格式不一致。同一个模型,同一天,结果天差地别。差别不在模型智力,而在上下文组织方式。这个案例让我意识到,喂给模型什么、不喂什么,本身就是一种需要认真对待的工程决策。
1.2 上下文模式不是“聊天记录”,而是“信息管线”
很多人把 context-mode 理解成“把聊天记录完整保存,再原样喂回去”,这其实是最大误区。原始聊天记录是低信噪比的信息源,里面有一半是寒暄、重复、跑偏内容;而真正有用的是从对话、代码、文档、工具结果中提取出来的“与当前任务相关的有效事实”。
context-mode 的本质是一条信息管线:采集、筛选、压缩、注入。每一步都是一层决策。采集决定收什么,筛选决定留什么,压缩决定丢什么,注入决定怎么摆。你可以把它想象成后厨配菜系统:模型是灶台上的厨师,上下文是提前洗好、切好、按顺序排好的食材。配菜配得好,大火快炒几十秒就出锅;配菜乱成一堆,厨师再厉害也得手忙脚乱。
1.3 为什么这个模式近两年突然这么热
上下文窗口从几万 token 一路涨到百万级,“装得下”已经不是瓶颈,“装得好”成了新瓶颈。模型对长上下文的利用并不均匀,大量实验都发现了一个现象:处在长文本中间位置的信息最容易被忽略,这就是常说的“lost in the middle”。窗口越大,这个问题越突出,因为你往里塞的东西越多,关键信息被淹没的概率越高。
另一个推手是 Agent 应用的爆发。Agent 的上下文里既有用户的话,又有工具返回的 JSON、代码片段、网页正文、记忆检索结果,全是异构数据。这时候单纯靠手动整理提示词已经不够用,业界开始提 Context Engineering 这个概念,本质就是用一套工程化手段管理上下文。context-mode 就是这套工程化手段的落地形态,它把本来靠感觉的事变成了可设计、可复用、可调优的流程。
2. 拆开 context-mode:采集、筛选、压缩、注入四层设计
2.1 采集层:不是所有信息都有资格进上下文
采集层最容易犯的错是“先全收,再靠筛选兜底”。在真实系统里,数据源非常多:日志、代码、文档、聊天记录、数据库返回、第三方 API 响应,如果不做源头控制,后面的筛选层再强也扛不住海量脏数据。
采集层必须坚持三个原则。
第一,来源标注。每一条上下文条目都要带上 source 元数据,比如来自哪个文件、什么时间、什么类型、置信度多高。没有来源的信息就像没有出处的传言,后续既没法追溯,也没法做时效判断。
第二,去重与版本管理。同一个文档可能被反复读取,同一段代码可能有多个版本。采集时按内容哈希去重,并记录版本号,只保留最新版本。
第三,源头控制。在采集阶段就和任务目标对照,能早扔就早扔。比如任务是“修 A 接口的 bug”,那么底层日志里与 A 接口无关的消息,在采集阶段就可以直接丢弃,不要给后续流程添堵。
下面是一个最简单的采集元数据结构参考:
| 字段 | 作用 | 示例 |
|---|---|---|
| content | 上下文内容本身 | 报错信息、代码片段 |
| source | 来源标识,用于追溯 | src/utils/date_parser.py#L42 |
| timestamp | 产生时间,用于时效判断 | 2025-06-12T10:23:00Z |
| kind | 内容类型 | code,log,doc,chat,tool |
| priority | 初始优先级 | 1 到 5,5 最高 |
2.2 筛选层:任务相关性是唯一的保留标准
采集完的信息仍然太多,筛选层的任务是把“可能有用”变成“当前任务确实需要”。这里面有两套常用技术路线。
轻量路线是关键词加权打分。把用户请求解析成一组关键词,然后对每条上下文计算命中情况:命中标题词加分,命中正文词加分少一点,完全不相关的直接淘汰。再加上 priority 权重和时间衰减权重,最后按分数取 top_k。这套方案实现成本低,适合快速验证场景。
重量路线是向量检索加重排。先用 embedding 把上下文和用户请求都转成向量,做语义相似度召回,再用更重的 rerank 模型精排。缺点是计算成本高,优点是能处理同义表达,比如用户说“日期格式问题”,上下文中写的是“timestamp mismatch”,向量检索也能捞出来。
无论用哪条路线,筛选前都要先做一件看似简单但极容易被忽略的事:把用户意图翻译成“需要哪些事实”的清单。修 bug 需要的是报错、相关代码路径、最近改动;写方案需要的是现状、约束、历史决策。意图不同,筛选标准完全不同,没有统一的关键词列表能同时服务两种任务。
2.3 压缩层:把“长而全”变成“短而准”
压缩层是 context-mode 里最考验功力的环节。压缩方式通常有三种。
第一种是摘要式压缩,调用模型对长文本生成摘要。这种适合解释性内容,比如设计文档的背景说明、讨论记录的结论部分。第二种是结构化压缩,把自然语言转成关键字段,比如把一段工具返回的 JSON 只保留必要字段,或者把一段配置说明转成表格。第三种是截断式压缩,只保留开头、结尾或关键词所在的行,适合代码报错这一类本来就有固定格式的内容。
压缩有一条硬性原则:不得吞掉数字、标识符、专有名词。如果摘要里丢了具体的端口号、变量名、报错码,这个摘要就是不合格的。我见过太多人让模型做摘要,结果端口从 8080 变成“某个端口”,变量名从parse_date变成“日期解析函数”,模型后面就只能靠猜。
比较稳的做法是混合压缩:先把实体和关键数字用正则或 NER 抽取出来单独存放,再让模型只压缩叙述性文字,最后把实体列表拼回去。压缩率可以做到 60% 以上,同时关键信息零丢失。
2.4 注入层:放在哪里、用什么格式,决定了模型怎么用
信息经过筛选压缩之后,最后一步是注入。很多人把注入想得太简单,以为只要把内容拼进 prompt 就行。实际上放置位置和格式对效果影响巨大。
先说位置。模型对长文本不同位置的注意力是不一样的。任务指令应该放在开头或结尾,这两处是注意力最强的位置;需要精细推理的关键证据,比如报错信息、关键代码,要尽量靠近结尾;大量辅助性背景材料放在中间。如果你把最关键的证据埋在中段,模型很可能直接忽略它。
再说格式。代码用代码块包好,结构化数据用 JSON 或表格呈现,历史对话保持对话格式。格式混乱会让模型花额外精力去理解结构,真正干活的算力就少了。
最后是预算。我一般会把上下文预算分成四块:系统指令占 20%,任务目标占 10%,最新用户请求占 10%,检索事实占 45%,历史摘要占 15%。这个比例可以根据场景调,但一定要有意识分配,不能让某一类内容无限膨胀。
| 层 | 核心职责 | 常用技术 | 常见误区 |
|---|---|---|---|
| 采集 | 统一信息源,带元数据入库 | 结构化日志、事件总线、哈希去重 | 什么都要,整库注入 |
| 筛选 | 按任务相关性缩小范围 | BM25、向量检索、rerank | 只看相似度不看任务目标 |
| 压缩 | 降长度、保关键信息 | LLM 摘要、正则抽取、截断 | 压缩时丢失数字与标识符 |
| 注入 | 决定位置、格式与预算 | Prompt 模板、token 预算控制 | 所有内容无差别拼接 |
3. 手写一个最小可用的 context-mode 管线
理论讲再多,不如代码能跑。下面我把上面四层设计实现成一个不到 150 行的 Python 骨架,你可以直接复制运行,然后替换成自己的数据源和模型接口。
3.1 先定义数据模型:上下文条目长什么样
在动手写逻辑之前,先把数据结构定下来。我用 dataclass 定义ContextItem,里面包含五个字段,对应前面表格里的元数据。
from dataclasses import dataclass from datetime import datetime import re @dataclass class ContextItem: content: str source: str timestamp: datetime kind: str # code / log / doc / chat / tool priority: int = 1 # 1 - 5,5 最高这个结构是整个管线的地基。后续的筛选、压缩、注入都依赖这些字段,所以采集层一开始就要保证它们被正确填充。如果源头信息没有时间和来源,宁可丢弃,也不要把脏数据放进来。
3.2 采集:把散落的信息源拧成统一结构
真实场景里,采集层要对接 git diff、日志系统、向量库、文件系统等等。这里我只做一个模拟版本,用三个函数分别代表从代码库、报错日志、历史讨论中采集信息,返回值统一为ContextItem列表。
def collect_code_files(): return [ ContextItem( content="def parse_date(raw):\n return datetime.strptime(raw, '%Y-%m-%d')", source="src/utils/date_parser.py", timestamp=datetime(2025, 6, 12, 10, 0), kind="code", priority=4, ), ContextItem( content="date = parse_date(row['date']) # ValueError here", source="src/etl/pipeline.py#L37", timestamp=datetime(2025, 6, 12, 10, 5), kind="code", priority=5, ), ] def collect_logs(): return [ ContextItem( content="ValueError: time data '2025/06/12' does not match format '%Y-%m-%d'", source="logs/app.log", timestamp=datetime(2025, 6, 12, 10, 6), kind="log", priority=5, ), ] def collect_history(): return [ ContextItem( content="用户反馈:日期字段偶尔解析失败,怀疑是分隔符不一致", source="chat/thread_1024", timestamp=datetime(2025, 6, 12, 9, 30), kind="chat", priority=3, ), ] def collect_all(): return collect_code_files() + collect_logs() + collect_history()注意这里的priority已经在源头标好。报错日志和报错代码行是 5,普通工具函数是 4,历史讨论是 3。这个初始优先级会直接影响筛选阶段的结果,所以采集时要想清楚:什么信息对当前任务最重要。
3.3 筛选:用关键词加权把相关事实捞出来
筛选层我做一个轻量实现:从用户请求里拆关键词,对每条上下文做匹配打分,再加上优先级和时效权重。语义检索的接入点也保留,等你有向量库之后,只需替换打分函数即可。
def score_item(item: ContextItem, keywords, now: datetime): text = item.content.lower() score = 0.0 for kw in keywords: if kw in text: score += 1.0 elif kw in item.source.lower(): score += 0.5 score += item.priority * 0.8 # 时效权重:超过 24 小时,按小时衰减 hours = (now - item.timestamp).total_seconds() / 3600 if hours > 24: score *= max(0.3, 1.0 - (hours - 24) * 0.01) return score def filter_items(items, keywords, top_k=5): now = datetime.now() ranked = sorted( items, key=lambda x: score_item(x, keywords, now), reverse=True, ) return ranked[:top_k]这一步体现的筛选逻辑很朴素但够用:关键词命中说明和当前问题直接相关,优先级高说明是核心证据,时间近说明信息还没过期。我见过很多生产环境用比这复杂得多的方案,但基本逻辑都是这三条。先跑通这个版本,再考虑升级向量检索,不要在第一天就引入重依赖。
3.4 压缩:截断与实体保护
压缩阶段我希望演示“实体保护”的思路:先把内容里的数字、变量名、关键符号抽出来,再截断,最后把保留的实体信息附加回去。这对于日志和报错信息尤其重要。
ENTITY_PATTERN = r"[A-Za-z_][A-Za-z0-9_]*\(\)|\b\d{2,5}\b|\b[A-Za-z_][A-Za-z0-9_]*\b" def extract_entities(text: str): return list(set(re.findall(ENTITY_PATTERN, text))) def compress_item(item: ContextItem, max_chars=300): entities = extract_entities(item.content) if len(item.content) <= max_chars: return item head = item.content[: int(max_chars * 0.6)] tail = item.content[-int(max_chars * 0.4):] compressed = head + "\n...[压缩中段]...\n" + tail compressed += f"\n[关键实体] {', '.join(entities[:20])}" return ContextItem( content=compressed, source=item.source, timestamp=item.timestamp, kind=item.kind, priority=item.priority, )从extract_entities可以看到,我把函数调用形式的标识符、两到五位的数字、普通变量名都当成需要保护的实体。真实场景里你还需要保护 IP、端口、订单号这类格式,可以往正则里继续加规则。这段代码想表达的核心是:压缩是“删冗余保关键”,不是简单截断。
3.5 注入:组装成模型能直接开工的 Prompt
最后一步是把处理好的上下文拼进 prompt 模板,同时标注每条信息的来源。标注来源有两个作用:一是让模型可以引用具体文件,减少凭空编造;二是当你调试结果时能回头检查哪条上下文起了作用。
def build_prompt(user_query, items, instruction=None): lines = [] if instruction: lines.append(f"[系统指令] {instruction}") lines.append("[任务目标] 请基于以下上下文完成用户请求。") lines.append("[上下文开始]") for idx, item in enumerate(items, 1): lines.append(f"{idx}. [{item.kind}] ({item.source})") lines.append(item.content) lines.append("[上下文结束]") lines.append(f"[用户请求] {user_query}") return "\n".join(lines) def run_pipeline(user_query, keywords, instruction=None): items = collect_all() filtered = filter_items(items, keywords) compressed = [compress_item(i) for i in filtered] return build_prompt(user_query, compressed, instruction) if __name__ == "__main__": user_query = "日期字段解析失败,请定位原因" keywords = ["date", "parse", "valueerror", "strptime"] prompt = run_pipeline(user_query, keywords) print(prompt)跑一遍的输入大概是这样的结构:系统指令在最前,检索事实按顺序排在中间,用户请求放在最后。注意关键报错内容因为优先级高、时效新,被筛选排在前面;而用户原始请求永远在最后,让模型在输出时能直接对照最新问题。这个模板不是最优解,但它是可运行的最小闭环,你可以在它上面继续调。
4. 实战踩坑:跑进生产环境后遇到的那些问题
代码骨架能用只是开始。真正把 context-mode 跑进生产环境,我前前后后踩了不少坑,下面五个是最典型、也最容易复现的。
4.1 上下文过载:窗口没满,模型先“迷路”
第一个坑是过度追求“全”。我当时负责的一个客服机器人,检索层很给力,每次能召回 50 条相关知识库条目,每条都跟用户问题沾边。结果模型经常忽略最关键的那一条,反而用次相关的知识回答问题。
问题在于信噪比。50 条“相关但不够核心”的信息塞进去,单看每一条都合理,合在一起就把最核心的那几条稀释了。再加上位置效应,中间部分的信息更容易被忽略。
修复方法是狠下心控制数量。我先把 top_k 从 50 砍到 8,再把结果交给 rerank 模型精排,只保留最核心的 3 到 5 条。结果准确率反而提升了。核心教训:上下文宁可精,不要多;信息量不等于信噪比。
4.2 过期上下文污染:昨天的结论带偏今天的任务
第二个坑来自记忆和历史的时效。我们的工具支持多轮会话,早期设计是把所有历史消息都保留并压缩进上下文。有一次上午模型分析说是“A 模块的问题”,下午开发人员已经修复了 A 模块并给出新报错,但因为上午的错误结论还在上下文里,模型继续按 A 模块排查,浪费了很多时间。
这是典型的上下文污染。旧结论没有被标记为失效,模型分不清哪些信息描述的是“当前状态”,哪些描述的是“历史过程”。
对策分两步。第一步是给上下文加失效机制:当代码发生变更、任务状态更新时,把相关旧条目标记为 invalid 或降低时效权重。第二步是推理前做“上下文刷新”:把“当前已知事实”单独放一块,把“历史过程”放另一块,并明确告诉模型哪些已是过去式。模型就不会再抱着旧结论不放。
4.3 多轮对话指代断裂:模型不知道“那个函数”是哪个
第三个坑在压缩阶段。用户第一轮说“看下get_user函数”,第二轮说“在那个函数里加个判断”。上下文压缩时,摘要模型把get_user改成了“用户查询函数”。第二轮模型面对“在那个函数里加判断”,完全不知道“那个”指的是哪个函数。
根因是我让摘要模型自由压缩了所有内容,没有保护标识符。模型做摘要时天然倾向把内容泛化成更“通顺”的表达,函数名、变量名、类名这种精确符号往往被牺牲。
修复方案是前面代码里演示的实体保护:在压缩前用正则把get_user这类标识符抽取成实体清单,压缩完再把实体列表附加回去。同时,多轮对话里我把“已提到的实体”单独存成一个字段,每一轮都原样保留在上下文中,模型就能流畅建立指代关系。
4.4 摘要压缩吞掉关键数字:8080 变成了“某个端口”
第四个坑和 4.3 一脉相承,但更隐蔽。有一次文档问答场景,原始文档写着“服务默认监听 8080 端口,超时时间 30 秒”。模型摘要后变成“服务默认监听某个端口,超时时间为几十秒”。用户追问具体端口,模型真的猜了一个数字。
模型做摘要本质上是在做概率化的文本改写,遇到不理解的数字时,它倾向于模糊化处理。但技术场景里端口、超时、版本号、错误码恰恰是最不能模糊的信息。
我后来定了一条铁律:所有数字、版本号、错误码、地址走正则抽取,永远不经过摘要模型;摘要只处理叙述性文本。关键数字在上下文中逐字保留,必要时加粗或单独成行,确保它们不会出现在注意力盲区。
4.5 只喂上下文不喂任务目标:模型成了“知道很多但不知道干嘛”的百科全书
最后这个坑不算技术问题,但比技术问题更常见。很多人的 prompt 里堆了一大堆上下文,却没有清楚说明“你是谁、你的任务是什么、输出什么格式”。模型就像被塞了一堆资料但没有拿到任务书的实习生,只能把知识综述一遍。
比如“根据以下信息回答用户问题,回答请用中文”这种系统指令约等于没有。更好的做法是把角色、目标、约束、输出格式都写清楚。系统指令可以写成:“你是资深后端工程师。你的任务是定位日期解析报错。限制:只基于提供的代码与报错信息,不猜测。输出格式:先给失败原因,再给出可执行的修复代码块。”
任务指令要放在两个强位置:prompt 开头和结尾。开头定方向,结尾收束输出。上下文中间的部分才不会把目标带偏。
我把上面五个坑和对应的排查思路整理成了一张表,方便你对照自检:
| 症状 | 可能原因 | 快速验证方法 | 修复方向 |
|---|---|---|---|
| 信息都相关但答非所问 | 上下文过载,关键信息被稀释 | 把 top_k 砍到 5 条再测 | 精排 + 减量注入 |
| 答案被旧信息带偏 | 旧结论未失效 | 检查上下文中是否含过时描述 | 版本化、失效标记、时间衰减 |
| 模型分不清“那个函数” | 标识符被摘要泛化 | 看压缩后是否保留函数名 | 实体抽取与列表保留 |
| 端口、数量被猜错 | 摘要模型泛化数字 | 检查关键数字是否原样存在 | 数字不走摘要模型 |
| 输出像综述不像干活 | 系统指令没有任务目标 | 读一遍系统指令是否具体 | 强化角色、目标、输出格式 |
5. 不同场景下的 context-mode 适配思路
5.1 AI 代码助手:按调用链与报错组织上下文
代码助手和普通问答最大的区别在于,代码信息高度结构化,散装文本喂进去效率极低。我的经验是,不要整文件塞上下文,而是抽取三样东西:函数签名、调用链、报错栈。函数签名告诉模型这个函数接收什么、输出什么;调用链告诉模型这个函数在哪些地方被用到;报错栈直接锚定问题位置。
实现上可以用 tree-sitter 这类解析器抽取 AST 中的函数声明和调用关系。关键代码片段按行号锚定,比如src/etl/pipeline.py#L37,这样模型修改代码时能准确找到位置。编译错误和测试输出要原样保留,不要做摘要,因为错误信息里哪怕少一个字符都可能导致误判。
5.2 文档问答:分层摘要加引用溯源
文档问答场景里,文档往往很长,整篇注入不现实,只检索片段又容易断章取义。我用的是分层摘要策略:先给模型一个文档地图,列出文档包含哪些章节和各章节主题;再给命中章节的中层摘要;最后给具体段落的原文。三层信息同时注入,模型既能看到全局结构,又能看到精确细节。
引用溯源在这个场景是刚需。每条检索片段都要跟着来源信息,回答时强制模型标注“根据文档第 X 章”。这既提升了可信度,也方便用户回去核实。一旦模型开始引用不存在的章节,你也能立刻从注入的上下文里定位问题——一定是来源标注环节出了差错。
5.3 Agent 工具调用:结构化结果与调用链并重
Agent 场景里,上下文的最大特点是工具返回结果非常结构化:JSON、状态码、字段值。我的做法是,工具返回先做一层结构化摘要,只保留当前任务需要的字段,减少 token 占用。但有一个例外:失败信息必须原样保留,不能摘要,因为失败原因往往藏在细节里。
同时要把调用链记录下来。模型做完一次工具调用后,上下文里应该能看到“调用了哪个工具、传了什么参数、返回了什么”。没有调用链,模型在多步任务里会忘记自己做到哪一步,反复调用同一个工具或者跳过必要步骤。这是 Agent 掉进死循环最常见的原因之一。
5.4 本地工具与 IDE 插件:把隐式上下文显式化
最后一个场景是本地工具和 IDE 插件。这类应用有个独特优势:能拿到用户当前行为。编辑器选区、光标所在函数、最近打开的文件、终端输出,这些都是高价值的隐式上下文。
但隐式信息必须转成显式标签,模型才知道该用哪部分。我习惯把这些信息包装成带标签的上下文块,比如[当前选区]、[光标附近函数]、[最近终端输出]。带上标签之后,模型能更准确地判断哪些信息描述的是用户正在操作的对象,哪些只是背景资料。这个简单的改动比我预想的提升大得多。
| 场景 | 核心数据源 | 推荐注入策略 | 最该防的坑 |
|---|---|---|---|
| AI 代码助手 | 函数签名、调用链、报错栈 | 报错前置,代码按行号锚定 | 整文件灌入 |
| 文档问答 | 检索片段、目录结构 | 地图 + 中层摘要 + 原文三段式 | 断章取义、无引用 |
| Agent 工具调用 | 工具返回、调用链 | 结构化摘要 + 失败原文 | 只留结果不留调用过程 |
| IDE 插件 | 选区、光标、最近文件 | 显式标签分组注入 | 隐式上下文无标签 |
最后说说我个人折腾 context-mode 最大的体会:它的核心不是“塞得多”,而是“决策什么该塞”。早期我总想用满上下文窗口,结果窗口越大,模型越容易在无关信息里打转。后来我给自己定了一个土规矩——每次注入之前,先问一句:如果我是模型,只靠这些信息,能不能立刻开始干正事?不能,就继续删。
配合这个规矩,我手里的 AI 工具从“偶尔惊艳、时常抽风”慢慢变成了“稳定可用”。如果你也要在自己的应用里加 context-mode,建议从最小的四层管线开始,先用关键词筛选和截断压缩跑通,再逐步上向量检索和模型摘要,每加一层复杂度都要回头验证响应质量。多轮迭代几次,你会回来感谢曾经那个愿意删上下文的自己。