ARTICLE DETAIL

资讯详情

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

AI Agent上下文工程实战:三层记忆架构与Token压缩方案

AI Agent上下文工程实战:三层记忆架构与Token压缩方案 1. 上下文工程到底在解决什么问题做 Agent 开发的人十有八九都经历过这样的场景本地跑得好好的智能体一上生产环境就开始胡言乱语或者干脆报一个agent execution terminated due to error翻日志发现是上下文超了。更隐蔽的情况是上下文没超但模型开始忘事——前面明确说过的约束聊到第十轮就丢了。这类问题表面看是模型能力问题根子上其实是上下文工程没做好。所谓上下文工程说白了就是管理好每一次送进模型的那些 token。它跟提示词工程不是一回事。提示词工程关心的是这句话怎么写模型才听得懂上下文工程关心的是在有限的窗口里我该放什么、放多少、什么时候放、什么时候扔。一个 Agent 跑几十轮工具调用每轮都往上下文里塞工具返回结果如果不做管理token 消耗是指数级往上走的成本和延迟都会失控。这套东西适合谁看如果你正在搭 AI Agent、写多 Agent 协作框架、或者在做 Agent 记忆相关的选型那这篇基本就是给你准备的。如果你只是调调 API 写个问答机器人可能用不上这么重的东西但了解一下分层记忆的设计思路也没坏处。我下面会从整体设计讲到具体实现包括 token 怎么算、上下文怎么压、记忆怎么分层尽量给到能直接抄作业的方案。2. 整体架构设计与方案选型思路2.1 为什么不能全塞进去了事最朴素的做法是把所有历史消息原封不动塞进上下文。早期窗口小的时候大家还收敛点现在动辄 128K、200K 的窗口很多人就放飞了。我实测过一个客服 Agent不做任何管理跑到第 30 轮对话时单次请求的输入 token 已经到 6 万多响应延迟从 1.2 秒涨到 8 秒多成本翻了十几倍而且模型对早期关键信息的召回率反而下降了——这就是典型的上下文过载。大模型处理长上下文有个特点中间部分的信息容易被忽略业内叫lost in the middle。你把 6 万 token 塞进去真正被有效利用的可能就头尾那部分。所以上下文工程的核心目标不是塞更多而是塞得更准。基于这个判断我倾向于把整个上下文拆成几个职责分明的层各层用不同的策略管理。2.2 三层记忆的职责划分我把 Agent 的记忆分成三层这个划分方式在社区里也比较常见但每层的具体实现差异很大层级存储内容生命周期典型实现短期记忆当前会话的原始消息流单次会话内存队列 滑动窗口工作记忆压缩后的会话摘要、当前任务状态单次会话结构化摘要对象长期记忆跨会话的事实、偏好、经验持久化向量库 结构化存储短期记忆保证最近发生了什么不丢工作记忆保证当前任务的目标和进度清晰长期记忆保证这个用户是谁、以前聊过什么能跨会话延续。三层各管一段互不干扰这是整个设计的地基。2.3 为什么选压缩 检索而不是纯检索有人会问既然向量检索这么成熟为什么不干脆把所有历史都存向量库每次按需检索我试过纯检索方案问题在于对话的时序性和连贯性会丢。用户说就按刚才那个方案改一下刚才那个方案在向量检索里很难精准命中因为它依赖的是紧邻的上下文不是语义相似度。所以我的方案是混合的近期消息保留原文滑动窗口中期消息做压缩摘要远期和跨会话信息走检索。这样既保住了连贯性又控制了 token 总量。这个取舍很关键后面所有实现都围绕它展开。3. Token 管理的核心细节与实操要点3.1 Token 到底怎么算才准很多人用len(text) / 4粗略估算 token英文场景勉强能用中文直接崩。中文一个汉字通常占 1 到 2 个 token标点和特殊符号另算。我建议直接用对应模型的 tokenizer比如 tiktoken 或者模型厂商提供的计数接口。import tiktoken def count_tokens(text: str, model: str gpt-4) - int: enc tiktoken.encoding_for_model(model) return len(enc.encode(text))但要注意工具调用的返回结果、JSON 结构、系统提示词都要算进去很多人只算用户消息结果实际请求超了。我一般会预留 15% 到 20% 的 buffer因为模型输出也要占窗口而且不同厂商对 function call 的 token 计算方式有差异。3.2 预算分配给每一层留多少假设模型窗口是 128K我的分配策略是这样的系统提示词 工具定义固定占用通常 2K 到 5K这部分不压缩长期记忆检索结果最多 2K按相关性取 top-k工作记忆摘要最多 3K结构化压缩短期记忆原文剩余全部但设一个硬上限比如 60K输出预留至少 8K这样算下来实际输入控制在 70K 左右留足余量。为什么要留这么多因为工具返回结果不可控一个网页抓取可能就返回几万 token必须提前设防。提示预算分配不是拍脑袋要基于你的实际业务统计。我建议先跑一周日志统计 P95 的单轮 token 消耗再倒推分配比例。3.3 动态裁剪的触发时机裁剪不能等超了才做要在每次组装上下文之前就判断。我的做法是维护一个 token 计数器每加一条消息就累加一旦超过短期记忆的预算上限就触发压缩流程把最老的一批消息交给模型做摘要摘要结果并入工作记忆原始消息从窗口移除。这里有个坑摘要本身也要消耗 token 和一次模型调用。如果每轮都触发摘要成本反而更高。我的经验是设置一个水位线比如短期记忆用到 80% 预算时才触发一次批量压缩一次压掉 30% 的量避免频繁调用。4. 上下文压缩的具体实现方案4.1 摘要压缩怎么压才不丢关键信息最直接的压缩方式就是让模型总结。但总结一下这种提示词效果很差模型会丢掉具体的数字、ID、约束条件。我用的是一套结构化摘要模板SUMMARY_PROMPT 请将以下对话压缩为结构化摘要严格保留 1. 用户明确提出的约束和偏好原话保留关键部分 2. 已确认的事实和决策含具体数值、ID、名称 3. 当前任务的进度和待办事项 4. 未解决的问题 丢弃寒暄、重复确认、已被推翻的中间方案。 对话内容 {conversation} 输出格式 - 约束与偏好 - 已确认事实 - 任务进度 - 待解决问题 实测下来这种结构化摘要能把 10K token 的对话压到 800 到 1200 token关键信息保留率明显高于自由总结。核心思路是告诉模型什么必须留、什么可以扔而不是让它自己判断。4.2 工具结果的压缩策略Agent 场景里 token 消耗大户往往是工具返回。一次搜索返回 20 条结果一次网页抓取返回整页 HTML这些直接塞进去就是灾难。我的处理分三步结构化提取工具返回后先用规则或小模型提取关键字段比如搜索只留标题、URL、摘要截断保护对超长文本做首尾截断保留开头和结尾中间用省略标记按需展开完整结果存到外部存储上下文里只放引用 ID模型需要时再通过工具取回第三步是关键这叫引用式上下文。模型看到的是[搜索结果已缓存ID: search_001共 20 条]需要细节时调用fetch_detail(search_001, index3)。这样上下文里永远只有轻量的引用token 占用极小。4.3 压缩的边界什么绝对不能压有些东西压了就出事。系统提示词里的安全约束、工具调用的参数格式、当前正在编辑的代码或文档原文这些必须原样保留。我踩过一次坑把工具 schema 也做了摘要结果模型生成的参数格式全错排查了半天才发现是压缩惹的祸。判断标准很简单如果这段内容被改写后会导致模型行为出错就不能压。压缩只针对信息密度低、冗余度高的部分比如闲聊、重复确认、已废弃的中间推理。5. 分层记忆的落地实现5.1 短期记忆滑动窗口 优先级队列短期记忆我不用简单的 FIFO 队列而是带优先级的。每条消息打一个重要性分数用户明确指令、工具关键返回分数高寒暄分数低。窗口满时优先淘汰低分消息。class ShortTermMemory: def __init__(self, max_tokens: int): self.messages [] # (message, importance, tokens) self.max_tokens max_tokens self.current_tokens 0 def add(self, message, importance1.0): tokens count_tokens(message[content]) self.messages.append((message, importance, tokens)) self.current_tokens tokens if self.current_tokens self.max_tokens: self._evict() def _evict(self): # 按重要性升序淘汰保护最近的消息 self.messages.sort(keylambda x: (x[1], -len(self.messages))) while self.current_tokens self.max_tokens * 0.7: msg, imp, tok self.messages.pop(0) self.current_tokens - tok注意淘汰目标是降到 70% 而不是刚好卡线留出缓冲避免频繁触发。5.2 工作记忆结构化的任务状态对象工作记忆不是一段文本而是一个结构化对象我通常包含这些字段working_memory { task_goal: 帮用户完成季度报表分析, constraints: [只统计华东区, 排除退货订单], confirmed_facts: {数据源: sales_2024Q3.csv, 口径: 含税}, progress: [已加载数据, 已完成清洗], pending: [计算同比, 生成图表], summary: 用户需要华东区季度报表... }每次组装上下文时把这个对象序列化成紧凑文本注入。它的好处是模型一眼就能看到任务全貌不会因为历史消息被压缩而丢失目标感。这个对象由模型在每轮结束后更新我用一个专门的状态更新提示词来驱动。5.3 长期记忆向量检索 结构化双写长期记忆我采用双写策略事实性信息进结构化库如用户偏好、账号信息经验性信息进向量库如历史解决方案、相似案例。检索时两路并行结构化结果直接注入向量结果按相似度取 top-3。向量库的选型上小规模用 FAISS 就够上规模再考虑 Milvus 或 Qdrant。关键不是选哪个库而是 embedding 的质量和分块策略。我的经验是按语义单元分块别按固定字数切否则检索出来的片段经常断头断尾。注意长期记忆写入要有去重和冲突检测。我遇到过用户改了偏好但旧偏好还在库里检索时两条都命中模型就懵了。解决办法是给每条记忆加时间戳和状态标记检索时优先取最新的有效记录。6. 常见问题与排查技巧实录6.1 典型问题速查表现象可能原因排查方向模型忘记早期约束短期记忆淘汰过早检查重要性评分提高约束类消息权重响应突然变慢上下文 token 激增打印每轮 token 数定位是哪类消息膨胀工具参数格式错误工具 schema 被压缩确认 schema 未进入压缩流程摘要后信息丢失摘要提示词不够结构化改用结构化模板明确保留字段跨会话记忆串味长期记忆未按用户隔离检查检索时的过滤条件报错 execution terminated上下文超窗口加 token 预检超限前强制压缩6.2 几个我踩过的坑坑一摘要的摘要。如果压缩后的摘要又被再次压缩信息会逐层衰减几轮下来就面目全非。我的做法是摘要只压原始消息不压已有摘要工作记忆里的摘要始终保持一份新的压缩结果合并进去而不是覆盖。坑二token 计数和实际不符。不同厂商对 system message、function call 的计费方式不一样本地算的和服务端算的经常对不上。上线前一定要用真实请求验证别信本地估算。坑三压缩触发太频繁。早期我把水位线设得太低结果每两轮就压一次模型调用成本反而上升。后来改成用量到 80% 且至少积累 10 轮才触发成本降了一半。6.3 调试上下文的小技巧我习惯在开发环境把每轮实际发送的上下文完整打印出来包括各层占用的 token 数。这样一眼就能看出哪层在膨胀。生产环境则记录 token 统计到监控设置告警阈值。没有可观测性的上下文工程就是盲人摸象这一步千万别省。另外我会准备一组回归测试对话每次调整压缩策略后跑一遍看关键信息是否还在。这比拍脑袋调参靠谱得多。7. 一些实操中的个人体会这套方案我在几个项目里跑下来最深的体会是上下文工程没有银弹全是权衡。压缩率高了丢信息低了省不下 token检索 top-k 取多了噪声大取少了漏关键。这些参数都得基于自己的业务数据去调别人的配置只能当起点。还有一个反直觉的点有时候增加上下文反而降低效果。我做过对比同一个任务塞 50K token 和塞 15K 精准 token后者准确率更高。所以别迷信大窗口把该放的放准才是正道。最后分享一个我常用的自检方法把组装好的上下文单独喂给模型问它当前任务是什么、有哪些约束、下一步该做什么如果它答得清楚说明上下文质量过关如果答得含糊那就是压缩或分层出了问题。这个方法简单但特别有效建议你也试试。
返回列表