☰
从Prompt Engineering到Context Engineering:LLM Agent上下文工程实践指南
2026/9/26 23:45:31 网站建设 项目流程

1. 从Prompt Engineering到Context Engineering:一次被逼出来的范式转移

过去两年,我参与过不少LLM应用从0到1的搭建,也帮团队做过大量Prompt调优。说实话,早期那种“把提示词写得漂亮一点”就能显著提升效果的红利期,早就过去了。现在你打开任何一个稍微复杂点的Agent项目,会发现真正决定成败的,往往不是那句提示词写得多精妙,而是模型在每一次推理时,到底“看到”了什么。这就是Context Engineering要解决的核心问题。

先把这个概念说清楚。Context Engineering,直译过来是“上下文工程”,指的是围绕LLM的上下文窗口,系统性地设计、组织、压缩、检索和动态注入信息的工程实践。它和Prompt Engineering最大的区别在于:Prompt Engineering关注的是“怎么问”,而Context Engineering关注的是“模型在回答之前,它的上下文里应该有什么、以什么形式存在、按什么优先级排列”。

为什么这个转变是必然的?因为Agent类应用的复杂度上来了。一个典型的ReAct风格Agent,单次任务可能要经历十几轮“思考-行动-观察”的循环。每一轮都要把历史对话、工具返回结果、系统指令、当前任务状态全部塞进上下文。如果只是简单拼接,上下文窗口很快就会被撑爆,而且大量无关信息会稀释模型的注意力,导致推理质量断崖式下降。我见过太多项目,Prompt写得无可挑剔,但因为上下文管理混乱,Agent跑到第五轮就开始“胡言乱语”。

所以这篇文章,我想从一线工程的角度,把Context Engineering这件事拆开讲透。它适合正在做LLM应用、Agent系统、RAG管线的开发者,也适合那些发现“Prompt调不动了”的团队技术负责人。我会讲清楚它的核心构成、和ReAct等范式的关系、实际落地时的关键决策点,以及我自己踩过的那些坑。

2. Context Engineering到底在工程上管什么

2.1 上下文窗口不是“越大越好”的垃圾桶

很多人有个误区:既然现在模型支持128K甚至更长的上下文,那我全塞进去不就行了?实测下来,这个想法非常危险。原因有两个层面。

第一是成本。上下文长度直接决定推理费用,尤其是高频调用的Agent场景,token消耗是线性甚至超线性增长的。我做过一个粗略测算,一个中等复杂度的Agent任务,如果每轮都把完整历史塞进去,到第十轮时单次调用的输入token可能是第一轮的8到10倍。这不是小数目。

第二是效果。学术界有个被反复验证的现象叫“Lost in the Middle”:当上下文很长时,模型对中间部分信息的召回率明显低于开头和结尾。也就是说,你塞进去的信息越多,模型真正“用上”的比例反而可能越低。这就像你给一个人递了一百页资料让他现场答题,他大概率只会翻前几页和最后几页。

所以Context Engineering的第一个工程决策就是:什么该进上下文,什么不该进,什么该以压缩形式进。这需要你对任务的信息依赖关系有清晰判断,而不是无脑堆砌。

2.2 上下文的四个层次:系统层、任务层、记忆层、观察层

在实际项目里,我习惯把上下文分成四个层次来管理,这样职责清晰,也方便做动态裁剪。

系统层是相对静态的部分,包括角色设定、输出格式约束、安全边界、工具使用规范等。这部分通常变化不大,但要注意的是,它应该放在上下文的最前面,因为模型对开头信息的遵循度最高。

任务层是当前这一轮要解决的具体目标,比如“用户想查询上季度华东区的销售数据”。它需要足够明确,但也不能太啰嗦。我的经验是,任务描述控制在两三句话内,把关键约束点出来即可。

记忆层是最容易被忽视也最考验工程能力的部分。它包括长期记忆(跨会话的用户偏好、历史结论)和短期记忆(当前会话的对话历史)。长期记忆通常需要外部存储加检索,短期记忆则需要做滚动压缩。我一般会用“摘要+关键实体”的方式压缩历史对话,而不是保留原始逐字记录。

