1. 上下文工程到底在解决什么问题
第一次听到“上下文工程”这个词,很多人会下意识觉得又是新瓶装旧酒。毕竟提示词工程的热度还没完全过去,怎么又冒出来一个新概念。但我自己在实际项目里踩过几轮坑之后发现,这两者关注的根本不是同一个层面的事情。提示词工程更像是在琢磨“怎么把一句话说漂亮”,而上下文工程关心的是“在模型开口之前,它的脑子里应该装进哪些信息”。
举个很直观的例子。你让一个AI助手帮你分析一份合同里的风险条款,如果只给它一句“请分析这份合同的风险”,它大概率会给你一堆泛泛而谈的套话。但如果你在它开始工作之前,就把合同全文、相关的法律条文摘要、公司内部的合规红线文档、以及过去类似合同的处理记录一起塞进它的上下文窗口,它给出的分析质量会发生质的变化。这个“塞什么进去、怎么塞、塞多少”的过程,就是上下文工程的核心工作。
我之所以对这个话题特别有感触,是因为在过去一年多的项目实践中,我观察到同一个模型在不同上下文配置下的表现差异,有时候比换一个模型还要大。很多人花大量时间纠结“用哪个模型”,却忽略了“给模型看什么”这个更关键的变量。上下文工程要解决的,就是让AI在生成回答之前,能够看到它真正需要看到的信息,同时把那些会干扰它判断的噪音挡在外面。
这个领域适合谁来关注呢?如果你是在做AI应用开发的工程师,上下文工程直接决定了你的产品效果上限。如果你是在用AI工具处理日常工作的普通用户,理解上下文的组织逻辑能让你得到更靠谱的输出。甚至如果你只是对AI感兴趣、想搞清楚为什么有时候它很聪明有时候又很蠢,了解上下文工程也能帮你建立更准确的认知。
2. 核心思路拆解:为什么是“看到该看的东西”
2.1 从“提示词”到“上下文”的认知升级
早两年大家聊得最多的是提示词怎么写。网上流传着各种“万能提示词模板”,什么角色设定、任务描述、输出格式三件套。这些东西有没有用?有用,但作用被严重高估了。我做过一个对比实验,同一个任务,用精心设计的提示词模板加上空白的背景信息,和用很朴素的指令加上充分的相关资料,后者的输出质量稳定碾压前者。
原因其实不复杂。大语言模型的工作原理决定了它本质上是一个“基于已有信息做概率预测”的系统。你给它的上下文越相关、越充分、越结构化,它预测出正确答案的概率就越高。提示词只是上下文的一个组成部分,而且往往不是最重要的那个部分。真正决定输出质量的是:模型在当前对话中能“看到”的全部信息。
这就引出了上下文工程的第一条核心原则:信息的相关性比指令的精巧性更重要。与其花半小时打磨一句指令的措辞,不如花十分钟想想有没有遗漏关键背景信息。
2.2 上下文窗口不是越大越好
现在很多模型都支持很长的上下文窗口,动辄几十万甚至上百万token。这听起来很美好,好像可以把所有东西都塞进去。但实际用下来,事情没这么简单。
我做过一个测试,把一个大约五万字的项目文档全部塞进上下文,然后问一个需要综合多处信息才能回答的问题。结果模型确实“看到”了所有内容,但它的回答质量反而不如我只给它三页最相关的摘要。为什么?因为信息过载会稀释注意力。模型在处理长上下文时,对每个位置的关注度并不是均匀分布的。大量无关或弱相关的内容会分散它的“注意力资源”,导致真正关键的信息被淹没。
所以上下文工程的第二条原则是:精准投喂优于全量灌输。你需要像一个编辑一样,从海量信息中筛选出最相关的那部分,用最清晰的方式呈现给模型。这比一股脑全塞进去要有效得多。
2.3 上下文的层次结构
在实际操作中,我习惯把上下文分成几个层次来管理。最底层是系统级指令,这部分通常由应用开发者预设,用户看不到但始终生效。往上一层是会话历史,也就是之前几轮对话的内容。再往上是当前轮次用户输入。最上面是动态检索到的外部知识,比如从知识库中匹配到的文档片段。
不同层次的信息在上下文中的位置、格式、详细程度都应该有所区别。系统级指令要简洁明确,会话历史要保留关键结论但可以压缩细节,外部知识要标注来源和置信度。这种分层管理的思路,能让模型在处理复杂任务时更有条理。
注意:很多开发者把所有信息平铺在同一个层级里,导致模型分不清哪些是硬性约束、哪些是参考建议。这种“扁平化”的上下文结构是输出质量不稳定的常见原因。
3. 核心细节解析与实操要点
3.1 信息筛选:怎么判断什么该让AI看到
信息筛选是上下文工程里最考验判断力的环节。我的经验是围绕三个维度来评估每一条候选信息:相关性、时效性、权威性。
相关性最好理解,就是这条信息和当前任务有没有直接关系。但这里有个容易犯的错误,就是把“沾边”当成“相关”。比如你在做一个技术方案评审,某篇文档提到了类似的技术栈但应用场景完全不同,这种信息塞进去反而会误导模型。我的做法是问自己:如果删掉这条信息,模型的回答会变差吗?如果答案是“不确定”或“不会”,那就删掉。
时效性在快速变化的领域特别重要。过时的信息不仅没用,还可能有害。我一般会给每条信息标注时间戳,在构建上下文时优先选择最近的内容。如果必须使用旧信息,会在前面加上明确的说明,比如“以下内容来自两年前的文档,部分细节可能已经变化”。
权威性决定了信息的可信度权重。来自官方文档、经过验证的测试报告、有明确出处的数据,这些应该优先于个人笔记、论坛讨论和未经验证的推测。在上下文里,我会用不同的标记来区分信息等级,让模型知道哪些是“硬事实”,哪些是“参考意见”。
3.2 信息组织:格式决定理解效率
筛选出该给AI看的信息之后,怎么组织这些信息同样关键。我试过很多种格式,总结下来有几个原则比较通用。
第一,用清晰的分隔符把不同来源的信息隔开。模型对结构化的输入理解得更好。我通常用Markdown的标题层级来组织,每个信息块有明确的标题和来源标注。
第二,关键信息要前置。虽然理论上模型能处理任意位置的信息,但实际测试下来,放在上下文前部和后部的信息被有效利用的概率更高。所以我会把最重要的约束条件和核心参考资料放在最前面,把补充性的背景信息放在后面。
第三,控制单条信息的长度。一条信息如果超过一定长度,模型对它的理解精度会下降。我的经验是单条信息块控制在500到1000字比较合适,超过这个长度就考虑拆分或摘要。
第四,用表格和列表来呈现结构化数据。如果信息本身有明确的字段和对应关系,用表格比用自然语言描述要高效得多。模型对表格的理解准确率明显更高。
3.3 动态上下文:根据任务阶段调整信息供给
一个常被忽略的点是,上下文不应该是静态的。在任务的不同阶段,模型需要看到的信息是不一样的。
以我做过的一个代码审查助手为例。在理解代码意图的阶段,上下文里主要是代码文件、相关的需求文档和架构说明。到了识别潜在问题的阶段,我会动态加入常见的代码缺陷模式库和团队的历史审查记录。到了生成修改建议的阶段,又会加入代码风格指南和最佳实践文档。
这种动态调整的好处是,每个阶段模型看到的都是最相关的信息,不会被其他阶段的内容干扰。实现上可以通过工作流引擎来控制,在每个步骤开始前重新构建上下文。
提示:动态上下文的关键是定义清楚每个阶段需要什么信息,以及信息之间的依赖关系。建议先用文档把流程画出来,再动手写代码。
3.4 上下文压缩:长对话中的信息保鲜
多轮对话场景下,上下文会随着轮次增加不断膨胀。如果不做处理,很快就会超出窗口限制,或者因为信息过载导致质量下降。上下文压缩是必须掌握的技能。
我的做法是分三步走。第一步,识别关键信息。每轮对话结束后,提取出结论性的内容、用户明确表达的偏好、以及达成的共识。第二步,压缩历史。把之前的详细对话替换成摘要,只保留关键结论和必要的上下文衔接。第三步,定期清理。对于已经不再相关的历史信息,直接移除。
这里有个细节需要注意:压缩后的摘要要保留足够的上下文衔接信息,否则模型可能会“忘记”之前讨论过什么。我通常会在摘要里保留时间线和因果关系,比如“用户先提出了A需求,然后补充了B约束,最终确认了C方案”。
4. 实操过程与核心环节实现
4.1 搭建一个上下文管理的基本框架
说了这么多原理,接下来分享一个我在实际项目中用的上下文管理框架。这个框架不依赖特定平台,用任何编程语言都能实现。
核心思路是维护一个上下文对象,包含几个固定的区域:系统指令区、任务背景区、参考资料区、对话历史区、当前输入区。每个区域有独立的更新逻辑和容量限制。
class ContextManager: def __init__(self, max_tokens=8000): self.system_instructions = "" self.task_background = [] self.references = [] self.conversation_history = [] self.current_input = "" self.max_tokens = max_tokens def add_reference(self, content, source, relevance_score): # 按相关性排序,低分内容在容量不足时优先淘汰 self.references.append({ "content": content, "source": source, "score": relevance_score, "timestamp": time.time() }) self.references.sort(key=lambda x: x["score"], reverse=True) self._trim_to_fit() def _trim_to_fit(self): # 估算当前token数,超出限制时从低优先级内容开始裁剪 while self._estimate_tokens() > self.max_tokens: if self.references: self.references.pop() elif self.conversation_history: self.conversation_history.pop(0) else: break def build_prompt(self): sections = [] if self.system_instructions: sections.append(f"## 系统指令\n{self.system_instructions}") if self.task_background: sections.append(f"## 任务背景\n" + "\n".join(self.task_background)) if self.references: ref_text = "\n\n".join([ f"### 参考资料:{r['source']}\n{r['content']}" for r in self.references ]) sections.append(f"## 参考资料\n{ref_text}") if self.conversation_history: sections.append(f"## 对话历史\n" + "\n".join(self.conversation_history)) sections.append(f"## 当前输入\n{self.current_input}") return "\n\n".join(sections)这个框架的核心逻辑是:所有信息都带优先级标记,当总容量接近上限时,从优先级最低的内容开始裁剪。这样能保证最重要的信息始终在上下文中。
4.2 信息检索与注入的完整流程
在实际应用中,上下文工程往往和检索系统配合使用。完整的流程大致是这样的:
用户输入一个问题后,系统首先对问题进行意图识别和关键词提取。然后根据提取的关键词,从知识库中检索相关文档片段。检索结果经过重排序,选出最相关的若干条。这些片段被格式化后注入到上下文中。同时,系统会检查对话历史,如果之前讨论过相关话题,把相关轮次的摘要也加入上下文。最后,所有信息按照优先级排列,组装成最终的提示。
这个流程里有几个关键决策点。检索返回多少条结果?我的经验是先用较大的召回数量(比如20条),然后通过重排序模型精选出3到5条。注入时给每条结果标注来源和相关性分数,让模型自己判断可信度。
还有一个容易忽略的细节:检索到的信息之间可能有冲突。比如两份文档对同一个问题给出了不同的答案。这时候需要在上下文里明确标注冲突,并提示模型注意甄别。我通常会在冲突信息前加上“注意:以下两份资料存在不一致”的说明。
4.3 参数选择与容量规划
上下文窗口的容量分配需要根据任务类型来定。我总结了一个粗略的分配比例,供参考:
| 任务类型 | 系统指令 | 任务背景 | 参考资料 | 对话历史 | 当前输入 |
|---|---|---|---|---|---|
| 单轮问答 | 10% | 20% | 50% | 0% | 20% |
| 多轮对话 | 10% | 10% | 30% | 40% | 10% |
| 文档分析 | 5% | 15% | 65% | 5% | 10% |
| 代码审查 | 10% | 20% | 40% | 20% | 10% |
这些比例不是固定的,需要根据实际效果调整。但有一个原则是不变的:参考资料和任务背景应该占据最大的份额,因为它们是模型做出准确判断的基础。
容量估算方面,中文文本大致可以按1个token对应1.5到2个汉字来估算。英文文本按1个token对应0.75个单词估算。实际使用时建议留出20%的余量,避免因为估算误差导致超出限制。
4.4 效果验证与迭代
上下文工程不是一次性的工作,需要持续迭代。我通常用几个指标来衡量效果:回答准确率、信息引用率、以及用户满意度。
回答准确率通过人工评估或自动评估来测量。信息引用率是指模型的回答中正确引用了上下文中提供的资料的比例,这个指标能反映上下文是否被有效利用。用户满意度则通过反馈机制收集。
迭代时每次只改一个变量,比如调整参考资料的数量、改变信息的排列顺序、或者修改格式。这样能清楚地知道哪个改动带来了效果变化。我见过很多人一次性改一堆东西,结果效果好了不知道是哪个起了作用,效果差了也不知道该回退什么。
5. 常见问题与排查技巧实录
5.1 模型“看不到”关键信息怎么办
这是最常见的问题。你明明把信息放进上下文了,但模型的回答里完全没有体现。排查思路如下:
首先确认信息确实在上下文里。有时候是代码bug导致信息没被正确注入,或者被后续的裁剪逻辑误删了。打印出最终组装的提示,逐段检查。
如果信息确实在,但模型没用到,可能是位置问题。试试把关键信息移到上下文更靠前或更靠后的位置。中间位置的信息被忽略的概率最高。
还可能是格式问题。如果关键信息淹没在一大段文字里,模型可能注意不到。试试用加粗、标题、或者单独成段的方式来突出它。
最后一个可能是信息之间的冲突。如果上下文里有和关键信息矛盾的内容,模型可能会选择相信另一个。检查一下有没有这种情况。
5.2 上下文超限的应急处理
有时候任务紧急,来不及做精细的上下文管理,但信息量又确实超出了窗口限制。这时候可以用一些应急手段。
最直接的是摘要压缩。把长文档用模型自己生成摘要,然后用摘要替代原文。但要注意摘要可能丢失细节,适合对精度要求不极端的场景。
另一个方法是分块处理。把任务拆成多个子任务,每个子任务用独立的上下文窗口处理,最后汇总结果。这种方法适合文档分析类任务。
还可以用检索替代全量注入。不把所有文档都塞进去,而是根据当前问题动态检索最相关的片段。这需要有一个可用的检索系统。
注意:应急处理只是权宜之计,长期来看还是要把上下文管理机制建设好。我见过太多项目因为上下文管理混乱导致效果不稳定,最后不得不推倒重来。
5.3 多轮对话中的信息丢失
多轮对话场景下,随着轮次增加,早期的重要信息可能会被压缩或裁剪掉。用户会感觉AI“失忆”了。
我的解决方案是维护一个“关键信息池”。每轮对话结束后,提取出需要长期记住的信息,单独存储。这些信息不参与常规的裁剪逻辑,始终保留在上下文中。比如用户的偏好设置、已经确认的事实、达成的共识等。
另一个技巧是在对话历史压缩时,保留“决策点”的完整记录。比如用户在哪一轮做出了什么选择,基于什么理由。这些信息对后续对话的连贯性很重要。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| 模型忽略关键信息 | 位置不佳/格式不突出/存在冲突 | 检查信息位置和格式 | 前置关键信息,用标题突出 |
| 回答质量不稳定 | 上下文结构扁平/优先级不清 | 检查信息分层 | 建立层次化上下文结构 |
| 多轮后效果下降 | 历史信息过载/关键信息丢失 | 检查压缩逻辑 | 维护关键信息池 |
| 检索信息未被使用 | 相关性低/格式混乱 | 检查检索质量和注入格式 | 提高检索精度,规范格式 |
| 输出与资料矛盾 | 资料冲突未标注 | 检查资料一致性 | 标注冲突并提示模型甄别 |
5.5 几个踩坑之后才明白的道理
第一个道理:上下文工程的上限取决于信息质量。如果你的知识库本身内容质量差、更新不及时、结构混乱,再好的上下文管理技巧也救不回来。先把信息源治理好,再谈工程优化。
第二个道理:不要追求“全自动”。我试过完全自动化的上下文构建流程,效果并不好。关键环节还是需要人工判断,比如哪些信息该保留、哪些该丢弃、冲突信息怎么处理。自动化可以处理80%的常规情况,但剩下20%的边界情况需要人来兜底。
第三个道理:测试用例要覆盖边界情况。我一开始只测试正常流程,结果上线后遇到各种意外情况。后来养成了习惯,专门设计一批“刁钻”的测试用例,比如信息冲突、信息缺失、信息过载等场景,提前发现和解决问题。
第四个道理:上下文工程和提示词工程不是对立的。好的上下文需要好的指令来引导模型使用,好的指令也需要好的上下文来支撑。两者配合才能发挥最大效果。我见过只重视其中一方面的项目,效果都不太理想。
6. 不同场景下的上下文策略差异
6.1 知识问答场景
知识问答是上下文工程应用最广泛的场景。核心挑战是在海量知识中找到最相关的内容,并以模型容易理解的方式呈现。
我的策略是“检索-重排-注入”三步走。检索阶段用向量相似度和关键词匹配双路召回,保证召回率。重排阶段用交叉编码器对候选结果精细排序,保证精度。注入阶段把最相关的3到5条结果按相关性降序排列,每条标注来源和置信度。
这个场景下有个特殊考虑:要防止模型“编造”知识库里没有的信息。我会在系统指令里明确要求“仅基于提供的参考资料回答,如果资料中没有相关信息,明确说明不知道”。同时在参考资料区用醒目的格式标注边界。
6.2 代码生成与审查场景
代码场景的上下文有其特殊性。代码本身有严格的语法结构,相关的文档和注释也很重要。
我通常会把上下文分成几个区域:代码文件区、接口文档区、编码规范区、历史审查记录区。代码文件按依赖关系排序,被依赖的放在前面。接口文档只保留与当前代码相关的部分。编码规范提取最关键的几条,不需要全文。
代码审查场景下,我还会加入一个“常见缺陷模式”区域,列出团队历史上经常出现的问题类型。这能显著提高审查的召回率。
6.3 创意写作场景
创意写作和前面两个场景很不一样。它不需要精确的事实依据,但需要丰富的素材和明确的风格指引。
这个场景下,上下文里更多的是风格样例、情感基调描述、以及一些意象素材。我会收集一些和目标风格接近的文本片段作为参考,但不会给太多,避免模型直接抄袭。同时用明确的指令描述想要的风格特征,比如“简洁有力,多用短句,避免形容词堆砌”。
创意场景下,上下文的“留白”也很重要。不要把每个细节都规定死,给模型留出发挥空间。我通常只提供核心设定和关键约束,具体表达让模型自己决定。
6.4 数据分析场景
数据分析场景需要把数据、分析方法和业务背景结合起来。
上下文里通常包含:数据表的schema描述、样本数据、分析目标说明、以及可用的分析方法列表。数据表的描述要准确,字段含义、数据类型、取值范围都要说清楚。样本数据不用太多,几行有代表性的就够了。
这个场景下,我特别注重“分析目标”的描述。要明确告诉模型分析的目的、关注的指标、以及输出的格式要求。模糊的目标会导致模型给出泛泛的分析,没有实际价值。
7. 工具链与基础设施建议
7.1 上下文管理的基础组件
搭建一套可用的上下文管理系统,需要几个基础组件。第一个是信息存储层,用来存放各种来源的资料。可以用向量数据库存语义检索的内容,用关系数据库存结构化信息,用对象存储存原始文档。
第二个是检索层,负责根据查询找到相关信息。通常需要支持多种检索方式:语义检索、关键词检索、混合检索。检索结果要能按相关性排序。
第三个是上下文组装层,把检索到的信息、对话历史、系统指令等组装成最终的提示。这一层需要处理格式转换、容量控制、优先级排序等逻辑。
第四个是效果评估层,用来监控上下文质量并持续优化。需要记录每次请求的上下文构成、模型输出、以及效果反馈。
7.2 评估与监控指标
没有度量就没有优化。我通常关注几个核心指标:
上下文利用率,衡量注入的信息有多少被模型实际引用。这个指标低说明要么信息不相关,要么格式有问题。
回答准确率,通过人工标注或自动评估来测量。这是最终的效果指标。
响应延迟,上下文越长处理越慢,需要在效果和速度之间找平衡。
成本指标,上下文越长token消耗越大,需要控制成本。
这些指标需要持续监控,发现异常及时排查。我一般会设置告警阈值,比如上下文利用率低于某个值就触发检查。
7.3 持续优化的方法论
上下文工程的优化是一个持续过程。我的做法是建立“收集问题-分析原因-实施改进-验证效果”的闭环。
收集问题阶段,重点关注那些模型表现不佳的案例。分析原因时,从上下文的角度找问题:是信息缺失、信息过载、还是信息组织不当。实施改进时,每次只改一个变量,便于归因。验证效果时,用同一批测试用例对比改进前后的表现。
这个循环跑得越多,对什么上下文配置在什么场景下有效就越有感觉。积累到一定程度,很多决策可以凭经验快速做出,不需要每次都做完整测试。
8. 我个人的一些实践体会
上下文工程这个方向,我越做越觉得它像是一门“信息策展”的手艺。核心能力不在于写多复杂的代码,而在于判断什么信息对当前任务真正重要,以及怎么把这些信息以最容易被理解的方式呈现出来。
我最大的体会是:少即是多。刚开始做的时候,总担心信息不够,恨不得把所有相关资料都塞进去。结果发现信息越多,模型反而越糊涂。后来学会了做减法,只保留最核心的信息,效果反而更好。这就像给朋友指路,你不需要把整个城市的地图都给他,只需要告诉他几个关键路口怎么走。
另一个体会是:格式即信息。同样一段内容,用不同的方式呈现,模型的理解效果差异很大。用标题分层、用表格对比、用列表枚举,这些格式本身就在向模型传递“这些信息是什么关系”的信号。花时间设计信息的呈现格式,回报率很高。
还有一个体会是:上下文工程需要领域知识。你越了解一个领域,就越知道什么信息重要、什么信息可以忽略、信息之间是什么关系。这也是为什么通用型的上下文管理工具效果往往不如针对特定领域定制的方案。如果你在做某个垂直领域的AI应用,花时间深入理解这个领域,比研究通用的上下文技巧更有价值。
最后分享一个我常用的检查清单,在每次构建上下文之前过一遍:
- 这个任务最核心的约束是什么?有没有放在最显眼的位置?
- 模型需要哪些背景知识才能理解当前任务?这些知识是否完整?
- 有没有可能产生歧义的地方?需不需要额外说明?
- 信息之间有没有冲突?冲突是否被明确标注?
- 上下文的长度是否在合理范围内?有没有可以精简的部分?
- 信息的格式是否清晰?模型能不能快速定位到关键内容?
这个清单看起来简单,但每次认真过一遍,都能发现一些之前忽略的问题。养成习惯之后,上下文的质量会有明显提升。