同一个模型、同一套参数,只改动请求体里的上下文内容,我最近在一个客服问答系统上做了一次对照测试:回答的可用率从四成出头涨到了接近九成。这个提升幅度,比我折腾模型选型和微调投入的时间换来的收益大得多。当时我就觉得,"上下文工程"这件事值得拿出来认真聊聊。
上下文工程(Context Engineering)说白了就是一门"管理模型视野"的技术——在有限的计算窗口里,决定什么时刻把哪些信息、以什么结构、什么顺序放进模型看得见的范围,让AI真正看到它该看的东西。它和提示词工程最大的区别在于:提示词工程关注"怎么问",上下文工程关注"给什么、给多少、怎么编排"。这篇文章里我不打算讨论多高深的理论,就把我在真实项目里总结的方法、踩过的坑、以及一套可以直接抄走的上下文构造流程分享出来。
起初我特别不理解所谓"上下文不够好"到底差在哪。模型不是号称有几十万token的窗口吗?把文档塞进去不就行了吗?后来被现实狠狠教育过一轮之后,我才意识到:上下文工程的核心命题从来不是"能不能塞下",而是"在有限的注意力资源里,模型有没有可能看到重点"。
1. 先搞清楚:模型跑偏,通常不是引擎问题,而是视野问题
1.1 一个典型的"视野缺失"案例
我之前接入过一个客服问答系统,知识库里有明确的七天无理由退货政策,但用户问的是"超出七天还有没有别的退款路径",当时的处理非常糟糕——模型基于自己的通用知识回答了一堆主流电商平台的售后规则,什么"特殊商品30天可退"之类的,但没有一条和业务实际政策对上。
当时第一反应是模型能力不行。后来把请求里的上下文调出来一看,才发现问题非常清晰:请求体里只带了一句系统提示和用户的原话,知识库里的售后条款压根没进到上下文里。模型不是笨,是它根本没看到该看的东西。这就好比让一个顶尖行业顾问空着手回答你的问题,不给他任何资料、不告诉他业务边界,他只能凭常识发挥,发挥得越顺滑,离你的业务实际可能越远。
从那次之后,我把"上下文是模型视野"这句话当成了整个AI应用设计的出发点。任何一个大模型应用的上限,不是模型本身决定的,而是你愿意让它看到什么决定的。模型参数是固定的,业务知识是动态的,两者的桥梁就是上下文。
1.2 上下文为什么是稀缺资源:三个绕不开的限制
很多刚开始做AI应用的同学会有一个错觉:上下文窗口这么大,多给点总没错。实际运行几次就会发现问题没那么简单。
第一个限制是token窗口再大也不够用。一个中大型企业知识库动辄几十上百个文档,全塞进去可能几百万token。即便拿主流模型的超大窗口来看,也只能覆盖一个零头。更别说很多业务场景还要兼顾成本和响应速度,不可能无限堆输入。
第二个限制是注意力退化。Transformer架构下的模型在做长文本处理时,注意力分布会随着序列变长而变得稀薄,中间位置的被关注度会明显下降。这是行业里反复验证过的现象:关键的条款如果埋在一大堆无关内容中间,模型很可能直接"看漏"。你给得越多,它记住得越少,反而是一种常见的失败模式。
第三个限制是位置偏差。模型对上下文开头和结尾的内容更敏感,对中间内容的敏感度相对弱。这也让编排顺序成了一件有讲究的事:核心结论要放前面,需要现场执行的指令要放后面,中间区域用来放背景资料。
这三个限制叠加在一起,就引出了上下文工程的核心问题:模型看到的东西,应该被当成一种需要精心管理的稀缺资源来对待。就像给一个只能看一小块地方的人指路,你得决定让他先看哪、重点看什么。
我把这个思路延伸到项目协作里,给团队立了一条规矩:上线前必须回答三个问题——这个请求真正需要哪些信息来作答?哪些信息可以不放?哪些信息必须放在显眼的位置?回答不了这三个问题,就不允许把功能提交测试。
2. 上下文工程的基本工具箱:六个组件一次拆解
上下文不是单纯的一段文本,它是由多种组件配合构成的。我习惯把它拆成六个部分来打理,每个部分承担不同的职责,也有各自容易踩的坑。
2.1 系统提示:设定角色、边界与默认行为
很多人写系统提示喜欢长篇大论,角色设定写一大串,恨不得把公司价值观都写进去。我的经验是,系统提示里只有三样东西值得放:角色边界、行为约束、输出格式偏好。
角色边界是告诉模型"你是谁、你在服务谁"。行为约束是告诉模型"什么不能做",比如不能编造业务政策、不能泄露用户隐私、不知道时必须明说。输出格式偏好则是为了让下游解析省事,比如"只返回JSON"。
占比上,系统提示通常只占整体上下文的一成左右。写得越短越好。我见过一份六百多token的系统提示,里面有一半是"热情周到、用户至上"这种话,模型没变得更热情,反而把核心指令的权重稀释了。系统提示更像路标,不是说明书。
2.2 用户输入规范化:挤掉噪音,保留槽位
用户的原话往往是脏的,有口语、有歧义、有无关信息。直接把它原样丢进上下文,会白白占用模型注意力。我的做法是在进入上下文之前,先对用户输入做一次轻量解析,把关键槽位抽出来:用户的诉求是什么、涉及的订单号或商品是什么、用户情绪是否激烈、有没有时间限定。
这不是非要上多复杂的语义解析模型,很多时候用一次指令让大模型自己抽取槽位就够了。把抽取结果填进一个结构化的上下文模板里,模型后来处理起来会清晰很多。这一步看似多余,但能让上下文里的"含金量"显著提升。
2.3 示例增强:用最少的内容教模型最难的事
Few-shot示例是上下文工程里性价比最高的组件之一。遇到那些"规则说不清、看例子秒懂"的业务场景,与其在系统提示里反复解释,不如直接给两个高质量的例子。
示例选择上有讲究:一个覆盖标准情况,一个覆盖边界情况。比如客服场景中,一个例子是"查询订单状态"的正常处理,另一个是"用户情绪激烈要求立即退款"的紧急处理。两个例子就能把处理框架讲清楚。
示例的token预算要控制,我一般会控制在上下文总量的一到两成。太多示例反而会让模型过度拟合示例的形式,丢失灵活性。示例还需要定期检查,因为业务策略变了,旧示例会变成误导。
2.4 外部知识检索:给上下文装上定向瞄准镜
这是让上下文工程产生质变的关键组件。几乎所有的业务问答系统都会配一个知识检索环节:先把知识库切成小块做向量化,用户提问时检索出最相关的几个块,再放进上下文。
很多人以为检索只是"找到相关就行",实际要精细得多。检索召回的数量、每个块的尺寸、相关性得分的阈值、以及是否要做重排(Rerank),都会直接决定进入上下文的内容质量。这块我会在第三部分的实战案例里详细展开。
2.5 历史会话调度:只让模型记住该记住的
多轮对话场景下,历史会话怎么放进上下文,是一个看起来简单、实际很容易做错的事情。常见错误是把最近20轮对话全部塞进去,里面有大量"嗯""好的""我再看看"之类的无意义内容,既烧token,又把关键信息淹没。
我的做法是把历史会话做一个摘要和裁剪的双重处理:超过一定轮数的旧消息先压成一句状态摘要,只保留当前轮真正依赖的关键信息。同时还要区分:有些信息是需要长期跟住的槽位,比如用户选中的商品ID、收货地址;有些信息是一次性的,比如"这不错""好的"。前者必须保留,后者可以直接丢掉。
2.6 压缩与遗忘:主动管理模型的短期记忆
很多人会忽视遗忘机制在上下文工程里的作用。模型的短期记忆就是我们塞给它的上下文,它记住的只能是这些内容。一旦对话变长,为了给新信息腾出空间,就必须主动遗忘旧信息。
上下文压缩可以分两级来做。第一级是为了维持单次请求体积,把已经聊过的内容用摘要替代;第二级是为了防止信息污染,把与当前任务无关的旧信息直接移除。这样做不仅能控制成本,还能减少模型被历史错误带偏的概率。你回顾一下那些翻车的AI对话案例,很多都是从上下文里积累了太多错误前提开始的。
我把这六个组件整理成了一个自检清单,接任何一个大模型项目时都会先过一遍:系统提示里有没有多余内容?用户输入有没有结构化抽取?有没有配上高质量的示例?知识检索的召回质量验收过没有?历史会话的保留策略合理吗?遗忘逻辑是否已经生效?
3. 实战复盘:一个客服问答系统的上下文构造全过程
理论讲了这么多,真正动手时会发现每一环都有细节问题。我拿一个实际做过的项目来完整复盘一遍,这个项目我称它为"某品牌售后问答系统",规模不大,但遇到的坑非常典型。
3.1 第一版实现的失败:把全部资料堆进上下文
第一版实现几乎是所有新手都会做的方案:把整个知识库的明文文档、近十轮历史聊天记录、一份超长的系统提示,全部拼接进上下文,一次性发给模型。当时想的是"反正模型窗口够大,多给点信息总没错"。
上线之后立刻被打脸。问题有三个:第一,token消耗量大得惊人,每次请求的输入成本比预想高了好几倍;第二,响应延迟明显变长,用户体验很差;第三,也是最要命的,准确率没有想象中高,甚至出现了模型把无关文档里的旧政策当成当前政策回答的情况。
后来把请求日志拉出来一看,真相非常直观:模型输入里可能有几万token的文档内容,但真正与用户问题相关的只有一两个段落,其余全是干扰项。模型被大量无关信息包围,就算窗口装得下,注意力也分配不过来。
3.2 改造第一步:先让意图解析模块上场
第一版翻车后,我做的第一件事不是优化检索,而是先做用户输入的意图解析。不管用户说得多绕,先抽出来三个字段:意图类型(例如查订单、问政策、投诉)、实体槽位(例如订单号、商品类目)、情绪判断(正常、不满、愤怒)。
这一步的价值在于,后续的检索策略可以根据意图分流。比如"查订单"就走订单接口,"问政策"才走知识库检索,"投诉"则要走安抚话术模板。上下文构造开始有了主次,不再一股脑把所有能力都塞给模型。
意图解析我最初想用规则做,发现覆盖不住口语化表达,后来改成让大模型自己解析,用一段很轻量的指令加两个意图示例,准确率就到了可接受的范围。这一步本身也是在实践上下文工程——给模型一个足够清晰的解析任务,它就能稳定地挤出结构化的状态数据。
3.3 改造第二步:把知识库检索做成准入口
意图解析完成后,知识检索就成了上下文工程的主战场。我需要保证进入上下文的文档片段,必须和用户当前问题高度相关。
具体参数上,我做了三处调整。第一,知识块(chunk)的尺寸从每块两千字压到了五百到八百字,太长的块容易在中间埋关键信息,太短的块又缺少上下文关联性。第二,召回数量从十个压到了三个,把那些相关性得分明显偏低的文档挡在门外。第三,引入了重排环节,先向量召回二十个候选块,再用一个轻量重排模型选出最相关的三块,精准度提升非常明显。
同时我加了一个硬性下限:如果检索到的最高相关分都低于阈值,那就认为知识库里没有答案,直接把一条"未找到相关内容"的提示放进上下文,同时引导模型说"该问题需要人工客服介入"。这一步本质上是在给模型划定退出路径,不懂装懂是AI应用最不能碰的红线。
3.4 改造第三步:设计可复用的上下文模板
到这里,上下文的组件凑齐了,接下来就是编排。我给这个客服系统设计了一套固定的上下文结构,每次请求都按这个结构来组装。结构大致是这样:
{ "system": "你是某品牌售后客服助手。只依据提供的资料回答,资料缺失时明确说明。", "user_intent": { "type": "query_refund_policy", "slots": { "order_id": "20250601A123", "product_category": "电子设备" }, "sentiment": "normal" }, "retrieved_contexts": [ { "source": "售后政策/退款规则.md", "relevance_score": 0.91, "content": "七天无理由退货政策适用于尚未拆封的商品。超出七天且存在质量问题的订单,可申请售后维修或换货,具体以检测结果为准。" } ], "conversation_summary": "用户此前询问了订单物流信息,已查得包裹已签收。", "current_query": "超过七天能退款吗?" }这套结构的好处是非常直观:系统提示放最前,明确总任务;意图解析结果放在用户原话之前,相当于先给模型一个"我理解你要问什么"的摘要;检索资料放在中间,作为回答的事实依据;最后的当前问题作为收尾,让模型在结尾处能聚焦回用户到底问的是什么。
这个模板用起来之后,模型的输出稳定性提升明显。我后来复盘原因,是我把上下文里的信息密度放在了正确的位置:开头是规则,结尾是问题,中间是证据。模型在开头拿到行为边界,在结尾看到当下任务,注意力分配自然就合理了。
3.5 改造效果:一组分量很足的对照数据
我把改造前后的数据整理成了一张表,放在项目周报里,说服力比任何口头解释都强。
| 指标 | 改造前(全部资料堆入) | 改造后(结构化上下文) |
|---|---|---|
| 单次请求输入token数 | 约26000 | 约3800 |
| 平均响应延迟 | 约5.6秒 | 约1.8秒 |
| 人工评估可用率 | 43% | 88% |
| 平均每请求成本 | 基准值 | 降至约1/6 |
| 幻觉类问题占比 | 31% | 6% |
单看某个数字可能没什么感觉,但"可用率翻倍+成本降到六分之一"这个组合拳,在业务决策者眼里就是核弹级的说服力。后来这个项目的经验也成了团队内部其他AI项目复用的基线方案。
这里我想特别强调一下:改上下文结构不是改完就算完,还要关注模型输出的稳定性。上下文里的组件顺序一旦确定,尽量别随意打乱。模型对规律的上下文结构有很强的适应性,今天一个顺序、明天一个顺序,会让输出质量起起伏伏。
4. Token预算:上下文工程的成本意识
说句实在话,很多AI应用死在技术验证之后的成本测算上。上下文工程做得越精细,成本优化空间就越大,因为它直接影响的是每次请求的输入token数,而这恰恰是大模型API成本里的大头。
4.1 一次请求里的钱都烧在哪了
用户通常只关注输出token的价格,因为那是肉眼可见的生成内容。但实际业务场景里,输入token往往才是成本黑洞。一次请求要带上系统提示、检索资料、历史会话、当前问题,这些输入token是按量计费的。单次两三千token的输入看起来不多,一旦每天有几十万次调用,差距就很吓人了。
我习惯在项目里把每次请求的上下文token数当成核心监控指标。设计下线时,会定一个目标值:单次请求的输入token必须控制在多少以内,超了就砍组件。别小看这个"砍"字,它逼着你去优化检索召回、压缩历史会话、精简系统提示,而不是靠钱硬扛。
4.2 给上下文做一份预算分配方案
我做上下文工程有一个习惯,先给token做预算,再往每个组件里填内容。没有预算的上下文,就像没有预算的采购单,一定会失控。
| 上下文组件 | 预算比例参考 | 常见问题 |
|---|---|---|
| 系统提示 | 10%以内 | 写得过长,塞进无效理念 |
| 用户意图结构化数据 | 5%左右 | 解析失败导致字段缺失 |
| 检索/知识资料 | 40%左右 | 召回太多无关块 |
| 历史会话摘要 | 10%左右 | 摘要重述而非提炼状态 |
| Few-shot示例 | 15%左右 | 示例过时或与当前业务不符 |
| 当前用户输入 | 20%左右 | 未裁剪,夹带大量噪音 |
这个预算不是死的,但能提供一个审视方向。比如某次发现检索资料占了整个上下文的七成,那就要先追问检索阈值是不是设太低了。某次发现用户输入占了一半还多,那就要去看看输入规范化模块是不是没生效。
我在代码里写了一个简单的token估算函数,每次构造完上下文后可以快速打印token总数,作为预算检查的辅助手段。
def estimate_tokens(text: str) -> int: # 中文场景下,粗略可按字符数的0.6至0.7估算 return int(len(text) * 0.65) def check_context_budget(context_sections: dict) -> None: total = 0 for section, text in context_sections.items(): tokens = estimate_tokens(text) total += tokens print(f"{section}: {tokens} tokens") print(f"TOTAL: {total} tokens") if total > 4096: print("WARNING: over budget, review retrieval and history sections")4.3 三个省钱的上下文优化手段
第一个手段是检索返回前裁剪。从知识库检索出来的块,往往首尾有标题、来源等元信息,直接塞进上下文会白白烧掉一部分token。我通常会在进入上下文之前,用一个小模型或规则把块里的"正文核心区"提取出来,压缩掉无关外壳。
第二个手段是缓存复用。同一个用户的连续请求之间,系统提示和知识库资料往往是不变的。可以在服务层做一层缓存,把这些不变的组件拼成一段长文本缓存起来,避免每次都全量重复计费。这个优化在长会话场景下效果尤其明显。
第三个手段是模板层故意留白。上下文模板里的组件能不放就不放,比如"当前商品介绍"这种组件,在没有检索到相关信息时要允许整个组件置空,而不是放一句"未找到相关商品"之类的占位文本。看似小事,但日积月累能省下可观的成本,还能避免占位文本干扰模型判断。
做上下文工程越久,我越觉得它像是给每次请求做一次"预算审批":花出去的每个token都要有明确的理由,说不清理由的token一律不放行。
5. 踩坑实录:上下文构造里的常见误区和完整排查链路
这一节把我在项目里踩过、以及在评审别人代码时见过的典型错误集中盘点一下。每一条都是真金白银换来的教训。
5.1 误区一:上下文越长越好
这是流传最广的误解。有人总觉得给模型的信息越多,它回答越准。实际数据显示,当上下文里的无关内容增多时,模型对关键信息的敏感度会下降。我自己做过一次实验:同一道题,塞入不同长度的无关背景文本,回答准确率从92%一路下降到71%。
正确的做法是,把上下文当成一块有限的画布,只画与当前任务有关的线条。判断某段内容是否该放进去,标准很简单:删掉它,模型对这个请求的回答会不会受影响。不会受影响的,就不该留。
5.2 误区二:检索召回只看数量不看质量
很多团队上线检索时只盯住一个指标:召回了多少条。实际上一堆低相关度的结果塞进上下文,比不检索还糟糕。低相关内容会占用大量token,还会把模型注意力从真正相关的段落上引开。
我后来在项目里加了召回质量门禁:只有相关性得分超过阈值的块才允许进入上下文。同时配合一个简单的规则——如果排序前几的块分数差距很大,只保留前两三个即可,不要机械地全量保留。
5.3 误区三:系统提示里放"正确的废话"
"你是一个乐于助人的AI助手""请给出准确、详细、有用的回答"这类话,几乎不产生任何业务价值,却白白消耗token。更关键的是,这类泛泛的正面描述不会让模型表现更好,反而可能让它变得更啰嗦、更爱堆砌正确的空话。
真正有价值的系统提示应该指向具体业务:回答范围、禁区、格式、以及当资料缺失时的行为。我在每个项目里都会做一次系统提示瘦身,把每一句话都用"删掉它会不会影响模型行为"来检验。
5.4 误区四:没有给模型留"不知道"的出口
很多系统在知识库里没有答案时,模型会强行编一个"合理"回答。这不能全怪模型,很多时候是上下文里根本没有告诉模型"不知道时该怎么办"。上下文工程必须显式地给模型一条退出路径。
我的做法是在系统提示里写清楚:当检索资料与当前问题无关、或未检索到相关资料时,必须明说"该问题我这边没有对应的政策依据,建议转人工核实"。同时,在Few-shot示例里配一个"资料缺失"的负面示例,让模型的应对方式是可预期的。
5.5 排查链路:一次上下文问题的完整倒查流程
遇到模型输出不对时,我有一套固定的倒查流程,从结果一路往上游推,可以避免大量的无效尝试。
第一步是回看输出,确定症状类型:是答案错误,还是答非所问,还是幻觉编造。第二步是查看上下文快照,确认关键信息是否真的在输入里、是否被放在了显眼位置。第三步是检查检索环节:召回的相关性得分多少、阈值是否生效、有没有无关内容混入。第四步是检查压缩环节:历史会话摘要有没有把关键槽位压缩丢。第五步是检查模板组装逻辑:组件顺序是否被改动、是否有占位符残留。
这五步里,至少七成的问题会在第二步被发现——关键信息压根没进上下文。很多人碰到模型输出不对,第一反应是换模型、调参数,折腾一圈下来发现是上游上下文处理的问题。把倒查流程当成肌肉记忆之后,排查效率能提升一个量级。
为了让这个排查链路落地,我在代码里加了一份"信息覆盖核对表",每次请求都记录以下几个问题:本次请求关联的知识库条目是什么、检索模块返回了几条、实际放进上下文的条数是几条、历史会话中的关键槽位是否保留。这份核对表在出问题时就是最清晰的断案线索。
| 排查步骤 | 检查重点 | 常见发现 |
|---|---|---|
| 输出症状确认 | 是答错、跑题,还是编造 | 确定是否与上下文相关 |
| 上下文快照核对 | 关键信息是否出现在输入中 | 发现检索漏召回或放错了位置 |
| 检索环节检查 | 召回数量、得分、阈值情况 | 发现大量低分内容混入 |
| 压缩环节检查 | 摘要是否丢失关键槽位 | 发现历史槽位被摘要吞掉 |
| 模板组装检查 | 结构顺序、占位符、缓存情况 | 发现组件被跳过或顺序错乱 |
6. 长期落地:上下文治理比上下文构造更重要
前面讲的都是单次请求里怎么把上下文做好。但一个真正跑生产的AI应用,上下文工程还需要长线维护。我见过太多系统刚上线时表现不错,跑了两三个星期开始飘,查了半天才发现是知识库更新了、检索切片没更新,或者系统提示被动过一版导致行为漂移。
6.1 建立上下文切片日志
我给团队立的第一个规矩是:每一次请求都必须记录完整的上下文切片。记录格式是请求ID、时间戳、输入组件的名称和内容、各组件token数、最终模型输出。这个日志看起来存储成本不小,但它是排查一切线上问题的根。
有了这个日志,A/B测试、问题回溯、用户投诉分析都有了抓手。模型答错了,直接定位到该次请求,看看上下文里到底给了什么,10分钟内就能判断是检索问题还是提示词问题,而不是靠拍脑袋。
6.2 把上下文模板当成代码来管
上下文模板必须有版本管理。谁改了系统提示、改了检索策略、改了历史会话摘要逻辑,都要有记录、有评审、可回滚。我倾向于把模板和策略配置单独建仓库,用标准的版本管理流程来管。
每次改动模板,都要在评估集上跑一遍回归。我在这里说的评估集不是抽几条样例看感觉,而是固定的几百条覆盖各种用户意图的高质量测试集,每次模板改动都全量跑一遍,对比可用率变化。没有这个评估集兜底,上下文优化很容易变成盲人摸象。
6.3 用A/B测试验证每个上下文策略
有些优化看起来有道理,实际可能是负优化。我在"是否要在上下文里加入用户历史购买记录"这个决策上就栽过跟头:直觉上觉得加上购买记录,回答会更个性化,结果A/B测试显示,加了购买记录后,模型开始频繁引用订单数据来回答政策问题,可用率反而下降了7个百分点。
从那以后,"直觉驱动"的上下文改动我基本都会压一压,改成"数据驱动":先小流量上线,对比核心指标,数据通过了再全量。上下文工程的每一步都应该是可度量、可验证的。
关于上下文工程的长期演进方向,我自己保持高度关注的是模型记忆层的演进——当模型产品本身开始自带长期记忆机制时,上下文工程在应用层的侧重点会有所变化,但"管理模型视野"这件事不会消失,只会从"每次请求怎么拼"升级为"长期记忆和当前上下文怎么协同"。在这个方向上,提前把单次请求的上下文结构做得足够干净,未来做记忆协同时会省掉不少重构的力气。
做了这么多上下文工程项目,我最大的体会是:决定不下放什么,比决定放什么更能体现水平。AI的视野是需要管理的稀缺资源,你只要把真正该看的东西按正确的顺序放到它面前,绝大多数模型都能给你惊喜;反之,就算换成最强的模型,塞给它一堆看过就忘的无关信息,它也只会给你一个平庸甚至跑偏的回答。这个原则,在我做过的每一个项目里都验证过,以后大概还会继续验证下去。