观察层是Agent执行过程中工具返回的结果、环境反馈等。这部分信息量大、噪声多,必须做过滤和结构化。比如一个搜索工具返回了20条结果,你不能全塞进去,而要提取最相关的3到5条,并且统一格式。

把这四层分开管理之后,你会发现上下文的组装变成了一个可配置、可测试的工程问题,而不是靠感觉拼字符串。

2.3 动态组装:上下文不是拼一次就完事

真正复杂的Agent场景里,上下文是每一轮都要重新组装的。这意味着你需要一个上下文管理器,它能在每次模型调用前,根据当前状态决定:哪些记忆要召回、哪些观察结果要保留、哪些历史要压缩、哪些工具描述要动态加载。

我自己的做法是维护一个“上下文预算表”。比如给系统层分配500 token,任务层300 token,记忆层2000 token,观察层3000 token,留出余量给模型输出。当某一层超预算时,触发对应的压缩策略。这个预算不是拍脑袋定的,而是根据实际任务的token消耗分布反复调整出来的。

这里有个很实用的技巧:把工具描述做成按需加载。很多Agent项目一上来就把所有工具的完整schema塞进系统提示,结果光工具描述就占了两三千token。实际上,你可以先只给模型一个工具名称列表,等它决定要用某个工具时,再动态注入该工具的详细参数说明。这样能省下大量上下文空间。

3. ReAct范式下,上下文是怎么被“消耗”掉的

3.1 拆解一次ReAct循环的上下文膨胀过程

ReAct(Reasoning + Acting)是目前Agent最常用的范式之一,它的核心循环是:Thought → Action → Observation → Thought → ...。看起来简洁,但每一轮都在往上下文里追加内容。

我拿一个真实案例来拆。假设用户问:“帮我查一下北京今天天气,如果下雨就提醒我带伞。”第一轮,上下文里有系统提示、用户问题、可用工具列表。模型输出Thought和Action:调用天气查询工具,参数是“北京”。第二轮,工具返回了天气数据,这段Observation被追加进上下文。模型再输出Thought:今天有雨,需要提醒用户。然后调用提醒工具。第三轮,提醒工具返回执行结果,模型输出最终回答。

看起来只有三轮,但如果你把每一轮的完整上下文打印出来,会发现token数在快速攀升。到第三轮时,上下文里已经包含了前两轮所有的Thought、Action和Observation。如果这个任务再复杂一点,比如需要查多个城市、对比多个数据源,循环次数轻松上十轮,上下文膨胀会非常严重。

3.2 上下文膨胀带来的三个典型故障

我在实际项目中遇到过三类由上下文管理不当引发的故障,非常有代表性。

第一类是“指令遗忘”。当上下文太长时,模型会逐渐忘记系统提示里的格式要求。比如你要求它每次输出JSON,跑到后面几轮它开始输出自然语言了。这不是模型能力问题,而是系统指令被淹没在了大量中间内容里。

第二类是“工具误用”。当历史Observation里包含大量工具返回的原始数据时,模型可能会把某些数据误认为是当前可用的工具参数,导致调用错误。我遇到过模型把上一轮搜索结果的URL当成新一轮的API端点,直接发起了一个无效请求。

第三类是“推理死循环”。上下文里保留了太多失败的尝试记录,模型会倾向于重复之前的错误路径,而不是尝试新方案。这有点像人陷入思维定势,越看之前的记录越觉得只能那么做。

解决这三类问题的核心思路是一致的:不要让上下文无限增长,要有主动的压缩和清理机制。比如每三轮做一次历史摘要,把已经完成的子任务从详细记录压缩成一句话结论;对于失败的尝试,只保留“某方案不可行”的结论,删掉具体过程。

3.3 用“状态机”思路管理Agent上下文

