
做Agent开发的人应该都有过这种体验前期跑Demo的时候怎么快速怎么来记忆直接塞进上下文里反正任务短、数据少问题不明显。可一旦接上正经业务多轮对话拉长、用户量上来、知识库频繁召回你就会发现两个东西在同时告警——钱包和延迟。尤其是长期记忆听起来很美好落地的时候一个比一个烫手。这篇文章是Agent系列的第4.6篇我单独把“长期记忆”和“前缀缓存优化”拎出来写因为这两个东西单独看都不难难的是把它们串成一个稳定、低延迟、成本可控的方案。我先说结论长期记忆的核心不光是“能存能查”更重要的是“每次请求别把同样的东西翻来覆去地算”。而前缀缓存就是专门干这个事的。适合谁来读如果你正在做Agent开发或者你的Agent已经接了向量数据库、想把知识库/用户画像/历史纪要做得真正可规模化这篇文章能帮你省掉一大笔GPU预算和一堆无意义的推理等待。我会把原理、选型、参数、以及我踩过的坑一次性讲透。1. 为什么Agent长期记忆这么难搞1.1 先把记忆分个层不是所有记忆都值得长期存很多刚入门Agent的人一说“长期记忆”就想着建个向量库往里怼数据结果怼了一周发现查询效果很差Agent该忘还是忘该贵还是贵。问题通常出在你没分清记忆的类型。我把Agent记忆分成三层来设计短期记忆Working Memory当前会话窗口内的上下文包含最近几轮对话、临时推理结果跟着请求走请求结束就释放。长期记忆Long-term Memory跨会话的知识、用户偏好、历史结论、领域知识存到外部存储按需注入。场景记忆Episodic Memory特定任务或时间段内发生过的事比如上次这个用户把“价格上限设成5000”下次聊的时候要自动继承。前两个大家都知道第三个容易被忽略但它恰恰是长期记忆里最有价值的部分。场景记忆的特点是有“版本感”和“时效性”不像知识库条目那样静态也不像会话上下文那样短暂。我踩过的一个典型坑是把所有记忆都倒进同一个向量库以为向量检索能搞定一切。结果用户随口一句“还是老样子”系统完全接不住。后来我才把“事实型记忆”和“事件型记忆”分开存事实走向量检索事件走时序记录加摘要效果直接上了一个档次。1.2 直塞记忆的代价Token烧钱、上下文爆缸、重复计算早期我做的长期记忆方案特别粗暴把用户历史记录、知识库命中文档、角色设定一股脑拼进system prompt一次请求发过去。小规模测试没问题一旦数据量上来立刻暴露出三个问题第一是Token成本爆炸。长期记忆检索2000字、角色工具定义3000字、历史会话摘要1500字再加上当前问题一次请求轻松上万Token。如果一天一万次调用费用肉眼可见地涨。第二是上下文被挤爆。现在主流模型窗口很大但大不代表可以浪费。记忆内容放太多模型的注意力会被稀释回复质量明显下降。更麻烦的是窗口被无关记忆占满后真正重要的当前指令可能被挤到“看不见”的位置模型表现就像失忆一样。第三是重复计算——这是最隐蔽、也最烧钱的一点。上面那套prompt里系统提示、工具定义、角色设定、记忆注入这些内容大多数情况下每一轮对话都是一样的。你每问一次模型就要把这些内容从头到尾重新算一遍prefill。大模型的推理分prefill和decode两个阶段prefill负责把输入转成KV Cachedecode负责逐个字生成。对于Agent场景输入动辄几千Tokenprefill的耗时和算力开销非常可观但用户根本不想看你这些共同内容的计算过程他只想知道你答案什么时候出来。1.3 现有记忆方案的瓶颈召回很快重算很慢后来我给长期记忆接了向量数据库用的是常见的“召回-重排-注入”流程。检索这块确实快了几百毫秒内能召回相关记忆。但问题转移到了推理侧召回来的记忆和系统指令等固定内容拼在一起服务端每次请求做相似度检索可以命中缓存模型侧的prefill却依旧从头算一遍。有些团队为了省prefill时间干脆把长期记忆塞进一个固定的system prompt模板里然后利用推理框架的前缀缓存。但大多数人根本没意识到这里的“前缀”可以精细设计也不知道如何跟Agent的长期记忆系统配合结果就是缓存命中率极低优化了个寂寞。所以说长期记忆的方案要想真正能打光解决“存”和“查”不够必须同时解决“每次请求的重复计算”问题。这就要请出我们的主角——前缀缓存。2. 前缀缓存它到底优化了什么2.1 一个朴素的问题相同的系统提示为什么要反复算先解释一个基本概念大模型生成回答的时候并不是“看一眼你的问题就直接写答案”而是把你的全部输入文本系统提示、历史记录、当前问题先做一次attention计算生成一个叫KV Cache的东西再用这个Cache逐步生成回复。KV Cache可以理解为模型对输入内容的“理解快照”后续每一个字的生成都要参考它。问题就来了如果Agent的固定部分系统提示工具定义长期记忆摘要占了2000 Token用户问了10次模型就被迫对这2000 Token做了10次一模一样的prefill计算。这就像你每天做同一顿早餐每次都要重新洗菜、切菜、开火明明成品切片放在冰箱里就能直接用。前缀缓存Prefix Caching就是来解决这个问题的它把输入序列按“块”Block切分并哈希如果新请求的前缀跟之前某个请求的前缀一致直接复用那段文本对应的KV Cache跳过一次重复的prefill计算。说直白点同样的开头我只算一次剩下的时间专心回答你新问的东西。2.2 主流推理框架怎么落地的vLLM与SGLang在开源推理框架里这件事的成熟度已经很不错了。最常用的是vLLM它的Auto Prefix CachingAPC机制会为每个KV Block计算哈希并缓存请求进来后按前缀逐块匹配能命中的直接跳过计算。SGLang则更进一步用RadixAttention实现了一种前缀树结构多个请求之间共享前缀树节点在支持系统提示等重前缀的同时还能处理多个不同分支的复用。你可能会想这不就是缓存吗有什么稀奇的。关键在于命中粒度。过去很多人在应用层做会话级缓存整个会话结果缓存粒度太粗换个问题就全部失效。而前缀缓存是Token级的、Block级的细粒度复用同样是这个会话你说“上午好”和“下午好”前面固定的系统指令照样能命中缓存只有后面变化的几个Token需要重新计算。注意这不是什么黑魔法是需要硬件支持的——前缀缓存必须复用显存里的KV Cache帧率越高越吃显存。所以后续选型时要特别关注显存负载和缓存淘汰策略。2.3 前缀缓存为什么天然适配Agent场景Agent场景有一个非常明显的特点输入前缀高度稳定后缀高度动态。角色设定、工具定义、任务指令、长期记忆摘要这些内容一场对话内几乎不变而用户每句话、Agent每轮回复则是动态变化的。这种“前稳后动”的结构正好是前缀缓存命中的最佳形态。更妙的是如果多个用户共享同一套Agent技能和角色设定比如同一企业内部部署的通用助手那这些公共部分在前缀缓存里是可以被所有用户复用的。也就是说你为1000个用户跑同一套“系统提示词模板”前缀缓存的收益会被放大到接近全员共享的程度。我一开始没意识到这一点后来把角色设定和工具定义从逻辑上从总体prompt中剥离开、固定放最前发现TTFT首Token延迟肉眼可见地降了。这就是前缀缓存和Agent结构互补的直接证据。2.4 长期记忆和前缀缓存怎么结合不是把记忆硬塞进前缀既然前缀缓存的命中取决于“前缀稳定”那长期记忆这种“每次问答都可能变化”的内容是不是就跟前缀缓存完全无关不是的关键看你怎么组织记忆注入位置。我给长期记忆内容划分成了两个区域静态长期区用户级不变的知识角色画像、固定偏好、领域背景占长期记忆的大头这部分适合放进前缀里稳定复用。动态召回区本次对话相关的临时记忆最近行为、临时事件这部分每次查询都不同不适合放进前缀放在前缀之后、当前消息之前的“上下文”段。很多人做长期记忆全部记忆都走动态召回每次都拼在最新位置这样前缀缓存基本没用。我调整之后把长期记忆里高达70%的静态内容固化成前缀前缀命中率从打骨折直接拉到80%以上整个推理链路的耗时和成本都大幅改善。3. 落地实操把长期记忆做成可复用的前缀3.1 总体架构记忆管线 前缀注入下面是我在项目中真正跑通的方案结构一句话版本是这样长期记忆存储 → 分层召回 → 生成结构化的静态前缀和动态上下文 → 发送给带前缀缓存的推理服务存储侧我用了双引擎向量数据库负责事实型知识的相似召回关系型数据库或KV库负责事件型记忆的时序和版本管理。召回侧不搞“一次向量TopK就完事”而是做了“向量召回 时间衰减重排 关键信息抽取”确保注入记忆的质量。推理侧我用的是vLLM作为推理服务显存足够时开启--enable-prefix-caching。服务层额外做了一层轻量的记忆摘要缓存同一用户短时间内的多次请求不会反复对同一个长历史跑向量检索。整个链路最有价值的设计点是长期记忆的“静态部分”被显式标记出来并在prompt构建阶段固定放在最头部与动态部分严格分离。这个设计直接决定了前缀缓存能吃到多大红利。3.2 记忆存储选型不要一上来就上重型武器长期记忆的存储选型很多团队一上来就上Milvus结果运维成本比业务开发还高。我这里给一个更务实的选型逻辑数据量在百万级以内、单机部署、想快速验证直接用Chroma或者LanceDB轻量开发友好能给足你折腾空间。数据量上了千万、需要分布式、有高可用要求再切换到Milvus或Qdrant并在前面挂一层缓存。混合检索可以考虑Elasticsearch或OpenSearch既能做向量又能做关键词过滤适合记忆类型杂的情况。我前几个项目一直用Chroma后来用户量起来、记忆量过百万条才迁移到Milvus。迁移过程不难因为长期记忆的数据模型本身是稳定的每条记忆包含内容、时间戳、来源场景、记忆类型、归属用户、可选的元数据标签。向量只是其中的一个维度不要把全部希望押在“语义相似”上。Embedding模型方面中文场景推荐bge-m3系列对长文本和多语言支持都不错。如果硬件的推理能力紧张可以单独跑一个embedding服务或者用模型服务自带的embedding接口但要确保embedding的“版本一致性”——换模型后要整体重建索引否则新旧向量空间不一致召回效果会很拧巴。3.3 记忆召回从“只按相似度”到“相似新鲜重要”向量库召回有一个经典痛点把最相关的记忆捞出来了但可能全是三个月前的旧事对当前决策毫无价值。尤其是长期记忆里存了大量用户过去的临时操作记录这些记录相似度高但时效性差。我的做法是给召回结果加一个综合评分score α * 向量相似度 β * 时间衰减因子 γ * 记忆重要性权重时间衰减给每条记忆打一个衰减系数比如距离当前超过7天的记忆相似度得分打折。重要性权重从应用上报的关键事件用户主动确认、系统判定为关键行为提取这类记忆更值得保留。结果重排取Top30候选后按综合分排序最终注入5~10条进prompt。这个做法不算复杂但收益很大。它避免了长期记忆变成“语义相似的僵尸档案”让Agent真正拥有“记得住重要事情”的能力。3.4 前缀构建稳定与动态分离的关键实操前缀构建是整个方案里最容易被忽略、也最容易出错的一步。直接分享我的模板结构[固定系统区] 任务设定 / 角色人格 / 工具定义说明 [静态长期记忆区] 用户画像摘要 用户固定偏好价格上限、沟通风格、使用习惯 长期知识摘要按领域分层提炼 [动态上下文区] 最近N条事件记忆JSON列表 当前任务临时状态 [用户输入] 当前用户问题或指令固定系统区和静态长期记忆区在同一个用户或同一类用户的多次请求中保持不变是前缀缓存的主要命中段。动态上下文区每次请求由记忆管线重新生成放在两个固定段之后、当前输入之前。这样设计的好处是用户提任何问题前面的静态区域都稳定vLLM的Prefix Cache能直接命中动态区域虽然每次都变但只影响后面部分。同时我不会把动态召回的所有记忆都放进去而是做一个“记忆摘要”用一次轻量LLM调用把10条记忆压成几句话。这样既能减少Token又能保持动态区长度可控。以下是伪代码级实现思路方便你自己复现def build_agent_prompt(user_id, query, memory_store): # 1. 组装静态前缀 static_profile memory_store.get_static_profile(user_id) # 长期沉淀的事实信息 tool_defs load_tool_definitions() # 工具定义全局共享 # 2. 召回动态记忆 candidates memory_store.search(user_id, query, top_k30) # 向量召回 reranked memory_store.rerank(candidates, recency_weight0.3, importance_weight0.2) dynamic_memory summarize_memory(reranked[:8]) # 压成简短摘要 # 3. 拼接出请求体注意静态前缀保持稳定 system_prefix f{task_system_prompt}\n{tool_defs}\n{static_profile} dynamic_context format_dynamic_memory(dynamic_memory) # 4. 发送给开启 prefix caching 的推理服务 messages [ {role: system, content: system_prefix}, {role: user, content: f[记忆]\n{dynamic_context}\n\n[问题]\n{query}} ] return messages这个结构看起来简单但稳定和不稳定部分的边界一定要守死。如果你的系统前缀里混入时间戳、随机user id、动态计数之类的东西前缀缓存会立刻失效命中率直接跌破30%。4. 关键参数与服务配置让缓存真正跑起来4.1 vLLM的启动参数与显存预算前缀缓存不是默认全开的你需要显式配置。如果你用vLLM部署模型至少要关注下面几个参数参数作用我的经验值--enable-prefix-caching开启自动前缀缓存必须开启--gpu-memory-utilization指定GPU显存可用比例0.90左右给KV cache留空间--max-model-len最大上下文长度根据业务定不建议盲目拉满--max-num-batched-tokens批量token上限配合并发压测调整--swap-spaceCPU内存交换空间如果显存紧张给16~32G这里最容易被忽略的是--gpu-memory-utilization。前缀缓存本质上是“用显存换命中率”如果你把GPU显存全部让给模型权重KV缓存和前缀缓存就没地方放了开启前缀缓存也没意义。建议给KV cache和前缀缓存预留至少20%~30%的显存空间。如果开启后显存OOM优先调低--max-model-len而不是降低利用率。如果你用的是SGLang则需要在启动时开启--enable-radix-cache它默认就是开启的。RadixAttention的树形结构对多轮对话的复用能力比vLLM的线性前缀缓存更强但SGLang的环境配置稍微麻烦一点。我这里主要以vLLM为例因为对大多数团队来说它的生态更成熟。4.2 前缀设计的可观测性怎么知道命中率到底高不高优化了半天如果你连命中率都看不到那等于白干。vLLM的metrics接口会暴露prefix cache相关的统计数据最常见的两个指标是vllm:prefix_cache_hit_rate记录缓存命中率。vllm:num_preemptions和vllm:num_cached_tokens反映缓存Token数量和被抢占次数。我当时是在Prometheus里接了这个指标按小时观察。命中率稳定在70%以下的话说明前缀稳定性没做好优先检查是不是某些“看似固定”的前缀内容被动态拼进去了命中率到了80%以上TTFT能明显降下来。另一个土办法是直接对比请求耗时。开前缀缓存前一个system prompt 2500 Token的请求TTFT可能在800ms左右开启并命中后TTFT能降到300ms甚至更低差别非常直观。4.3 低成本时的缓存策略给静态内容一个滚动版本有的团队太穷显存很小根本装不下大量KV缓存。这时就要做缓存降级我常用的策略是“静态内容分段缓存 最近最少使用LRU淘汰”思路。具体来说把静态前缀按逻辑段拆成多个Block比如“系统设定块”“工具说明块”“用户画像块”。这些块之间没有硬依赖缓存池满了就优先淘汰“用户画像块”这种个体性强的块保留“系统设定工具说明”这种所有用户共享的块。因为共享块的命中收益远大于个人块淘汰时先牺牲个人块的缓存才能保住全局的优化下限。如果你用vLLM它内部自带的缓存策略已经足够好你不用手动控制。但如果你是自己写推理服务一定要按这个“共享优先”的思路设计缓存淘汰否则缓存塞满个人块后公共部分反而被淘汰整体命中率就很尴尬。5. 常见问题与排查技巧实录5.1 命中率低的元凶动态内容混入前缀有一次我压测时发现前缀缓存命中率只有10%左右排查了一圈最后发现是前缀里拼了一个“请求时间戳”字段本来想着方便服务端跟踪请求结果每次请求前缀都不一样缓存直接失效。这就是前缀缓存的第一大铁律前缀必须是完全确定的任何一个字节的变化都会导致密钥不匹配。排查时优先看这几个位置前缀里是否有毫秒级时间戳或随机数是否把用户ID、会话ID放在system prompt最前面是否把“当前天气”“今日日期”等动态信息放进了静态区prompt模板是否带有多余的空格、换行差异肉眼看不出来但哈希会变尤其是最后一点很多团队的prompt模板存储在数据库里访问时不小心加了个空格或换行前缀就变了。我后来在服务端加了一个“前缀规范化”模块将静态前缀的字符串做标准化去掉多余空白、统一换行符并在部署时做了版本校验。这个小改动直接把命中率拉了20个百分点。5.2 显存炸了缓存和KV Cache打架开启前缀缓存后显存占用会明显上升这很正常。但如果上升速度快得离谱或者频繁触发KV Cache淘汰导致命中率忽高忽低就要考虑--max-model-len设得太大导致KV缓存和管理开销暴涨并发请求太多前缀缓存和正常的KV缓存互相挤占显存缓存淘汰策略过于激进命中率不稳定我当时遇到的情况是用了90%的显存预算看着很满但命中率只到50%。后来把--max-model-len从32768调到16384同时把--gpu-memory-utilization降到0.85反而稳定了命中率还高了。原因很简单给缓存腾出了更多有效空间55432个4000多Token的请求里KVCache复用的比例上去了整体性能自然改善。另外如果你的部署支持多卡可以考虑把前缀缓存放第一张卡上做显存隔离效果更干净。但这个对大多数团队来说运维成本偏高单卡优先调参就行。5.3 长期记忆内容陈旧前缀缓存反而放大了问题前缀缓存能减少重复计算但它也暴露了一个隐患如果静态长期记忆区的内容长时间不更新Agent的行为会越来越“过时”。比如用户一个月前把“默认城市”改成上海但你的静态记忆里一直写的是北京前缀每轮命中一次错误就被放大一次。我的解决办法是给静态记忆加版本号。用户画像或偏好一旦变更就重新生成一条静态前缀并且旧前缀的缓存在一段时间后自然过期。这样既不会因为频繁更新而打碎前缀缓存又能保证记忆不过分陈旧。具体频率我采用“事件驱动 定期重建”用户主动修改偏好时立刻重建其他情况每24小时做一次轻量更新。大多数情况下静态记忆更新频率远低于请求频率所以牺牲一次命中率换取记忆新鲜度是绝对划算的。5.4 常见问题速查表现象可能原因解决措施前缀缓存命中率长期低于50%前缀里混入时间戳/随机串prompt模板空白不统一规范化前缀剥离动态字段到动态区开启前缀缓存后显存OOMgpu-memory-utilization过高max-model-len过大调低比率压缩上下文长度命中率波动大、忽高忽低缓存淘汰太激进并发模型和缓存争抢显存降低并发上限合理分配显存预算记忆内容反应迟钝静态记忆未更新静态记忆加版本号定期重建前缀换了embedding模型后召回质量下降新旧向量空间不一致统一embedding版本并重建索引动态召回的临时记忆挤占窗口注入记忆过多且未压缩每次只注入5~10条做摘要压缩最后说点实在的前缀缓存和长期记忆的结合本质上是一对“存储换时间”的组合拳。长期记忆负责把知识沉淀下来前缀缓存负责让这些沉淀内容在推理时“零成本”复用。我在实际项目中把这套方案跑通之后最直观的感受就是不用再天天盯着Token账单叹气了响应速度也终于能压到用户可以接受的范围。最后再分享一个小技巧如果你也打算用vLLM建议把模型的调度策略从随机调度改成按会话亲和性调度这样同一个用户连续发来的请求更容易落到同一个GPU Worker上前缀命中率还能再高一点。具体的调度方式不同版本有差异建议升级到较新版本后再试。做Agent规模化很多时候拼的不是哪个模型更强而是这些细节抠得够不够深。