告别Agent失忆:基于Milvus向量库的生产级长期记忆模块设计

发布时间:2026/9/18 3:15:41
告别Agent失忆:基于Milvus向量库的生产级长期记忆模块设计 我见过不少Agent项目最后都死在同一个地方不是模型能力不够不是工具链不完善而是Agent聊了几轮之后把几分钟前自己刚刚确认过的关键信息忘得一干二净。为了让上下文能塞进模型窗口大家都习惯用截断来处理超长历史但截断的本质是把记忆丢进碎纸机问题并没有被解决只是被推迟而且埋得更深。这里说的“失忆”不止是丢一句用户偏好还包括丢工具调用结论、丢中间判断、丢已经验证过的边界条件。靠无脑加大上下文窗口也救不回来我在这方面栽过跟头后来把记忆模块从简单的对话截断重构为基于Milvus向量库的生产级方案才算是真正把“记忆”这件事做扎实。这篇文章就把我的完整思路、代码、踩坑过程和选型理由写出来给正在搭Agent的朋友一个可以直接落地的参考。1. 截断是把记忆丢进碎纸机朴素方案的代价先聊聊大多数团队都在用的方案。对话历史一长为了不超过上下文窗口上限最常见的就是滑动窗口截断也就是只保留最近N轮对话。这种做法在Demo阶段没问题但一旦Agent需要跨多轮完成任务问题就立刻暴露。比如用户在第3轮说过“我吃素”到第19轮你安排餐厅推荐时这已经是20轮之前的信息早就被截掉了。用户会非常困惑为什么刚说过的要求你转头就忘这不是模型傻是我们压根没把该记的东西交给模型。1.1 截断方案的三种“失忆”症状我自己总结过朴素截断至少会带来三类非常典型的故障。第一类是前置依赖丢失。Agent在中间某一步调用了工具拿到了一个重要的中间结果比如“当前服务器API只支持HTTP/2”后面几轮要做方案设计时结果已经被截掉。Agent只能重新去调工具浪费Token不说还可能因为拿到的状态不一致给出冲突建议。第二类是用户画像消失。用户对输出风格的偏好、回答语言的偏好、某些领域的擅长程度往往是在对话早期建立起来的。截断把这些偏好删掉之后Agent会在同一轮会话里出现前后风格不一致一会儿详细一会儿简略让用户怀疑背后不是同一个人设。第三类是任务上下文闭环失败。多步骤任务里面第5步往往依赖第1步产出的参数比如“前面已经创建了项目test-ai现在可以继续用这个项目ID”。如果第一步信息被截断后续所有步骤都会陷入“重新寻找ID”的循环工具调用反复失败整个执行链基本就废了。这三种情况我都在真实项目里见过而且排查起来特别头痛因为表面上看是模型能力问题实际上是我们用截断强行抹掉了必要信息。1.2 为什么“总结历史”也救不回来有人会说截断太粗暴那我用LLM把历史对话总结成摘要只放摘要进上下文不就行了我试过这条路也走不顺畅。一方面摘要本身是损失很大的压缩过程。你让模型总结“用户查询了三次商品价格第二次显示库存不足”摘要很可能只留下一句“用户查询商品价格后遇到库存问题”。等下一轮需要精确知道第二次查询时返回的具体错误码时摘要里根本找不到。另一方面摘要会逐渐积累出幻觉风险。总结一次没问题但滚动式多层总结后最初的细节会越变越模糊模型甚至会在摘要里补充一些原本不存在的“合理信息”把整个记忆链污染掉。你根本说不清哪个结论是原话哪个是摘要自己脑补的。所以我的结论很明确摘要适合做“提炼过的粗线索”但不能替代原始记忆条目。真正生产级的Agent记忆要做的是让每条记忆都保留原始语义、时间戳、来源类型再通过检索按需取回而不是一股脑把所有历史塞进窗口。1.3 记忆和上下文不是一回事这里有个认知要掰开上下文和记忆是两个不同层级的东西。上下文是当前这一步模型能看到的“工作区”工作区大小由窗口决定记忆是Agent长期的“存档”它可以很大很长只是在需要用的时候按需加载到工作区里。我们不需要让所有记忆都同时在上下文中我们需要的是一个存储系统和一个检索机制把和当前目标最相关的记忆挑出来放进工作区。这跟人脑很相似你不会随时回忆起所有事但你做事时会主动想起相关经验。所以Agent记忆模块的实质是解决“存什么、怎么存、怎么取”这三个问题。下一节我就从这几个问题入手做设计。2. 动手之前先想清生产级记忆模块到底要记什么在设计具体方案前我先列了一堆需求。生产级记忆模块和毕业论文里的玩具Demo不一样它必须能支撑真实业务条目可能上百万查询延迟要可控还要能应对多Agent实例共享的情况。因此我不能只做一个“把聊天记录存进向量库”的简单脚本而是要把记忆分成不同类型再针对不同类型设计存储结构。2.1 记忆也不是一锅炖情节、语义、程序性记忆我把Agent记忆分为三类这个分法参考了认知科学里常见的分类但做了工程化改造。第一类是情节记忆记录具体发生过的事件比如“用户在某年某月某日反馈过注册流程卡顿”“上一次工具调用返回了HTTP 500”。这类记忆的特点是带时间、带场景、带具体结果适合用事件流的方式追加写入。第二类是语义记忆记录抽出来的通用事实比如“用户倾向于先看结论再看细节”“用户使用的是Python技术栈”“项目的生产环境数据库是PostgreSQL”。这类记忆是长期稳定的偏好跨任务、跨会话都有效。第三类是程序性记忆记录任务执行模式比如“每次创建新服务前需要先检查命名空间是否已存在”“用户提出需求时agent习惯先拆解成3个步骤再开始”。这类记忆往往从历史工具调用链中提取出来对提升Agent的稳定性和执行力帮助最大。在真正落地时三类记忆可以共用一套存储结构但写入来源和检索权重有所不同。情节记忆随对话实时写入语义记忆需要定期从情节记忆中提炼程序性记忆则由工具调用链结构生成。这个设计帮我后面省了很多事因为存储层统一了API设计也简单。2.2 一条记忆条目应该长什么样我最终给记忆条目设计了一组字段包含内容本身和检索所需的元数据。核心字段如下每个字段都是后来生产实践中逐步加上的。字段类型说明例子memory_idint64 / varchar全局唯一ID推荐确定性生成用来去重agent_ctx_hash_bucketagent_idvarchar所属Agent实例标识多实例隔离的最重要字段customer-support-v3event_typevarchar记忆类型情节/语义/程序性对应枚举episodic, semantic, proceduralcontentvarchar格式化后的文本内容也是嵌入模型输入用户反馈注册流程卡顿重试3次才成功vectorfloat_vectorcontent的嵌入向量1024维向量sourcevarchar来源如user/tool/llm_summarytooltsint64事件发生时间戳用于时间过滤与排序1720000000expire_atint64过期时间0表示永不过期1722600000importancefloat重要性权重用于召回重排0.9这个表看起来平平无奇但实战中有几个不容易注意的细节。一是agent_id必须作为标量过滤条件否则多个Agent共用一个collection时召回会互相污染。二是expire_at必须单独设计因为向量库不像Redis那样天然支持TTL应用层需要考虑过期清理。三是content不要直接存原始JSON日志而是要存格式化过的、适合模型阅读和嵌入模型的文本这个在一开始就做规范化后续检索质量会好很多。2.3 接口设计越小越好我最后只保留了四个接口编程上的抽象也尽量轻量。class MemoryStore(Protocol): def write_memory(self, memory: MemoryItem) - None: ... def recall(self, agent_id: str, query_text: str, top_k: int, filters: dict | None) - list[MemoryItem]: ... def forget(self, agent_id: str, memory_ids: list[str]) - None: ... def clean_expired(self) - int: ...为什么接口要这么精简因为Agent项目的核心复杂度在于推理与工具调度记忆模块如果做成一堆复杂API开发根本用不顺手。小而清晰的接口意味着存储后端随时可以替换今天用Milvus明天想换成别的向量库只要实现这4个方法就行。同时Agent主程序和记忆存储解耦后测试时我用本地内存实现线上切到Milvus一套逻辑跑两边减少了环境依赖问题。接口定完之后才开始选存储引擎。这一段我做了不少对比下一节详细说。3. 存储选型为什么最后落在Milvus上Memory模块的存储选型决定了下半年的运维体验所以我花了不少时间对比。我把每个候选方案都画过简单原型最后只留下了Milvus。下面说说我的判断依据和实际对比结果。3.1 传统存储做向量检索是真的别扭我一开始试过Redis和SQLite做记忆存储并不是因为它们不能存而是因为“语义检索”这个核心需求它们给不了。用Redis我能想到的方案是把对话按Key存成ZSet按时间戳做范围查询。但这样只能做关键词匹配或严格顺序回放做不到“和当前意图相似”的召回。比如用户说“上次那个权限问题后来怎么解决的”你如果只按关键词“权限”去精确匹配Redis里的原始文本可能匹配到完全无关的权限讨论也可能漏掉一条表述不同但语义高度相关的记录。用SQLite加全文索引也类似。FTS能解决关键词匹配但中文分词的piece问题会让召回质量非常不稳定。而且随着记忆量增长SQLite单机的性能和并发能力很快成为瓶颈。用pgvector这样的PostgreSQL扩展会好一些毕竟它是正经数据库但我在项目里还需要处理亿级以上的向量数据、持续写入和按Agent隔离的标量过滤单机PostgreSQL的容量规划和运维成本也要认真对待。3.2 Milvus的哪个点真正打动人我最终选Milvus倒不是因为它在“向量检索速度排行”上一定第一名而是因为它把“向量检索”和“标量过滤”这两个能力结合得比较顺手。Milvus里的核心概念其实和数据库很像 - collection 表 - entity 一条记录 - field 字段 - partition 按某个字段做物理分区 - index 针对向量的ANN索引 - consistency 读写一致性级别用这套模型我可以把Agent记忆模型直接映射成一张表每一条记忆是一个entity包含agent_id这样的标量字段和vector这样的向量字段。检索时我可以在同一个search请求里指定filteragent_id customer-support-v3先按Agent隔离再在隔离结果里做向量近邻搜索这个能力太关键了。对比之下纯向量数据库如果标量过滤能力弱我就得把每个Agent拆成一个独立collection等Agent数量多了运维直接爆炸。Milvus的“一个collection 标量过滤 分区”模式让我用一套资源管理所有Agent却能在数据层做隔离这对我来说就是最直接的收益。3.3 跑起来成本没有那么高有些团队听说Milvus就觉得要上Kubernetes有点被吓到。实际上从单机版开始完全够用一个Docker Compose就能把Milvus Standalone和它的依赖等组件一起拉起来适合开发测试也适合中小规模的Agent业务。我在开发环境起服务的步骤非常简单核心流程如下准备好docker-compose.yml定义milvus-standalone服务暴露19530端口给客户端暴露9091端口给监控。额外挂载etcd和MinIO作为元数据和对象存储依赖这是Milvus启动的必要组件。执行docker compose up -d等待容器健康后用pymilvus连一下http://localhost:19530即可。Windows和macOS上只要Docker Desktop配置够用基本没什么差别。我平时在Windows开发机上也这样跑没有遇到诡异问题。唯一建议是给Docker多分配点内存至少8GB因为索引构建会吃内存。跑起来之后我逐步把记忆模块的代码补全接下来进入核心实现部分。4. 核心实现从建表到与Agent循环对接这节我直接给出完整的代码思路你可以把它当模板用。我使用的是当前新版pymilvus的MilvusClient接口整体写法比早期版本简洁很多。4.1 创建Collection的Schema我的collection名称叫agent_memory核心字段和前一节的表一致。创建schema之前最好先确认好向量维度和VARCHAR长度因为后期修改字段类型在Milvus里比较麻烦。from pymilvus import MilvusClient, DataType client MilvusClient( urihttp://localhost:19530, tokenroot:Milvus ) schema client.create_schema(auto_idFalse, enable_dynamic_fieldFalse) schema.add_field(field_namememory_id, datatypeDataType.INT64, is_primaryTrue) schema.add_field(field_nameagent_id, datatypeDataType.VARCHAR, max_length256) schema.add_field(field_nameevent_type, datatypeDataType.VARCHAR, max_length32) schema.add_field(field_namecontent, datatypeDataType.VARCHAR, max_length8192) schema.add_field(field_namevector, datatypeDataType.FLOAT_VECTOR, dim1024) schema.add_field(field_namesource, datatypeDataType.VARCHAR, max_length32) schema.add_field(field_namets, datatypeDataType.INT64) schema.add_field(field_nameexpire_at, datatypeDataType.INT64) schema.add_field(field_nameimportance, datatypeDataType.FLOAT) index_params client.prepare_index_params() index_params.add_index( field_namevector, index_typeHNSW, metric_typeIP, params{M: 16, efConstruction: 256} ) client.create_collection( collection_nameagent_memory, schemaschema, index_paramsindex_params, consistency_levelBounded )这里有几个点要特别说明。第一memory_id我故意没有用auto_idTrue而是自己生成确定性ID这是为了后面做写入去重这个坑我后面会详聊。第二metric_type我用了IP内积相似度使用前提是写入的向量都要做归一化这样才能保证内积等价于余弦相似度。第三consistency_level我建议用Bounded而不是默认的Strong因为Agent记忆场景对毫秒级一致性的要求没那么高Bounded能明显降低写入延迟。4.2 写入路径把事件变成可召回的记忆存储结构搭好之后下面写写入函数。核心流程是拿到原始事件格式化成文本生成向量和元数据一起插入Milvus。import hashlib import time import numpy as np from sentence_transformers import SentenceTransformer embedder SentenceTransformer(BAAI/bge-m3) def gen_memory_id(agent_id: str, content: str, bucket_seconds: int 600) - int: bucket int(time.time()) // bucket_seconds raw f{agent_id}|{content}|{bucket}.encode(utf-8) return int(hashlib.sha256(raw).hexdigest()[:16], 16) def write_memory(agent_id: str, event_type: str, content: str, source: str user, importance: float 0.5, expire_at: int 0) - int: vec embedder.encode(content, normalize_embeddingsTrue).astype(np.float32) memory_id gen_memory_id(agent_id, content) client.upsert( collection_nameagent_memory, data[{ memory_id: memory_id, agent_id: agent_id, event_type: event_type, content: content, vector: vec.tolist(), source: source, ts: int(time.time()), expire_at: expire_at, importance: importance, }], ) return memory_id这里的工程细节比代码本身更重要。一是normalize_embeddingsTrue必须打开否则你后面用IP内积算相似度结果会非常失真。二是gen_memory_id里我把内容哈希和时间桶结合这样短时间内同一条内容重复写入时memory_id不变配合upsert就能天然去重。三是这里没有直接同步等待Milvus刷盘Milvus默认是异步写入架构对Agent场景完全够用。4.3 召回路径混合搜索加上标量过滤召回是记忆模块的生命线写得好不好直接影响Agent能不能“想起来”。我的实现思路是把当前用户问题或当前Agent目标作为query文本转成向量后在collection内做向量搜索同时加上agent_id过滤和时间范围过滤。def recall(agent_id: str, query_text: str, top_k: int 8, event_type: str | None None) - list[dict]: query_vec embedder.encode(query_text, normalize_embeddingsTrue).astype(np.float32) filter_expr fagent_id {agent_id} if event_type: filter_expr f and event_type {event_type} results client.search( collection_nameagent_memory, data[query_vec.tolist()], filterfilter_expr, limittop_k, output_fields[content, event_type, source, ts, importance], ) hits [] for hit in results[0]: hits.append({ content: hit[entity][content], event_type: hit[entity][event_type], source: hit[entity][source], ts: hit[entity][ts], score: hit[distance], }) # 按重要性和时间做一次简单加权 hits.sort(keylambda x: x[score] * 0.7 min(x[ts] / 1e9, 1.0) * 0.3, reverseTrue) return hits召回时有个容易被忽略的点Milvus的search默认按向量相似度排序但Agent记忆场景里单看相似度不够还要考虑内容的重要性和时效性。我在这里做的加权排序比较粗糙但已经能解决“相似但不重要”的干扰问题。如果你的场景更复杂可以考虑加一个rerank模型把top_k先放大到20再对20条用LLM或cross-encoder精排效果会更好。4.4 和Agent循环对接把记忆注入上下文写好了存储和召回最后一步是把记忆模块挂到Agent主循环里。我常用的是比较朴素的注入方式每次Agent准备调用LLM生成回复前先把当前目标作为query召回若干条记忆再放进系统提示词的固定区块。def build_prompt_with_memory(agent_id: str, user_query: str, base_prompt: str) - str: memory_hits recall(agent_id, user_query, top_k6) memory_block \n.join([ f- [{item[source]}] {item[content]} for item in memory_hits ]) return f{base_prompt}\n\n# 可参考的历史记忆\n{memory_block}\n\n# 当前用户问题\n{user_query}这一步看似简单却是整个记忆模块最重要的产品化动作。记忆注入太多会让模型困惑注入太少又起不到参考作用。我经验是top_k在4到8之间效果较好超过10条后模型容易开始关注那些不太相关的边角记忆。另外记忆区要用明确的标题和列表分隔开模型才能分辨哪些是“历史资料”哪些是当前的直接指令。5. 嵌入模型和索引参数精度与速度的平衡很多人在记忆模块里只关注向量库却忽略了嵌入模型和索引配置对最终效果的决定性影响。实际上召回质量的上下限一半在嵌入模型一半在索引和查询参数的配合。5.1 嵌入模型怎么选我在这套模块里用的是开源模型BAAI/bge-m3主要原因是它对中文和英文都支持得比较好生成的向量维度是1024对Milvus来说完全能接受而且可以用sentence-transformers一行加载部署成本很低。如果你对中文场景的精度要求特别高可以考虑bge-large-zh系列但向量维度更高召回速度和存储成本上涨明显。如果团队有预算且需要多语言也可以换云端embedding服务但云端API的延迟和调用成本在记忆模块这种高频写入场景里会非常扎眼我建议先用开源模型跑通再根据业务压力做替换。嵌入模型最容易被忽略的一件事是版本锁定。一旦模型文件更新同一句话的向量分布可能变化之前存的所有历史向量和新向量就会存在“语义偏移”召回质量明显下降。我后来做了个简单机制在collection的description里写入嵌入模型名称和版本号每次部署调度时校验一旦发现版本不一致就触发历史向量重建。5.2 向量归一化与相似度度量这部分必须单独强调因为太多人在这里踩坑。我最终用的是IP内积但使用内积有一个硬性前提就是向量必须归一化。vec embedder.encode(content, normalize_embeddingsTrue)不加这行内积计算出来的分数会被向量模长干扰有些长文本天然分高导致召回偏向于“长度更大的记忆”而不一定是“语义最相关的记忆”。用COSINE距离虽然能在API层面规避一部分问题但部分版本的索引在COSINE度量下会额外做归一化提前自己normalize_embeddings总没有坏处还能让结果更可解释。5.3 HNSW参数怎么调Milvus里最常用的索引是HNSW我用在Agent记忆场景下的参数组合是M16、efConstruction256查询时设置ef64。M控制每个节点的最大连接数M越大图越密集召回越准但内存和构图时间也越大。efConstruction控制建图时的搜索宽度太大建图慢太小索引质量差。我给出的组合是中庸配置适合几百万到几千万条记忆的规模。如果你的记忆量特别大超过一亿条可以考虑IVF_FLAT或SCANN但参数调优会更麻烦前期我不建议一上来就用。搜索时的ef查询参数也是关键在client.search()的search_params里传{ef: 64}可以控制每次搜索的候选集大小。值越大召回越精确但延迟越高。我把64作为默认值实际在线峰值时单次查询也就几毫秒完全够用。如果客服类Agent对延迟要求极高可以降到32召回损失通常不是很明显。6. 生产硬化TTL、缓存和两层记忆架构代码能跑起来和能上生产是两码事。我在把模块推到线上前又做了几个层面的加固包括短期缓冲、过期清理、幂等去重。这些操作不是炫技而是把“偶尔失灵”变成“稳定可用”的关键。6.1 短期缓冲加长期存储搭配更合理一次对话过程中可能有非常多的小事件如果每条都立刻写向量库既浪费资源又增加查询延迟。我的思路是做一个“短期事件缓冲”层在Agent进程内维护一个最近的事件列表当列表达到一定长度或任务结束时再批量写入Milvus形成长期记忆。这个模式很像缓存加数据库的分层结构。短期缓冲里Agent可以直接访问最近N条上下文不需要做向量检索而recall接口只负责长期记忆的查询。这样既解决了上下文窗口内的即时性又保证了跨会话的持久性。实践中我会在session开始时把缓冲清空session结束时统一写入这样记忆的边界非常清晰。6.2 幂等写入和事件去重Agent在高并发或工具频繁调用时很容易产生重复记忆。例如一个工具被自动重试了3次如果你不处理系统里会留下3条几乎一样的记录。我用确定性memory_id加upsert方法从存储层就拦截了这种冗余。去重之外还要注意批量写入时的性能。我一般用client.upsert一次传入几十条数据而不是一条一条循环调这样写入速度会快很多。Milvus的批量写入能力很强单个请求里带上千条也不是问题但过大的批次会导致单次请求耗时变长我会控制在200条以内兼顾速度和稳定性。6.3 TTL过期清理的正确姿势长期记忆如果不清理会随着时间积累越来越多既占用存储又会把召回结果“污染”了。有些记忆是临时的比如“这几天用户正在测试某个新功能”过几天就没价值了。我给这类记忆设置expire_at字段并设计了一个后台清理任务。Milvus新版collection本身支持TTL属性可以在建表时设置collection.properties里的collection.ttl.seconds。但为了防止某个环境不支持我在应用层做兜底定时执行def clean_expired(client: MilvusClient, collection_name: str) - int: now int(time.time()) res client.delete( collection_namecollection_name, filterfexpire_at 0 and expire_at {now}, ) return res[delete_count]这种清理任务一般放在定时调度里每天凌晨跑一次即可。注意别在Agent在线高峰期做全量清理删除操作也会占用一定资源如果数据量大可能影响在线查询。7. 真实排坑记录这些坑我都是花了不少时间才爬出来最后一部分我把实际排障过程中遇到的问题集中整理出来。这些坑在官方文档里通常不会被专门写出来但遇到时真的会折磨人。7.1 召回结果全是最近的记忆相关记忆反而排不上来这个现象我排查了很久最后定位到两个原因。第一个原因是filter表达式写错了。比如直接写成fagent_id {agent_id}agent_id如果是字符串就必须加引号否则表达式不合法Milvus通常会报错但某些旧版本会静默退化成全部扫描结果可想而知。第二个原因是output_fields里没把importance返回出来我在排序时拿不到这个字段就会出现“最相关但不够新”的记忆被挤出top_k。解决办法也很简单先确认filter表达式格式再用client.query单独验证标量条件最后再把注意力放到排序策略上。排查顺序切记不要反了。7.2 VARCHAR长度设置太短导致内容被截断记忆条目的content并不是一条固定长度的数据工具调用结果可能非常长。我第一次建表时图省事把content的max_length设成了512结果上线后经常发现有些记忆内容变成了一堆被截断的半截话Agent拿到这些半截信息后理解经常跑偏。Milvus里VARCHAR字段长度在collection创建后就不好改了所以我后来重新建了collection一次性把max_length调到8192。建议你在设计阶段就预留充足空间不要省这几个字节。如果确实遇到超长内容应该在写入前做切分先把一条长记忆拆成多个chunk再分别写入并保证每个chunk都带有同一批元数据。7.3 多Agent实例并发写入的归属问题当同一个Agent服务被部署成多个实例时如果所有实例共享同一个Milvus collection忘记在写入时设置agent_id就麻烦了。我出现过一次事故测试环境多条记忆没有归属结果生产Agent查询时把这些脏数据也带了出来。我的防御措施包括所有写入方法强制要求agent_id如果为空直接抛异常同时在collection级别增加一个约定不允许插入空agent_id的数据。另外还会用client.query定期抽查每个agent的记忆条数发现异常立刻报警。7.4 备份时别忘了带上嵌入模型这个坑最隐蔽。有一次我误删了collection准备从备份恢复数据。数据是恢复了但嵌入模型版本恰好也更新过导致旧向量和新向量之间映射关系对不上整个记忆库几乎等于作废。从那以后我的备份策略分两层一是Milvus数据本身的备份比如用milvus_backup工具导出的文件二是模型版本说明和代码版本号的归档。恢复时先确保模型版本完全一致再恢复向量数据。这套流程虽然多了一步但能避免把整个记忆库变成一堆没有意义的浮点数。另外Milvus里废弃的collection不用急着删除可以先改名为_deprecated_xxx保留一段时间等确认新方案稳定后再物理删除。这种操作成本很低却能给恢复留一条后路。整套记忆模块跑到现在我最深的体会是Agent的长期记忆不在于“记住全部”而在于“在正确的时候取出正确的那一小部分”。截断是最省事的方案但也是最容易在关键时候掉链子的方案。把记忆存储和检索交给Milvus之后Agent不再每轮都像一个刚入职就失忆的新人它开始从历史里吸取经验这种变化在产品体验上是能明显感受到的。如果各位正在被Agent失忆问题折腾我推荐按照这套思路从设计记忆条目开始一步步替换掉截断逻辑你会发现排查起各种灵异问题都顺了很多。