后来我换了一个思路,不再把Agent看成一条不断追加的对话流,而是看成一个状态机。每个状态有自己独立的上下文需求,状态切换时,只保留与当前状态相关的信息。

具体做法是:定义一个任务状态对象,包含当前阶段、已完成子任务列表、待办子任务列表、关键实体和约束。每次调用模型前,根据当前状态动态生成上下文,而不是把历史全量传入。这样上下文长度基本是恒定的,不会随轮次增长。

这个思路的代价是需要额外维护状态逻辑,但收益非常明显:推理稳定性大幅提升,token成本可控,而且调试起来容易得多——你可以单独测试每个状态的上下文组装逻辑。

4. 上下文压缩与检索:工程落地的核心战场

4.1 摘要压缩:什么时候压、压到什么程度

上下文压缩最常用的手段是摘要。但摘要不是随便压的,压得太狠会丢关键信息,压得太松等于没压。我的经验是分场景处理。

对于对话历史,我通常按“轮次”压缩。比如每5轮对话做一次摘要,保留用户的核心诉求、已达成的结论、未解决的问题。摘要长度控制在原文的20%到30%。

对于工具返回结果,我按“信息密度”压缩。结构化数据(如JSON)通常保留关键字段即可;非结构化文本(如网页内容)则提取与当前任务最相关的段落。这里可以用一个小模型来做提取,成本低且效果好。

对于系统指令,原则上不压缩,但可以拆分。把必须每轮都遵守的硬约束放在最前面,把参考性的说明放在后面,这样即使后面被截断,核心约束还在。

4.2 检索增强:不是所有记忆都值得召回

长期记忆的召回是另一个关键点。很多项目一上来就做向量检索,把top-k个记忆片段全塞进上下文。但实测下来,召回数量比召回质量更容易出问题。

我的做法是:先做粗排,用向量相似度召回10到20条候选;再做精排,用一个小模型判断每条候选与当前任务的相关性,只保留最相关的2到3条。而且,召回的记忆要带上时间戳和置信度,让模型自己判断是否采信。

还有一个容易被忽视的点:记忆的格式。原始对话记录作为记忆召回,效果往往不如结构化摘要。我一般会把记忆存成“情境-行动-结果”的三元组形式,召回时直接注入这个结构,模型理解起来更高效。

4.3 上下文隔离:多Agent协作时的信息边界

当系统里有多个Agent协作时,上下文管理会变得更复杂。我的原则是:每个Agent只看到自己需要的信息。比如一个负责搜索的Agent,不需要知道最终输出的格式要求;一个负责格式化的Agent,不需要知道搜索的中间过程。

这需要设计一个上下文路由层,根据Agent的角色和当前任务,从全局状态中抽取相关字段,组装成该Agent的专属上下文。这样做的好处是每个Agent的上下文都很短、很聚焦,推理质量和速度都会提升。

5. 那些文档不会告诉你的实操坑

5.1 上下文顺序的微妙影响

前面提到“Lost in the Middle”,但具体怎么排还是有讲究的。我的实测经验是:最关键的约束放开头,当前任务放结尾,参考信息放中间。因为模型对开头和结尾的注意力最集中。

另外,工具返回结果如果很长,尽量把最重要的字段放在最前面。比如一个搜索结果列表,把标题和摘要放前面,把完整URL和元数据放后面。这样即使后面被截断,核心信息还在。

5.2 Token计数不是精确科学

不同模型对token的切分方式不同,同一个字符串在GPT和Claude下的token数可能差10%到20%。所以做上下文预算时,一定要用目标模型对应的tokenizer来计数,不能拿一个通用估算糊弄。

而且,很多API返回的usage字段是事后统计的,你不能依赖它来做实时预算。我的做法是在本地用tokenizer库预先计算,留出10%到15%的余量。

5.3 上下文缓存的双刃剑

现在很多模型支持上下文缓存(比如把系统提示缓存起来复用),这确实能省钱。但要注意:缓存的内容一旦更新,缓存就失效了。如果你把动态内容也放进缓存区,会导致频繁失效,反而增加成本。

