
做 AI Agent 这一年多我踩过最多的坑不在模型选型也不在工具调用而在上下文。一个 Agent 跑几轮就开始答非所问把用户随口一句“你看着办”当成新的任务去执行这种事我遇到过不下十次。后来我把注意力从模型本身挪到上下文工程上才慢慢摸到门道。这篇文章想和你聊聊AI Agent 背后的上下文到底该怎么设计、怎么管理、怎么压缩以及我在实际项目里踩过的坑和验证过的方案希望对正在搭 Agent 或者准备搭 Agent 的朋友有点用。1. 上下文工程到底是什么Agent 的“工作记忆”管理1.1 Prompt 工程没解决的烂摊子在 AI Agent 火起来之前大家聊得最多的是 Prompt 工程。写提示词、调格式、加 few-shot 示例目的就是让大模型一次性把任务做对。但 Agent 不一样它是多轮、多工具、多目标的组合体。你让它查天气、订机票、比价、下单这不是一次对话能完成的它需要持续记住用户说过什么、自己做过什么、现在进行到哪一步。Prompt 工程解决的是“一次性把话说清楚”而上下文工程解决的是“连续多次把账记明白”。我见过最典型的翻车现场用户让 Agent 订机票Agent 已经选好了航班用户紧接着说“帮我确认一下行李额”模型直接把之前选航班的操作忘了又重新去搜了一遍航班。不是模型傻是开发的时候没有把历史操作写进上下文或者写了但没传对。上下文工程管的就是这套东西——决定什么信息该进、什么信息该留、什么信息该丢。1.2 Agent 的上下文不是越长越好很多人有个直觉上下文窗口越大越好。现在主流模型的窗口动辄 128K、200K甚至往 1M 冲好像什么都塞进去就没问题了。但我实际用下来发现这个直觉害人不浅。第一是贵。Token 是计费的你每次请求都要把上下文原封不动传给模型传得越多单次成本越高。一个 128K 窗口的普通会话光系统提示词加历史记录可能就吃掉几十 K Token跑几十轮下来账单非常难看。第二是慢。上下文越长首字延迟越高体感很明显128K 塞满之后的响应速度能比短上下文慢好几倍用户根本等不住。第三也是最重要的长上下文不等于高质量上下文。模型对中间部分的注意力天然偏弱学术界管这叫“Lost in the Middle”。你塞进去五万字历史记录模型很可能忽略掉最关键的那一句。所以我给上下文工程定的第一原则是在有限预算内让最关键的信息始终处于最容易让模型看到的位置。1.3 Token 预算是上下文工程的起点做上下文工程先要有“记账”思维。你可以把每一次请求想象成一个限座的办公室座位总数等于上下文窗口每个字符占一个座。系统提示词占多少、工具定义占多少、历史对话占多少、检索结果占多少全都要算清楚。我一般按下面这个公式来控制输入消耗 系统提示词 工具定义 对话历史 检索内容预留生成空间 上下文窗口总大小 × 20%~30%也就是说输入内容不能超过整个窗口的七到八成剩下的一定要留给模型输出。如果输出空间被挤占模型会开始胡编哪怕你给它的知识再准确它也可能会话说到一半就截断或者强行用几个字糊弄过去。程序员都知道要给服务留冗余上下文也一样。2. 上下文的分层设计四层各司其职2.1 四层上下文模型我习惯把 Agent 的上下文拆成四个部分每一层职责不同更新频率也不同。这样做的好处是调优的时候能精准定位不用每次瞎猜是哪里出了问题。层次典型内容更新频率占用预算系统层角色定义、行为规则、输出格式基本不变10%~15%工具层工具 Schema、调用约束、返回值格式按需变化15%~20%对话层多轮用户消息、助手回复、工具调用结果每次变化30%~50%数据层检索出的知识片段、业务数据库查询结果每次变化10%~20%系统层是 Agent 的“人设”和“公司章程”。比如“你是一个财务分析助手所有回答必须给出数据来源不确定的地方要明确说不知道”这些内容要长期稳定不能每次对话都变。我之前见过有人把系统提示词也塞进历史对话里滚动更新结果模型一会儿觉得自己是客服一会儿觉得自己是写代码的行为完全失控。工具层容易被忽视。每次给模型注册一个工具都要附带完整的 JSON Schema 说明这个很吃 Token。你注册十个工具光工具定义可能就吃掉五六千 Token。所以工具不是越多越好用不到的就不注册。我在一个项目里曾经一口气挂了二十多个工具结果模型经常选错后来精简到六个核心工具准确率反而上去了。对话层是最难管理的它承载了用户意图、中间成果和 Agent 的思考过程。这个层级的核心问题是历史会无限膨胀必须引入衰减和压缩机制后面我会专门讲。数据层则是外挂的知识库通过 RAG 把不相干的长文本挡在上下文之外只把最相关的内容临时搬进来。2.2 对话历史要按“衰减机制”处理对话层最容易失控。用户聊了五十轮每轮都有来有回还有工具调用的中间结果加起来轻轻松松破万 Token。我不建议全量塞进上下文而是建议做时间维度的衰减处理最近几轮完整保留更早的对话做摘要再早的只提取关键约束存进结构化记忆。这样做有三个好处。第一模型能拿到最近的完整信息不会被历史噪音干扰。第二摘要之后 Token 占用大幅下降成本可控。第三早期对话里的明确约束一旦沉淀到记忆里就不容易被后续聊天冲淡。比如用户开头说“我预算三千以内”这句话如果只在第十五轮的历史里模型到第四十轮大概率已经忘了但如果它被提取成一条结构化记忆“预算约束不超过 3000 元”每一轮都会被稳定注入。关于保留多少轮没有统一标准跟场景强相关。我的经验是决策类任务保留最近六到十轮比较稳妥。轮数太少记不住需求太多注意力会被稀释。具体数值需要你拿自己的数据多测后面我会给一个调参的思路。2.3 检索内容要“按需投放”数据层是“只见新人笑不见旧人哭”。很多同学拿到向量数据库就把所有内容一股脑搜出来塞进上下文结果 top-5 召回结果里有两三条是无关的模型反而被误导。检索内容必须经过二次筛选控制在真正需要的范围内。我的做法是先做查询改写把用户最新的说法转成更适合检索的形式然后混合召回向量检索加关键词匹配一起上再做一次重排重排之后只保留 top-2 或 top-3 条进入上下文。进入上下文的知识片段要带上“此段来源文档、时间戳”之类的元信息模型回答错了用户还能查证而且这些元信息能帮模型判断信息的时效和优先级。3. 上下文压缩与记忆管理的主流方案3.1 截断与滑动窗口最简单但不建议裸用纯滑动窗口是很多人的第一版方案。实现成本极低固定保留最近 N 轮更早的直接丢弃。它适合 demo不适合生产。问题很明显用户十分钟前提的需求可能还挂在窗口外面模型自然就忘了。我见过一个客服 Agent用户第一轮就报了订单号后面又聊了二十轮售后细节开发者只保留了最近十轮结果模型在最后一步要求用户重新提供订单号体验极其割裂。滑动窗口不是不能做而是必须搭配一个前提被划出窗口的内容要先把关键信息抽出来。只划走、不沉淀等于失忆。3.2 摘要压缩用小 Token 换回关键信息摘要压缩是目前最实用的降本方案。思路是把早期对话喂给模型生成一段几百字的摘要用它替代原来的几千 Token 历史。代价是细节丢失所以它更适合那些“大概知道用户想要什么就够”的场景。对事实细节要求特别高的场景比如法务、医疗、财务摘要只能作为兜底不能是唯一方案。我通常会在上下文用量超过阈值时触发一次摘要然后把摘要放在上下文的最前面。摘要本身也要分两种粒度全局任务摘要和本轮动态信息。全局摘要告诉你这个 Agent 整体在做一件什么事比如“用户在规划一次北京到成都的自驾游已确认 6 月 10 日出发预算是 5000 元”本轮动态摘要告诉你现在执行到哪一步了比如“已完成路线初筛等待用户确认是否优先走高速”。这两种信息混在一起的话摘要往往会写得太笼统丢了关键阶段信息。3.3 RAG 与混合检索外挂知识库的正确姿势RAG 的核心价值是让 Agent“用的时候能找到不用的时候不占地方”。但纯向量检索有硬伤向量擅长语义近似却不擅长精确匹配产品编号、日期、人名这些信息经常翻车。我做知识库检索基本都走混合检索BM25 管关键词精确匹配向量管语义扩展两者分数融合后取 top-N再进重排。重排器我一般用一个轻量级 cross-encoder虽然每次查询会多一点计算时间但能把真正有用的片段顶上第一。没有重排阶段的 RAG召回率再高上下文里也总混着噪音模型答错一半是检索兜底没做好。此外分块策略很重要。我踩过的坑是块切得太小比如每块 256 字符结果把一条完整的业务流程拦腰切断模型只看到了一半。后来我改成“按语义段落分块块与块之间保留 10% 重叠”效果立竿见影。块太大也不行512~1024 个字符通常是比较稳的区间。3.4 结构化记忆让 Agent 记住该记住的摘要本质上是一段非结构化文本靠它去还原事实容易出错。我更倾向再加一层结构化记忆用数据库表或者 JSON 字段显式存三件事情景记忆上一次用户提到过什么、最近一次任务进展。语义记忆用户的长期偏好、明确约束比如“预算 3000 内”“讨厌辣”“周一例会”。程序记忆某个流程上次是怎么走的比如“上次用户取消订单时走的退款流程是 X”。结构化记忆最大的优势是稳定。摘要可能会被压缩丢掉细节但 JSON 里的字段提取一次就在那里每次构建 Prompt 时直接注入不会因为历史被裁剪而消失。我在一个项目管理 Agent 里用了这个方法把“当前优先级最高的两个任务”单独存成字段每轮对话都注入Agent 再也没有因为聊太久而忘记用户置顶的任务。4. 落地实现用 Rust 写一个轻量上下文管理器4.1 为什么选 Rust上下文管理本质是状态管理加规则调度用什么语言都能写。我之所以选 Rust主要因为它适合做 Agent 服务端的底座并发安全、性能稳定、内存可控。Agent 服务往往要同时扛很多会话Rust 的 async 生态做高并发很顺手再加上 serde_json、tokio 这些老搭档写起消息处理来非常干净。当然这不是说大家都要去学 Rust。如果你做的是快速原型Python 完全够用。但如果你要面对的是长时间运行、多租户、高并发的 Agent 服务有一个用 Rust 写的上下文模块真的很省心。当年 CrewAI、LangGraph 这些框架也都在强调可控性其实底层做的最重要的一件事就是管理上下文流转Rust 只是把这个事情做到更精细的工具。4.2 上下文对象的数据结构我先把消息抽象成几种固定类型。关键在于工具调用结果不能混进普通对话消息它需要被单独标记这样才能在压缩和淘汰的时候区别对待。use serde::Serialize; #[derive(Clone, Debug, Serialize)] pub enum Message { System { content: String }, User { content: String }, Assistant { content: String }, ToolCall { name: String, args: String }, ToolResult { name: String, output: String }, } #[derive(Clone)] pub struct ToolDef { pub name: String, pub schema: String, } pub struct Context { pub system: String, pub tools: VecToolDef, pub messages: VecMessage, pub budget: usize, }这个结构看起来平平无奇但“区分消息类型”这一点至关重要。之后做预算统计时你可以按类型拆开看是哪一层超了做淘汰时也可以优先淘汰 ToolResult而不是把用户的请求先丢。4.3 Token 预估与预算分配这里要写一个 token 预估器。真实线上环境最好用对应模型的分词器但这套代码里我用一个估算函数就够了误差控制在 15% 以内就能满足预算调度的需求。fn estimate_tokens(text: str) - usize { let chinese_count text.chars().filter(|c| !c.is_ascii()).count(); let ascii_count text.len() - chinese_count; chinese_count ascii_count / 4 1 }中文按一个字一个 Token 估英文按四个字符一个 Token 估再补一个保底。这个粗略算法对中文项目的预判很准之前我在实际项目里和真实 tokenizer 对比过误差基本可接受。预算分配我是按比例算的。假设窗口是 128K我给系统层留 15%工具层留 20%输出预留 25%剩下 40% 才是历史加检索的户头。这个比例不是死的场景不同要调。比如工具多的场景工具层占比就要往上提知识密集场景数据层占比要高一些。impl Context { fn current_usage(self) - usize { let system_cost estimate_tokens(self.system); let tool_cost: usize self.tools.iter().map(|t| estimate_tokens(t.schema)).sum(); let history_cost: usize self.messages.iter() .map(|m| match m { Message::System { content } estimate_tokens(content), Message::User { content } estimate_tokens(content), Message::Assistant { content } estimate_tokens(content), Message::ToolCall { name, args } estimate_tokens(name) estimate_tokens(args), Message::ToolResult { name, output } estimate_tokens(name) estimate_tokens(output), }) .sum(); system_cost tool_cost history_cost } fn needs_compaction(self) - bool { self.budget 0 self.current_usage() self.budget * 7 / 10 } }达到 70% 水位我就触发压缩。为什么不是 80% 或者 90%因为摘要压缩本身也是一次模型调用它也要消耗上下文你要留给压缩动作足够的操作空间。如果等到 95% 再动手可能连摘要都生成不完。4.4 压缩与检索的联动触发压缩时我优先把最旧的 ToolResult 丢掉因为它们通常只影响当时那一轮不影响后续大局。接着检查是否能对某段连续历史做摘要同时把对话层里可以被结构化记忆替代的信息抽出来。trait Compressor { async fn summarize(self, messages: [Message]) - String; async fn extract_memory(self, messages: [Message]) - VecMemoryItem; } trait Retriever { async fn retrieve(self, query: str, top_k: usize) - VecRetrievedChunk; }构建最终请求时顺序固定为摘要 结构化记忆 → 系统提示词 → 工具定义 → 最近对话 → 检索片段。这个顺序让模型先看到稳定信息再看到当前任务最后看到参考资料逻辑上更顺。整个流程我最想强调的一点是不要等到请求发出前才去整理上下文。更好的是在每轮对话结束之后就异步去压缩历史、更新记忆。这样等用户下一次发消息的时候上下文已经是现成干净的请求延迟不会因为压缩而增加。5. 常见翻车现场与排查思路5.1 模型突然“失忆”忘了早期需求这是出现频率最高的问题。排查的时候第一步不要瞎调参而是把最后组装出来的完整 Prompt 打出来人工读一遍。绝大多数情况一看就明白早期的关键约束根本没在 Prompt 里或者被滑动窗口截掉了。我在日志里加了一行 debug 输出每次请求都会记录各层 Token 占比和最后一条系统提示词。有一次排查“模型忘了用户所在地”的问题打开日志发现对话历史里确实提过北京但那条消息已经被丢出窗口摘要里也没写清楚。解决方法是把“用户所在地”这类高频需求字段提取成结构化记忆并加入系统层必带字段。5.2 检索内容互相矛盾知识库里存在不同版本的信息比如新旧两版 API 文档说同一个接口的返回格式不一样。模型搜到两条冲突内容就会语无伦次。这个问题靠重排解决不了因为两条内容单独看相关性都很高。我的办法是检索结果带上来源和版本号重排时如果发现语义相近但结论冲突的片段只保留版本新的一条并在 Prompt 里注明“这两份资料存在冲突已采用最新版本”。如果模型必须在冲突信息下做判断我会在检索阶段加一层 dedup基于文本相似度聚一下类再决定保留谁。5.3 摘要把关键事实弄丢了摘要模型不会像 SQL 一样严格守约它天生会丢细节。压缩历史之后用户突然问到某一轮对话里的具体数字模型答不上来但是摘要里确实没写。我建议把摘要定位成“短期记忆的连续化”而长期事实交给结构化记忆。凡是用户明确给出的数字、日期、偏好在对话时就要用抽取模块及时落库不能指望摘要替你存着。我曾经做过一个对比一个纯摘要方案一个摘要是加结构化记忆方案跑同样 30 轮的任务后者对早期事实的回答准确率高出 30% 以上。摘要负责压缩过程结构化记忆负责锁定事实各干各的活。5.4 工具调用结果污染上下文工具返回结果往往又长又乱比如一个查询接口返回 5000 个字符的 JSON后面模型根本用不到。把这些原始结果一股脑塞进上下文不仅浪费 Token还会干扰模型判断。我的做法是给 ToolResult 单独设置“低优先级”进入上下文之前先做裁剪只保留与当前任务相关的字段。Rust 实现里我给 ToolResult 加了个 output_limit 参数超过 1000 字符就用一个小模型做字段级压缩把无关数据剔除干净再注入。这样既保证模型能拿到工具结果又不会让上下文被垃圾数据拖垮。另外工具调用尽量“少而准”一个批量工具能搞定的事不要拆成十个细粒度工具每多一次调用上下文就多一轮噪音。现象可能原因排查路径处理建议模型忘掉早期需求滑动窗口截断且无摘要沉淀打印完整 Prompt 看各层内容摘要 结构化记忆双管齐下反复调用同一个工具工具参数或结果未正确回填检查 ToolCall/ToolResult 配对顺序严格配对缺失则中止调用检索内容互相矛盾知识库版本冲突查看召回来源和版本号重排后去重只进最新版输出总是被截断输入超预算看 usage 日志预留 20%-30% 输出空间模型突然换口吻系统层被历史消息冲掉检查消息顺序系统提示词永远排最前且不被删6. 写在最后的一点体会6.1 上下文工程是一条学习路线不是一锤子功能刚上手的时候我也是到处抄框架看到一个 Agent 项目就去扒它的上下文代码。后来发现每个场景对上下文的需求都不一样。客服类是历史优先要尽量保住用户说过的话数据分析类是先例优先要保证工具结果和中间推算完整内容生成类是风格优先系统层的权重最大。你最好从这四个层下手逐层理解、逐层优化这条路基本不会走偏。我自己总结了一个学习路径先学会数 Token再会划窗口然后做摘要压缩接着上 RAG最后加结构化记忆。每往前走一步都能明显感受到 Agent 的稳定性上一个台阶。很多人一上来就搞复杂记忆框架反而连最基础的 Token 预算都没算明白最后底层不稳上层全是空中楼阁。6.2 给正在搭 Agent 的人几句忠告最后一小段分享几个我用真金白银换来的经验。第一每个 Agent 项目上线前一定把“上下文审计”作为上线检查项写清楚每层最多占用多少 Token超了怎么办。第二不要信任框架默认的上下文策略不管用的是 LangGraph、CrewAI 还是自己写的 Rust 服务默认的 memory 机制往往只适合玩具项目。第三日志里一定要记录每次请求的 Token 明细没有这个数据出问题就是盲人摸象。上下文工程的本质是把大模型当成一个有主见的协作者而不是一个无穷大的存储桶。你能让它看到的决定它能做到的。把这个认知建立起来你的 Agent 才算真正进入可维护、可扩展的阶段。