☰
AI Agent上下文工程实战:ReAct循环优化与本地部署策略
2026/10/8 4:49:21 网站建设 项目流程

1. 为什么上下文工程成了 AI Agent 的分水岭

做 AI Agent 的人这两年应该都有一个共同感受:模型能力越来越强,但真正决定一个 Agent 好不好用的,往往不是模型本身,而是你往它脑子里塞了什么、什么时候塞、塞多少。这就是**上下文工程(Context Engineering)**要解决的问题。

我最早接触这个概念是在做 ReAct 模式智能体的时候。当时用的是一个 7B 级别的本地部署大语言模型,任务很简单——让 Agent 根据用户问题自主决定调用哪个工具、传什么参数、拿到结果后怎么组织回答。Demo 跑通了,但一上真实场景就崩:多轮对话到第五六轮,模型开始胡言乱语,工具调用参数张冠李戴,甚至把上一轮的用户输入当成这一轮的工具返回值。排查了半天,问题不在模型权重,也不在 ReAct 框架的循环逻辑,而在于上下文窗口里堆了太多不该堆的东西。

这个经历让我意识到一件事:提示词工程(Prompt Engineering)解决的是"怎么问",上下文工程解决的是"给模型看什么"。前者是单次交互的技巧,后者是贯穿 Agent 整个生命周期的信息管理策略。一个 Agent 每跑一步,都要经历"组装上下文 → 模型推理 → 解析输出 → 执行动作 → 把结果写回上下文"这个循环。上下文工程就是在这个循环里做取舍:哪些历史要保留、哪些要压缩、哪些要丢弃、哪些要换一种表示形式重新注入。

关键词里提到的 ReAct、AI Agent 主流架构、基于 ReAct 模式构建能思考与行动的 AI 智能体,本质上都绕不开上下文管理。ReAct 的论文里其实已经埋了伏笔——Thought、Action、Observation 三段式循环,每一轮都会往上下文里追加内容,如果不加控制,上下文会线性膨胀,直到撑爆窗口或者让模型注意力涣散。

这篇文章我想把上下文工程这件事拆开讲透。不是泛泛谈概念,而是从上下文到底由哪些部分组成、每一部分该怎么管、不同 Agent 架构下的上下文策略有什么差异、本地部署大模型时上下文工程有哪些特殊约束这几个角度,结合我自己踩过的坑,给出一套可以直接参考的实操思路。适合正在搭建 AI Agent、调过 ReAct 框架、或者被上下文窗口问题折磨过的朋友。

2. 拆解 Agent 上下文:它到底由哪几块拼成

2.1 上下文不等于对话历史

很多人一提到上下文,第一反应就是"聊天记录"。但在 Agent 场景里,对话历史只是其中一块,而且往往不是最大的一块。一个典型 ReAct Agent 单步推理时,送进模型的上下文通常包含以下几类内容:

  • 系统指令(System Prompt):定义 Agent 的角色、能力边界、输出格式约束。这部分通常固定,但内容设计直接影响模型行为。
  • 工具描述(Tool Definitions):每个可调用工具的名称、功能说明、参数 schema。工具越多,这部分占用越大。
  • 任务目标(Task / Goal):用户原始请求,或者被拆解后的子目标。
  • 推理历史(Reasoning Trace):之前每一步的 Thought、Action、Observation 记录。
  • 外部检索内容(Retrieved Context):从知识库、文档、数据库里捞回来的片段。
  • 记忆(Memory):跨会话或长期积累的用户偏好、事实性信息。
  • 当前步的即时输入:这一轮要模型处理的直接指令。

把这七块摆在一起,你会发现对话历史可能只占两三成。真正吃 token 的往往是工具描述和推理历史。我做过一个统计,一个挂了 12 个工具的 Agent,光工具 schema 的 JSON 描述就接近 2000 token,如果每个工具的参数说明写得啰嗦一点,轻松突破 3000。而 ReAct 循环跑 10 步,每步的 Thought + Action + Observation 平均 300 token,又是 3000。这两项加起来就 6000 了,还没算系统指令和检索内容。

2.2 上下文的四种"污染"

理解了组成,就能理解为什么上下文会出问题。我把常见的上下文问题归纳为四种污染:

