
刚接触大模型应用开发时我踩过最痛的一个坑不是模型答得不对而是它“转头就忘”。用户上一轮刚交代过“我是做跨境生意的平时喜欢晚上处理邮件”下一轮再聊它表现得像个初次见面的陌生人。大模型的上下文窗口就那么大你没法把所有历史对话永久塞进去于是“记不住”成了所有对话型应用都绕不开的硬伤。我做完第一版带记忆功能的对话助手后最大的感受是给AI做记忆不是简单地把聊天记录存进数据库而是一个涉及抽取、存储、召回、更新、遗忘的完整工程。这篇文章我会把从零搭建一套私有大模型记忆系统AI-Memory的完整思路、关键参数、代码实现和踩过的坑一次说清楚。想给自己的智能客服、AI助手或Agent加上跨会话记忆能力的同学这篇应该能帮你省下不少试错时间。1. 为什么需要给AI装记忆先想清楚需求很多人第一次接触AI Memory时脑子里想的是“给AI接一个数据库让它记住对话”。方向没错但真做起来会发现问题远比想象中复杂。在动手之前先把需求拆透不然很容易做出一个“存了很多用不上多少”的鸡肋系统。1.1 上下文窗口的物理极限先看一个最基础的问题大模型的上下文窗口是有限的。以常见的商用大模型和应用场景为例即使上下文窗口已经扩展到几十万token你也不可能把用户过去三个月的历史对话全部原样塞给模型。一方面每次调用都要消耗token成本成倍上涨另一方面上下文越长模型对关键信息的注意力越容易被稀释。我见过一个团队把几十轮对话全部拼进prompt结果模型开始“答非所问”因为真正重要的信息被海量的闲聊天淹没了。所以AI记忆系统存在的第一个理由是把海量历史压缩成少量高价值信息。它承担的是一个“信息过滤器印象维护员”的角色而不是“仓库管理员”。我们要做的是通过某种机制提取出值得长期留存的片段在需要的时候精准地调出来而不是把原始日志直接当记忆用。1.2 跨会话记忆是体验的分水岭判断一个对话应用是“玩具”还是“可用的产品”跨会话记忆往往是一个重要分水岭。没有记忆的客服系统每通电话都是从“请问您有什么需求”开始用户每次都要重复订单号、说明来龙去脉有了记忆之后系统能认出老用户知道上次处理到哪一步关闭工单的效率相差巨大。实际项目里跨会话记忆带来的典型收益有几个用户不必重复描述自己偏好、身份、公司背景、产品信息这些不用每次重新交代。多轮任务可以断点续跑上次进行到哪一步、有哪些结论下次直接接着来。个性化体验从“叫得出名字”升级到“懂你的习惯”推荐、话术、节奏都能贴合用户。这些能力表面上是“记住对话”本质上是在做用户建模和业务状态管理。这也是我后来才想明白的AI Memory 不只是一个技术组件它直接决定产品的智能化上限。1.3 记忆在智能体架构里的位置如果你在做Agent这个问题更绕不开。虽然主流框架里常常把智能体拆成规划Planning、工具调用Tool Use、记忆Memory、行动Action几个模块但实际的记忆模块做得最“虚”很多团队直接把多轮历史记录原封不动地丢给模型。这么做的直接后果是Agent在做决策的时候只看得到当前会话上一轮已经问过用户“你偏好哪家物流”这种问题下一个动作又去问一次不仅让用户烦还白白消耗token。记忆模块在完整架构里承担的任务是给Agent提供“连续的身份感”和“跨时间的决策上下文”。没有它Agent只能做无状态请求转发所有需要状态的业务都做不了。2. 从零设计记忆系统分层与选型想清楚为什么做之后再看怎么做。我建议不要一上来就讨论用什么向量库先把记忆的分层概念立起来。这一步想透了后面的技术选型会清晰很多。2.1 记忆也分四层别把所有对话历史塞进同一个地方人脑的记忆也不是一个单一仓库。按时效和信息形态我习惯把AI记忆拆成四层记忆层含义典型内容存储形式召回方式工作记忆当前会话进行中的临时信息这一轮用户刚说的需求会话上下文直接拼进当前prompt情景记忆过去某次会话中发生的事件上周用户反馈过运费太高对话摘要/向量片段相似度召回语义记忆从经验中提炼出的稳定事实用户常用海运对时效不敏感结构化的属性-值对属性过滤向量程序记忆长期形成的习惯和流程用户每次都要先看质检报告规则模板/流程片段固定触发逻辑这个分层看起来简单但它能避免一个常见错误把语义记忆当情景记忆存。比如用户随口说过“我公司在义乌”这是一条情景记忆还是语义记忆答案取决于它是否稳定。如果只是某个话题中的背景属于情景如果它会影响后续所有订单处理就应该抽成语义记忆存成“用户公司所在地义乌”。分层的直接收益是召回时能根据当前任务类型选择不同的检索通道不会出现“查一个偏好翻出五段无关对话”的尴尬。2.2 存储方案怎么选别一上来就上向量库存储层的选型是社群问得最多的。我的建议很直接中小项目用PostgreSQLpgvector一个库搞定高频大规模场景再引入独立的向量数据库。原因很简单。记忆数据的特点是“多种混合”有一部分需要结构化过滤用户ID、实体类型、时间范围有一部分需要向量检索相似的语义内容。如果你为了向量检索专门引入一个Milvus或单独的向量服务就得维护两套存储还要处理两边的数据同步问题。而pgvector把向量字段直接做成表里的一个字段一张表同时支持“WHERE user_id xxx AND entity preference ORDER BY embedding - $query_vec LIMIT 10”既简单又不容易出错。用研的同事常说记忆系统90%的问题都能用“一张宽表向量字段常规索引”解决。我实测下来确实如此几万用户的记忆量pgvector完全扛得住等到单表千万级向量、QPS上百才需要考虑替换。缓存层我会用Redis放最近高频召回的“热记忆”避免每次请求都查向量库——因为向量计算最贵的就是那一下距离计算命中热点缓存很划算。2.3 记忆生命周期写入、召回、更新、遗忘设计完存储层再来看记忆的数据生命周期。我按顺序梳理一下写入阶段要解决的第一个问题是“什么值得写”。对话里90%的内容其实是不值得长期保存的寒暄和流程性内容。我们一般是等一轮对话结束后用模型对整段对话做一次记忆抽取判断产出结构化的记忆条目。召回阶段讲究“少而准”。每次请求从记忆库里拉出的条目不应该超过5~10条宁可少一点不能把最终token预算打爆。更新阶段是最容易被忽略的一环。很多系统只写不更导致用户上周说“我喜欢圆通”这周改成“改用顺丰了”库里还是两条偏好打架。正确的做法是对同一实体同一属性做“覆盖更新”而不是继续追加。遗忘阶段同样不能省。长期不访问的记忆要降权用户主动要求删除的要立即清掉。一个没有遗忘机制的记忆系统时间长了必然变成一座越住越堵的杂物房相关性会被噪声彻底淹没。3. 四个核心模块与关键参数接下来从工程实现角度拆解四个必须做扎实的核心模块。每个模块我都会给出关键参数和个人经验值你可以直接拿来当起点再调。3.1 记忆抽取并不是所有对话都值得记住记忆抽取模块是整个系统的入口也是决定记忆质量的第一道关。我见过不少团队把对话原文直接切片存入向量库结果召回回来的全是“哈哈”“好的呢”这种垃圾内容原因就是没有做抽取与筛选。推荐的方案是做“事件驱动式抽取”。在一轮会话结束后把整段对话交给一个轻量级抽取模型可以用较小的模型也能是大模型但要控制输出格式让它判断是否出现了值得长期留存的信息。我在工程里用的Prompt框架长这样你是记忆抽取引擎。请从下方的对话中提取需要长期记住的信息。 判断标准该信息是否会影响未来对话中系统的行为或回答是否是新出现的、且稳定的用户事实如果是输出JSON否则输出{should_store: false}。 必须输出严格JSON不要输出其他文字 { should_store: true, memory_type: semantic | episodic, entity: 该信息的主体比如用户、项目名, attribute: 属性名比如偏好、公司、截止时间, content: 一句话表达的核心事实, importance: 0到1之间的小数代表这条记忆的重要性, expire_at: 可选格式YYYY-MM-DD如果该条信息有时效性 }为什么要强调“必须输出严格JSON”因为实际工程中它会被程序解析并写入数据库如果模型输出带前缀后缀解析就得做一堆容错。我把抽取的调用做成异步任务对话结束后丢到队列里跑不影响用户主链路的响应速度。关键参数上importance阈值建议定0.6左右低于这个分的直接丢弃。太高会漏记关键信息太低会存一堆“用户点了杯咖啡但没喝完”的噪声。另外我强烈建议在提示词里强调“稳定”二字这样像“今天心情不好”这种瞬时状态就不会被当成长期记忆写入。3.2 记忆存储实体、属性、时间戳一个不能少抽取完的信息怎么存我坚持一个原则优先结构化向量只作辅助。很多教程上来就教你把文本塞进embedding模型然后存向量库但实际项目里这样做的教训非常明显。试想一个场景用户迁了城市把常用收货地址从“杭州”改成了“成都”。如果记忆是以一整段文本为粒度的你根本定位不到要更新哪条但如果存成“实体user_001属性city值成都”一条UPDATE就能精确完成。所以在建表时我坚持把结构化字段做完整user_id、entity、attribute、content、memory_type、importance、created_at、last_accessed_at。时间戳的重要性可能比很多人想得更大。记忆是有时效的比如“用户正在接触的供应商”下个月可能就不适用了。没有时间戳后续做时间衰减、做自动遗忘都会无从下手。我甚至建议在每条记忆上再挂一个source_session_id既能追溯到原始对话又能用来做纠错和审计。向量字段则是用来做语义召回的补充你不可能把所有用户事实都硬编码成属性键值所以content字段的embedding要保留。3.3 记忆召回打分公式与TopK的设定逻辑召回模块直接决定了最终拼进Prompt的上下文质量。初学者最容易犯的错误是“多多益善”一次召回三四十条。这不仅消耗大量token还会把真正的高价值记忆埋住模型反而更容易跑偏。我用的召回逻辑是三步走构建查询向量把当前用户问题做embedding。向量结构化双查向量检索召回与用户问题语义相似的记忆同时用user_id、entity这类字段做精确过滤。计算最终得分并排序综合相似度、新鲜度、重要性三个维度排序取TopK。最终得分的计算公式如下你可以在工程里直接用score w1 * cosine_similarity(query_vec, memory_vec) w2 * recency_score(created_at, now) w3 * importance其中recency_score我习惯用指数衰减recency_score exp(-(days_since_created) / half_life_days)参数方面w1推荐0.5~0.6w2推荐0.2~0.3w3推荐0.2。这样语义相关性的权重最大但不会让旧记忆完全消失。half_life_days半衰期天数对语义记忆设为30对情景记忆设为7效果相对均衡。TopK怎么定我的经验是先算token预算再定TopK。假设你当前上下文窗口是8K用户当前问题历史工作记忆占掉2K系统提示占1K那么留给长期记忆的预算只剩5K。按每条记忆平均200token计算TopK稳定在10~15条之间比较安全。一定不要盲目调高TopK宁可让召回少几条也要保证base prompt不超限。另外建议给模型加一句提示“下面提供的部分记忆可能过时若与当前对话矛盾请以当前对话为准。”这能显著减少过期记忆导致的错误回答。3.4 记忆迭代与遗忘避免系统越用越笨市面上很多记忆项目死于同一个病只进不出。记忆越堆越多召回时噪声越来越大系统的回答越来越糊涂。要命的是这种退化不是突然崩坏而是在用户不知不觉中慢慢变差等发现问题时用户已经流失了。我实际用的遗忘策略有三板斧覆盖更新同一实体同一属性新值覆盖旧值。用户改偏好地址、改称呼都走这个逻辑不做追加。时间衰减降权召回打分时超过一定天数的记忆自动乘一个小于1的系数让它们慢慢沉底。如果用户重新提到某个旧话题被召回后又会重新激活。显式遗忘提供用户可操作的“清除记忆”入口同时支持按实体删除。这个不只是合规需要更是产品体验的一部分——很多用户对“AI记住我的一切”心里发怵能一键清理反而会增加信任感。有人可能会问历史事实也是资产删掉不可惜吗我的答案是存档归存档召回归召回。你想保留原始数据可以放到离线数仓做统计但线上记忆库一定要保持“轻量化”。垃圾分类比囤垃圾重要得多。4. 可复现的实操从建表到接入LLM理论说了不少接下来上硬菜。我会给出一套可以直接“抄作业”的最小实现基于PostgreSQLpgvector用FastAPI提供记忆服务的读写接口再把它接入LLM应用层。这套方案我已经在多个项目里跑过几万用户规模下非常省心。4.1 数据模型与建表语句先定义核心表。我习惯把记忆拆成两张表一张存语义记忆结构化属性为主一张存情景记忆自由文本向量。两张表结构略有不同但基本骨架一致。下面给出语义记忆表的DDL-- 开启pgvector扩展 CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS memory_items ( id BIGSERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL, session_id VARCHAR(64), entity VARCHAR(128), -- 实体如 user_001 attribute VARCHAR(128), -- 属性如 city / preference content TEXT NOT NULL, -- 核心事实 memory_type VARCHAR(16) NOT NULL, -- semantic / episodic importance FLOAT DEFAULT 0.7, -- 重要性 0~1 embedding VECTOR(1536), -- OpenAI embedding 维度若换成别的模型需改 created_at TIMESTAMPTZ DEFAULT now(), last_accessed_at TIMESTAMPTZ DEFAULT now(), source_session_id VARCHAR(64), -- 来源会话ID用于追溯 deleted BOOLEAN DEFAULT FALSE ); CREATE INDEX idx_memory_user_entity ON memory_items (user_id, entity, attribute); CREATE INDEX idx_memory_type ON memory_items (memory_type); CREATE INDEX idx_memory_embedding ON memory_items USING hnsw (embedding vector_cosine_ops);几个字段的设计理由说一下。last_accessed_at是用来做热记忆淘汰的我起了一个定时任务定期把超过30天没被访问过的记忆标记为低优先级需要清理时按这个字段排序。deleted字段做软删除因为用户要求删除记忆后如果发现是误删短期内还能找回审计也方便。embedding VECTOR(1536)的维度取决于你用的嵌入模型OpenAI的text-embedding-3是1536如果换成开源模型比如bge-large就得改成1024先确认维数再建表。4.2 最小的记忆服务实现接下来写一个最小可用的记忆服务。我用FastAPISQLAlchemy但核心逻辑并不依赖框架换成Flask或Node根本改起来也不难。写入接口的逻辑是接收已经抽取好的结构化记忆生成embedding然后做“同实体同属性则覆盖更新”的判断# memory_service.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel, Field from sqlalchemy import create_engine, text import numpy as np import openai app FastAPI() engine create_engine(postgresqlpsycopg://user:passlocalhost/db) class MemoryIn(BaseModel): user_id: str entity: str attribute: str content: str memory_type: str semantic importance: float 0.7 session_id: str | None None def embed(text: str) - list[float]: resp openai.embeddings.create( modeltext-embedding-3-small, inputtext ) return resp.data[0].embedding app.post(/memory) def write_memory(m: MemoryIn): # 1. 生成向量 vec embed(f{m.entity}:{m.attribute}:{m.content}) vec_str [ ,.join(str(x) for x in vec) ] # 2. 同一实体属性已经存在时覆盖更新否则插入 with engine.begin() as conn: existed conn.execute( text(SELECT id FROM memory_items WHERE user_id:uid AND entity:e AND attribute:a AND deletedfalse), {uid: m.user_id, e: m.entity, a: m.attribute} ).first() if existed: conn.execute( text( UPDATE memory_items SET content:content, embedding:vec, importance:importance, last_accessed_atnow(), session_id:sid WHERE id:id ), {content: m.content, vec: vec_str, importance: m.importance, sid: m.session_id, id: existed.id} ) else: conn.execute( text( INSERT INTO memory_items (user_id, session_id, entity, attribute, content, memory_type, importance, embedding) VALUES (:uid, :sid, :e, :a, :content, :mtype, :importance, :vec) ), {uid: m.user_id, sid: m.session_id, e: m.entity, a: m.attribute, content: m.content, mtype: m.memory_type, importance: m.importance, vec: vec_str} ) return {ok: True, stored: 1}写代码时还有两个容易翻车的地方需要你特别留意。第一embedding的向量字符串拼到SQL里时pgvector不支持参数化列表很可能需要转成字符串再绑定格式是“’[0.1,0.2,0.3]’::vector”。我在上面示例里把vec转成了字符串实际生产还会再加一层校验防止长度不符。第二覆盖更新的粒度是按“实体属性”意味着写接口的调用方必须在抽取阶段就把entity和attribute识别准确否则会出现“同一个人两个实体名”的脏数据。这类问题通常靠抽取层的输出约束解决。再写召回接口。召回逻辑分成两部分先做结构化过滤再做向量召回最后合并打分app.get(/memory/recall) def recall_memory(user_id: str, query: str, top_k: int 8): qvec embed(query) qvec_str [ ,.join(str(x) for x in qvec) ] half_life_days 30.0 # 思路一轮SQL里同时算向量相似度、时间衰减、重要性分数 sql SELECT content, entity, attribute, importance, created_at, 1 - (embedding :qvec::vector) AS sim, exp(-EXTRACT(EPOCH FROM (now() - created_at)) / 86400.0 / :hl) AS recency FROM memory_items WHERE user_id :uid AND deleted false ORDER BY (0.6 * (1 - (embedding :qvec::vector)) 0.25 * exp(-EXTRACT(EPOCH FROM (now() - created_at)) / 86400.0 / :hl) 0.15 * importance) DESC LIMIT :tk with engine.connect() as conn: rows conn.execute(text(sql), {qvec: qvec_str, uid: user_id, hl: half_life_days, tk: top_k}).fetchall() return [{content: r.content, entity: r.entity, attribute: r.attribute, score: round(r.sim * 0.6 r.recency * 0.25 r.importance * 0.15, 4)} for r in rows]这段SQL直接调用了pgvector的运算符计算余弦距离再把相似度、新鲜度、重要性按权重加起来排序。性能上HNSW索引能保证几万条数据毫秒级返回。要注意的是如果以后要支持全文检索补充召回可以在表上加一个tsvector字段用PostgreSQL自带的全文本搜索做“关键词命中”召回再和向量结果做加权融合。这种混合召回在用户查询很短、缺乏语义信息时特别有用。4.3 怎么和LLM应用层对接记忆服务写完接入LLM应用层是整个链路见效的关键一步。这一步的核心就是一句话把召回的记忆整理成一份清晰的“记忆清单”放进System Prompt并明确告诉模型如何对待这些信息。下面是我实际使用的Prompt模板# 关于用户的长期记忆 以下信息是从用户过往对话中提取的长期记忆可能存在时效偏差请结合当前对话判断是否适用。 - 用户偏好喜欢结构化、带表格的产出不喜欢文字堆砌 - 事实信息用户公司位于杭州主要做跨境电商主攻北美市场 - 历史决策上次已确认邮件营销方案采用A计划尚未执行 # 当前对话 用户我们继续讨论上个月的营销方案。注意我给的记忆编号是有限的两三条而不是一个巨大的列表。接LLM时还要提供一个“记忆是否可用”的判定引导防止模型把过期记忆当作铁律。比如用户说“我搬家到深圳了”模型应当以当前对话优先而不是固执地沿用旧记忆“公司在杭州”。另一个实操细节是对话摘要。如果多轮会话内容太长但没有值得抽取成长期记忆的稳定事实我会把整段对话压缩成一段摘要作为情景记忆存起来。比如“用户与AI讨论了新品包装方案倾向环保材质但因成本暂缓”。这种摘要比原始对话日志更适合做召回因为它去掉了重复语义信息密度更高。4.4 异步化与性能兜底写接口的时候如果每次对话结束都同步等待LLM做记忆抽取用户会感到明显卡顿。更合理的方式是把抽取和存储丢到后台任务队列。我用的链路是这样的用户对话结束前主进程只负责正常回复。把整段对话发布到Redis队列或用Celery/RQ后台Worker消费。Worker调用小模型做抽取然后请求记忆服务写入。这样用户响应时间和记忆流程完全解耦。Worker失败也不用回滚正常对话流程只需把原始对话落盘等待补采。异步化引入后要特别关注写入顺序同一个user_id的两条记忆抽取任务如果并发执行覆盖更新时后完成的任务可能覆盖先完成的导致丢掉一条新信息。解决方案很简单用Redis分布式锁按user_id加锁或直接在数据库层做乐观锁给memory_items表加一个version字段更新时带上版本号。5. 实战中遇到的5个坑与排查方法最后把我在项目里真实踩过、并且身边同行也频繁踩到的坑整理一下。这部分最有价值因为很多细节你在任何文档里都查不到。5.1 记了等于没记召回命中率低现象是记忆明明写进去了但用户再次提到相关内容时系统却像失忆了一样。排查思路分几步第一步打开召回接口的debug日志打印召回的ID和打分。看看是不是分数普遍偏低过滤掉了很多候选。如果是说明相似度计算对短查询不敏感需要引入关键词/BM25召回兜底。第二步检查embedding模型的领域匹配度。通用embedding模型对专业术语、口语化表达的表征能力有限如果业务垂直度很高建议在领域语料上微调嵌入模型这通常能带来十几个百分点的召回提升。第三步看结构过滤是否过于严格。比如“entityuser_001”本身没问题但如果用户改了系统编号新会话里用了新的user_id旧记忆全部查不到这是常见的隔离性事故。5.2 上下文被记忆挤爆Token超限真实场景里我们经常遇到两个尴尬时刻一是用户问题本身很长再加上记忆清单和系统提示总长度直接超限二是推理模型在记忆太长时不仅答得慢还容易“失去重点”。我的经验是使用多级压缩策略。第一级召回时先限制TopK第二级对召回的每一条记忆在写入prompt前再做一次压缩比如只保留“实体属性值”的短句子省略来源上下文第三级如果仍超限就按分数从低往高剪掉。最后再加一层兜底系统在拼prompt之前先估算token如果预计超限就自动放弃低分记忆。宁可少放两条记忆也绝不能让prompt被截断或报错。5.3 记忆串台把A用户的记忆带到B用户的会话里这是多租户场景下最严重也最容易踩雷的问题。症状是用户B收到了基于用户A历史的回答且内容十分私人。根本原因基本都在SQL查询上要么召回接口没带user_id过滤条件要么向量库的collection是全局共享的查询时漏了租户维度要么测试时手写SQL没加where导致脏数据被反复检索。排查时直接看查询日志确认所有入口的召回SQL都带上了“AND user_id :uid”。这种问题一旦进入线上不仅丢用户还可能引发数据合规问题所以我建议在代码审查阶段就把“所有记忆查询必须带用户隔离条件”列为硬性规范宁可多写几行冗余也不要用隐式全局默认值。5.4 并发写入丢记忆一致性处理并发问题最常见的是“覆盖更新竞态”。用户在同一分钟内既修改了手机号又在另一个设备上更新了地址两个写入任务同时做“先查再写”后提交的任务可能覆盖掉前一个任务的新值导致一条事实丢失。工程上的解法有几层最简单的是调低并发粒度在写接口层对同一个user_id加一把应用锁更稳的是在表里加version字段每次更新带上“WHERE versionold_version”如果更新影响行数为0就重试一次。对消息队列消费者要保证消费顺序一致比如按user_id哈希路由到同一个Worker从源头避免乱序。5.5 隐私删除与用户可解释性在做记忆系统的过程中我越来越确定一件事记忆功能必须有用户可控的开关和删除能力。这不是为了应付合规审查而是真实用户真的会在意“AI到底记了我什么”。我们在这套方案里做了三件事第一提供一个“记忆管理”页面用户可以查看系统为自己存了哪些记忆条目第二提供“删除单条记忆”和“一键清空”两个操作删除走物理删除第三在写入敏感信息如身份证号、银行卡号时做字段级过滤不进入长期记忆库。做完之后用户投诉率明显下降留存反而上升了。这说明记忆系统的设计不能只站在“技术实现”的角度还要站在“用户信任”的角度。最后说几点长期实践下来的体会AI Memory这套东西做下来我最深的一个感受是它真正难的地方不在“存”的工程细节而在于“判断哪些该记”“何时该忘”这两个决策点。前者决定一个对话系统专不专业后者决定它聪不聪明。很多团队上向量库、上异步框架折腾了一个月回头发现主要瓶颈居然是抽取Prompt写得不够细致——模型把情绪化的随口一说当成了长期偏好整个系统就跟着跑偏。另外我对“记忆召回”的建议是不要追求一次做满。先跑最小闭环对话结束抽取、写库、按TopK召回、覆盖更新。跑通之后再一步步加时间衰减、混合召回、异步队列。这样迭代起来心里有数出了问题也容易定位。如果未来想继续扩展可以往知识图谱方向走把实体和实体之间的关系做成图记忆就不再是孤立的条目而是能在关联场景中自动激活的网状结构——那是AI Memory下一阶段的真正乐趣所在。