我记得很清楚三个月前第一次做带“用户记忆”的 Agent 时测试同事丢来一句“我刚说过我的公司是跨境物流你怎么转头就忘了”那一刻我就明白LLM 本身是金鱼脑每轮对话它都是第一次见你。后来查资料、调流程、踩坑发现真正决定一个 Agent 好不好用的往往不是模型多聪明而是两件事——用户记忆和知识库。这也是本篇 AI Agent 学习笔记要重点整理的内容。这一篇是系列第三篇前两篇分别整理了 Agent 的组成结构、工具调用与编排方式。这次把“用户记忆”和“知识库”放在一起写是因为实际做项目时这俩功能高度纠缠很多人嘴上说“Agent 要接知识库”做着做着又把用户信息也塞进去结果知识库越用越乱。如果你正在做 AI Agent 开发、RAG 应用搭建或者只是想给自己的 Obsidian/本地知识库接上大模型问答这篇笔记应该能派上用场。我会从记忆的分层设计开始逐步进入 RAG 的切分、向量化、检索重排等工程细节再把两条链路拼进一个最小可运行的 Agent 流程最后分享我在实际部署中反复踩过的坑和排查思路。1. 用户记忆Agent 的“人情味”从哪来1.1 先分清记忆的三层结构先明确一个底层事实大模型没有记忆。每一次 API 调用都是无状态的独立请求你传什么上下文它就基于什么回答上一轮对话它根本“看不见”。所谓给 Agent 加记忆本质上是我们在模型外部造出三层缓存再把缓存内容在恰当的时机拼进 prompt。第一层是会话内记忆也就是把多轮对话历史直接堆在上下文窗口里。实现最简单但长度有限几轮之后老对话就被挤掉了。第二层是长期事实记忆比如用户所在行业、职位、偏好、称呼这些稳定信息通常抽成结构化字段存起来下次对话按需要检索。第三层是画像摘要记忆它是从大量历史对话里提炼出的高层抽象比如“这位用户喜欢先听结论再听细节”“他对预算很敏感”这类记忆更新频率低但对回答风格的塑造作用很大。这三层记忆可以理解成一条加工流水线原始的对话日志是原材料会话内记忆是当前工作台事实记忆是常用便签画像摘要则是定期整理出来的用户档案。如果只用原始日志做记忆很快就会碰到 token 爆炸和成本失控的问题只有逐步提炼压缩才能让 Agent 在有限的上下文里留出空间给更重要的知识库内容。记忆类型存储载体典型内容更新频率召回方式会话内记忆LLM 上下文窗口当前多轮对话原文每轮追加直接拼接长期事实记忆向量数据库 / 键值库行业、职位、产品偏好对话中持续写入相似度检索 Top-k画像摘要记忆结构化数据库沟通风格、决策偏好定期压缩重写每次会话加载我自己在项目里最常用的组合是会话内记忆交给模型上下文长期事实记忆用向量库保存画像摘要则定期用一次 LLM 调用把近 N 轮对话“蒸馏”成几百字的结构化总结。这样既不会让上下文爆掉又能保证对老用户的回答有连续性。1.2 记忆的写入不是所有对话都值得记住记忆工程的第一个坑就是什么都想记。用户随口一句“今天天气真热”如果也写进长期记忆几天之后你的记忆库里全是噪声真正的关键信息反而被淹没了。所以记忆写入的前提是判断这句对话是否包含稳定、可复用的用户事实或偏好。我目前的写法是让 LLM 在每轮对话结束后或者每隔几轮批量处理一次输出结构化抽取结果建议值存成一个 JSON例如{ should_memorize: true, memory_items: [ { type: fact, key: company_industry, value: 跨境电商, confidence: 0.92, timestamp: 2025-01-18T10:32:00Z }, { type: preference, key: reply_style, value: 喜欢简洁结论, confidence: 0.75, timestamp: 2025-01-18T10:34:00Z } ] }用结构化输出而不是让模型直接写段话是因为后续更新和覆盖很好做按 key 定位字段拿新的 value 覆盖旧的即可。还要处理一类特殊情况——用户推翻旧信息。比如用户之前说“我们在用 MySQL”后来改口“已经全部迁到 PostgreSQL 了”这时记忆模块要按“同 key 冲突时以最新时间戳为准”的策略覆盖更新并且最好保留一条来源记录方便后续排查。批量还是实时写入也需要想清楚。我试过每轮对话都实时写记忆效果并不理想一是调用次数变多成本上升二是用户在一次对话里经常会修正自己前一轮说的和后一轮可能矛盾实时写会产生大量脏数据。现在我是把一个会话结束作为触发点对整段会话做一次记忆抽取和更新准确率和成本都更可控。1.3 记忆的召回按需取用别把家底全搬给模型记忆写入只是前半截真正决定体验的是召回。很多人以为记忆就是“把库里所有内容都拼进 prompt”这是误区。一个使用了一年的 Agent用户画像可能有几百条全部塞进去token 消耗大关键信息还容易被淹没。我实际使用的召回策略是三维度的组合。第一相关性优先把用户的当前问题做 embedding去记忆库做余弦相似度检索只取 Top 5 左右最相关的记忆项。第二时间衰减给每条记忆加时间戳超过一定期限没有命中的事实降权近期偏好优先。第三会话需求判断如果用户明显是问“今天帮我查一下快递”这时候只召回和快递、收件人相关的短期状态型记忆不需要把他的历史项目经历全部搬出来。另外有一个我踩过的坑记忆注入顺序太靠后被超长知识库内容挤出了上下文。后来我固定了 prompt 里的区块顺序——用户记忆放在最前知识库检索结果次之对话历史最后然后给记忆区块设一个硬性长度上限超过就只保留得分最高的几条。实测下来把记忆总长控制在上下文窗口的四分之一以内回答质量最稳定。2. 知识库与 RAG让 Agent 从“背过”变成“会查”2.1 先打破一个误区知识库不是让模型把文档“学进去”新手最常见的误解是把 PDF 传进系统模型就等于学会了文档内容。实际上目前绝大部分知识库应用走的是 RAG检索增强生成路线模型并没有“学会”文档而是在每次回答问题前先从文档库里检索出最相关的片段再把片段作为参考上下文交给模型生成答案。就像新入职的同事不会把公司制度全文背下来真遇到问题时去翻办公桌上的规范手册翻到哪页就按哪页回答。这个设计带来的直接好处有三个。第一知识更新便宜文档改了重新切分索引一遍就行不需要重新训练或微调模型。第二回答有出处你可以让模型在回答末尾标注参考了哪些文档片段这对企业场景极其重要。第三成本可控每次只需要把检索到的少量片段拼进 prompt不用把整个知识库喂给模型。用一句话概括 RAG 的完整链路文档导入 → 文本切分 → Embedding 向量化 → 存入向量库 → 用户提问 → 问题向量化 → 检索 Top-k → 重排 → 拼接 prompt → 模型生成。每一步都影响最终质量但最容易出问题的恰恰是最前面两步——切分和向量化。2.2 切分与向量化RAG 质量的第一道关口文本切分决定了检索的基本单元。切太大一个 chunk 里包含好几个主题检索时容易带回大量无关内容切太小语义被截断模型拿到手的片段前言不搭后语。我用中文实测下来固定按 300 到 500 个字符切一段配合 50 到 80 字符的重叠是相对稳妥的起点。重叠的作用是补偿切分边界导致的语义断裂让前后文信息不至于被一刀切掉。但“固定长度切分”只是兜底方案更好的做法是优先按文档结构切。Markdown 就按标题层级切Word/PDF 按章节标题切没有明显标题的长文再按段落和句子切。我之前处理一批产品手册时发现固定 500 字符切出来的片段经常把“适用场景”和“参数说明”混在一起检索命中了但答案不聚焦改成按二级标题切分之后命中精度明显提升。这里放一个我常用的简易分层切分参考实现# 简易分层切分先按标题切再按长度兜底 import re def split_document(text, max_chunk500, overlap50): blocks re.split(r\n(?## ), text) # 以 ## 开头的标题块作为一级分块 chunks [] for block in blocks: if len(block) max_chunk: chunks.append(block) else: sentences re.split(r(?[。]), block) current for sentence in sentences: if len(current) len(sentence) max_chunk: current sentence else: if current: chunks.append(current) current current[-overlap:] sentence if current: chunks.append(current) return chunks向量化的关键则是 Embedding 模型选型。中文场景优先选针对中文优化的模型比如 bge-m3、m3e 这一类英文为主的内容可以选 OpenAI 的 text-embedding-3 系列。还要注意向量维度不是越高越好维度高意味着存储和计算成本都涨实际效果不一定成正比。相似度计算我基本固定用余弦相似度简单稳定在绝大多数向量库里都是默认实现。2.3 检索与排序命中率才是知识库的试金石知识库不是搭好就能用真正的考验是“用户随便换一种说法你能不能把该有的内容查出来”。纯向量检索有一个典型短板它对同义词、缩写、精确编号的匹配比较弱。比如知识库里写“大语言模型”用户问“LLM”向量相似度可能并不高反过来也一样。因此生产环境我建议做混合检索一边用 BM25 这类关键词检索兜底一边用向量检索做语义召回最后把两路结果用 RRFReciprocal Rank Fusion融合排序。RRF 的思路很简单每个文档在两路结果里各有一个名次融合分就是把名次的倒数加起来排名越靠前贡献越大。公式大致是 score 1/(k rank_1) 1/(k rank_2)k 一般取 60。好处是不用调两路权重的比例不容易出现某一路特别强势把另外一路的优质结果全挤掉的情况。检索出来之后还建议加一道重排序。粗排阶段从向量库里捞回 Top 50再用一个专门的重排模型比如 bge-reranker把这 50 条精编成 Top 5。重排模型比向量检索更精细能结合查询和文档做深度语义匹配虽然慢一点但只对 50 条做精排延迟完全可接受。我实际调过的项目里加重排之后首答准确率普遍能提升十个点以上。另一个容易被忽略的是查询改写。用户提问往往很口语化直接拿去检索效果一般。可以在检索前先用 LLM 把问题改写成更适合搜索的形式补全主语、转换成标准术语、拆出关键实体。这个步骤增加一次模型调用但对最终命中率的提升非常明显所以我还是愿意在关键链路上保留这一环。3. 记忆 知识库配合让 Agent 又懂人、又懂行3.1 两者分开存、分开取prompt 里再合流知识库和记忆在本质上是两种完全不同的资产。知识库是领域公共知识比如公司制度、产品手册、行业研究报告它对所有用户一视同仁记忆是用户个性化情报只对特定用户生效。两者必须分开存储、分开召回绝不应该混在同一个库里。我见过有人图省事把用户偏好当成知识也传进了公共知识库结果用户 A 的一句“我喜欢简洁回答”被检索出来影响到了所有用户的回答风格这是灾难。分开存储之后在 prompt 层面再合流准备两个独立检索链路一个查记忆一个查知识库然后把结果分别放进两个命名区域。这样做的好处是逻辑清晰、方便调试而且可以分别控制注入上限。知识库内容再多也不会挤掉用户画像用户画像再长也不会抢占知识库的事实空间。我还习惯在 prompt 里把两个区域标注清楚让模型明确知道“这部分是这位用户的个性化情况”“这部分是检索到的权威资料”两者出现矛盾时模型也更清楚改以哪个为准。实测下来这种显式分区比把两堆文本混在一起塞给模型要稳定得多。3.2 一次完整对话里记忆和知识库各自发挥作用用一个实际场景串一下。假设你在做一个活动策划助手。第一轮用户说“我们公司是做母婴用品的品牌主打安全环保最近想做一次低成本联名活动。”这一轮对话结束后记忆抽取模块会写入两条一条事实——公司行业是母婴用品一条偏好——倾向于低成本玩法。过几天用户回来说“帮我想个联名方案预算不多。”这时 Agent 的处理逻辑是先把用户问题送去记忆库检索召回到“母婴用品”“低成本偏好”这些画像信息再去知识库检索“联名活动案例”“低成本活动预算模板”相关内容最后组装出一个三段式 prompt你现在是一名活动策划助手。 【用户记忆】 用户行业母婴用品 用户偏好低成本、安全环保理念 【知识库检索结果】 1. 母婴品牌联名案例片段 2. 低成本活动预算模板片段 【对话历史】 最近几轮原始对话 请结合用户记忆和知识库内容回答。如果知识库没有相关内容请明确说明不要编造。记忆负责“知道用户是谁”知识库负责“知道这个领域是什么”两者合起来回答才算真正既懂人又懂行。如果没有记忆Agent 会再次问用户“你们公司是做什么的”如果没有知识库它就只能凭训练数据里的泛泛常识硬编输出一堆正确但没用的空话。3.3 忘记与更新记忆也需要“断舍离”记忆不是写进去就一劳永逸。用户的行业可能变、职位可能变、偏好也可能随着项目变化。我给记忆表设计 TTL 和定期重写机制超过 90 天未被命中的长期事实自动降权每个季度对画像摘要做一次整体重写。更重要的是“可删除性”用户明确要求“别记住我的信息”时记忆模块必须能按用户维度清理所有关联记忆这是产品合规的底线不是可选项。实操上还有一个细节记忆更新要保留来源轮次。也就是每条 memory_items 都记录来自哪一轮对话、抽取时的原文摘要。这样一旦发现记忆错了能顺藤摸瓜定位到出问题的对话重新抽取修正而不至于变成一笔查不出处的糊涂账。4. 落地实操从 0 到 1 搭建一个带记忆的知识库 Agent4.1 技术路线怎么选Dify、MaxKB、还是自研 Pipeline真刀真枪做项目时先别急着选明星框架要先想清楚你的目标和约束。我建议按下面三条线去选路线。路线上手难度适合场景可控性典型工具可视化搭建低快速原型、Demo、中小业务中Dify、MaxKB半托管框架中需要一定定制化的生产项目中高LangChain / LlamaIndex自研流水线高大规模、强定制、性能苛求高向量库 Embedding 服务自建如果你现在处于学习阶段我特别推荐先用 Dify 这类可视化工具跑通一遍流程因为你能在界面上直接看到知识检索节点、记忆变量的数据流动对建立整体认知非常有帮助。等理解了每个节点的作用之后再考虑用 Python 自研或者用 LangChain 搭一套更灵活的版本。直接一上来就写全套自研代码很容易被切分、向量化、检索这些细节淹没最后连最基础的通路都跑不通。4.2 Dify 搭建知识库与记忆的工作流细节用 Dify 落地一个知识库 Agent核心步骤可以分为四段。第一段是配置模型和知识库。在设置里接入 LLM 的 API Key同时配置 Embedding 模型。创建知识库时有两个参数要特别留神分段长度和分段重叠。我处理中文文档时一般设 300 到 500 字符、重叠 50 到 80 字符如果文档是结构化 MarkdownDify 也支持按标题分段优先用这个模式。索引方式上“高质量模式”走向量检索命中率更高“经济模式”走关键词适合快速体验但不建议生产用。第二段是上传文档和召回测试。文档导入之后建议进知识库详情页看一眼切分预览确认没有把表格、代码块、标题乱切。然后赶紧做“召回测试”——把用户可能问的问题输入进去看返回的 chunk 是不是你预期的那几段。这一步最重要因为你在测试阶段多花十分钟就能在未来省下几十次线上吐槽。第三段是在 Chatflow / Agentflow 里编排链路。基础流程是用户问题进来 → 知识检索节点按问题去知识库取 Top-k → 把结果接到 LLM 节点一起生成回答。知识检索节点里有几个参数值得调检索上限我一般设 3 到 5 段太多容易稀释答案分数阈值低于阈值的片段直接丢弃能显著减少胡答重排序开关有条件就开命中率提升明显。第四段是实现用户记忆。Dify 里可以做两层记忆会话变量负责保存当前多轮对话中的临时状态比如“用户正在选的套餐编号”长期记忆则建议单独建一个“用户画像”数据集每次会话结束后用一条 LLM 节点把本次对话抽取出的用户事实写入这个数据集。要注意写入时机我习惯用“对话结束事件”触发更新避免每轮都写造成脏数据。抽取那个 LLM 节点记得用结构化输出直接输出 JSON方便后续按字段入库。4.3 最小 Python 版 RAG 记忆参考实现工具跑通之后如果你还想深入理解底层逻辑建议用 Python 自己写一版最小实现。下面这个示例刻意做了简化但链路是完整的——召回记忆、检索知识、组装 prompt、调用模型、异步写回新记忆每一步都有注释# 简化的 RAG 记忆示例仅用于理解链路 def answer(user_query, memory_store, docs_store): # 1. 召回用户记忆向量检索 Top-k user_memory retrieve_top_k(memory_store, user_query, k5) # 2. 检索知识库混合检索或纯向量检索 knowledge retrieve_top_k(docs_store, user_query, k3) # 3. 组装 prompt记忆放前知识靠后 prompt f 你是 AI 助手。 用户记忆{user_memory} 知识库内容{knowledge} 用户问题{user_query} # 4. 调用 LLM 生成回答 reply llm_call(prompt) # 5. 抽取本轮新记忆并写回建议异步执行 new_memory extract_memory(user_query, reply) memory_store.add(new_memory) return reply其中 retrieve_top_k 在最小示例里可以用“加载全部向量 → 算余弦相似度 → 排序取前 N”来实现生产环境则换成 Qdrant、Milvus 或 pgvector。extract_memory 就是用 1.2 节里的结构化抽取 prompt 调 LLM。生产落地时还需要加一些东西文档导入用离线任务处理不放在在线推理链路里向量化调用可以加缓存避免重复计算记忆写入与主流程解耦即使写入失败也不能拖垮回答主链路。4.4 测试与验证怎么判断记忆和知识库真的在生效搭建完成之后真正容易被跳过的是系统化测试。我自己的习惯是准备三大类验证用例记忆验证、知识验证、边界验证。记忆验证的做法是先让用户主动交代一个事实换几个话题后再回到相关问题看 Agent 记不记得知识验证则提问知识库里的专属内容确认检索命中并能引用来源边界验证最有价值——问一个知识库里不存在的问题看 Agent 会不会老老实实说不知道。如果它依然自信满满地编造答案说明知识库约束没有生效风险非常高。验证类型测试方法预期结果常见失败原因记忆验证对话后换话题再问相关事实能直接引用用户之前说的信息记忆写入失败或召回 top-k 未命中知识验证问知识库内的专属内容回答与知识库片段一致且有引用切分过碎、检索召回不聚焦边界验证问知识库外的问题明确回答不知道prompt 未强调依据来源回答每一类最好跑十组以上不同问法而不是只测一组就下结论。还可以打开详细的日志观察每一步实际拿到的记忆向量得分、检索到的 chunk 文本以及最终拼给模型的 prompt 长什么样。很多诡异问题乍一看是模型不行查日志之后才发现是检索环节根本没拿到该拿的内容。5. 高频问题与排查笔记5.1 检索经常空转或召回无关内容这个问题最常见的原因是 Embedding 模型选型不对。我见过英文模型硬扛中文文档出来的向量乱成一团怎么查都不准。先确认 Embedding 模型是否为中文优化过。第二个原因是用户提问太口语化直接拿去检索效果差建议在检索前加一道查询改写。第三是分段太碎每段只有一句话语义信息不足检索容易“看着像相关实则不相关”。另外加上最小分数阈值可以拦截低质量命中宁可少召回也好过让模型拿着无关内容硬编。5.2 知识库内容更新了回答还是旧内容这个问题多半出在索引更新上。你替换了文档文件但向量库里的老向量并没有被自动清理和重建检索时老片段仍然会被召回。解决方法是建立版本管理每次导入新版本文档就生成新的数据集版本确认无误后把 Agent 关联切到新版本而不是在旧数据集上原地覆盖。如果用了缓存层还要记得清理或设置合理的缓存过期时间。我实际踩过这个坑当时把文档传到本地知识库后怎么测都是旧回答最后发现是索引没重建白白浪费了一个晚上。5.3 记忆写了一大堆模型还是“记不住”先去看日志里的记忆召回得分。如果召回环节根本没把相关记忆查出来写得再多也没用。一个常见原因是用户画像存在向量库但问句和画像字段本身差异很大比如画像字段是“company_industry跨境电商”用户问题是“帮我写一封海外营销邮件”语义上相关度并不高Top-k 根本没命中。解决办法是把关键事实同时写入结构化字段每次会话主动加载少量高置信画像而不是完全依赖向量检索。另外检查 prompt 里记忆区块的位置和长度位置太靠后或长度超限都会被截断。5.4 长对话上下文爆炸成本越来越高随着对话轮数增加直接拼接历史会让上下文迅速膨胀。我的处理方法是滚动摘要每累计 N 轮就调用一次 LLM把历史压缩成摘要之后只保留摘要和最近几轮原文。记忆模块只存提炼后的关键事实不存对话全文。知识库检索的 chunk 也按需截断不要把整段塞进去。还有一个容易被忽略的细节如果每轮都调用记忆抽取token 开销会翻倍把抽取从每轮改成会话结束批量处理成本能立刻降下来记忆质量反而更稳。5.5 高频问题速查表现象最常见原因快速解决办法召回无关内容Embedding 模型不适配 / 分段过碎换中文优化模型调整 chunk 和 overlap更新后仍答旧内容向量索引未重建 / 缓存残留重建索引切换数据集版本记忆写了很多但用不上召回环节未命中 / 注入被截断增加结构化字段直查调整 prompt 顺序上下文爆掉历史全量拼接 / chunk 过长滚动摘要 按需截断知识库外问题被硬答prompt 未约束依据来源增加“无法回答时明确说明”指令中文切分乱按英文空格切 / 无分层按标点和标题结构切配置 overlap最后说点我自己的体会。把记忆和知识库放在一起做之后我才真正理解它们其实是两种完全不同的能力知识库让 Agent 变得专业记忆让 Agent 变得像人。如果你做的是行业问答机器人我建议先集中精力把知识库的检索命中率做到足够高再碰记忆但如果你做的是销售助手、客服助手这类强个性化产品记忆的优先级反而要提前否则用户会觉得这 AI 是“金鱼脑”体验会大打折扣。调试阶段一定要多开日志观察中间过程不要只看最终回复。我每次排查这类问题第一步永远是去看检索命中的得分和注入的 chunk 文本这一步往往能直接暴露问题根源比反复调 prompt 快得多。