第一种是冗余污染。同一信息在上下文里出现多次。比如工具描述里已经写了某个参数的含义,系统指令里又重复一遍;或者检索回来的文档片段之间有大量重叠。冗余不会直接让模型出错,但会稀释注意力,让真正关键的信息被淹没。

第二种是噪声污染。上下文里混入了与当前任务无关的内容。最典型的是多轮对话中,用户前面聊的闲天、Agent 前面跑偏的推理步骤,全都留在窗口里。模型在生成下一步时,会被这些噪声干扰,做出莫名其妙的决策。

第三种是冲突污染。不同来源的信息互相矛盾。比如系统指令说"回答要简洁",但检索到的文档示例里全是长篇大论;或者记忆里存着用户旧偏好,但用户这一轮明确说了新要求。模型面对冲突时,行为不可预测。

第四种是过期污染。已经完成使命的信息还留在上下文里。比如一个子任务已经结束,但它的中间推理过程还占着位置;或者工具调用已经返回结果,但调用请求本身还留着。

这四种污染不是孤立的,实际场景里经常叠加出现。我遇到过一次最离谱的:Agent 在第五轮把第二轮的一个工具返回值当成了当前轮的输入,原因就是第二轮那个 Observation 太长太显眼,而当前轮真正的输入被挤到了上下文末尾,模型注意力没跟上。

2.3 上下文窗口不是越大越好

现在很多模型支持 128K 甚至更长的上下文,于是有一种声音说"窗口够大就不用做上下文工程了"。这个想法很危险。

第一,长上下文不等于长有效注意力。业界有不少测试表明,模型在超长上下文里的信息召回率并不是线性的,中间部分的内容容易被忽略,这就是所谓的"lost in the middle"现象。你把关键信息塞在 64K 位置,模型未必能稳定找到。

第二,长上下文意味着高成本。不管是按 token 计费的 API,还是本地部署时的显存占用和推理延迟,上下文长度都是直接成本。本地部署大语言模型时尤其明显,KV Cache 会随着上下文增长线性膨胀,一个 32K 上下文的推理,显存占用可能是 4K 的好几倍。

第三,长上下文会拖慢推理速度。Transformer 的注意力计算复杂度随序列长度增长,上下文越长,每生成一个 token 的耗时越高。对于需要多步循环的 Agent 来说,这个延迟会被放大好几倍。

所以我的观点很明确:上下文工程的目标不是把窗口塞满,而是在保证任务所需信息完整的前提下,让上下文尽可能精简、干净、有序。这跟写代码时追求低耦合高内聚是一个道理。

3. ReAct 循环里的上下文管理实战

3.1 ReAct 的上下文膨胀是怎么发生的

先把 ReAct 的循环拆开看。一个标准的 ReAct 步骤是这样的:

Thought: 我需要先查一下今天的天气 Action: get_weather Action Input: {"city": "杭州"} Observation: 杭州今天晴,气温 18-26 度 Thought: 天气不错,可以推荐户外活动 Action: recommend_activity Action Input: {"weather": "晴", "temp_range": "18-26"} Observation: 推荐骑行、野餐 Thought: 信息够了,可以回答用户 Final Answer: 杭州今天天气很好...

每一步的 Thought、Action、Observation 都会被追加到上下文里,作为下一步的输入。跑 3 步,上下文里就有 3 组完整记录。跑 10 步,就是 10 组。这是线性增长,看起来还好。

但问题在于,很多 Agent 实现会把完整的工具返回结果原封不动塞进 Observation。比如查数据库返回一个 500 行的 JSON,或者检索文档返回 3000 字的原文,这些全进上下文,膨胀速度就是指数级的。我见过一个做数据分析的 Agent,单次 SQL 查询返回结果直接进上下文,跑到第三步就爆了 8K 窗口。

还有一个隐蔽的膨胀源:错误重试。工具调用失败时,很多实现会把失败信息、重试请求、重试结果全部保留。如果连续失败几次,上下文里就堆了一堆无效记录。

3.2 三种压缩策略的取舍

针对 ReAct 循环的上下文膨胀,我实践下来有三种压缩策略,各有适用场景。

