尧图精选

上下文工程实战:AI Agent上下文管理与ReAct循环优化

🕒 发布时间:2026/10/2 19:52:08 📁 来源:尧图网络
1. 上下文工程到底在解决什么问题1.1 从提示词工程到上下文工程的认知升级很多人第一次接触 AI Agent 开发时会把大部分精力花在“怎么写提示词”上。这个阶段我称之为提示词工程阶段核心思路是找到一句“魔法咒语”让模型输出理想结果。但真正把 Agent 跑在生产环境里的人很快会发现提示词写得再漂亮也扛不住多轮对话、工具调用、外部知识注入、历史状态累积带来的上下文膨胀。上下文工程要解决的核心问题是在有限的上下文窗口内动态地组装出对当前决策最有价值的信息集合。它不只是“写提示词”而是涵盖了系统指令、对话历史、工具定义、检索结果、中间推理状态、输出格式约束等多个维度的综合调度。你可以把它理解为提示词工程是写一封邮件上下文工程是管理整个收件箱、归档规则、优先级排序和自动回复策略。为什么这个区分很重要因为 Agent 和单轮问答的本质差异在于Agent 需要在一个循环里持续做决策。每一轮决策都依赖上一轮的输出而上一轮的输出又会成为下一轮上下文的一部分。如果不加控制上下文会像滚雪球一样越滚越大最终要么超出窗口限制要么让模型在噪声中迷失重点。1.2 上下文窗口不是越大越好现在很多模型宣称支持超长上下文动辄几十万 token。但实际用下来你会发现窗口大不等于效果好。我做过一个简单的对比测试同样一个多步推理任务把全部历史塞进 128K 窗口和只保留最近 5 轮加摘要后者的任务完成率反而更高。原因有两个一是模型在超长上下文中对中间位置的注意力会衰减二是无关信息会稀释关键指令的权重。所以上下文工程的第一原则是不是把所有信息都塞进去而是只放当前决策需要的信息。这听起来像废话但实操中很多人就是舍不得删历史总觉得“万一有用呢”。我的经验是凡是当前步骤用不上的信息一律不放进主上下文需要时再通过工具或检索动态拉取。1.3 一个典型的上下文组成清单为了让你有个直观感受我把一个 ReAct 风格 Agent 单轮决策时的上下文拆成以下几块上下文模块内容示例是否每轮都带典型 token 占比系统指令角色定义、行为约束、输出格式是5%–10%工具定义可用工具的名称、参数、描述是10%–20%对话历史用户输入、Agent 回复部分20%–40%检索结果从知识库拉取的片段按需10%–30%中间状态已执行步骤、观察结果是10%–20%输出约束JSON schema、格式示例是5%–10%这张表不是标准答案但能帮你建立“上下文是有预算的”这个意识。每一块都在抢 token你需要根据任务类型动态调整配比。比如工具调用密集的任务工具定义占比会上升知识问答密集的任务检索结果占比会上升。2. 核心细节解析与实操要点2.1 系统指令的写法约束比描述更重要很多人写系统指令喜欢用大段描述比如“你是一个专业的、友好的、乐于助人的助手”。这种话对模型行为的影响微乎其微。真正有效的是硬约束能做什么、不能做什么、遇到什么情况走什么分支。我常用的系统指令结构是这样的角色你是订单查询 Agent。 能力边界只能查询订单状态和物流信息不能修改订单。 工具使用规则 - 用户提供订单号时直接调用 query_order。 - 用户未提供订单号时先追问不要猜测。 - 工具返回错误时最多重试一次仍失败则告知用户稍后再试。 输出格式先给结论再给依据不超过 100 字。这种写法的好处是模型不需要“理解”你的意图只需要按规则执行。实测下来任务完成率比模糊描述高出不少。注意系统指令要放在上下文最前面并且每轮都带不要指望模型记住上一轮的系统指令。2.2 工具定义的粒度控制工具定义是上下文里最容易被浪费的部分。我见过一个 Agent 定义了 30 多个工具每个工具的描述写了 200 字光工具定义就占了 6000 token。结果模型经常选错工具因为选项太多、描述太像。我的做法是按场景分组动态加载。比如一个客服 Agent可以分成“订单类工具”“退款类工具”“账户类工具”三组。根据用户当前意图只加载对应组的工具定义。这样每轮的工具定义 token 能压到原来的三分之一选错率也明显下降。另外工具描述要写“什么时候用”而不是“这个工具是什么”。比如差的描述query_order查询订单信息。好的描述query_order当用户提供订单号并询问状态、物流、金额时使用。不用于修改订单。2.3 对话历史的压缩策略对话历史是上下文膨胀的主要来源。我的压缩策略分三层第一层是滑动窗口只保留最近 N 轮完整对话。N 的取值取决于任务复杂度简单问答 3 到 5 轮复杂任务 8 到 10 轮。第二层是摘要压缩把超出窗口的历史用模型生成一段摘要放在窗口前面。摘要要包含关键实体、已确认事实、未解决问题。第三层是状态外置把结构化的状态比如订单号、用户 ID、已执行步骤存到外部变量里需要时再注入上下文。这样历史对话可以大胆删关键信息不会丢。注意摘要压缩会引入信息损失对于需要精确回溯的任务比如多步计算不要用摘要替代原始数据而是把原始数据存到外部需要时重新拉取。2.4 ReAct 循环中的上下文管理ReAct 模式的核心是“推理-行动-观察”循环。每一轮循环上下文都会增加推理文本、工具调用、工具返回三块内容。如果不加控制五轮之后上下文就会翻倍。我的做法是在每轮循环结束时做一次上下文清理把上一轮的推理文本压缩成一句话结论。工具返回如果很长只保留关键字段其余丢弃。如果连续两轮没有进展强制注入“换策略”指令。这样能把 ReAct 循环的上下文增长从线性变成近似常数实测能多跑 3 到 5 轮才触及窗口上限。3. 实操过程与核心环节实现3.1 从零搭一个带上下文管理的 Agent下面我用一个订单查询 Agent 的例子把上下文工程的落地过程拆开讲。技术栈选择上我用 Python 加一个通用的大模型接口不绑定具体厂商方便你迁移。第一步定义上下文容器。我习惯用一个字典来管理各个模块而不是拼一个大字符串。这样方便按需裁剪和替换。context { system: SYSTEM_PROMPT, tools: [], history: [], scratchpad: [], retrieval: [] }第二步实现工具动态加载。根据用户输入的关键词决定加载哪组工具。def load_tools(user_input): if any(k in user_input for k in [订单, 物流, 发货]): return ORDER_TOOLS elif any(k in user_input for k in [退款, 退货]): return REFUND_TOOLS else: return []第三步实现历史压缩。这里用一个简单的滑动窗口加摘要。def compress_history(history, max_turns5): if len(history) max_turns * 2: return history old history[:-max_turns * 2] recent history[-max_turns * 2:] summary summarize(old) return [{role: system, content: f历史摘要{summary}}] recent第四步组装最终上下文。注意顺序系统指令在最前工具定义其次然后是摘要和历史最后是当前输入。def build_context(context, user_input): parts [context[system]] if context[tools]: parts.append(format_tools(context[tools])) parts.extend(context[history]) parts.append({role: user, content: user_input}) return parts第五步ReAct 循环。每轮结束后清理 scratchpad。def react_loop(user_input, max_steps8): context[tools] load_tools(user_input) context[history] compress_history(context[history]) for step in range(max_steps): messages build_context(context, user_input) response call_llm(messages) if is_final_answer(response): return response action parse_action(response) observation execute_tool(action) context[scratchpad].append({ step: step, action: action, observation: truncate(observation, 500) }) context[history].append({role: assistant, content: response}) context[history].append({role: user, content: f观察结果{observation}}) return 任务未完成请稍后重试这套代码不复杂但包含了上下文工程的几个关键动作动态工具加载、历史压缩、scratchpad 截断、循环上限控制。你可以直接拿去改。3.2 参数选择与计算过程上下文预算怎么分配我一般按窗口大小的百分比来算。假设模型窗口是 8K token我的分配是系统指令500 token工具定义1500 token历史对话2500 token检索结果2000 token输出预留1500 token这个配比不是固定的。如果任务以工具调用为主我会把工具定义提到 2500历史压到 1500。如果任务以知识问答为主检索结果提到 3000工具定义降到 500。计算 token 有个粗略公式中文大约 1 个字等于 1.5 到 2 个 token英文大约 1 个单词等于 1.3 个 token。实际开发中我会在每轮组装上下文后打印 token 估算值超过预算就触发压缩。3.3 实操现场记录一次上下文超限的排查有一次线上 Agent 突然开始胡言乱语回答和问题完全无关。我查了日志发现上下文 token 数从正常的 3000 飙到了 12000超出了模型窗口。原因是某个工具返回了一个超长的 JSON里面有大量嵌套字段直接塞进了历史。修复方案有两个一是在工具返回处加截断只保留关键字段二是在组装上下文前做一次 token 检查超限就触发压缩。我两个都做了之后没再出现类似问题。这个坑给我的教训是永远不要相信外部输入的长度。工具返回、检索结果、用户输入都可能超长。上下文工程必须包含防御性截断。4. 常见问题与排查技巧实录4.1 模型不按格式输出怎么办这是最常见的问题。你要求 JSON它给你一段解释加 JSON。我的排查顺序是检查输出约束是否放在上下文最后。模型对末尾内容的注意力最强格式要求要放在最后。检查是否给了示例。给一个正确的输出示例比写十句“请输出 JSON”都管用。检查温度参数。格式要求严格时温度调到 0 到 0.3。如果还不行用工具调用接口强制结构化输出而不是靠提示词。4.2 工具选错或参数传错工具选错通常是因为工具描述太相似或者工具太多。我的解决步骤先看工具描述把“什么时候用”写清楚。再看工具数量超过 10 个就分组动态加载。最后看参数定义参数名要自解释不要用arg1、arg2这种。参数传错还有一个常见原因是缺少示例。在工具描述里加一个调用示例能显著降低错误率。4.3 多轮对话后 Agent 忘记早期信息这是上下文压缩的副作用。我的做法是把关键信息外置成状态变量而不是依赖模型记忆。比如用户 ID、订单号、已确认的偏好都存在外部字典里每轮组装上下文时注入。这样即使历史被压缩关键信息也不会丢。4.4 常见问题速查表问题现象可能原因排查动作解决方向回答与问题无关上下文超限或噪声过多打印 token 数和上下文内容压缩历史、截断工具返回工具选错工具描述相似或数量过多检查工具定义分组加载、改写描述格式不符约束位置靠前或缺少示例检查输出约束位置移到末尾、加示例忘记早期信息历史被压缩检查摘要是否丢关键实体关键信息外置成状态循环不终止缺少步数上限检查循环控制加 max_steps 和换策略指令响应变慢上下文过长统计每轮 token动态裁剪、按需检索4.5 几个我踩过的坑第一个坑是把检索结果直接拼进历史。检索结果每轮都可能变拼进历史会导致历史快速膨胀。正确做法是检索结果单独放一块每轮重新检索、重新注入不累积。第二个坑是摘要用模型生成但不校验。模型生成的摘要可能丢关键信息甚至编造内容。我的做法是摘要只压缩对话不压缩结构化数据并且摘要生成后用规则校验关键实体是否保留。第三个坑是忽略输出预留。很多人算上下文预算时只算输入忘了输出也要占窗口。结果输入塞满了模型没空间输出直接截断。预留 15% 到 20% 给输出是必要的。第四个坑是系统指令写太长。系统指令每轮都带写 2000 token 就是每轮浪费 2000。我的经验是系统指令控制在 500 token 以内把细节放到工具描述和输出约束里。5. 上下文工程的进阶思路5.1 分层上下文架构当 Agent 变复杂时单一上下文容器会不够用。我现在的做法是分三层全局层系统指令、全局约束每轮都带不变。会话层当前会话的历史、状态随会话变化。步骤层当前步骤的检索结果、工具返回每步重建。这样分层的好处是每层的更新频率不同可以独立压缩和替换。全局层几乎不动会话层按轮压缩步骤层每步清空。实测能显著降低上下文管理的复杂度。5.2 上下文工程的评估指标怎么判断上下文工程做得好不好我关注四个指标任务完成率最终是否解决了用户问题。平均步数完成任务的循环次数越少越好。token 效率完成任务消耗的总 token越低越好。错误率工具选错、格式错误、循环不终止的比例。这四个指标要一起看。只追求完成率可能会把上下文塞满只追求 token 效率可能会牺牲完成率。我的经验是先把完成率做到 90% 以上再优化 token 效率。5.3 不同模型下的上下文策略差异不同模型对上下文的敏感度不一样。有的模型对系统指令遵循度高系统指令可以写简略些有的模型对末尾内容更敏感输出约束必须放最后。我的做法是给每个模型维护一套上下文模板换模型时先跑一轮评估再调整模板。另外小模型对大上下文的承受力更差。如果你用 7B 级别的模型上下文最好控制在 4K 以内并且要更激进地压缩历史。大模型可以放宽到 16K 甚至 32K但也不建议塞满。5.4 上下文工程和 RAG 的配合RAG 解决的是“知识从哪来”上下文工程解决的是“知识怎么放”。两者配合的关键是检索结果不要直接拼进历史而是作为独立模块每轮按需注入。检索的 top-k 也不要太大3 到 5 条足够太多会稀释关键信息。还有一个细节是检索结果的排序。我习惯把最相关的放最前面因为模型对开头和末尾的注意力最强。如果检索结果有冲突在注入时加一句“以下信息按相关度排序优先参考前面的内容”。5.5 上下文工程的未来方向从我个人观察来看上下文工程正在从“手工调参”走向“自动优化”。现在已经有一些工作在尝试用模型自己管理上下文比如让模型决定哪些历史该保留、哪些该丢弃。这个方向很有意思但目前的可靠性还不够关键任务还是得靠规则兜底。另一个方向是上下文压缩的专门模型。用小模型做摘要和压缩比用大模型便宜得多速度也快。我在一些项目里试过用 1B 级别的模型做历史压缩效果可以接受成本降了一个数量级。最后再分享一个小技巧如果你不确定上下文该怎么配比先做一个最小可用版本把系统指令、工具定义、最近三轮历史、当前输入放进去跑一批测试用例看失败案例集中在哪再针对性调整。上下文工程没有万能公式都是根据具体任务迭代出来的。我在实际项目里的体会是前两版上下文设计基本都会推倒重来第三版才趋于稳定所以别怕改快速迭代比一次设计到位更现实。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →