恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Agent上下文工程实战:从Token预算到多轮记忆隔离
首页
资讯中心
/
Agent上下文工程实战:从Token预算到多轮记忆隔离
Agent上下文工程实战:从Token预算到多轮记忆隔离
发布时间:2026/10/8 20:57:29
我把一个Agent从测试环境推到灰度用户会话跑到第14轮就彻底“断片”了——它把用户两小时前刚改过的收货地址又填回了旧地址。日志里看得很清楚那一轮模型的上下文里压根没有更新后的地址信息它只能凭旧记录作答。这个问题的根子不在模型推理能力而在上下文工程没做好。先给不熟悉这个词的朋友解释一下上下文工程Context Engineering是相对于Prompt Engineering提出的概念但范围宽得多。Prompt Engineering关心的是“给模型的那段话怎么写”而上下文工程关心的是“在模型每次推理时它到底能看见哪些信息这些信息如何被取舍、排序、压缩和拼装”。在Agent场景里模型看到的不是一个固定prompt而是系统指令、历史对话、工具调用结果、知识库片段、用户目标等多路信息拼成的“动态上下文包”。怎么把这个包组装好让Agent在多轮任务中不丢关键信息、不爆炸token、不串线就是上下文工程要做的事。这篇主要面向两类人一是已经在做Agent开发被多轮对话失忆、token成本飙升、并发session互相干扰折磨过的工程师二是刚入行想系统了解Agent内部机制的学习者。我不会绕弯子直接讲清楚上下文膨胀的路径、token预算怎么分配、压缩怎么做、并发场景怎么隔离最后附上我在实际项目里踩过的坑和排查方法你可以直接拿去用。1. 为什么Agent比普通聊天机器人更依赖上下文工程1.1 从“一问一答”到“完整工作流”的转变普通聊天机器人本质上是一个“请求-响应”模型用户发一句模型回一句上一轮结完就结束。顶多保留最近几轮消息做引用对话一长就用滑动窗口截断丢一点历史也能接受。Agent就不一样了。它的核心循环是接收任务 → 分析拆解 → 调用工具 → 观察结果 → 再分析 → 再调用……直到完成目标。每一步都会产生新的中间信息工具返回的JSON、API响应、代码执行结果、用户模糊反馈。这些信息不是说完就扔的它们是后续决策的依据。比如我在开头提到的收货地址场景Agent需要先通过意图识别判断“用户要改地址”再调用查询接口拿到当前订单再调用更新接口写入新地址。最后一步解密时它必须还记得“用户要改的地址是哪个新地址”这件事。中间只要有一次上下文被挤出窗口整个任务就带偏了。这个差异决定了Agent对上下文的需求是刚性的不是锦上添花。聊天机器人丢上下文最多答得尴尬一点Agent丢上下文会直接做错事而且错得理直气壮。1.2 上下文膨胀的三条路径多轮、工具、并行子任务Agent上下文会膨胀主要有三条路径。第一条是多轮对话本身。客服Agent处理一个退款请求用户先描述问题、再补充订单号、中间可能发个截图、最后问“大概多久到账”。每次交互的历史都要保留几十轮下来光是消息体就已经很可观。第二条是工具调用结果。一个Agent调用一次天气接口返回3K token调用一次数据库查询返回10K token再调用一次文档解析返回50K token。一个复杂任务可能触发十几次工具调用光这一项就能吞掉几万token。工具返回格式如果设计得差比如把一整个大JSON对象原样塞进上下文那爆炸速度更快。第三条是并行子任务。现代Agent架构经常把一个大任务拆成多个子任务并行执行比如“分析这份财报并对比行业均值”可能会同时跑三个独立分支一个出摘要、一个拉竞品数据、一个做财务指标计算。每个分支都有自己的中间上下文最后汇总时要把所有分支的关键结论拼在一起。这条路径的上下文增长是指数级的最考验架构设计。所以你会发现Agent质量差很多时候不是模型不够强而是上下文的管理跟不上。2. 上下文工程核心环节窗口分配与信息优先级2.1 先给Token做预算把上下文窗口当工程资源很多人用128K窗口的模型就觉得上下文“很大”随便塞。这是一个典型的误区。窗口大不代表可以乱用因为token数量和推理延迟、成本的关系呈线性——你喂给模型100K token它每次推理都要把这100K从头到尾算一遍响应时间和费用都跟着涨。更麻烦的是信息一多模型注意力的有效分配就被稀释出现“大海捞针”失败的概率显著上升。所谓长上下文更多是给了你“可以不用急着压缩”的余量不是让你全塞进去。正确做法是每个Agent上线前先做token预算。你把一次典型任务从头到尾推演一遍估算每个环节需要多少token然后给各块划配额。下面这个是我在项目里实际用过的预算模板以128K窗口为例上下文区块预算tokens说明系统指令 工具定义8K固定内容不随会话变化用户目标 / 当前任务2K每次从对话里重新提炼后维护最近N轮对话和工具调用20K无损保留建议保留最近3~5轮检索得到的知识库片段10K只放与当前步骤相关的片段历史摘要与结构化记忆5K对早期内容压缩后的产物安全缓冲剩余给模型输出、临时中间结果留余量预算表做完你就对每次调用的成本“长度”有了刻度的概念。之后在做任何上下文改动时第一反应都是“这会让token多花多少”而不是“塞进去再说”。这个习惯本身就能帮你躲掉大部分上下文失控的坑。2.2 信息优先级排序什么必须留、什么可以压预算定了接下来是取舍。我总结了一套比较实用的优先级规则按“不可丢失程度”从高到低排最高优先级系统指令中的核心安全约束、当前任务目标和用户偏好的最新状态。这些是Agent行为边界丢了会出严重事故。高优先级最近的工具调用结果、用户最近一两轮的明确指令。它们是当前推理的直接依据。中优先级较早期的对话历史、历史工具调用记录。可以用摘要压缩后保留。低优先级知识库中与当前步骤关联度低的片段、冗余的中间日志、重复的系统提示词。有一个技巧是系统指令里的固定部分不一定每次都要原样全量放前面。现在很多框架都支持把系统指令静态缓存然后只追加变化部分。如果你的模型API不支持前缀缓存那就把系统指令里“不会变的规则”压缩到最精简把“会变的部分”比如用户当前任务场景单独拎出来动态更新。2.3 一个可复用的上下文结构模板实际组装时我习惯把上下文按固定顺序排成下面这样这样既便于排查也便于模型稳定读取系统指令固定规则精简版当前任务目标区每次开始新任务时重写关键事实区用户偏好、地址、订单状态等从历史中提取的实体信息最近对话与工具调用无损保留最近3-5轮历史摘要区前面轮次压缩后的摘要本次回复的参考材料知识库片段、工具结果摘录注意第5和第2的区别历史摘要区可以压缩但当前任务目标区不建议压缩。用户可能在对话中途改需求如果不把最新目标单独提炼出来模型很容易被早期的旧目标带着走。第一次做这个模板的同学可能会问实体信息区怎么维护我的方案是在每轮对话结束时跑一个轻量的信息提取模型更新一个JSON对象。比如用户提到“改成杭州”我就更新delivery_address: 杭州市这个字段。下一轮组装上下文时这个字段值直接填进关键事实区。这样即使历史里那段对话被压缩掉了事实还是在。3. 压缩与记忆分层上下文不减怎么都是白搭3.1 压缩不是简单摘要结构保留比笼统概括更重要说到压缩最容易踩的一个坑就是“让模型总结一下历史”。笼统总结会把关键细节丢失。比如用户在第3轮说“不要用顺丰”摘要写成“用户提过快递偏好”等到第20轮下单时Agent根本不知道要不要避开顺丰。更好的做法是结构化压缩。不是让模型写一段话总结而是让它按固定字段抽取已完成的步骤列表动词短语当前待办步骤已经确认的事实清单实体、偏好未解决的问题/需要用户澄清的点这样压缩出来的结果是可查询的后续步骤需要时可以直接读取某个字段而不是让模型在一坨摘要文字里找信息。压缩后建议把原文丢弃而不是“摘要和原文都留着”——都留着压缩就没有意义了等于没省token。3.2 记忆分层工作记忆、情景记忆、语义记忆上下文工程做到后面本质是在做记忆系统。业界比较认可的分层是把记忆分成三类工作记忆当前这个任务中必须随时可用的信息存在于上下文中任务结束即清空。情景记忆一次会话或一个任务中发生的事件序列比如上一轮调用了哪个API、返回了什么结果。这是压缩摘要的主要对象。语义记忆跨会话积累的、关于用户的稳定知识比如“用户是杭州的”“偏好顺丰”“常用手机号是xxx”。应该存到外部数据库或向量库而不是靠上下文窗口硬扛。做Agent时这个分层最重要的意义是你知道什么信息往哪里放。工作记忆放上下文情景记忆定期压缩语义记忆落库。很多团队把什么都往上下文里丢会话一长就崩就是因为没有分层的概念。3.3 压缩结果如何回灌压缩不是一次性的。会话进行中每隔N轮或达到某个token阈值就要对“最旧的一段未压缩内容”做一次增量压缩然后把压缩结果放回历史摘要区。回灌时要注意顺序新增的摘要放在旧摘要之后而不是之前。为什么因为模型对最新的摘要印象更深同时你希望它在需要追溯早期信息时能先看到较新的摘要再决定要不要翻更早的内容。放反了模型容易被旧摘要误导。我常用的策略是双阈值触发当“未压缩原始内容”超过15K token时对最旧的未压缩部分做一轮压缩当“摘要区”超过5K token时对摘要做二级合并把两段摘要合成一段更紧凑的。这个数值你可以按实际模型调核心思想是无损区保持小且新鲜有损区保持结构清晰、可查询。4. 并发场景下的上下文管理不要让一个token串到别的会话4.1 会话隔离上下文不能跨任务串Agent一旦上线马上就会面对并发问题。几十个用户同时发起任务每个session都在跑自己的多轮循环。这要求上下文必须按session严格隔离模型的每次调用只能拿到当前session的上下文包。有些框架设计得很坑用一个全局变量存对话历史。单线程demo没问题一上并发就全串了。排查这个问题的第一个地方就是上下文存储位置——它必须挂在session级别而不是应用或模块级别。在Python里尤其要小心类属性一个不小心的history []写在class body里所有实例共享同一份历史并发一高必然出锅。隔离做的同时还有一个容易忽略的细节工具调用上下文也要隔离。比如两个用户同时触发“查询订单”工具工具自身的调用日志、中间变量不能混。解决办法是让工具调用也通过session传入而不是在工具内部用全局状态。4.2 Prompt Cache把固定前缀从成本账单里摘出去并发场景下的另一个重点是利用Prompt Cache。现在主流模型API基本都支持按前缀缓存只要请求的开头部分跟之前的请求相同开头部分可以按缓存价格计费速度也快很多。这意味着你要刻意设计上下文的结构把静态内容放在最前面。比如系统指令 工具定义的8K token如果放在上下文包的最开头且内容保持不变那么每次请求这8K都走缓存实际付费和计算开销大幅下降。这也是为什么我在2.3节里强调固定顺序的根本原因字段顺序不只是为了模型读得舒服更是为了利用缓存。再看实际数据假设一个Agent平均每轮调用消耗25K token其中8K是固定前缀。一百个并发用户每人跑30轮如果缓存命中率做到100%固定部分的成本几乎可以忽略。很多团队说“Agent太贵跑不起”检查一下上下文开头是不是每天都在变变则缓存全部失效成本直接回到原点。4.3 共享上下文的权限边界并发场景还有一种情况是“多个用户共享同一个Agent但上下文不能串”。典型场景是同一个团队的工作空间里成员A和成员B都向同一个Agent提任务但Agent的上下文应该包含各自的私有信息同时能看到团队公共信息。这种共享上下文不能在拼接时粗暴地把公共信息直接插到个人历史里。更稳妥的做法是“分区组装”公共知识区团队的百科、项目文档、工具白名单放固定区私有上下文个人偏好、个人会话历史放动态区。组装时检查权限确保私有区只能被当前用户对应的session访问。在框架层面这要求上下文组装函数必须显式传入当前subject的身份而不是从某个全局配置里取。5. 主流框架里的上下文工程LangGraph、Spring AI与Rust5.1 LangGraph用状态循环管理上下文的流动LangGraph现在算是用LangChain生态做Agent最主流的底层编排框架。它的核心是StateGraph定义了一个全局状态对象节点之间的每次交互都读和改这个状态。上下文工程在这一架构下的天然实现方式就是“改状态”而不是“改prompt”。举一个具体的做法用户消息进来后先走“提取节点”把关键信息写入state的关键事实区再走“检索节点”把知识库片段放入上下文的参考区再走“Llama模型调用节点”此时组装上下文的顺序固定为state里的固定系统指令 - 当前目标 - 关键事实 - 最近的messages。LangGraph的messages字段自带reducer可以把新消息追加进列表也可以按条件截断——这个reducer就是你的损益控制点。这里有个实用的经验不要在LangGraph里把messages无限追加。每轮循环结束后在状态更新函数里做一次“无损区裁剪”只保留最近几轮原始消息更早的主动压缩成summary message放回状态。这样图跑得再长上下文也不会失控。网上常见的基于FastAPI LangChain LangGraph搭建Agent的文章基本也是这个思路FastAPI负责session管理和请求入口LangGraph负责状态流转LangChain的Memory或者自定义的Transform节点负责上下文压缩。这套组合上手快但要注意它的Memory组件默认是全量保留还是要自己加压缩逻辑。5.2 Spring AIJava生态里如何管上下文Spring AI是Java/Spring生态里很值得关注的Agent框架。它给Java开发者带来了一个比较标准的上下文抽象ChatMemory接口支持多种存储后端包括内存、Redis、Cassandra等。它的工作方式是每次请求模型前先从ChatMemory里取出与该会话关联的消息历史组装到请求里请求结束后再把新消息写回。可以设置一个消息窗口大小比如保留最近20条超出部分会按配置策略裁剪。坑点在于默认实现是纯滑动窗口没有摘要压缩。Java项目如果对话轮数长历史消息直接爆窗口。我自己改造时一般会实现一个Advisor或自定义的ChatMemory包装类在消息写入前对旧消息做一个摘要化合并。Spring AI的ConcurrentMessageWindowChatMemory在并发处理上做得还可以因为它基于ConcurrentHashMap按会话维度做了隔离不会出现全局状态串行问题这点对Java服务非常重要。5.3 Rust轻量高性能Agent里的上下文设计Rust在AI Agent生态里属于偏底层、偏性能的路线。优势在于内存可控、并发安全、单机吞吐高。如果你用Rust写Agent上下文工程关注的点会不太一样第一上下文数据的传输效率。Rust里用Arc 或者字节数组缓存上下文片段可以做到多会话共享大段静态上下文而不拷贝这点在内存层面能省不少。第二并发管理自带的语言级保证——每个session一个Actor或一个状态结构体编译器帮你挡住数据竞争上下文串线这类问题在Rust里基本可以靠类型系统规避。但Rust的短板是生态不如Python/Js多现成的Agent框架少多数情况要自己搭。我自己如果用Rust做一般会定义SessionContext结构体内部按用途分字段system、facts、history_summary、recent_messages。然后每个动作函数接受SessionContext上下文组装逻辑统一收敛到一个纯函数里方便测试和压测。纯函数化是上下文工程里一个被低估的设计原则组装上下文的过程越没有副作用越容易定位问题。6. 可观测性与评估上下文工程到底做得好不好6.1 用受控任务测“关键信息保持率”评估上下文工程最直接的方式是做受控实验而不只是看几个业务指标。我自己用过一个很管用的指标关键信息保持率。事先准备一组带“必须记住的事实”的测试任务比如“初始地址是上海第5轮改成杭州最后一步要按杭州下单”。跑完整个会话检查最终输出是否用的是杭州地址。事实保持率越高说明上下文压缩和组装越靠谱。这套测试最好是自动化回归每次改了上下文逻辑就全量跑一遍防止越改越差。我在项目里用LangSmith做过类似的追踪把每轮模型输入和输出记录成trace然后再针对失败样本做人工分析。工具不需要多高级关键是样本要固定、可重复、覆盖用户改需求、长任务、多工具调用这几类高发场景。6.2 记录中间报文没有日志就不要谈排查上下文工程排障最痛苦的一件事是你不知道模型那一轮到底看到了什么。所以从第一天就要把每一轮发往模型的prompt快照存下来至少存关键片段。我在项目里是把完整上下文存到一个专门的日志表字段包括session_id、轮次序号、token总量、各上下文区块的排序和截断信息。事故发生时直接回放对应轮次的prompt一眼就能看出是哪个区块把关键信息挤掉了。有一个常见误区是只存模型输出、不存模型输入。模型输出错了你只知道它错了不知道它为什么错。输入快照才是根因分析的锚点。6.3 成本和延迟联动观察质量与开销一起看上下文工程做得好不好不只是“没出错”就行。同一个任务上下文组织得好token消耗可以差出好几倍。我在灰度监控里会同时盯着两个指标每轮平均token数和修正重试率。如果重试率升高说明上下文有问题导致模型反复出错如果token数一路飙升说明压缩没生效或内容冗余。这两个指标同时恶化优先查上下文单独恶化其中一个优先查工具结果长度或压缩阈值。把它们画在一张Dashboard上看能很快发现异常模式。比如token翻倍但重试率同时上升大概率是某个工具返回了大块文本把真正有用的信息挤出了注意力区。这时候不是去加窗口而是优化工具返回——让工具只返回必要字段或者把大块文本拆成结构化摘要再放回上下文。7. 避坑清单上下文中最容易踩的几个雷7.1 系统指令被历史淹没有些Agent跑着跑着就开始不听系统指令里的安全约束比如忽略“只返回JSON”的要求开始生成大段解释。排查时发现系统指令还在上下文开头但后面跟了上万token的历史整体比重被稀释了。对策有三条一是把系统指令精简到极致只留硬约束二是每隔N轮在关键事实区“重申”一次硬约束三是利用prompt cache让系统指令始终走缓存减少重复计费。7.2 工具返回结果爆炸工具返回一个几十K的大JSON直接塞进上下文这是最常见的上下文失控原因。我踩过最狠的一次是会话里调了个财务接口返回了5年的流水明细Agent在后续每一轮推理里都在重复“看”这份明细。解决思路分三层工具侧做字段裁剪只返回当前需要的最小集Agent侧做结果摘要调用完立即把长结果压缩成结构化摘要上下文组装侧设置单块上限超过上限的直接截断只保留开头字段和关键数值。7.3 目标漂移用户改需求改丢了多轮对话里用户改需求是Agent最容易做错事的时候。第5轮说“改成杭州”第6轮模型还是按上海继续走。这不是模型傻而是你的上下文没把“最新目标”单独提炼出来并放在高优先级位置。对策在2.3节已经说了维护一个动态任务目标区每轮更新。这里只补充一个实战细节用户改需求时除了更新目标区还要顺手把相关的事实区字段一起更新并在更新后附带一个短说明比如“地址已从上海改为杭州后续所有相关操作按杭州处理”。7.4 兜底方案关键信息写不进上下文时怎么办即使上下文工程做得再好也总有极端情况上下文预算不够、某条信息被误裁剪、工具链太长。我会在Agent里加一个“关键信息兜底校验”在某几个关键步骤比如下单、改配置、发消息执行前做一次规则校验检查必须字段是否存在。如果发现地址字段缺失或过期宁可停下来反问用户也不要让Agent瞎猜。很多事故就是因为Agent觉得“差不多能猜”结果猜错收不了场。这个校验的实现很轻一个if判断加一次数据库读取但对稳定性提升非常明显。上线跑了几周我发现大部分“看起来是模型不聪明”的问题最后都能追溯到上下文里少了一条本该存在的字段。做上下文工程没有一招鲜的方案。它更像是在优化一个动态系统的内存管理——预算、分配、压缩、缓存、隔离每一环都要贴合你自己的业务形态。说到学习路径我个人的建议是先找个简单的客服Agent练手刻意把它的session削短逼自己设计压缩和记忆策略再逐步放宽阈值然后把每轮模型入参的日志完整留一份做完一轮功能就对照日志复盘一次。这条路走完你对上下文工程的理解会扎实很多。