策略一:滑动窗口 + 摘要。保留最近 N 步的完整记录,更早的记录用一段摘要替代。摘要由模型自己生成,比如"前 5 步完成了天气查询和活动推荐,结论是推荐户外活动"。这种策略适合步骤多但每步信息量不大的任务。

策略二:Observation 截断与结构化。工具返回结果不直接进上下文,而是先经过一层处理:超长的截断,非结构化的转成结构化摘要。比如数据库查询返回 500 行,只保留前 10 行加一个"共 500 行"的说明;文档检索返回长文,只保留最相关的段落加出处。这种策略适合工具返回结果体积大的场景。

策略三:关键信息提取。每步结束后,让模型从 Observation 里提取出对当前任务真正有用的信息,只把提取结果写回上下文,原始 Observation 丢弃。这种策略压缩率最高,但多了一次模型调用,有延迟成本,而且提取质量依赖模型能力。

我一般会组合使用:Observation 先做结构化截断(策略二),跑超过 8 步后启用滑动窗口摘要(策略一),对于检索类工具的结果额外做关键信息提取(策略三)。

3.3 一个可落地的上下文组装函数

下面这段伪代码是我在多个项目里复用过的上下文组装逻辑,用 Python 风格写,重点是展示思路:

def build_context(task, history, tools, memory, max_tokens=6000): # 1. 系统指令固定占用,预留预算 system_block = render_system_prompt(tools) budget = max_tokens - count_tokens(system_block) # 2. 任务目标始终保留,优先级最高 task_block = render_task(task) budget -= count_tokens(task_block) # 3. 记忆按相关性筛选,只取 top-k relevant_memory = retrieve_memory(memory, task, top_k=3) memory_block = render_memory(relevant_memory) budget -= count_tokens(memory_block) # 4. 历史记录从近到远填充,超预算就摘要 history_block = "" for step in reversed(history): step_text = render_step(step) if count_tokens(step_text) <= budget: history_block = step_text + history_block budget -= count_tokens(step_text) else: # 预算不够,对剩余历史做摘要 summary = summarize_history(history[:history.index(step)+1]) history_block = render_summary(summary) + history_block break return system_block + task_block + memory_block + history_block

这个函数的核心思想是优先级排序 + 预算控制。系统指令和任务目标不可压缩,记忆按相关性筛选,历史记录从近到远填充,预算不够就摘要。这样能保证最关键的信息永远在上下文里,次要信息按需保留。

注意:预算分配不是固定的,要根据任务类型调整。工具调用密集的任务,系统指令(含工具描述)的预算要留足;长对话任务,历史记录的预算要留足。我一般会先跑几次真实任务,统计各部分的实际 token 占比,再定预算。

3.4 工具描述的精简技巧

工具描述是容易被忽视的 token 大户。我总结了几个精简技巧:

  • 参数说明用短句,不用完整句子。"city: 城市名" 比 "city: 请输入你想要查询天气的城市名称" 省一半 token。
  • 合并同类工具。如果 get_weather_today 和 get_weather_forecast 逻辑相似,合并成一个带 mode 参数的工具。
  • schema 用紧凑格式。JSON schema 里的 description 字段能省则省,模型主要看参数名和类型。
  • 动态加载工具。不是所有任务都需要所有工具。根据任务类型,只把相关工具的描述放进上下文。比如用户问天气,就不需要把数据库查询工具的描述塞进去。

动态加载工具这一招效果特别明显。我有个项目挂了 20 多个工具,全量描述接近 5000 token。改成按任务类型动态加载后,平均每次只加载 5-6 个工具,token 占用降到 1200 左右,模型选工具的准确率反而提升了——因为干扰项少了。

4. 不同 Agent 架构下的上下文策略差异

4.1 单 Agent 与多 Agent 的上下文边界

单 Agent 架构下,所有信息都在一个上下文里,管理相对简单,就是前面说的压缩和优先级排序。但多 Agent 架构下,上下文管理会复杂一个量级。

多 Agent 通常有两种模式:共享上下文和独立上下文。共享上下文是所有 Agent 读写同一个上下文池,好处是信息一致,坏处是容易互相污染,而且并发写入需要加锁。独立上下文是每个 Agent 有自己的上下文,通过消息传递交换信息,好处是隔离性好,坏处是信息同步有延迟,而且消息传递本身也要设计格式。

