ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

上下文工程实战:从设计到排查,让大模型应用稳定可控

上下文工程实战:从设计到排查,让大模型应用稳定可控

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 把上下文当产品来迭代

最后一点体会:上下文模板不是一次写完就完事的代码,它更像一个需要持续打磨的产品。每次用户反馈、每次效果波动,都是改进它的线索。我见过做得最好的团队,会把上下文模板纳入版本管理和评审流程,改动前评估影响,改动后跟踪指标。这种工程化的态度,才是上下文工程真正发挥价值的前提。

说到底,模型能力是给定的,但上下文是你完全可控的。把这块可控的部分做到极致,往往比追新模型带来的提升更实在、更持久。我在多个项目里反复验证过这一点:当上下文组织得当,哪怕用中等能力的模型,也能做出让用户满意的产品。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进