ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent落地分水岭:上下文工程与Token预算实战

Agent落地分水岭:上下文工程与Token预算实战 1. 为什么上下文工程成了 Agent 落地的分水岭过去一年我帮几个团队做过 AI Agent 的落地评审发现一个很反直觉的现象同一个模型、同一套工具、同一个业务场景两个团队做出来的 Agent 效果能差出三倍以上。一个能稳定完成多步任务另一个跑三步就开始胡言乱语、忘记目标、重复调用同一个工具。排查到最后问题几乎从来不在模型本身而在一个被大多数人忽略的环节——上下文工程Context Engineering。很多人把上下文工程等同于写 Prompt这是最大的误解。Prompt 是单次输入的一段文字而上下文是模型在某一时刻能看到的全部信息总和系统指令、历史对话、工具返回结果、检索到的文档片段、当前任务状态、甚至包括 token 预算的分配方式。Agent 和普通对话机器人的本质区别在于Agent 是多轮、有状态、会调用外部工具的这意味着它的上下文是动态增长的、会被工具输出污染的、会随着步数累积而爆炸的。你不管它它就会自己把自己淹死。这篇文章我想把上下文工程这件事拆透。它适合已经跑通过一个 Demo、但发现 Agent 一上真实任务就崩的人也适合正在设计 Agent 中台、需要给多个业务方提供稳定能力的架构同学。我会讲清楚上下文到底由哪几块组成、每一块该怎么管、token 预算怎么算、工具返回结果怎么裁剪、长任务怎么压缩记忆以及那些只有真正踩过坑才知道的细节。全文基于我在实际项目里的做法不是理论推演。先说一个我自己的判断2026 年之后Agent 的竞争力不在模型选型而在上下文管理。模型能力会趋同API 会越来越便宜但如何在有限窗口里塞进最有价值的信息这件事是工程问题是每个团队必须自己解决的。下面进入正题。2. 拆开 Agent 的上下文它到底由哪几块拼成2.1 上下文不是一段文字而是五个来源的叠加我习惯把 Agent 单次推理时喂给模型的上下文拆成五个来源这样排查问题时能快速定位是哪一块出了问题系统层System角色设定、行为约束、输出格式要求、可用工具清单。这部分相对静态但最容易写臃肿。任务层Task当前要完成的目标、拆解后的子任务、验收标准。这是 Agent 的北极星丢了它 Agent 就迷路。记忆层Memory历史对话摘要、长期偏好、之前步骤的关键结论。注意是摘要不是原文。工具层Tool I/O每一次工具调用的入参和返回结果。这是上下文膨胀的头号元凶。检索层Retrieval从知识库、文档、数据库里捞出来的相关片段也就是常说的 RAG 部分。这五块加起来才是模型真正看到的全部。很多人的 Agent 出问题是因为只优化了系统层把 Prompt 写得花里胡哨却对工具层和记忆层放任不管。结果就是Prompt 写得再漂亮第 8 步工具返回了一坨 3000 token 的 JSON直接把前面的指令挤出了有效注意力范围。2.2 为什么工具返回结果是上下文污染的重灾区我拿一个真实场景举例。你让 Agent 去查一批订单状态工具返回的原始 JSON 可能是这样的{ code: 0, message: success, data: { total: 128, list: [ {order_id: SO20260101001, status: shipped, create_time: 2026-01-01 10:23:11, logistics: {...}, items: [...]}, ... ] } }128 条订单每条还带物流轨迹和商品明细轻松突破 2 万 token。模型真正需要的可能只是128 条订单中有 3 条处于异常状态。你把原始 JSON 全塞进去等于让模型在垃圾堆里找针既浪费预算又降低准确率。我的做法是在工具层和模型层之间加一个结果整形器工具返回原始数据整形器负责提取关键字段、聚合统计、截断超长列表只把模型决策需要的最小信息集传下去。这一步看起来简单但它对 Agent 稳定性的提升比换一个更强的模型还明显。2.3 上下文窗口的有效区远比标称值小现在动辄 128K、200K 甚至 1M token 的窗口让很多人产生了窗口够大就不用管上下文的错觉。实测下来完全不是这么回事。模型对上下文的注意力是不均匀的开头系统指令和结尾当前问题的注意力权重高中间部分容易被稀释这就是常说的lost in the middle现象。我做过一个粗糙但有效的测试把关键约束放在上下文的第 1 段、中间、最后一段分别测 Agent 的遵守率。结果是首尾放置时遵守率能到 90% 以上放中间时掉到 60% 左右。所以哪怕你有 1M 窗口关键信息也必须往首尾放中间塞的应该是可容忍丢失的背景材料。提示不要因为窗口大就无脑堆内容。窗口大小决定的是能不能塞下而上下文工程决定的是塞进去有没有用。这两件事完全不同。3. Token 预算给上下文做一份财务规划3.1 先算清楚你的预算上限做上下文工程的第一步不是写代码是算账。假设你用的是 128K 窗口的模型你不能真的用满 128K因为输出也要占额度通常要预留 4K 到 8K工具调用的 schema 定义本身占额度留 10% 到 20% 的缓冲防止某一步突然超长。我一般的分配是这样的以 128K 为例上下文分区预算占比对应 token 数说明系统指令 工具定义10%约 12K静态尽量精简任务目标 当前状态10%约 12K必须常驻不可裁剪历史记忆摘要20%约 25K滚动压缩工具返回结果30%约 38K动态需整形检索片段20%约 25K按相关度排序输出预留 缓冲10%约 12K不可挪用这张表不是死的但它给了我一个硬约束任何一块超了就必须从这块内部裁剪而不是去挤占别的分区。很多 Agent 崩掉就是因为工具返回结果无限膨胀把任务目标挤没了。3.2 用 tokenizer 精确计数别靠字符数估算我见过太多人用字符数除以 4来估算 token中文场景下这个误差能到 50% 以上。正确做法是用对应模型的 tokenizer 精确计数。以常见的开源 tokenizer 为例from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(your-model-tokenizer) def count_tokens(text: str) - int: return len(tokenizer.encode(text, add_special_tokensFalse)) # 在拼接上下文前逐块计数 blocks { system: system_prompt, task: task_state, memory: memory_summary, tool_result: tool_output, retrieval: retrieved_docs, } total sum(count_tokens(v) for v in blocks.values()) print(f当前上下文占用: {total} tokens)这个计数函数要在每次拼接上下文前调用而不是只在开发时测一次。因为工具返回是动态的你必须实时知道当前用了多少预算才能决定下一步是压缩还是截断。3.3 预算超了怎么办三级降级策略当计数发现超预算时我一般按这个顺序降级裁剪工具结果只保留关键字段长列表截断到前 N 条 统计摘要。这一步通常能省下最多。压缩历史记忆把更早的对话轮次合并成摘要用模型自己生成一段到目前为止发生了什么。降低检索片段数量从 top-10 降到 top-3或者只保留每段的前 200 字。注意顺序先动工具结果再动记忆最后动检索。因为工具结果的信息密度最低大量冗余字段记忆次之检索片段通常是精挑细选过的最不该动。注意降级策略必须是自动触发的不能靠人工判断。我一般设两个阈值用到 70% 预算时开始轻度压缩用到 90% 时强制降级。这样能避免跑着跑着突然爆窗。4. 工具返回结果的整形把垃圾挡在模型之外4.1 整形器的三个职责工具结果整形器Result Shaper是我认为上下文工程里性价比最高的一个组件。它干三件事提取从原始返回里挑出模型决策真正需要的字段聚合把列表类数据转成统计摘要比如共 128 条异常 3 条异常订单号是……截断对超长文本做首尾保留 中间省略或者按相关度排序后取前 N。我举个具体例子。原始工具返回 128 条订单整形后传给模型的是订单查询结果摘要 - 总数128 - 状态分布已发货 120待发货 5异常 3 - 异常订单SO20260101007物流停滞超 48h、SO20260101042地址异常、SO20260101103支付失败 - 时间范围2026-01-01 至 2026-01-15从 2 万 token 压到 100 token 以内信息量却几乎没损失。模型拿到这个摘要完全能做出优先处理这 3 条异常订单的决策。4.2 整形规则要按工具类型定制不同工具的返回结构差异很大整形规则不能一刀切。我一般按工具类型分几类处理工具类型典型返回整形策略列表查询类订单、用户、日志列表统计摘要 异常项全量 正常项截断详情查询类单个对象的完整字段白名单字段提取丢弃无关字段文本生成类长文档、报告首尾保留 中间省略或按段落相关度筛选状态查询类布尔值、状态码直接透传无需整形错误返回类异常堆栈、错误码提取错误类型 关键信息丢弃堆栈细节这张表是我踩了很多坑总结出来的。比如错误返回一开始我把完整堆栈传给模型结果模型被一堆技术细节带偏反而忘了原始任务。后来改成只传错误类型 一句话描述模型就能正确判断是重试还是换方案。4.3 一个容易忽略的点工具入参也要管大家都在管工具返回但工具入参同样会污染上下文。尤其是当 Agent 需要调用一个参数复杂的工具时它自己生成的入参 JSON 可能很长。如果这个入参在后续轮次里一直被保留在上下文中就会持续占用预算。我的做法是工具调用完成后把入参压缩成一行摘要比如把{query: {...}, filters: {...}, pagination: {...}}压成查询订单筛选异常状态分页第1页。这样既保留了我做过什么的信息又省下了大量 token。5. 记忆管理让 Agent 记住该记的忘掉该忘的5.1 短期记忆和长期记忆要分开处理Agent 的记忆我分成两层短期记忆当前任务内的对话历史、已完成的步骤、中间结论。任务结束就丢弃。长期记忆跨任务的用户偏好、领域知识、历史决策。需要持久化存储。这两层的管理策略完全不同。短期记忆的核心是滚动压缩长期记忆的核心是按需检索。很多人把两者混在一起结果要么短期记忆太长爆窗要么长期记忆检索不准。5.2 短期记忆的滚动压缩怎么做短期记忆不能无限增长我一般用滑动窗口 摘要的组合保留最近 N 轮比如 5 轮的完整对话更早的轮次合并成一段摘要由模型生成摘要里必须包含已完成的关键步骤、当前任务状态、未解决的问题。生成摘要的 Prompt 我一般这么写请把以下对话历史压缩成一段不超过 300 字的摘要必须保留 1. 已经完成的关键操作及其结果 2. 当前任务的进展状态 3. 尚未解决的问题或待确认的信息 不要保留寒暄、重复的确认、已被推翻的中间结论。 对话历史 {history}关键是那句不要保留什么。不明确告诉模型丢弃什么它会把所有东西都塞进摘要压缩就白做了。5.3 长期记忆的检索要带时效衰减长期记忆检索最容易犯的错是只看相关度不看时间。用户三个月前说过的偏好可能早就变了。我的做法是在检索打分里加一个时效衰减因子最终得分 相关度得分 × 衰减系数 衰减系数 exp(-λ × 距今天数)λ 的取值看业务快变的偏好比如当前项目λ 取大一点稳定的知识比如用户所在行业λ 取小一点。这样能保证检索出来的记忆既相关又新鲜。提示长期记忆一定要有遗忘机制。我见过一个 Agent 因为记住了用户半年前的一次错误操作之后每次都在错误的路径上打转。记忆不是越多越好是越准越好。6. 长任务下的上下文压缩实战6.1 什么时候该压缩什么时候不该压缩是有代价的每次压缩都会损失信息而且压缩本身要消耗一次模型调用。所以不是所有长任务都值得压缩。我的判断标准是任务步数超过 10 步且每步都有工具调用 → 必须压缩任务步数少但单步返回巨大 → 优先整形不一定要压缩任务需要严格追溯每一步细节比如审计场景→ 压缩要谨慎改用外部存储 按需检索。6.2 分层压缩把不同信息用不同力度压我一般把压缩分成三档轻压缩只去掉冗余字段和重复内容保留原文结构。适合最近几轮。中压缩合并同类项生成结构化摘要。适合中间轮次。重压缩只保留结论和状态丢弃过程。适合很早的轮次。这样做的原因是越近的信息越可能被用到压缩力度要轻越远的信息越可能只是背景可以压得狠一点。一刀切地压缩所有历史会导致近期关键信息也被损失。6.3 压缩后的验证不能省压缩完不能直接用必须验证。我一般做两个检查关键信息是否还在把压缩前的关键实体订单号、用户 ID、任务目标列出来检查压缩后是否都还在。压缩比是否合理如果压缩后 token 数没降多少说明压缩没起作用如果降得太狠比如降了 95%要警惕信息损失过多。def validate_compression(original: str, compressed: str, key_entities: list) - bool: # 检查关键实体是否保留 for entity in key_entities: if entity not in compressed: print(f警告关键实体丢失 - {entity}) return False # 检查压缩比 ratio count_tokens(compressed) / count_tokens(original) if ratio 0.8: print(f警告压缩效果不足压缩比 {ratio:.2f}) if ratio 0.05: print(f警告压缩过狠可能丢失信息压缩比 {ratio:.2f}) return True这个验证函数我几乎每个项目都会加它帮我抓到过好几次压缩把关键订单号弄丢了的事故。7. 那些只有踩过才知道的上下文工程细节7.1 系统指令要短而硬不要长而软我早期写系统指令喜欢面面俱到写个两三千字结果模型反而抓不住重点。后来改成只保留硬约束角色、必须遵守的规则、输出格式。软性的风格描述能删就删。系统指令越短模型对它的遵守率越高因为它不会被自己的其他描述稀释。7.2 工具定义本身也是上下文负担每个工具的 schema 定义都要占 token。如果你给 Agent 挂了 30 个工具光工具定义就可能吃掉上万 token。我的做法是按任务动态加载工具当前任务只需要 5 个工具就只挂这 5 个其余的不进上下文。这一招对多工具 Agent 的效果提升非常明显。7.3 上下文里的顺序会影响模型决策同样的信息放在不同位置模型的决策可能不同。我一般遵循这个顺序系统指令 → 任务目标 → 当前状态 → 工具结果 → 检索片段 → 当前问题。把当前问题放最后是因为模型对结尾的注意力最高能确保它聚焦在现在要干什么上。7.4 别让 Agent 自己管理上下文有些框架让 Agent 自己决定要不要压缩记忆要不要丢弃历史听起来很智能实测很不稳。Agent 在任务中途很难准确判断哪些信息未来会用到。我的做法是把上下文管理做成框架层的确定性逻辑Agent 只负责决策任务本身不负责管理自己的记忆。职责分离之后稳定性提升一大截。7.5 监控上下文占用像监控内存一样上线之后我会给每个 Agent 加一个上下文占用的监控指标记录每一步的 token 使用量。一旦发现某个任务的上下文占用持续逼近上限就说明整形或压缩策略需要调整。这个监控帮我提前发现过好几次某个工具返回突然变大的问题。8. 一个可复用的上下文组装流程把上面所有东西串起来我实际项目里的上下文组装流程大致是这样的加载静态部分系统指令 当前任务需要的工具定义注入任务状态当前目标、已完成步骤、待办事项拼接记忆最近 N 轮完整对话 更早轮次的摘要整形工具结果对最近一次工具返回做提取、聚合、截断检索相关片段按相关度和时效打分取 top-K计算总 token用 tokenizer 精确计数触发降级超预算则按工具结果 → 记忆 → 检索顺序压缩按固定顺序拼接系统 → 任务 → 状态 → 工具 → 检索 → 当前问题记录监控指标把本次 token 占用写入监控。这套流程看起来步骤多但每一步都是确定性的不依赖模型判断所以非常稳。我把它封装成一个ContextBuilder类所有 Agent 共用改一处就能全局生效。class ContextBuilder: def __init__(self, tokenizer, budget128000): self.tokenizer tokenizer self.budget budget def build(self, system, task, memory, tool_result, retrieval, question): blocks [system, task, memory, tool_result, retrieval, question] total sum(self._count(b) for b in blocks) if total self.budget * 0.9: tool_result self._shrink_tool(tool_result) memory self._compress_memory(memory) retrieval self._trim_retrieval(retrieval) return self._assemble(system, task, memory, tool_result, retrieval, question)这个类的核心思想就是把上下文管理从每次手写变成统一组装。一旦统一你就能对它做监控、做优化、做回归测试。9. 我在这件事上的几点个人体会做了这么多 Agent 项目我最大的体会是上下文工程是一个减法的活不是加法的活。新手总想着往上下文里塞更多信息觉得信息越多模型越聪明老手想的是怎么把不必要的信息拿掉让模型只看到它真正需要的。这个思维转变是从能跑通 Demo到能上生产的关键。第二个体会是上下文工程没有银弹只有针对具体场景的调优。工具返回长什么样、任务有多少步、用户对延迟多敏感这些都会影响你的策略。所以别指望抄一套配置就万事大吉你得自己测、自己调、自己监控。第三个体会是把上下文管理做成确定性逻辑比让模型自己管要靠谱得多。模型擅长的是理解和生成不擅长的是资源管理和状态维护。把后者交给代码前者交给模型各司其职Agent 才能稳。最后分享一个我常用的小技巧给上下文每个分区打标签。比如用[SYSTEM]、[TASK]、[TOOL]、[MEMORY]这样的标记把不同来源的内容隔开。这样不仅模型更容易区分信息边界你自己排查问题时也能一眼看出是哪块内容出了问题。这个习惯帮我省下了大量调试时间。
返回列表