我做过一个多 Agent 的客服系统,一开始用共享上下文,结果销售 Agent 和售后 Agent 的推理记录混在一起,销售 Agent 经常引用售后 Agent 的工具返回值,闹了不少笑话。后来改成独立上下文 + 结构化消息传递,每个 Agent 只接收与自己相关的消息,问题就解决了。

独立上下文模式下,消息传递的设计很关键。我的做法是定义一套消息 schema,包含 sender、receiver、type、payload、timestamp 五个字段。payload 里只放必要信息,不放完整推理过程。接收方 Agent 拿到消息后,自己决定怎么把它融入自己的上下文。

4.2 规划型 Agent 的上下文预加载

规划型 Agent(比如先做任务分解再逐步执行的架构)有一个特点:在执行前就知道大概需要哪些信息。这给上下文工程提供了一个优化空间——预加载。

具体做法是:规划阶段产出任务分解后,分析每个子任务需要哪些工具、哪些知识,提前把这些内容加载到上下文里。这样执行阶段就不用反复检索、反复加载,减少了循环次数和上下文波动。

但预加载有个坑:预加载多了会污染,预加载少了要补加载。我的经验是预加载"大概率需要"的内容,把"可能需要的"留给执行阶段按需加载。判断标准是看子任务描述里有没有明确提到相关实体或工具名。提到了就预加载,没提到就按需。

4.3 反思型 Agent 的上下文回滚

反思型 Agent(比如会自我批评、自我修正的架构)有个特殊需求:上下文回滚。当 Agent 发现前面某一步推理错了,需要回到那个状态重新来。这时候上下文不能简单追加,而要把错误步骤之后的内容清掉。

实现回滚的关键是给上下文做版本标记。每一步写入上下文时,记录一个 checkpoint。需要回滚时,恢复到指定 checkpoint,丢弃之后的内容。这跟数据库的事务回滚是一个思路。

我踩过的坑是:回滚后忘了清理副作用。比如 Agent 前面调用了一个发消息的工具,回滚后重新推理,又调用了一次,结果消息发了两遍。所以回滚不只是上下文的事,还要考虑外部动作的补偿。我的做法是给有副作用的工具调用加幂等标记,回滚后重新执行时先检查是否已经执行过。

5. 本地部署大模型时的上下文工程约束

5.1 显存与上下文长度的硬约束

本地部署大语言模型时,上下文工程多了一个硬约束:显存。KV Cache 的大小与上下文长度、层数、隐藏维度、batch size 都相关。粗略估算,一个 7B 模型在 FP16 下,每 1K token 的 KV Cache 大约占用 0.5-1GB 显存(具体取决于模型结构)。如果你只有一张 24G 的卡,模型权重占了 14G,剩下 10G 给 KV Cache,那上下文最多也就 10K-20K。

这意味着本地部署时,上下文预算比 API 场景紧张得多。API 场景你可以用 32K 甚至 128K 上下文,成本只是钱;本地部署你超了就是 OOM,直接跑不起来。

所以本地部署的上下文工程要更激进地压缩。我的做法是:

  • 系统指令极致精简。能用一个词说清的不用一句话。
  • 工具描述按需加载。绝不全量加载。
  • 历史记录严格滑动窗口。超过 5 步就摘要。
  • Observation 强制截断。单条不超过 200 token。

5.2 小模型的上下文敏感度

本地部署常用的是 7B、13B 这个量级的模型,它们对上下文的敏感度和 GPT-4 级别的大模型不一样。大模型能容忍一定程度的噪声和冗余,小模型不行。上下文里稍微多一点无关内容,小模型就容易跑偏。

我做过对比测试:同一个 ReAct 任务,用 7B 模型跑,上下文里加 500 token 的无关历史,工具调用准确率从 85% 掉到 60%;用大模型跑,同样加 500 token 噪声,准确率只从 95% 掉到 92%。差距非常明显。

所以本地部署小模型时,上下文工程的标准要更严:宁可少给,不可多给;宁可结构化,不可自然语言堆砌;宁可显式指令,不可让模型自己推断。

5.3 上下文格式对推理速度的影响

还有一个容易被忽视的点:上下文的格式会影响推理速度。同样的 token 数,结构化的 JSON 比自然语言段落推理更快,因为模型对结构化格式的解析更高效。而重复的模式(比如每步都是 Thought/Action/Observation 三段式)比杂乱的格式更快,因为注意力模式更规整。

我在本地部署时做过测试,把历史记录从自然语言改成紧凑的 JSON 数组,同样的任务,端到端延迟降低了约 15%。这个提升在需要多步循环的 Agent 上累积起来很可观。

6. 上下文工程的常见误区与排查清单

6.1 五个我踩过的典型坑

坑一:把上下文工程当成提示词工程。以为改改系统提示词就行了,结果发现问题是历史记录太长。这两个是不同层面的问题,提示词工程管"怎么说",上下文工程管"给什么看"。

坑二:无脑保留全部历史。觉得信息越多越好,结果模型被噪声干扰。历史记录要有选择地保留,不是全留。

坑三:摘要做一次就不管了。摘要本身也会过期。任务推进后,早期摘要可能已经不重要了,要定期重新摘要或丢弃。

坑四:忽视工具返回结果的格式。工具返回什么就塞什么,不做处理。应该统一做结构化截断。

坑五:不做上下文监控。不知道每步上下文里到底有什么、占多少 token,出了问题只能瞎猜。应该加日志,记录每步上下文的组成和 token 分布。

6.2 上下文问题排查清单

遇到 Agent 行为异常时,我一般按这个清单排查:

排查项检查内容常见问题
上下文长度当前 token 数 vs 窗口上限接近上限导致截断
信息位置关键信息在上下文的哪个位置关键信息在中间被忽略
冗余程度是否有重复信息工具描述与系统指令重复
冲突检查不同来源信息是否矛盾记忆与当前指令冲突
过期内容是否有已完成任务的残留旧 Observation 干扰
格式一致性上下文格式是否统一混用自然语言和 JSON

这个清单我打印出来贴在显示器边上,出问题就逐项过一遍,基本能定位到原因。

6.3 上下文工程的效果度量

上下文工程做得好不好,不能凭感觉,要有度量。我常用的几个指标:

  • 任务成功率:最直接的指标,上下文优化前后对比。
  • 平均循环步数:上下文干净时,Agent 决策更准,步数会减少。
  • 平均 token 消耗:衡量压缩效果。
  • 工具调用准确率:上下文里工具描述清晰时,这个指标会提升。
  • 端到端延迟:上下文短了,推理快了,延迟降低。

我一般会建一个小的评测集,20-30 个代表性任务,每次调整上下文策略就跑一遍,看这几个指标的变化。没有度量,优化就是盲人摸象。

7. 我个人的一些实操体会

上下文工程这件事,说到底是在信息完整性和信息精简性之间找平衡。给少了,模型缺信息做不出正确决策;给多了,模型被噪声干扰。这个平衡点没有通用答案,只能针对具体任务、具体模型去调。

我的经验是,先做减法,再做加法。一开始不要想着怎么把信息塞全,而是先想"这个任务最少需要什么信息"。把最小必要集跑通,再逐步加信息,每加一项都验证是否有正向收益。这样能避免一开始就堆一大堆东西,后面想减都减不掉。

还有一个体会是,上下文工程要和工具设计一起考虑。工具返回结果的格式,直接决定了上下文处理的难度。如果工具返回的是结构化、精简的结果,上下文工程就轻松很多。所以我在设计工具时,会刻意让工具返回"对 Agent 友好"的格式,而不是"对人友好"的格式。比如返回 JSON 而不是自然语言段落,返回摘要而不是全文。

最后说一个具体的技巧:给上下文加"锚点"。在上下文的关键位置加一些标记,比如[TASK]、[MEMORY]、[HISTORY],让模型能快速定位不同区块。这个技巧在小模型上效果尤其明显,能显著提升模型对上下文结构的感知。我实测下来,加了锚点后,7B 模型在长上下文里的信息召回率能提升 10-15 个百分点。

上下文工程不是一个一劳永逸的事,Agent 每迭代一次、工具每增加一个、任务类型每扩展一种,上下文策略都要重新审视。把它当成一个持续优化的工程问题,而不是一次性的配置任务,才能真正发挥它的价值。

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

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

立即咨询