1. 从一个真实困境说起:为什么模型越强,效果反而越不稳定
很多人第一次接触大模型应用开发时,都会经历一个相似的阶段:模型选最贵的,参数拉到最大,提示词写得像小作文,结果上线之后效果时好时坏,用户投诉不断。你反复检查代码,发现逻辑没问题;你换更强的模型,效果提升了一点点,但稳定性依然很差。问题到底出在哪?
答案往往不在模型本身,而在上下文工程。所谓上下文工程,指的是围绕模型输入窗口所做的一切设计与管理工作——包括系统指令怎么写、历史对话怎么裁剪、外部知识怎么注入、工具返回结果怎么组织、多轮状态怎么维护。它决定了模型在每一次推理时"看到"什么,而模型看到什么,直接决定了它输出什么。
我见过太多团队把90%的精力花在选模型和调参上,只留10%的精力随手拼一下提示词,结果就是系统表现像开盲盒。反过来,一些看起来"模型并不先进"的产品,因为上下文组织得极其讲究,实际体验反而更稳、更准、更可控。这就是上下文工程的价值所在:它是模型能力与实际业务效果之间的那层放大器,也是决定一个AI应用能不能真正落地的分水岭。
这篇文章适合三类人看:一是正在做AI应用但效果不稳定的开发者;二是负责AI产品设计、需要理解技术边界的产品经理;三是刚入门、想知道"提示词之外还有什么"的学习者。我会从设计思路、核心细节、实操过程到问题排查,把上下文工程这件事掰开揉碎讲清楚,尽量让你看完就能上手改自己的项目。
2. 上下文工程到底在解决什么问题
2.1 模型没有记忆,一切靠"现场喂"
要理解上下文工程为什么重要,先得接受一个反直觉的事实:大模型本身是无状态的。它不像数据库那样存着你的历史数据,每次调用对它来说都是"第一次见面"。你感觉它在跟你连续对话,其实是你每次把之前的对话内容重新塞给它,它才"看起来"记得。
这个机制带来一个直接后果:模型的表现完全取决于你这一次塞给它的内容。塞得清楚,它就答得清楚;塞得混乱、冗余、自相矛盾,它就跟着混乱。很多人抱怨"模型理解能力差",其实往往是输入里同时存在三套互相打架的规则,模型只能猜,猜错就被骂。
打个生活化的比方:模型像一个能力极强但完全没有记忆的临时工。你每次找他干活,都得把背景、要求、参考资料、上次进度全部重新讲一遍。讲得好,他干得漂亮;讲得含糊,他就自由发挥。上下文工程,就是研究怎么把这份"交接说明"写到极致。
2.2 上下文窗口是稀缺资源,不是无限仓库
现在主流模型的上下文窗口动辄几十万token,很多人就觉得"空间大,随便塞"。这是个危险的误区。窗口大不等于可以滥用,原因有三。
第一,注意力会被稀释。上下文越长,模型对其中每一部分的关注度就越平均,关键信息容易被淹没在大量无关内容里。你把整本手册塞进去,模型反而找不到那一条真正相关的规则。
第二,成本随长度线性甚至超线性增长。每次调用都按输入token计费,长上下文意味着真金白银。一个每天调用百万次的应用,上下文从2000token优化到800token,省下的成本相当可观。
第三,长上下文存在"中间遗忘"现象。大量实测表明,模型对上下文开头和结尾的信息利用得最好,中间部分容易被忽略。这意味着你把最重要的指令放在中间,等于白放。
所以上下文工程的核心矛盾是:信息要足够全,但窗口要足够省,且关键信息要放在模型真正看得见的位置。这个平衡怎么找,就是下面要讲的重点。
2.3 上下文工程与提示词工程的区别
很多人把两者混为一谈,其实层次不同。提示词工程关注的是"这一句话怎么写更好",是单点的措辞优化;上下文工程关注的是"整个输入窗口怎么组织",是系统级的结构设计。
| 维度 | 提示词工程 | 上下文工程 |
|---|---|---|
| 关注对象 | 单条指令的措辞 | 整个输入窗口的结构 |
| 时间尺度 | 一次调用 | 多轮、多会话、全生命周期 |
| 核心问题 | 怎么问得更清楚 | 该放什么、放多少、放哪里 |
| 典型手段 | 角色设定、示例、格式约束 | 检索、裁剪、压缩、分层、缓存 |
| 失败表现 | 答非所问 | 时好时坏、越用越乱 |
提示词工程是上下文工程的一个子集。你把单条提示词写到极致,如果上下文组织是乱的,整体效果依然上不去。反过来,上下文结构设计好了,单条提示词哪怕朴素一点,系统也能稳定运行。
3. 上下文工程的核心组成与设计原则
3.1 一个完整的上下文窗口应该包含哪些层
我在实际项目里习惯把上下文分成五个层次,从稳定到易变依次排列。这个分层不是教条,但它能帮你在设计时想清楚"什么该放哪"。
- 系统层:角色定义、能力边界、输出规范、安全约束。这部分几乎不变,适合放在最前面,也适合做缓存。
- 知识层:从外部检索到的文档、数据库查询结果、知识库片段。这部分每次可能不同,是上下文工程的主战场。
- 状态层:当前任务进度、已确认的事实、用户偏好。多轮对话里尤其重要,决定了模型"现在处于哪一步"。
- 历史层:之前的对话轮次。需要裁剪和摘要,不能无脑全塞。
- 当前层:用户这一轮的输入。放在最后,紧挨着模型要生成的位置,利用近因效应。
这个顺序背后的逻辑是:越稳定的越靠前,越相关的越靠后。模型对结尾部分的注意力最强,所以最关键的当前任务信息必须放在最后。
3.2 设计原则一:最小必要信息原则
新手最容易犯的错是"宁多勿少",觉得多给点总没坏处。实际上,无关信息是负资产。它会稀释注意力、增加成本、还可能引入矛盾。
我的做法是:对每一段准备放进上下文的内容,问三个问题。第一,没有它模型能不能完成任务?第二,它和当前任务的相关度有多高?第三,它会不会和其他内容冲突?三个问题过一遍,能砍掉一大半冗余。
举个具体例子。做一个客服问答系统,用户问"我的订单什么时候到"。你不需要把用户过去三个月的所有订单都塞进去,只需要塞当前这一单的物流状态。塞多了,模型可能去回答另一个订单,反而出错。
3.3 设计原则二:结构化优于自然语言堆砌
同样一批信息,用自然语言散着写和用结构化格式组织,模型的理解效果差别很大。结构化格式包括JSON、Markdown表格、带标签的段落等。
比如你要告诉模型三条规则,散着写是"你要礼貌,不能泄露隐私,回答要简短"。结构化写是:
[角色] 客服助手 [约束1] 语气礼貌 [约束2] 不透露用户隐私信息 [约束3] 回答控制在三句话内后者模型更容易逐条遵守,也更容易在出问题时定位是哪条没生效。我在多个项目里对比过,结构化组织能让指令遵循率提升明显,尤其是约束类指令。
3.4 设计原则三:动态组装,而非静态拼接
成熟的上下文工程不是写死一个超长提示词,而是根据当前情况动态组装。用户问技术问题就注入技术文档,问账单就注入账单数据,问闲聊就只保留基础人设。这种"按需加载"的思路,既省窗口又提准确率。
实现上,通常是一个组装函数:输入是用户意图和会话状态,输出是最终拼好的上下文。这个函数是整个系统的核心,值得反复打磨。我一般会把它单独抽出来做单元测试,针对不同意图验证组装结果是否符合预期。
4. 实操:从零搭建一套上下文组装流程
4.1 第一步:明确任务与意图分类
动手写代码前,先把任务边界划清楚。你的应用要解决哪几类问题?每类问题需要哪些信息?这一步没想清楚,后面全是返工。
以我做过的一个内部知识助手为例,任务分三类:查制度、查数据、闲聊。查制度需要检索文档库,查数据需要调用查询接口,闲聊只需要人设。三类任务需要的上下文完全不同,所以第一步就是做一个轻量意图分类器。
意图分类不一定要用大模型,关键词匹配加少量规则往往就够用,还更快更省。只有当意图边界模糊、规则写不清时,才上模型分类。
4.2 第二步:搭建检索与信息筛选环节
知识层的信息通常来自检索。检索质量直接决定上下文质量,这一步值得下功夫。我的经验是:检索召回要宽,注入上下文要严。
具体说,检索阶段可以召回20条候选,但注入模型前要经过重排序和阈值过滤,最终只留3到5条最相关的。宁可少而精,不要多而杂。很多团队检索召回一堆,全部塞进去,结果模型被噪声带偏。
筛选时除了相关度,还要看新鲜度和权威性。过期的制度文档、来源不明的数据,即使相关度高也要谨慎。我一般会给每条信息打一个综合分,相关度占大头,时效和来源各占一部分,低于阈值的直接丢弃。
4.3 第三步:历史对话的裁剪与摘要
多轮对话里,历史层是最容易失控的部分。用户聊了二十轮,你不可能全塞进去。常见做法有三种,各有适用场景。
- 滑动窗口:只保留最近N轮。简单,但会丢失早期关键信息。
- 摘要压缩:把早期对话总结成一段话。保留信息,但摘要本身可能失真。
- 关键信息抽取:只保留对话中确认的事实和状态,丢弃寒暄。信息密度最高,实现也最复杂。
我通常用组合策略:最近三轮保留原文,更早的做摘要,摘要里只保留事实性内容。这样既保住了近期语境,又不至于把窗口撑爆。摘要的触发时机也有讲究,不要每轮都重新摘要,那样成本高且不稳定,一般累积到一定轮数或token量再触发。
4.4 第四步:组装、缓存与版本管理
所有材料准备好后,按前面说的五层结构组装。组装时注意两点:一是关键指令放首尾,二是给每段内容加清晰的分隔标记,让模型知道哪段是哪段。
缓存是容易被忽略的优化点。系统层内容几乎不变,很多模型服务支持对固定前缀做缓存,能显著降低延迟和成本。把稳定内容放前面,正好契合缓存机制。
版本管理同样重要。上下文模板改一次,效果可能大变。我习惯给每次模板变更打版本号,记录变更内容和对应的效果指标。出问题时能快速回滚,也能积累"什么样的组织方式更有效"的经验。
5. 常见问题与排查技巧实录
5.1 效果时好时坏,怎么定位
这是最典型的问题。排查思路是固定变量:先固定输入,多次调用看输出是否稳定;再固定模型,换不同上下文组织方式对比。如果同一输入同一上下文,输出波动大,那是模型采样参数的问题;如果换上下文组织方式效果差异大,那就是上下文工程的问题。
我一般会准备一组标准测试用例,每次改上下文模板都跑一遍,看通过率变化。没有这套基准,优化就是凭感觉,很容易越改越差。
5.2 模型忽略指令,怎么办
指令被忽略通常有三个原因。一是指令埋在中间被淹没了,挪到开头或结尾试试。二是指令之间互相冲突,模型不知道听谁的,需要梳理优先级。三是指令太抽象,模型不知道怎么执行,需要给具体示例。
我踩过的一个坑是:系统提示里写了"回答要简洁",又在示例里给了一段很长的回答。模型看到示例比指令更具体,就跟着示例走了。示例的权重往往高于抽象指令,这点一定要记住。
5.3 上下文越长效果越差
如果发现加信息反而变差,先检查是不是加了矛盾信息或无关信息。其次检查关键信息的位置,是不是被挤到中间去了。最后考虑是不是超出了模型的有效注意力范围,虽然窗口没满,但有效利用的部分可能早就到顶了。
解决办法是压缩和分层。把不必要的信息砍掉,把必要的信息提炼成要点,把关键指令重新放到首尾。实测下来,精简后的上下文往往比堆满的效果好得多。
5.4 多轮对话越聊越乱
多轮场景的乱,多半出在状态管理上。模型不知道当前进行到哪一步,就会重复问已经问过的问题,或者基于过时信息作答。解决办法是显式维护一个状态对象,每轮更新,并把它放进上下文的状态层。
状态对象里放什么?放已确认的事实、当前任务阶段、待办事项。比如订票场景,状态里记着"出发地已确认、日期未确认",模型就知道下一步该问日期,而不是重新问出发地。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决手段 |
|---|---|---|---|
| 输出波动大 | 采样参数或上下文不稳 | 固定输入多次测试 | 调低随机性、稳定上下文 |
| 忽略指令 | 位置差、冲突、太抽象 | 检查指令位置与优先级 | 挪位置、去冲突、加示例 |
| 越长越差 | 噪声、矛盾、超有效范围 | 逐段删减对比 | 压缩、分层、重排 |
| 多轮混乱 | 状态未维护 | 检查状态对象 | 显式维护并注入状态 |
| 成本过高 | 上下文冗余 | 统计token分布 | 裁剪、缓存、摘要 |
6. 几个容易被忽视的进阶技巧
6.1 用"占位符"控制信息密度
在模板里预留占位符,运行时按需填充,比写死内容灵活得多。占位符还能帮你做A/B测试,同一套骨架换不同填充策略,快速对比效果。我习惯把占位符命名得足够语义化,比如{{retrieved_docs}}、{{user_state}},一眼能看出这里该放什么。
6.2 给模型"退路",而不是逼它硬答
上下文里明确告诉模型:如果信息不足,就说不知道,不要编。这一条能大幅降低幻觉。很多人怕模型说不知道显得不智能,其实对用户来说,一句诚实的"我需要更多信息"远比一段自信的错误答案有价值。
6.3 定期做上下文"体检"
系统上线后,上下文会随着需求迭代不断膨胀,慢慢变得臃肿。我建议每隔一段时间做一次体检:统计各层内容的token占比,看看有没有哪层异常膨胀;抽查实际请求的上下文,看看有没有明显冗余。这个习惯能帮你及时发现"上下文腐化"。
6.4 把上下文当产品来迭代
最后一点体会:上下文模板不是一次写完就完事的代码,它更像一个需要持续打磨的产品。每次用户反馈、每次效果波动,都是改进它的线索。我见过做得最好的团队,会把上下文模板纳入版本管理和评审流程,改动前评估影响,改动后跟踪指标。这种工程化的态度,才是上下文工程真正发挥价值的前提。
说到底,模型能力是给定的,但上下文是你完全可控的。把这块可控的部分做到极致,往往比追新模型带来的提升更实在、更持久。我在多个项目里反复验证过这一点:当上下文组织得当,哪怕用中等能力的模型,也能做出让用户满意的产品。