1. 学习 Agent 越往后,越发现“记忆”不是功能而是架构决策
我是在做系列笔记第三篇的时候踩到一个问题的。当时我搭的 Agent 已经能调用工具、能拆解任务、能在一次会话里完成多轮推理,看起来什么都会。但换一个新会话来问它:“你还记得我上次让你查的那几款数据库吗?”它直接愣住了。它确实“读过”那部分内容,但那些内容只存在于上一次对话的上下文窗口里,会话一结束就全部清空。那一刻我突然意识到,Agent 的能力上限根本不取决于模型多聪明,而取决于它能记住多少、能调用多少外部信息。这也正是我把第四篇笔记的主题定在“用户记忆与知识库,从四种存储格式到 RAG 检索”的原因。
用户记忆和知识库是两种不同性质的信息载体。用户记忆回答的是“这个用户是谁、他偏好什么、我们之间发生过什么”;知识库回答的是“我手上的资料里有哪些事实和内容可以用”。它们在 AI Agent 里的落点也不同:用户记忆偏个性化、强更新、量不一定大;知识库偏领域化、相对稳定、通常需要支持语义检索。本文这篇笔记不是泛泛讲概念,我会从存储格式的选型逻辑讲起,一直讲到知识库如何切片、如何向量化、如何做 RAG 检索,再加上我实际调试中的失败记录。内容面向正在做 Agent、准备接知识库或者想给 Agent 增加记忆能力的开发者,硬核居多,但也尽量把原理讲到“小白能听懂”的程度。
先说一个我的判断:很多 Agent 项目做不好记忆,不是因为模型不够强,而是从一开始就没想清楚“记忆应该放在哪里”。所以这篇笔记的第一部分,我们先不急着谈技术选型,而是先把记忆本身拆开来看。
1.1 上下文窗口不是记忆,它只是一块临时工作台
在 AI Agent 开发里最常见的误解,就是把模型的 context window 当成记忆。大模型确实能在一段很长的上下文里保持连贯性,但这个“连贯”有严格的边界:一次会话、有限 token。一旦超出上下文窗口,早期内容会被截断;一旦会话关闭,所有内容都不复存在。
我习惯把 context window 类比成你的办公桌。桌面上能摊开很多资料,方便你当下处理;但办公桌不是仓库。真正的记忆系统,应该是把需要长期保留的内容从桌面上归档到仓库里,下次需要时再精确取回。同样,AI Agent 里的记忆,指的是可以跨会话持久化存储,并在恰当时间被检索回来的信息。所以设计 Agent 的时候,第一件事就是分清:哪些内容只存在于本次会话工作区,哪些内容要落入持久化存储。
举个例子,一个客服类的 Agent,用户问“我的订单多久能到”,这个查询上下文不需要长期保存,但用户的姓名、所在城市、历史投诉记录、上次沟通过的结论,这些都是长期记忆。如果一个 Agent 把这些全部塞进每次的上下文里,很快就会被 token 撑爆;如果不塞,又无法回答问题。这就要靠记忆的分层来化解。
1.2 我给 Agent 记忆做的三级分类:用户画像、历史对话、业务资料
我在实际设计记忆结构时,会把需要“记住”的内容拆成三类,每一类的存储方式、更新频率、检索方式都不一样。
第一类是用户画像和偏好型记忆。包括用户的姓名、性别、年龄段、工作领域、偏好设定、常用术语、产品偏好,甚至用户明确说过的“以后推荐内容不要包含短视频”这样的指令。这类记忆的特点是:单条体积小、结构化程度高、更新不频繁、读取频繁。比如一个私人助手 Agent,用户首次说过“我在上海做 UX 设计”,后面每次对话都应当默认这个背景,不需要再问一次。
第二类是历史会话与关键决策型记忆。比如用户上次和你讨论了哪些话题、最终选型是什么、哪些方案被否定了、当时的理由是什么。这类记忆的特点是:体积不断增长、存在明显的时序关系、有时候需要按语义去检索。用户可能隔了一个月问:“上次我们说的那个低代码平台你帮我再看一眼”,这里就不能简单地拉出最后一条聊天记录,而要能从历史中找到“低代码平台”相关的上下文片段。
第三类是业务知识和资料库型记忆。这块就是通常说的知识库,常见来源有产品文档、培训手册、行业报告、历史工单、企业 wiki。这类内容不属于某个用户的隐私偏好,而是 Agent 回答问题时的事实依据。它通常比较稳定,但量往往很大,需要支持“用户问一句自然语言,Agent 能定位到文档中若干片段并组织答案”。
把这三类分开,是我后来做记忆系统最受益的一步。因为如果你把它们混在一起存,比如把所有聊天记录和文档全部灌进同一个向量库,不做任何标签和分区,那检索时返回的片段会来自各个层面,跟用户意图对不上是常态。分开之后,每条数据就带上了它自己的“身份”,召回时能精确限定范围。
1.3 知识库本质上是可替换的外部记忆,这也是 RAG 存在的理由
我自己的理解是:知识库也是一种记忆,只不过它更像是“外部硬盘”,而不是“大脑内部的神经元连接”。模型参数里其实也封装了一部分知识,那是它预训练阶段学到的,但那是静态的,无法针对你的业务做即时更新,也无法精确追溯来源。知识库存在的意义,是把模型的知识与业务库的知识解耦,让 Agent 在面对领域内问题时可以实时去查阅外部资料。
这就是 RAG(检索增强生成)的底层逻辑。Agent 收到用户提问,先从外部知识库中找到若干最相关的片段,再把问题和片段一起交给模型,让模型基于给定资料来生成回答。这样做有三个直接好处:一是业务知识可以随时改文档来更新,不用重新训练模型;二是回答可以附上引用来源,可追溯;三是能明显减少模型凭记忆“胡编”的风险,只要检索准确,回答就更可控。
而 RAG 的效果上限,几乎完全取决于两件事:知识库里资料的组织方式和检索质量。组织方式出问题,检索结果就乱;检索质量不达标,再强的模型也会被噪音上下文带偏。这两种问题我都实际遇到过,后面我会详细展开。如果你现在正准备给 Agent 加知识库,我的建议是先读完全文再动手,很多坑能提前避开。
2. 四种存储格式逐个过一遍:JSON、关系表、向量库、图库分别装什么
接下来进入这节笔记的硬核部分。标题里写了“四种存储格式”,我先说明白是哪四种:JSON/文本文件这类轻量文件存储、关系型数据库、向量数据库、图数据库。它们不是竞争关系,而是应对不同记忆类型的工具。我在给某一种记忆选择存储时,会问自己三个问题:这些数据需要按语义查找吗?需要频繁更新吗?需要在实体之间做多跳关系遍历吗?答案不同,落库选择就不同。
2.1 JSON 与文件级存储:适合固定的用户画像,别存对话流水
先看最简单的 JSON。很多 Agent 框架里有一个概念叫 memory file,典型存在方式是一个 JSON 文件,里面存一组结构化字段,比如用户的基本信息、个性设定、偏好设置。不同框架的实现细节不完全一样,但原理很接近:Agent 启动时读取这个文件形成系统提示的一部分,对话过程中如果用户提出了新的偏好设定,Agent 会通过工具调用更新这个文件。
JSON 存储的优势非常明显:可读性好、实现起来几乎零成本、方便调试。你可以在不依赖任何数据库的情况下直接打开文件看内容,看 Agent 到底记住了什么。对个人项目或者单用户场景,用户画像这种本身就很适合用 JSON 存。下面给出一个最小的记忆文件示例:
{ "user_id": "u_2024001", "profile": { "name": "林一", "city": "上海", "job": "产品经理", "industry": "企业服务", "level": "老用户" }, "preferences": { "answer_style": "简洁干练,少讲废话", "topic_filters": ["不做短视频推荐"], "tools_frequently_used": ["sql", "excel"] }, "updated_at": "2025-06-18T21:40:00+08:00" }这样一份文件,注入到 Agent 的 prompt 里,比让用户每次重新介绍自己高效得多。但 JSON 这种格式有明确的边界:它不适合存随时间持续增长、需要按语义查找的对话历史。原因很好理解。第一,JSON 是按整体加载的,如果对话历史有几万条记录全放里面,一次读取就会把文件撑大,每次注入都消耗大量 token;第二,JSON 无法做条件过滤,取一段记录也要读全量,性能会越来越差;第三,如果多个请求同时更新同一份文件,容易出现写冲突。
所以你如果用 JSON 存记忆,我建议只装那些“一次性写入、低频更新、单条体积小”的用户画像数据,并且保留 updated_at 这类时间戳字段,便于理解记忆的新鲜度。一旦发现某个文件超过几百 KB 还在频繁读取,就该考虑迁移到更合适的存储了。
2.2 关系型数据库:用户画像和高频业务记忆的稳定底座
对需要支持多用户、有权限隔离、需要按字段条件查询的 Agent 来说,关系型数据库是很自然的选择。拿 PostgreSQL 举例,我们可以为记忆设计一张很直接的表:
CREATE TABLE user_memory ( id BIGSERIAL PRIMARY KEY, user_id VARCHAR(64) NOT NULL, memory_type VARCHAR(32) NOT NULL, -- profile / preference / history_summary / etc content TEXT NOT NULL, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_user_memory_user_type ON user_memory (user_id, memory_type);这张表把用户画像、偏好规则、历史摘要都作为记录存起来,每一条都带 user_id 做隔离。这样做的好处是支持百万级甚至更高的用户量,更新某一项偏好时不需要重写整个用户文件,而且可以用条件过滤精确地取数据。比如 Agent 收到用户说“以后我的邮件回复语气要更专业”,你只需要 UPDATE 一条记录,而不是把整份记忆重写一遍。
很多知名的开源知识库项目,比如 Dify 和 AnythingLLM,看起来像是有自己独立的存储体系,其实它们的底层配置数据、文档记录、会话记录依然依赖 PostgreSQL 这类关系型数据库。如果你深度使用过这些项目,就会发现它们都会建议你提供数据库连接串。这也说明了一个趋势:复杂的 Agent 场景不会只用单一存储,关系表承担的是元数据和业务结构化数据,向量承担的是语义检索数据。
关系型数据库不仅仅适合存用户画像。如果你的业务场景要求“用户最近一次选择的模型”“用户知识库的权限名单”“历史会话的摘要条目”这类明确的最新状态或可枚举查询,关系表都是更可靠的方案。有一点要特别提醒:不要在一个 PG 表里塞 BLOB 类型的原始大文本然后每次全字段读取,那样性能会很难看。把长内容单独放,需要的时再取。
2.3 向量数据库:为“语义相似”而生的检索层,但不是万能库
向量数据库是 RAG 架构里的核心组件。它做的事情是:把一段文本通过嵌入模型转换成一个高维数值向量,然后存储起来,查询时用同样的嵌入模型把问题变成向量,再在库里做相似度计算,召回最接近的片段。
为什么需要它?因为传统数据库是按字段精确匹配的,它处理不了“用自然语言找相似含义”的问题。比如你的知识库里有一段话写的是“服务有效期为购买之日起 12 个月,到期前 30 天可在控制台续费”,用户问的是“我的会员还能用多久?”,SQL 的 LIKE 查询根本匹配不到“将会员”和“有效期”这两个不同表述,但向量相似度可以做到。这就是语义检索的价值。
常见的向量数据库包括 Qdrant、Milvus、Chroma、Weaviate,以及 PostgreSQL 的 pgvector 扩展。如果是个人学习项目,Chroma 或者 pgvector 都够用;如果是生产并行度高的项目,Milvus 和 Qdrant 会更合适。但我前面也说了,向量数据库不是万能库,它不适合做精细的事实同步和状态管理。假设用户在个人设置里改了自己的称呼,这属于单条精确更新,你不需要也没有必要把它变成向量再召回。更合理的做法是把用户最新状态放在缓存或关系库里,向量库只存那些“真正需要靠语义捞回来”的历史记忆片段。这里有一条很重要的工程原则:能用精确查询解决的,就不要使用语义检索,否则召回结果会不可控。
使用向量数据库还有一个很多人忽略的细节,就是metadata 过滤。你不能把不同用户的历史记忆全部向量化之后不做区分地放进同一个 collection,否则用户 A 提问时很可能会召回用户 B 的私密内容。正确做法是给每条向量记录附上 user_id、timestamp、memory_type 之类的元数据,查询时先做元数据过滤,再做语义相似度检索。这是向量数据库落地的安全红线。
2.4 图数据库:当记忆需要“顺着关系走”的时候才值得上
第四种格式是图数据库,代表产品是 Neo4j。它的数据模型是节点和边,特别适合表达实体之间的多跳关系。举个例子,如果 Agent 服务的是一个知识管理场景,用户有“项目 A”“项目 B”,每个项目关联了不同的人员、技术栈、供应商,人员之间还有协作关系,这个时候如果想回答“哪些供应商同时服务于客户 X 和客户 Y 的项目”这类关系型问题,图数据库会比向量库和关系库都更自然。
但图数据库在大多数 Agent 项目里的优先级并没有那么高。因为它引入的复杂度较高,查询语言 Cypher 也不是所有团队都熟悉。我自己的判断是:如果应用的核心是“实体关系密度高、需要通过关系链进行推理”的知识图谱场景,才值得引入图数据库;如果只是把几份文档切片做问答,那向量库就够了,不需要为了“像图谱”而强行建模。
一个比较现实的做法是复合使用:向量库承担语义召回,关系表承担状态和权限,图数据库在业务出现明确的关系探索需求时再单独建设模块。对于刚开始学习 Agent 的开发者,我的建议是先驾驭好 JSON/关系表 + 向量库的组合,图数据库放在后面遇到真实场景时再学,效果会更好。
2.5 四种格式的选择参考表
为了便于后面查,我把四种格式的选择思路整理成下面的表。每次要“存一种记忆”的时候,对照这张表就行。
| 存储格式 | 适合存放的记忆内容 | 检索方式 | 典型代表 | 不适合的场景 |
|---|---|---|---|---|
| JSON / 文件 | 单用户画像、简单偏好、Agent 配置 | 整体加载 | Memory File 类机制 | 高并发、持续增长、语义检索 |
| 关系型数据库 | 用户画像、历史摘要索引、权限信息、知识库元数据 | SQL 条件查询 | PostgreSQL、MySQL | 大段非结构化文本的语义检索 |
| 向量数据库 | 对话历史片段、文档切片、需要按语义召回的内容 | 相似度检索 + 元数据过滤 | Qdrant、Milvus、pgvector | 精确状态更新、业务事务处理 |
| 图数据库 | 实体关系图谱、多跳关联查询 | 图遍历 | Neo4j | 通用知识文档问答、低关系密度场景 |
这张表不是死的。很多项目最终都会走向混合存储:用户画像存关系表、细碎偏好用 JSON 缓存、历史对话按摘要进关系表同时切片进向量库、事实性资料统一进 RAG 知识库。理解了每种格式的边界,再组合使用心里就有底了。
3. 知识库接入实操:从切片、向量化到检索的完整链路
说完存储选型,现在我们进入知识库的具体实现。不管你是从零写代码,还是用 Dify、AnythingLLM、Coze 这类平台工具,底层跑的都是同一条流水线:源文档采集、清洗、切片、向量化、写入向量库、查询时做召回。很多人以为知识库就是“把 PDF 传进去就能用了”,这是误解。传文件只是第一步,切片和向量化的质量决定整个 RAG 系统的表现。
3.1 为什么做检索增强而不是微调模型
在动手前,先回答一个绕不开的问题:要给 Agent 注入业务能力,为什么选 RAG 而不去做模型微调?
我的观点是,两者解决的问题不同。微调适合改变模型的行为风格和固定输出格式,比如让模型学会用某种语气、某种领域术语的说话习惯;但如果要注入的是持续更新的业务事实,比如产品手册、售后政策、最新报价,微调就很不划算了。一是文档业务更新频繁,每次更新都重新花钱训练不现实;二是微调后的事实仍然可能出错,模型会一本正经地编造不存在的条款;三是微调结果不可解释,无法告诉用户“这句话来自哪份文档”。
反之,RAG 的机制是外挂知识库。上线之后想改知识内容,只需要替换对应的文档片段,不需要动模型本身。这种解耦对业务非常重要。还有一个容易被忽略的点:RAG 能提供引用来源。在企业场景里,客服回答用户“根据某政策第几条”能不能点开溯源,决定了这套系统能不能真正落地,而微调模型很难做到这一点。
3.2 切片策略:粒度大小直接决定召回质量
知识库处理的第一个关键动作是切片。切片就是把长文档拆成若干段。切片长度会直接影响召回效果:切得太长,一段内容包含多个主题,向量化后语义被平均化,查询召回时片段回答问题不精准,还会浪费上下文 token;切得太短,单个片段信息量不足,模型拿到片段后找不到完整上下文,同样回答不好。
常见的切片策略有几种,我实际用下来各有适用场景。第一种是按固定长度切片,比如每 512 个 token 一段,适合内容结构比较简单、段落都不太长的文档。第二种是按文档结构切片,比如优先保留 Markdown 的标题层级,把同一个二级标题下面的所有内容作为一个大段,如果太长再按小标题拆。这种方法对操作手册、API 文档这类层级清晰的文本效果很好。第三种是递归切分,比如 LangChain 里的 RecursiveCharacterTextSplitter,按分隔符优先级逐层尝试,在尽量不破坏语义的前提下控制长度。
这里特别想强调一个工程细节:重叠(overlap)。切片时通常会给相邻片段之间设置一小段重叠区,比如每段 500 token,重叠 50 token。这样设计是为了避免一句话在切片边沿被拦腰斩断,从而保证查询时能拿到完整的语义。我在实际测试中发现,不设置重叠时,知识的召回率会明显下降,尤其遇到长句、引用格式的文本更是如此。建议你项目中至少要保留 5% 到 10% 的重叠比例,具体数值根据文档的句子长度微调。
还有一类更高级的做法叫父子切片。做法是生成两层索引:父块是大段落,子块是小片段。检索时先召回小片段的向量,但最终把该小片段所属的父级段落提交给大模型。这解决了“片段虽准但上下文不够”的矛盾。可以把父块理解成章节,把子块理解成章节里的一个重要句子,查询先定位句子,回答却带着整个章节的逻辑去生成。在我处理过的高质量长文知识库中,父子切片的综合效果是最好的,尤其文档的推理链路跨越多个自然段时。
3.3 向量化操作细节:选对嵌入模型,保存元数据
切片完成之后,每一段文本会经过嵌入模型变成向量。嵌入模型的选择要结合你内容的主要语言。如果你的知识库以中文为主,尤其建议选择对中文支持更好的模型,比如目前常用的 bge-m3、m3e 系列,以及各家云厂商提供的中文向量模型;如果知识库本身就是英文技术文档,用 OpenAI 的 text-embedding-3-small 这类通用模型也够用。我在对比测试中观察到中文长尾词、口语化表达在不同嵌入模型上的表现差距很大,上线前建议用你自己的知识库做一组 mini 测试,不要只看宣传指标。
向量化还有几个容易踩的坑。第一,模型输入通常有 token 限制,切片总长度必须控制在限制以内,所以前面切片参数不能拍脑袋定太大。第二,调用嵌入接口时建议做批量处理而非逐条调用,既能减少网络开销,也能避免触发服务方限流。第三,插入向量库的每条 record 都要带上必要的 metadata,至少包括文档 ID、切片 ID、来源名称、用户 ID(如果是用户私有知识库)、更新时间,这样后面才能做过滤。没有 metadata 的向量库就像一堆没有标签的档案,后期想按来源筛选或者做权限隔离都无从下手。
向量库写入完成之后,知识库的索引工作就算告一段落。实际跑起来你会看到,真正影响使用体验的还在于查询端的检索和重排逻辑。这里牵出 RAG 链路中最重要的一个判断:拿什么标准决定“哪些片段是用户问题的答案依据”。如果只是单纯比对问题和所有切片的向量相似度,取 TopK 直接拼给模型,很多场景是不够的,问题就出在语料的碎片化和向量相似度对“精确匹配”的天然弱势上。
3.4 平台工具越方便,越要理解背后的检索设置
Dify、AnythingLLM、Coze 这类平台大大降低了知识库的接入门槛。尤其 Dify 的知识库流水线做得相当直观,你上传文档后可以选择分段规则、索引方式、检索模式,还提供召回测试工具。但我提醒一句:用平台不等于不用理解原理。这些平台只是把链路封装了,它内部的切分设置、检索模式选错,依然会导致模型回答质量差。
如果在 Dify 里搭建知识库,我建议重点关注三个设置。一是分段设置,默认分段规则很可能不适合你的文档,要根据文档结构改成“按标题层级切分”或者更合适的规则。二是索引方式,对于需要高质量回答的知识库,建议开启高质量模式(使用 Embedding 模型而非关键词索引)。三是检索模式,从“向量检索”切换成“混合检索”,然后配合 Rerank 模型会明显更稳。不要怕 Retrival 测试麻烦,把用户经常问的问题拿出来逐条跑召回测试,比上线后再返工高效得多。
4. RAG 检索不准?我从召回和重排两个环节逐个排查
知识库搭好之后,最常收到的反馈是:“文档我已经传进去了,但 Agent 回答还是不对。”这里边有相当一部分问题出在检索环节,而不是模型本身太笨。我把自己调 RAG 的过程拆成一条完整的排查链路,希望能在这篇笔记里帮各位复现。你可以对照着检查:用户提问之后,系统到底检索出了什么片段?片段是否真的跟问题相关?这些片段被注入提示词后,信息之间是否冲突?回答最终有没有忠实于给定材料?
4.1 先定位问题出在哪一环,不要一上来就怀疑模型
有一次我需要让 Agent 查一份内部技术规范,文档已经切好了,向量库也写进去了,但无论用户怎么问,回答都差一截。比如用户问“服务超时时间默认是多少毫秒”,Agent 给出的答案来自另一段讲客户端重试策略的内容,虽然两个片段离得不远,但答非所问。
我当时的第一反应是换更强的模型,结果换完之后问题依旧。后来我打开知识库的召回测试面板,逐条查看查询召回的前几个片段,才发现问题出在切分上:原始文档里“服务超时”“重试策略”“连接池大小”这些不同主题的配置项在源文件里是逐条排列的,我的语义切分器把它们整体切成了一大段,导致一个向量包含了好几个配置项。查询“默认超时时间”时,召回的片段虽然关联到“超时”两个字,但实际文本同时包含大量重试策略细节,模型看多了无关信息就被带跑偏了。
从此我养成了一个习惯:无论用什么 RAG 框架,先做召回测试,再讨论生成结果。Dify 的知识库里自带“召回测试”,AnythingLLM 也提供对话前检索的可视化展示。如果发现召回的片段本身不相关,那是检索和切片的问题;如果召回片段已经足够相关但回答仍然不对,这时才应该去检查提示词和模型参数。不要在还看不清召回结果的时候就去调 prompt,那样只会越调越乱。
4.2 召回层优化:混合检索、查询改写与重排序
在召回层面,向量相似度检索不是万能的。它的弱点在于对专有名词、缩写、数字敏感。比如用户的问题里包含具体型号“XTR-330”,但文档里全称写的是“XTR 330 工业控制器”,两者语义接近,向量可能会漏掉或匹配不准,此时传统的关键词检索往往有奇效。
所以业界的主流方案不是二选一,而是混合检索。混合检索的做法很简单:把向量召回 TopK 结果和 BM25 关键词召回 TopK 结果合并起来,使用某种融合算法(比如 RRF,Reciprocal Rank Fusion)合并排序,取综合最优的若干片段。这样做的好处是两条路互补:向量负责理解语义,关键词负责精确捕捉特殊符号和编号。尤其在技术文档、产品型号、法规条文这类充满精确实体词的知识库里,混合检索几乎是必须配置。
有的场景还需要额外的查询改写。用户提问的口语化程度很高,比如“上次那个事情后来怎么样了”,这种问题直接拿去向量检索,效果基本上不会好。需要在检索之前,让 Agent 结合会话上下文把问题改写成独立自足的描述,比如“我们上次讨论的供应商合同审批流程后来怎么样了”。现在不少 Agent 框架都支持这种查询改写模块,配合一个简单的大模型调用就能实现。改写消耗的那一点延迟,在召回质量提升面前完全值得。
召回合并之后,还有一个值得加上的组件是重排序(Rerank)。先用较轻量的方式召回 20 到 50 个候选片段,再用专门的 Rerank 模型做精细排序,取前 3 到 5 个提交给大模型。Rerank 模型直接对“查询-文档片段”的匹配程度打分,准确率通常比单纯向量相似度更可靠。中文场景里可以考虑使用 bge-reranker 系列,也可以接入云厂商提供的 Rerank API。实际测试下来,Rerank 能明显把“正确的答案片段”往前推,它带来的提升往往比换更强的大模型更直接。
4.3 索引层容易忽略的脏数据问题
除了检索算法本身,索引层的质量同样重要。你清洗过的文档、切好的切片,是不是真的干净,直接影响最终效果。这里的“脏数据”有三种情况比较典型。
第一种是重复文本。同一份知识被上传了多次,导致向量库里存在大量重复片段,召回时把同样的内容重复占住几个位置,挤压了其他有效片段的空间。处理方式是在写入前做去重,可以用 MinHash 或 SimHash 做文本相似度去重,也可以在写入时检查文档标题与内容哈希。第二种是坏切片。有些 PDF 转出来的文本段落错乱、或者大段代码和日志混在一起,会被切出一些无意义的片段。需要在文档导入时增加清洗逻辑,按源文件的实际情况过滤掉那些明显不属于文档正文的内容。第三种是缺少版本意识。文档更新后,旧切片没有失效,新旧内容同时存在于向量库里。当新旧信息矛盾时,模型会优先采信哪个片段完全不可控。这时的对策是每条向量记录都带 version 或 updated_at 元数据,查询时或写入时主动过滤掉旧版本。
我对知识库项目的一个基本态度是:先保证进库资料的干净,再谈检索算法的优化。垃圾进、垃圾出这句话,放到 RAG 链路上被展现得特别彻底。你可以在重排模型上花很多精力,但如果切片里混着大段无关代码,再好的重排也无法挽回。
4.4 如何衡量一个知识库检索系统的效果
说到评估,这也是我在热词里看到很多人问的“RAG 知识库指标有哪些,如何理解各指标”。我给自己的项目做评测时,通常会把指标拆成检索层的指标和生成层的指标。
检索层最常看的是召回率(Recall)和命中率(Hit Rate)。命中率是 TopK 里是否存在至少一条正确片段的比例,可以理解为“系统有没有把正确答案相关的内容找出来”;召回率更严格,看的是正确答案片段有多大比例进入最终结果。重排的效果好不好,可以直接看 MRR(Mean Reciprocal Rank),即正确答案的排名位次的倒数均值,MRR 越高说明正确答案排得越靠前,也就越容易被模型提取到。我自己做回归测试时,会先准备 50 到 100 组“问题-期望片段来源”的标注集,跑一遍管线算出这几个指标,用数字判断优化是否有提升。
生成层指标则需要看回答的忠实度(Faithfulness)和答案相关性(Answer Relevance)。忠实度指的是生成内容是否严格基于召回片段,有没有自己编造事实;答案相关性是最终答案对用户问题的覆盖程度。市面上也有 RAGAS 这类开源评测框架,能自动对这些维度打分,但你不要完全相信自动打分器的结论,关键用例最好还是人工看一下。我的体会是,自动评测适合做回归和趋势监控,人工评测适合做上线前的一次性把关,二者配合使用效果最好。
5. 落到一个 “用户画像 + 对话记忆 + 知识库” 的三层结构实例
前面四节把存储和 RAG 拆开讲了一遍。这一节我想把它们整合到一个具体项目里,做一个可以直接参考的原型设计。场景设定我是一个面向企业用户的 AI 助理,用户每天会问产品问题、让助手帮忙查文档、提需求,我需要让这个 Agent 做到三件事:记住用户的身份和常用偏好;能够回溯我和用户之间讨论过的历史结论;能够在回答当前问题时引用公司最新版的产品文档。
5.1 数据结构设计:一张画像表 + 一张历史记忆表 + 一个知识库集合
先看数据层。用户画像用关系表,直接存结构化字段,这是精确信息的主要来源。历史记忆则既存摘要又存向量。为什么不能只存摘要?因为摘要压缩了太多细节,当用户需要追问上次的某个细节时,只有向量库能把最原始的表达捞出来。我建议的做法是关系表里存会话摘要,同时把会话内容切段后写入向量库,每段带上 user_id 和 session_id。知识库就更直接了,一个独立的 Collection 专门装业务文档切片,不区分用户,但区分文档版本。
代码层面的最小实现可以这样定义:
# 伪代码,用于说明链路组合方式 from typing import List import json class AgentMemory: def __init__(self, db, vector_store, embedder): self.db = db # 关系型数据库,保存用户画像和历史摘要 self.vector_store = vector_store # 向量库,保存对话历史片段与知识库切片 self.embedder = embedder # 嵌入模型 def get_user_profile(self, user_id: str) -> dict: row = self.db.query( "SELECT profile FROM user_memory WHERE user_id = ?", user_id ) return json.loads(row["profile"]) if row else {} def retrieve_knowledge(self, query: str, top_k: int = 3) -> List[str]: # 从通用知识库 Collection 里召回,并按元数据过滤版本 results = self.vector_store.search( collection="product_docs", query_vector=self.embedder.encode(query), top_k=top_k, filter={"doc_version": "2025.06"} ) return [r["content"] for r in results] def retrieve_history(self, user_id: str, query: str, top_k: int = 2) -> List[str]: results = self.vector_store.search( collection="chat_history", query_vector=self.embedder.encode(query), top_k=top_k, filter={"user_id": user_id} ) return [r["content"] for r in results]看到没有,关键点在于把两类 Collection 分开:产品文档是公共知识,不带 user_id 过滤;对话历史是私有的,必须带 user_id 过滤。这是一个安全性的分离,绝对不能混。
5.2 运行时的工作流程:先生成提示词,再检索,最后带引用输出
当用户发来一条消息时,完整的处理流程是这样的。
第一步,从关系表取出用户画像,组装成系统提示词的一部分。这里要注意控制提示词的长度,画像信息本来就不长,全部注入问题不大。第二步,对用户输入做一次判断,识别它到底属于“查资料型”还是“回顾历史型”,还是两者兼有。比如“帮我对比一下我们选购的那款软件和企业版的功能差异”,就需要同时检索知识库和该用户的历史会话。第三步,执行混合检索:知识库召回结果、用户历史召回结果、如果有必要的话再把用户最新的几条订单记录从关系库取出来。第四步,对召回片段做重排,把最相关的 3 到 5 个片段放进最终的上下文。第五步,提示词里明确告诉模型“你在回答时只能依据给定的上下文片段,如果答案不在上下文里就明确说不知道”。最后,还要把知识来源附加在回答后面,让用户知道结论来自哪份文档。
我在学习过程中也总结了一个简化的判断口诀:简单状态找关系库,开放问答找知识库,相似经历找历史向量库。当用户问到“我上次大概提了个什么需求”这种需要语义匹配历史的时候,才值得去向量库里捞;如果只需要知道“用户的套餐等级”这样的精确值,直接查关系表就够了,完全不必绕道向量检索。
5.3 关于“要不要给每句话都做长期记忆”的取舍
做多了之后,我对记忆和知识库有了一个新的认知:记忆也是有成本的。这里的成本不只是存储费用,更重要的是每次请求都要从记忆候选里挑出真正有用的内容,挑不好就会污染上下文。如果你想给 Agent 加长期记忆,我的建议是不要什么都记,只记那些极可能在未来被复用到的信息。例如用户主动表达的偏好、历史对话的阶段性结论、关键决策的理由;而一句“今天天气不错”之类的寒暄,该丢就丢。
我自己现在常用的策略是:会话进行中先在工作区短暂保存完整对话,等到一个业务单元结束时做摘要抽取,抽取出来的结论存进关系表或向量库;对于用户明确纠正过的表述,例如“我不喜欢这种详尽的回答风格”,则作为高优先级偏好写入用户画像,并且在系统提示词里固化。这种分频率的记忆写入方式,既保证记忆不过期,又不会因为每条消息都写库而造成性能浪费。
另外值得留意的还有数据过期问题。用户画像不是永远不变的。用户说了一句“我现在开始做跨境电商了”,之前在互联网行业的画像就需要更新。所以系统设计时要允许记忆被覆盖或者标记为过时,可以采用时间戳加置信度的方式,例如画像字段带 last_updated,抓取检索结果时优先返回新的偏好。对知识库来说同样如此:新版本的文档上线后,旧版本就应该被下线,不让 Agent 引用已经失效的资料。
最后说一个我踩过很多次之后的体会:加入用户记忆和知识库之后,Agent 的调试复杂度会上一个台阶。以前你只需要调 prompt,现在你要同时判断问题改写、检索召回、重排结果和上下文拼接,每一个环节都可能出错。所以我的习惯是,给所有关键环节都打印结构化的日志,记录下“用户原始问题、改写后的问题、召回的 Top 片段、每一条片段的得分、重排后的排序、实际注入的上下文、最终回答”。排查问题的时候,这些日志能让你少走很多弯路。在这个系列的学习过程里,我明显感受到,所谓 Agent 能力,本质上是把记忆、知识这些碎片化模块按正确的方式组装起来,而组装用的胶水,就是你对自己数据模型的深入理解。