ARTICLE DETAIL

资讯详情

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

给大模型装上“长期记忆”:AI记忆系统设计与落地实践

给大模型装上“长期记忆”:AI记忆系统设计与落地实践 写AI应用最头疼的不是模型选型也不是Prompt调优而是“记忆”。做过AI助手、聊天机器人、Agent类项目的朋友应该都有体会模型本身是“记不住事”的你和它聊十句话它可能连你第一句说过什么都忘了。我自己在做一个ai-memory相关的记忆模块时被这个问题折磨过很久也踩了不少坑。这篇就聊聊我怎么做AI记忆、为什么这么做、以及实际落地过程中那些文档里不会写的问题。如果你也在做带长期记忆的AI应用或者正打算给自己的机器人加上“记住用户偏好”的能力这篇文章应该能省你不少试错时间。1. 先想清楚AI需要什么样的“记忆”很多人一上来就急着写代码、接向量库结果做出来的东西很鸡肋——记是记住了但该用的时候没用上或者记住了不该记的把整个对话搞得更乱。所以第一步不是动手而是把“记忆”这件事想清楚。1.1 大模型的“失忆症”上下文窗口不等于记忆先梳理一个基本概念。大模型每次请求都是独立的它只能看到当前对话窗口里提供的文本。哪怕你用的是支持128K甚至1M上下文的长窗口模型那也只是“临时工作台”不是真正的记忆。一旦会话结束、窗口被截断模型对刚才发生过什么一点印象都没有。这就好比一个人得了短期失忆症你每次跟他说话他都像是第一次见你。我们做ai-memory本质上就是给这个“失忆症患者”配一个外部笔记本让它在每次对话前把该记得的事情翻出来重新写进上下文里。所以记忆系统的核心痛点很简单存什么、怎么存、怎么取、怎么更新。这四个问题不解决再多花哨的技术也是白搭。1.2 三层记忆架构像人一样分层记东西我做这个项目时参考了认知科学里对人类记忆的划分方式把AI记忆分成了三层工作记忆Working Memory就是模型的上下文窗口用于处理当前任务。这部分由Prompt直接控制不需要额外存储。短期记忆Short-term Memory一次会话内的信息比如用户中途说“我刚才让你查的那个事情”指代的是什么。通常用会话状态或摘要来维护。长期记忆Long-term Memory跨会话的持久化信息比如用户的偏好、习惯、历史事实、既定目标。这层需要存储系统支撑也就是ai-memory模块真正要管的部分。这样的分层有个实际好处不是所有信息都值得永久保存。短会话的临时信息如果一股脑写进长期记忆检索的时候会把真正的关键信息淹没终端体验反而变差。1.3 很多人做记忆系统失败的原因没有想清楚“记忆的边界”我在调研同类项目时发现大家最常见的失败模式是“什么都想记住”。对话里的每个细节、用户随口一句玩笑、一次性的任务过程全部入库。结果检索时召回了一堆噪音模型被无关信息干扰输出的质量还不如不带记忆的时候。自己也踩过类似的坑后来痛定思痛给记忆的写入定了几条硬规矩只存跨会话仍然有参考价值的信息长期偏好、身份信息、关键事实会话内临时上下文用摘要承载不单独存细节情绪化、临时性、一次性指令不进入长期记忆明确区分“用户说的”和“系统推测的”推测部分要打上置信度标签。规则看起来简单但实际操作中非常管用。仔细想一步你需要的不是“记住所有话”而是“在合适的时机想起来该想的事”。2. 记忆系统的核心设计从数据流看懂全貌想清楚了记忆的边界下面要设计整个系统的数据流。ai-memory不是一个单点功能而是一条完整的数据链路从对话中提取关键信息经过加工处理写入存储然后在后续对话开始时检索召回最后注入Prompt参与生成。2.1 记忆写入什么时候该“记一笔”写入的触发策略决定了记忆系统的质量。我最开始用的方案很暴力每轮对话结束都把全部对话记录扔给模型做摘要再把摘要直接存进向量库。结果数据库里堆了几千条高度相似、信息冗余的摘要检索时连自己都分不清哪条重要。后来换成了“事件驱动定期提炼”的组合打法事件驱动当对话里出现明确的身份信息、偏好声明、目标设定、时间承诺等结构化信号时立即触发一次提取。定期提炼每几轮对话后把新发生的事件跟已有的记忆合并用模型做一次冲突检测和概要更新避免同一条信息不断膨胀。举个例子用户说“我平时用Python多一些不太喜欢Java”这属于明确的偏好声明应当写入。而用户说“今天天气不错”这种场景化的话不需要长期记录。写入的时候还要带上元数据时间戳、信息来源用户原话还是系统推断、关联话题标签、置信度。这些信息在后续检索排序时非常有用后面会详细说。2.2 记忆存储结构化存储还是向量库存储选型是重头戏。我见过不少项目一上来就上向量数据库美其名曰“语义检索”但实际上很多记忆条目用结构化存储更合适。两者的适用场景完全不同维度结构化存储如SQLite/PostgreSQL向量存储如FAISS/Chroma等查询方式精确匹配、规则过滤语义相似度召回适用数据明确的KV对姓名、城市、偏好标签自然语言描述的复杂记忆片段优点可解释、更新容易、无幻觉支持模糊语义查询、表达灵活缺点表达能力有限无法覆盖非结构化内容召回结果不可控、需要关系数据库配合我最终采用的是混合架构核心的用户画像类记忆用结构化表存储一段对话中不好抽成结构化字段的“软记忆”用向量存储。为什么这么设计因为像“用户说过他家有个10岁小孩”“他最近在准备一场技术分享”这类信息硬拆成字段会很别扭用语义检索反而更自然。而像“编程语言Python”“城市上海”这种简单事实用结构化方式存取既快又准没必要上向量。实际落地的时候如果你对召回质量没有充分把握建议先做结构化再逐步引入向量。向量检索的问题在于“看似相关实则无用”的召回太多做精排又需要额外投入初期成本不低。2.3 记忆召回别让检索拖垮整个体验存储解决了“有没有”的问题召回决定的是“准不准”。我做过一次对比实验同一段用户提问不用记忆辅助时回答还算正常接入了记忆但召回质量差时回答反而更糟糕——模型被错误或有歧义的旧记忆带偏了。原因在于召回应该是“按需提供”而不是“尽量多给”。上下文窗口虽然够大但塞满不相关记忆后模型会“选择困难”甚至把无关信息当成关键线索。我的处理策略分三个层次精确层先用结构化查询根据用户ID、话题标签做精确匹配比如“用户在系统里记录的时区、语言偏好”这些必须直接注入容不得半点差错。语义层用向量检索召回前若干条语义相似的历史记录数量上限严格控制我一般设5条并且每条附上时间和置信度让模型知道这些信息有多可靠。排序层结合场景的即时目标做动态过滤。比如当前任务是在写推荐文案那跟用户年龄、地域、消费习惯相关的记忆优先级高如果当前任务是在答疑技术背景、历史沟通记录优先级更高。经过这三层注入到上下文里的记忆数量少而精模型反而能输出更有个性、更贴合语境的回答。这个过程看似简单实际调优的功夫主要都在这一步。3. 实操手把手搭一个ai-memory模块理论部分聊了不少这部分直接上实操。下面是我实际跑通过的一套方案代码不复杂但胜在完整你在自己的项目里很容易改成适合自己的结构。3.1 环境与数据模型准备我用的环境是Python 3.11 SQLite 一个小型向量库此方案用Chroma因为本地运行轻量切换别的也不会影响整体架构。先定义一个基础的数据模型# memory_models.py from dataclasses import dataclass, field from datetime import datetime from typing import Optional dataclass class MemoryItem: memory_id: str # 主键 user_id: str # 关联用户 category: str # 类型profile / preference / fact / event content: str # 记忆文本 structured_data: dict field(default_factorydict) # 可选的结构化KV source: str user # user / inferred confidence: float 1.0 # 置信度 created_at: datetime field(default_factorydatetime.utcnow) updated_at: datetime field(default_factorydatetime.utcnow) access_count: int 0 # 召回次数用于后续清理这里关键的一个设计是access_count字段初看没什么用但在后续做记忆清理时价值很大——长期没被召回的记忆可以降权甚至归档。3.2 记忆写入的代码实现写入的核心是“提取-结构化-存储”三步走。我用LLM做信息提取用正则做二次校验尽量避免模型输出乱填的字段。提示词里让它严格区分“用户明确表达的”和“根据上下文推测的”并输出JSON。# memory_writer.py import json from typing import Dict, List from memory_models import MemoryItem import uuid EXTRACT_PROMPT 你是一个记忆提取器。根据下面的对话提取值得长期记住的信息。 注意 1. 只提取跨会话仍可能有价值的信息偏好、身份、长期目标、关键事实。 2. 忽略一次性指令和临时性话题。 3. 必须输出JSON数组每个元素包含 - category: profile/preference/fact/event - content: 自然语言描述 - structured_data: 如果合适给出KV结构否则为空对象 - source: user(用户明确说) 或 inferred(系统推测) - confidence: 0.0-1.0 对话内容 {conversation} def extract_and_save_memories(conversation: str, user_id: str, llm_fn): prompt EXTRACT_PROMPT.format(conversationconversation) response llm_fn(prompt) try: records json.loads(response) except json.JSONDecodeError: # 模型输出不是合法JSON时退回按规则硬提取 records fallback_rule_extract(conversation) items [] for rec in records: item MemoryItem( memory_idstr(uuid.uuid4()), user_iduser_id, categoryrec.get(category, fact), contentrec.get(content, ).strip(), structured_datarec.get(structured_data, {}), sourcerec.get(source, inferred), confidencefloat(rec.get(confidence, 0.5)), ) if item.content and item.confidence 0.6: # 过低置信度直接丢弃 save_to_db(item) items.append(item) return itemsfallback_rule_extract是我写的纯规则提取函数专门用于对抗模型输出不稳定。比如用正则识别“我喜欢/我偏好/我是/我在”等句式粗粒度提取兜底。虽然精度不如模型但也比什么都不存强。记住一点写入阶段的容错设计非常必要生产环境里LLM输出的非结构化、乱JSON问题远比你想的频繁。3.3 记忆检索与注入Prompt检索阶段要把精确匹配和语义匹配的结果合并然后按一个统一分数排序。# memory_retriever.py def retrieve_memories(user_id: str, query: str, top_k: int 5): # 1. 精确匹配从结构化字段里命中 exact_results query_exact_matches(user_id, query) # 2. 语义匹配向量召回 semantic_results query_semantic_matches(user_id, query, top_ktop_k) # 3. 合并排序分数 0.6 * 语义相似度 0.3 * 置信度 0.1 * 最近活跃度 merged merge_and_rank(exact_results, semantic_results) # 4. 信息注入附带来源时间与置信度说明 memory_text format_memory_for_prompt(merged) return memory_textformat_memory_for_prompt是我很看重的一个细节。它不是把记忆原文一股脑拼进Prompt而是生成一段带元数据的“记忆卡片”以下是对用户的历史记忆按相关性排序 1. [用户明确说置信度0.93天前记录] 用户使用Python为主不熟悉Java。 2. [系统推测置信度0.71周前记录] 用户近期可能在工作之余准备Kubernetes相关认证。注意看我在每条记忆上都标注了来源、置信度、时间。这看起来是小事但实战中能显著减少模型把旧信息或推测信息当铁律的情况。模型在生成时会自动权衡这些提示输出更谨慎、更准确。3.4 记忆更新与过期清理策略记忆不是存了就完事。一条记忆可能三个月后已经失效也可能用户后来明确否定过之前的说法。我会做两件事合并更新当新提取的信息跟已有记忆冲突时不直接覆盖而是先标记可疑再让模型判断是更新还是纠正。比如用户之前说“我喜欢Java”这周聊天里说“Java早就不用了现在写Rust”这时候如果不处理两条记忆同时存在会让模型精神分裂。定期清理每天凌晨跑一次清理脚本把长期未召回、置信度低、存在冲突标记的记忆放入冷存或直接删除。清理脚本的判据很简单access_count 0并且created_at超过30天就归档confidence 0.4的直接删除。# memory_cleanup.py def cleanup_stale_memories(): stale db.query( SELECT * FROM memories WHERE access_count 0 AND created_at NOW() - INTERVAL 30 DAY ) for m in stale: move_to_cold_storage(m.memory_id)这里我想特别提醒AI记忆系统如果只写不删半年后你的数据库会变成一个充满矛盾信息的垃圾场。清理不是可选项是必选项。4. 踩坑实录这些问题我都被问过最后一部分说说实际运行中遇到的高频问题和我的处理思路。这些问题你在官方文档里基本找不到答案但做真实项目时几乎避不开。4.1 模型总是把记忆和当前对话混淆这是最初级的坑但也是最影响体验的。现象是模型在回答当前问题时把旧记忆里的信息当成用户刚刚说的话导致回答方向跑偏。我的对策是前面提到的“记忆卡片”格式——每条记忆带来源标签并且明确写“这是历史记忆不是当前对话内容”。另外Prompt里我会加一句“请优先基于当前对话内容回答历史记忆仅作参考”。这两个小改动加在一起混淆问题基本消失了。4.2 向量检索召回了一堆不相关的内容此前做语义召回时发现一个奇特现象按相似度排在前面的记录很多并不是用户真正关心的只是用词相近。比如用户问“天气怎么样”它能返回一条“用户说过的某天天气不错”的历史记录。这是向量检索典型的语义粒度问题。解决方案不是换一个更强的向量模型而是在召回后加一道“硬过滤”根据记忆的类别标签、时间和用户当前意图做预筛选。比如当前意图是“问答工具类”就把类目限制在profile和fact当前意图是“闲聊推荐类”才把preference也放进来。我的经验是召回前的规则过滤比召回后的精细排序提效更明显而且规则清晰、结果可解释。4.3 记忆更新时出现“新旧共存”的矛盾用户改口、情况变化导致的记忆冲突处理起来最费脑子。最简单的做法是“后写覆盖先写”但风险很大——模型提取时可能失误把一句玩笑当成新偏好写入了结果真实信息被覆盖。我的处理方案写入前用已有记忆做冲突检测。def detect_conflicts(new_item, existing_items): conflict_texts [] for item in existing_items: if item.category new_item.category and item.content ! new_item.content: conflict_texts.append((item, new_item)) return conflict_texts检测到冲突后我会把两条信息都放进Prompt里让模型判断是更新、合并、还是保留两者。比如前面的“Java转Rust”场景模型应当输出“更新”因为这是用户明确的转变而如果是“用户说Python不错”和“用户说Python同时也很烦人”这种情绪化表达则不应该覆盖保留原有偏好即可。这个方案会多消耗一点模型调用但收益是记忆系统的抗干扰能力大幅提升值得做。4.4 会话中包含隐私信息怎么处理才安全做ai-memory躲不开隐私问题。用户可能在对话里透露姓名、地址、电话等敏感信息这个我处理得比较保守默认情况下非必要的身份信息不进入长期记忆进入长期记忆的内容在存储前做一次“脱敏检查”如手机号、邮箱、身份证号用正则识别后打码记忆系统提供导出和删除接口用户随时可以清除自己的全部记忆数据。有些功能前期不做后面合规和用户信任的账会加倍还回来。即使技术上能存也别什么都往库里塞在这个问题上宁保守勿激进。4.5 记忆召回影响响应速度成本也不好控制每次对话前既要查结构化库又要查向量库还要调用模型做排序总延迟容易飙到几百毫秒。对交互型产品来说这是不可接受的。我的优化思路是“按需分档”简单场景比如只查用户昵称、时区直接用内存缓存秒回复杂场景需要语义召回才走完整链路。同时给所有检索操作加上超时控制记忆检索超时就直接跳过别让记忆系统拖垮主业务。成本方面关键是把提取记忆的那次模型调用频率降低。比如不是每轮对话都提取而是对话停顿几秒后或用户主动结束一条话题时再提取一次。实测下来整体模型调用成本能节省30%-50%。还有一个小技巧最后分享给你如果你在初期不确定记忆系统怎么做找个现成的“记忆管理测试集”用20-30条真实的对话记录去模拟全流程先把召回质量和冲突处理跑通再谈扩展和技术优化。做AI记忆的同行应该都认同一点记忆链路里真正重要的不是用了多酷的模型或框架而是你有没有把数据流、召回策略和更新机制磨到顺手。我的这套方案谈不上ToB级的完善但作为一个能落地的ai-memory模块它把记忆这件事变得“可知、可控、可维护”后续在你自己的项目里按需嫁接会顺畅很多。
返回列表