ARTICLE DETAIL

资讯详情

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

从上下文窗口到向量数据库:构建AI Agent长效记忆的完整指南

从上下文窗口到向量数据库:构建AI Agent长效记忆的完整指南 做 Agent 做得越久我越觉得“记忆”才是决定体验上限的那道坎。模型能力再强如果每次对话都像第一次见面聊两句就忘光你的名字、偏好和昨天刚交代的事情那它充其量只是个“高级聊天框”谈不上是你的助手。这个系列前面两篇聊了 Agent 的整体框架和工具调用这篇我单独把记忆拎出来完整讲一遍“让 Agent 记住你”这件事该怎么做从记忆类型怎么划分、技术方案怎么选到用 LangGraph 落地一个最小可跑的记忆模块再到我实际踩过的坑。无论你是在做客服机器人、个人知识助手还是带记忆的编程 Agent这一篇都能给你一套可以直接抄作业的思路。先说我自己的结论记忆不是单一技术而是一整套“写入、存储、召回、更新、遗忘”的工程闭环。很多人以为接个向量数据库就万事大吉结果存了一堆乱七八糟的文本召回时又答非所问最后体验还不如不带记忆。真正好用的记忆系统一定是有结构、有分层、有更新策略的。这篇文章我会把每个环节都拆开讲也会给出能跑的最小代码示例希望对正在做 AI Agent 的朋友有帮助。1. 我理解的 Agent 记忆先搞清楚要解决什么问题1.1 没有记忆的 Agent只是在假装聊天前两年做大模型应用时我最头疼的一个问题就是“上下文断层”。用户上午跟 Agent 说“我家里有两个小孩一个 6 岁一个 10 岁周末经常要去公园”下午再问“帮我推荐周末亲子活动”Agent 完全不记得之前的对话从零开始猜需求。用户会明显觉得自己在跟一个“金鱼”聊天。这个问题表面上看是“上下文窗口不够大”实际上更核心的原因是我们把所有对话都当成了一次性请求没有把其中有价值的信息沉淀下来。大模型的上下文窗口本质上只是“短期工作区”它负责当前这一轮的理解和生成但它天然是易失的关掉会话、超过窗口限制信息就没了。要让 Agent“记住你”就需要在模型之外单独构建一个持久化的记忆层。我当时给团队定的目标是四个字越用越懂。Agent 至少要能记住三类信息——用户说过的事实比如家庭成员、职业、常用工具、用户的偏好比如喜欢简洁回答还是详细解释、讨厌术语、以及历史对话里已经确认过的结论比如上次已经推荐过某个方案这次别再重复推荐。让这些信息跨会话、跨请求地沉淀下来才是“记住你”的本质。1.2 我在动手之前先把记忆分成了四类刚开始做记忆模块时我犯过一个错把所有记忆都塞进同一个表里结果查询、更新、召回全都混乱。后来参考认知科学里的记忆分类把 Agent 的记忆拆成四类整个架构才清晰起来。记忆类型典型内容推荐存储更新频率短期记忆当前对话上下文、正在处理的任务状态上下文窗口、Redis、内存缓存每轮更新长期记忆用户事实、偏好、历史重要事件向量数据库、关系型数据库低程序性记忆工具调用流程、任务执行步骤代码、规则引擎、技能库低频工作记忆当前任务的中间结果、临时变量状态对象、图状态高频这四类的读写策略完全不一样。短期记忆要的是“快”尽量少做语义加工直接给模型拼接上下文就行长期记忆要的是“准”需要在写入时做抽取、去重和结构化程序性记忆一般不太由对话动态生成更多是 Agent 开发者预先定义或者从历史工具调用中归纳出来工作记忆则跟着任务生命周期走任务结束就可以清掉。把记忆分类这个动作最大的价值是让我在做技术选型时不再纠结“用什么数据库统一存储”。短期记忆你用向量库就是浪费长期记忆你要硬塞进上下文窗口也会很快爆掉。先分类再决定每一类用哪个方案这是做记忆模块的第一步也是最容易被跳过但最值得花时间的一步。2. 技术选型不同记忆用不同存储别让一套方案打天下2.1 短时记忆不要硬塞给向量库很多入门教程上来就是“给 Agent 装一个向量数据库”其实短期记忆根本不需要向量化。短期记忆要解决的核心问题是在一段连续对话里怎么让模型不忘记前面说了什么。这里最常用的技术是上下文管理和滑动窗口。我的做法是给对话设置一个“最近 N 轮”窗口比如保留最近 5 轮完整消息更早的消息如果信息密度高就压缩成摘要放进上下文。这个摘要不是简单截断而是调用模型用几句话把关键信息提炼出来比如“用户提到自己是前端工程师正在用 React 重构项目周五要上线”。这样既控制 token 成本又不丢失核心线索。为什么不用向量库做短期记忆因为向量召回本身有延迟而且召回结果还需要再拼回上下文链路长、不稳定。短期记忆是要“喂”给模型的应该尽量贴近原始语义、顺序而不是经过向量化之后的相似性匹配。缓存放不下或者上下文超限时再做“摘要压缩”这是成本收益比最高的方案。我见过有团队把每轮对话都拆分成片段存入向量库每个请求再从库里召回相关片段拼进上下文结果用户问一个简单问题系统先做 3 次向量查询延迟多了几百毫秒关键信息还经常被漏掉。原因就是短期对话有很强的顺序依赖向量检索并不擅长处理这种“紧挨着前文”的语境。记住短期记忆走上下文窗口长期记忆才走向量库。2.2 长期事实记忆向量库加 Embedding 是基本盘长期记忆要解决的核心问题是跨会话。用户上个月说过“我在备考 PMP”这个月问“周末怎么安排”Agent 应该联想到备考进度主动建议安排复习时间。这种联想能力靠关键词匹配很难做到因为“PMP”“考试”“复习”“周末安排”这些词在字面上并不完全重叠但语义上是相关的这时候就需要向量语义检索。向量数据库选型我实际测过几个常用方案Chroma 适合本地原型轻量、零配置几千条数据体验很舒服PostgreSQL 加 pgvector 适合本来就有 PG 的业务少引入一个组件数据还能用 SQL 管理Milvus 适合真正的海量数据、高并发场景但运维成本高Redis 的向量检索则适合低延迟、小规模场景复用已有缓存集群。我个人的建议是个人项目可以先从 Chroma 或者本地的 SQLite 向量扩展开始上了生产再多花力气上独立向量库。Embedding 模型的选择同样关键。中英文混合场景下我优先测试了开源的中文向量模型比如 BGE 系列整体效果明显好于直接用英文模型维度上 1024 维或 768 维是主流维度太高会占内存太低了语义区分度又不够。选模型时要注意最后所有写入向量库的文本包括用户对话、知识文档、记忆条目都要用同一个 Embedding 模型。各存各的召回时查询向量和库里的向量不在同一空间结果必然不对。2.3 结构化画像偏好与事实用 JSON 比向量更靠谱向量库擅长语义召回但它不擅长“精确回答”。用户说“我女朋友喜欢喝冰美式”存向量库后你问“用户女朋友喜欢喝什么”确实能召回相关片段但如果要做个性化推荐、做规则判断每次都要靠向量召回文本片段再让模型理解既慢又不稳定。更靠谱的做法是把高置信的事实和偏好抽成结构化 JSON 存起来。我会在长期记忆里单独维护一个用户画像表字段包括用户 id、偏好标签、关键事实、更新时间、来源会话 id 等。比如{ user_id: u_12345, preferences: { coffee: 冰美式, answer_style: concise, language: 中文 }, facts: [ {key: job, value: 前端工程师}, {key: children, value: 2} ], updated_at: 2025-04-10T18:30:00Z }这张表不需要模型每次召回只要在构建系统提示词时用模板拼进去就行精确、便宜、零延迟。比如用户问“给我推荐一杯咖啡”系统提示词里已经有“用户偏好咖啡冰美式”模型输出自然更贴合。向量召回这时反而是查漏补缺的角色。对于没抽成结构化的、又比较重要的描述性记忆比如“用户上次提到想去大理旅行因为喜欢洱海边安静的环境”这种细节很难全部压进 JSON就靠向量库存原文、语义召回。所以我的长期记忆是两层结构结构化画像负责“精确事实”向量库负责“模糊语义”两者各管一段组合起来才是完整的长期记忆。2.4 MCP 协议和记忆服务统一接口还是自定义实现最近 MCPModel Context Protocol协议讨论很火它在 Tools、Resource、Prompt 三个层面给了统一标准不少人也开始用 MCP 把记忆能力暴露给 Agent。我的看法是MCP 适合做“记忆能力的标准化接口”比如对外提供“写一条记忆”“查用户记忆”“删除记忆”这几个 Resource让不同 Agent 都能通过统一协议读写这在多 Agent 协作场景里很实用。但我不建议把整套记忆逻辑全塞进 MCP Server 里就万事大吉。记忆系统需要后台异步更新、定时清理、冲突合并这些都不是简单的请求响应能覆盖的。我会把 MCP 当作一层“门面”真正的存取逻辑仍然放在独立的内存模块里由 Agent 的主流程调用。比如用户说了一句话主流程先做实体抽取和偏好识别再决定是写入结构化画像还是向量库最后才把结果暴露给需要访问记忆的其他服务。同时要注意MCP 工具的粒度不能太粗也不能太细。太粗比如只有一个“save_all_memory”Agent 不知道该不该调用写进去的内容一团糟太细比如拆成“save_coffee_preference”“save_job_info”工具数量爆炸模型选工具都选不过来。我实践下来比较稳的粒度是围绕“记忆操作”而不是“记忆内容”write_memory、search_memory、update_memory、delete_memory再加 meta 里的 memory_type 区分短期、长期、结构化。3. 落地实操给 Agent 装一套能“记住你”的记忆模块3.1 总体流程写入、存储、召回、更新、遗忘记忆模块不是一个装完就跑的一次性功能而是一条持续运转的数据管道。我这里先给一个总体流程后面小节再逐一展开细节。写入阶段每一轮对话结束后判断这轮对话里是否有值得长期留存的信息。判断依据可以是规则比如出现“我喜欢”“我讨厌”“我在做”等表达也可以交给大模型做信息抽取。抽取出的信息包括用户说了什么事实、表达了什么偏好、是否与旧记忆冲突。存储阶段把抽取结果写入对应存储。偏好和事实写入结构化画像描述性信息写入向量库关键结论可以追加到长期摘要。这一步要注意去重和合并避免同一个事实被重复存储。召回阶段在模型生成回答前根据当前用户问题和会话状态从记忆系统里召回相关信息拼进提示词。召回分两类结构化画像直接读取语义记忆向量检索 top-k。更新阶段当用户新表达与旧记忆不一致时以最新为准。旧记忆不能直接删要有状态标记或者版本号避免“今天说喜欢喝热拿铁明天说错了”这种反复横跳把画像搞乱。遗忘阶段定期清理低置信、长期未命中的记忆条。长期不访问的记忆可以归档超过 TTL 的临时记忆直接删除。遗忘不是 bug是让记忆系统保持健康的必要机制。我当时是先用日志把每一步的输入输出都打出来跑完 100 条真实对话再逐步调整抽取规则和更新策略。记忆模块最大的特点是“没有显而易见的正确”必须用真实数据反复调。3.2 用 LangGraph 跑通一个最小记忆流程LangGraph 很适合做 Agent 的状态流编排因为记忆本身就是跨节点的状态。我下面给一个简化版的最小实现目标是在每次对话前从记忆库召回信息对话结束后把新信息写入记忆库。from langgraph.graph import StateGraph, END from typing import TypedDict, List import chromadb class AgentState(TypedDict): user_id: str messages: List[dict] memory_results: List[str] # 初始化向量库 client chromadb.PersistentClient(path./agent_memory) collection client.get_or_create_collection(user_memory) def recall_memory(state: AgentState): user_id state[user_id] query state[messages][-1][content] # 召回与该问题语义相关的记忆 results collection.query( query_texts[query], n_results3, where{user_id: user_id} ) return {memory_results: results[documents][0]} def generate_answer(state: AgentState): memory_text \n.join(state[memory_results]) context f以下是该用户的记忆信息\n{memory_text} # 这里接入你自己的 LLM 调用 # response llm.chat(context str(state[messages])) return {messages: [{role: assistant, content: 基于记忆的回答}]} def save_memory(state: AgentState): user_id state[user_id] last_user_msg [ m for m in state[messages] if m[role] user ][-1][content] # 简化处理把用户消息作为记忆写入 # 生产环境应该用抽取后的结构化数据 collection.add( documents[last_user_msg], ids[f{user_id}_{hash(last_user_msg)}], metadatas[{user_id: user_id, time: 2025-04-10}] ) return state # 构建图 graph StateGraph(AgentState) graph.add_node(recall, recall_memory) graph.add_node(generate, generate_answer) graph.add_node(save, save_memory) graph.set_entry_point(recall) graph.add_edge(recall, generate) graph.add_edge(generate, save) graph.add_edge(save, END) app graph.compile()这段代码的意图很简单recall 节点在回答前先查记忆generate 节点把记忆信息拼进提示词save 节点在回答后写入新记忆。实际项目里需要替换成正确的 LLM 调用、更严谨的抽取逻辑和去重策略但整体骨架是通用的。LangGraph 的好处是状态对象 AgentState 天然承载 user_id、messages、memory_results你不用自己维护复杂的全局变量。有一点要注意save_memory 不应该保存每一轮所有内容那会造成大量重复和噪音。我在生产代码里是先调用一个抽取函数只有抽取到“值得记住”的信息才入库。判断标准可以是包含具体偏好、明确事实、或与旧记忆存在更新关系。否则宁可少存也不能乱存。3.3 记忆更新不是追加而是合并只写不更新记忆模块很快就会被污染。一个典型场景用户昨天说“我喜欢喝拿铁”今天说“最近减脂换成美式吧”。如果系统只做追加向量库里同时存在“喜欢喝拿铁”和“喜欢喝美式”两条召回时模型可能给出完全矛盾的推荐。我处理这类冲突的思路是“合并优先追加兜底”。具体的做法是给每条记忆加一个状态字段active、superseded、archived。写入新记忆前先检索一下有没有同主题的 active 记忆如果有且内容冲突把旧记忆标记为 superseded同时写入新记忆。这样召回时优先取 active 记忆历史信息不会完全丢失又不会干扰当前判断。还有一个容易忽略的点是“置信度”。用户闲聊时说了一句“我可能想去日本玩”这是一个低置信意向不应该立刻写入长期画像否则用户明天随口改口画像就乱套。我会给抽取结果打一个置信度分数比如明确表达“我特别喜欢”“我讨厌”之类的高置信语料直接写入带“可能”“也许”“想一下”等模糊词的先放进短期草稿区等第二次确认再升级为长期记忆。这种机制能显著降低记忆漂移。不要小看这个细节真实聊天里模糊表达远多于明确表达没有置信度机制的记忆系统就是个垃圾场。4. 避坑指南我在记忆模块里踩过的坑4.1 找了个英文 Embedding 模型中文召回效果稀碎第一批上线时为了省事我直接用了当时默认的英文 Embedding 模型测试英文问题好像还行一旦用户用中文提问召回结果完全对不上。比如用户之前说“周末想去爬山”改成中文问“有什么户外运动推荐”返回的记忆条目经常是无关联的商品描述。问题不在向量库而在 Embedding 模型对中文语义的理解不够。后来换成了针对中文优化过的开源向量模型并且用同一模型重新生成了全量索引中文召回效果立刻好了很多。这里有两个操作要点第一是切换模型后必须重建索引旧索引的向量空间和新模型不一致不重建会出现“查询正常但召回全是错”的诡异问题第二是在生产环境给 Embedding 模型加一层封装方便后续平滑替换不要散落在一堆业务代码里。另外如果你们的用户含有大量术语、专业名词比如医学、法律最好用自己的数据微调一个领域 Embedding但这不是第一步要做的事先用通用模型跑通觉得差了再针对性优化。4.2 重复写入同一个事实存了几百遍另一个容易踩的坑是把“用户说过的每句话”都当成记忆入库。我最早做的版本用户一句“我住上海”就被写进了向量库过两天用户又提了一次“我住在上海”系统再写入一次。三个月下来向量库里同一个事实的重复记录可能有几百条一来浪费存储二来召回时返回的全是相似的重复内容浪费上下文空间还让模型分不清哪个是最新版本。我的解法分两层写入前先做语义查重用向量召回 top1计算相似度超过阈值的直接放弃写入或者走更新分支写入后加定时任务定期按 user_id 聚合相似记录把重复项归档。查重阈值要调我这边 0.92 以上基本可以认定是重复但也有误判场景所以更稳妥的是把“语义重复但表达不同”的信息合进一句话比如“用户住在上海工作在杭州”而不是简单丢弃。4.3 隐私边界记忆越全风险越大做记忆系统一定会接触到用户的隐私比如联系方式、家庭住址、健康状况、工作单位。网上有很多讲“Agent 记住你”的教程只讲能力不讲边界但我建议你在设计的第一天就把隐私边界想清楚。不是说不能存储而是要区分哪些信息是服务所必需的哪些信息只是闲聊中顺带提到的。我的原则是明确收集、最小必要、支持删除。明确收集是指写入前让用户知情不能偷偷把“用户家住哪个小区”记下来还浑然不觉。最小必要是只保存你想用来做个性化的字段无关信息不入库。支持删除是必须提供“清除我的记忆”功能接口可以从 delete_memory_by_user_id 开始同时清理结构化画像和向量库里的所有相关条目。别小看这块很多 Agent 项目在演示时没人管一上真实用户就出事。哪怕只是合规要求也够你重写一遍。4.4 每次请求都查一遍向量库延迟高到崩溃记忆召回如果放进在线链路必须考虑延迟。最初我的实现很粗暴用户每发一条消息先向量召回再从画像表读偏好还要再查一遍历史摘要三次串行请求走完才调大模型。结果单次请求因为记忆系统多了 600 到 800 毫秒延迟用户体感明显变差。优化思路是“缓存 异步”。用户画像属于低频变化的可以直接缓存到 Redis按 user_id 存一份 JSON每次请求直接读缓存只有画像变更时才回写。向量召回可以做成并行和画像读取同时发起不要串行。记忆写入放到对话结束之后异步执行不要在用户等待回答时同步写库。还有一招是在非高峰时段做记忆预取比如用户打开应用时先加载最近记忆到会话上下文真正提问时就能少一次召回。5. 常见问题速查与调试实录5.1 明明存了Agent 却说“我不记得”这种情况我排查过很多次原因多半不在“没存”而在“没召回”或“没喂给模型”。常见原因有三个召回阈值设得太高导致相关记忆根本没进 top-k 结果。向量召回正常但提示词里没有把召回结果拼进去模型根本看不到记忆。存储时没带 user_id检索时按当前用户过滤后为空。排查方法是把记忆召回模块单独拉出来打日志输出请求问题、召回 top5 的相似度分数、最终拼进提示词的记忆文本。我见过不少项目日志一打就发现是过滤条件写错了比如检索时传成了 session_id而不是 user_id。调试记忆问题别盲猜先看日志。5.2 记忆串台A 用户说的话跑到了 B 用户头上多用户场景下最怕的就是记忆串台。出现这种情况八成都跟检索过滤条件有关要么写入时 metadata 里没写 user_id要么召回时没带过滤条件。Chroma 这类向量库支持 where 条件过滤一定要记住写入和召回都按 user_id 隔离。如果你们是多租户场景建议在 collection 层面就按租户拆分或者所有 metadata 统一加 tenant_id 和 user_id 两个字段。另外测试时不要只用一个用户账号测要同时创建两个测试账号来回切换验证记忆是否真的隔离。这个坑我踩过上线前没做双用户测试结果一上生产就有人反馈“看到别人的偏好”吓得我赶紧补隔离。5.3 旧记忆误导模型用户已经改主意画像还停留在过去记忆系统最大的副作用是“刻舟求剑”。用户明明说过喜欢喝拿铁但后来改喝美式了如果画像不同步模型就会一直推荐拿铁。这类问题的根源是记忆更新策略没做好。我的建议是所有长期画像字段必须有 updated_at并且在召回时按时间过滤过期记忆降权。同时要在提示词里告诉模型“以下记忆信息来自不同时间如果与当前用户表达冲突以用户最新说法为准。”给模型一个处理冲突的指引能避免模型完全被旧记忆带偏。生产环境可以再加一道规则检测到用户明确否定旧记忆时立刻触发画像更新并把旧条目标记为 superseded。5.4 记忆测试怎么做别只测“还记得吗”功能测试之外我建议设计一套“记忆剧本”专门验证记忆系统的写入、召回、更新、遗忘。比如写入剧本告诉 Agent“我在准备雅思考试”再问“我最近在忙什么”看是否记得。更新剧本先说“我喜欢喝热拿铁”隔一阵说“我现在改喝冰美式了”再问“推荐杯咖啡”看是否推荐冰美式。遗忘剧本制造大量低置信碎片检查它们是否进入草稿区而不是长期库。隔离剧本两个用户分别写入不同偏好交错提问确认互不影响。删除剧本调用删除接口后确认向量库和画像表都清干净。这套剧本建议写成自动化测试用例每次修改记忆逻辑后直接跑一遍回归。记忆系统不像普通接口有明确的对错只有通过一套稳定剧本持续验证才能保证迭代过程中不悄悄变坏。最后再分享一个我一直在用的习惯记忆模块上线后我会定期导出真实记忆样本人工看 20 到 30 条记录重点检查有没有重复、过时、错误、隐私风险。不要完全依赖自动化评估肉眼扫一遍往往能发现测试用例覆盖不到的问题。让 Agent 记住你不是把用户所有的话都塞进库而是有选择、有结构、有更新地理解用户。在我做过的所有 Agent 项目里记忆模块永远是投入产出比最高也最需要耐心打磨的部分希望这篇能给你省下一些弯路。
返回列表