我的做法是严格区分静态区和动态区。静态区放系统指令、工具schema、固定示例,这些内容长期不变,适合缓存。动态区放任务描述、记忆召回、观察结果,每轮都变,不参与缓存。

5.4 调试上下文问题的实用手段

当Agent行为异常时,第一件事应该是把完整上下文打印出来。我见过太多人盯着Prompt改来改去,结果问题出在上下文里混入了脏数据。

我一般会做一个上下文可视化工具,把四个层次用不同颜色标出来,并且显示每部分的token占比。这样一眼就能看出是不是某一层膨胀了,或者某条记忆被错误召回了。

另外,建议在开发阶段保留每次调用的完整上下文快照,方便做回归测试。当你调整了压缩策略或召回逻辑后,可以用同一批历史快照跑一遍,对比输出变化。

6. 从Prompt到Context:一个团队的能力升级路径

6.1 角色分工的变化

在Prompt Engineering时代,调优工作往往由一个人完成,写提示词、测效果、改措辞。但Context Engineering涉及检索、压缩、状态管理、工具编排等多个模块,很难由一个人包揽。

我观察到的趋势是,团队里开始出现专门的“上下文工程师”角色,或者由后端工程师和算法工程师协作完成。后端负责状态管理和数据管线,算法负责召回策略和压缩模型。这种分工下,接口设计变得很重要——上下文管理器应该是一个独立的服务,对外提供“给定状态,返回组装好的上下文”的能力。

6.2 评估体系的建立

Prompt时代评估很简单:拿一批测试问题,看输出对不对。但Context Engineering的评估要复杂得多,因为上下文是动态变化的。

我建议从三个维度建立评估:任务完成率(最终目标是否达成)、上下文效率(token消耗与任务复杂度的比值)、推理稳定性(多轮运行的结果一致性)。这三个指标能帮你判断上下文策略是否健康。

6.3 工具链的选型思路

目前市面上还没有一个“开箱即用”的Context Engineering框架,更多是靠组合现有工具。我的选型原则是:状态管理用成熟的工作流引擎,检索用向量数据库加轻量精排模型,压缩用便宜的小模型,编排逻辑自己写。

不要试图找一个万能框架,因为每个项目的上下文结构都不一样。把每个环节做成可替换的模块,比押注一个框架更稳妥。

7. 我个人的几条经验法则

做了这么多项目,我总结了几条在Context Engineering上反复验证有效的法则,分享出来供参考。

法则一:上下文是稀缺资源,不是免费存储。每往上下文里加一段内容,都要问自己:这段信息对当前决策真的必要吗?如果删掉它,模型还能做对吗?如果答案是“可能也行”,那就删掉。

法则二:压缩要趁早,不要等爆了再压。我习惯在上下文达到预算的70%时就触发压缩,而不是等到95%。因为压缩本身也需要调用模型,留出余量才能从容处理。

法则三:让模型做它擅长的事,别让它做检索。很多人喜欢把大量候选信息塞给模型,让它自己挑。但模型的检索能力远不如专门的检索模块。正确的做法是:检索模块负责找,模型负责用。

法则四:上下文结构要稳定,内容可以变。模型对格式的敏感度很高。如果你每轮的上下文结构都不一样,模型需要花额外精力去理解结构,而不是解决问题。保持结构一致,只换内容,效果会好很多。

法则五:永远保留一个“逃生出口”。当上下文管理出问题时,要有一个降级方案。比如压缩失败时,直接截断最老的历史;召回失败时,回退到只用当前任务描述。这个逃生出口能在关键时刻保住系统的可用性。

最后说一个我最近在尝试的方向:把上下文组装逻辑做成可配置的DSL,让非工程人员也能调整上下文策略。这样产品经理可以直接参与调优,而不需要每次都找工程师改代码。目前还在早期阶段,但初步效果不错,上下文策略的迭代速度提升了好几倍。如果你也在做类似的事情,欢迎交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询