Agent 速成笔记 · 第 8 章 记忆与检索

Agent 速成笔记 · 第 8 章 记忆与检索

源:Datawhale《Hello-Agents》第 8 章 | 定位:工程实现(认知科学 → 系统设计)| 一句话:给"无状态、内置知识静态"的 LLM 外挂上记忆系统(Memory)与检索增强生成(Retrieval-Augmented Generation, RAG)两条能力线。


0. 一章速览(30 秒)

  • 一句话:LLM 本身不记事儿、也不知道训练数据之外的事;本章用一个记忆系统(四种记忆类型 + 管理器统一调度)和一条 RAG 管道(MarkItDown 转 Markdown → 智能分块 → 向量化 → 检索 → 增强生成)把这两个洞补上,且二者都以工具形式挂到 Agent 上。
  • 本章解决什么问题:让智能体在跨会话的长周期里记住用户与经验,并让回答基于可查证的外部知识而不是凭空生成。
  • 必须记住的 5 个点:
    1. LLM 的两大根本局限是对话状态的遗忘(无状态)与内置知识的局限(静态、有限、有时效截止点)。
    2. 记忆不是"一个向量库",而是四类分工:工作记忆(短期、TTL、纯内存)、情景记忆(事件、时间序列)、语义记忆(抽象知识、知识图谱)、感知记忆(多模态)。
    3. 记忆系统的完整工作流是编码 → 存储 → 检索 → 整合 → 遗忘,对应 add / search / consolidate / forget 四个关键操作。
    4. RAG 的完整链路是切分 → 向量化 → 索引 → 相似度检索 → 重排 → 拼进上下文;每个环节都有独立的优化空间。
    5. 检索质量的三板斧:多查询扩展 MQE(提召回)、假设文档嵌入 HyDE(补语义鸿沟)、统一扩展检索框架(扩候选池 + 去重合并)。

1. 从认知科学到智能体记忆(8.1)

1.1 人类记忆的分层——它就是系统设计的蓝本

认知心理学把人类记忆分成若干层次,这些层次几乎一对一映射到了工程实现里:

层级 持续时间 容量 特征
感觉记忆 Sensory Memory 0.5–3 秒 巨大 暂存感官原始输入
工作记忆 Working Memory 15–30 秒 有限(7±2 个项目) 当前任务的信息处理
长期记忆 Long-term Memory 可达终生 几乎无限 分程序性与陈述性两类

长期记忆再向下细分:程序性记忆(技能与习惯,如骑自行车)与陈述性记忆(可用语言表达的知识),后者又分为语义记忆(一般知识与概念,如"巴黎是法国首都")与情景记忆(个人经历与事件,如"昨天的会议内容")。

为什么重要:不是科普装饰。"容量有限 + 会话级生命周期"对应工作记忆,"时间序列 + 事件完整性"对应情景记忆,"抽象知识 + 关系推理"对应语义记忆——四种记忆类型的参数设计(50 条上限、TTL 清理、时间近因性加权)全部由这张表推出。

1.2 LLM 为什么需要记忆与 RAG:两个根本局限

局限一:无状态导致的对话遗忘。 LLM 在设计上是无状态的——每次请求(API 调用)都是一次独立、无关联的计算,模型不会自动"记住"上一次对话。由此产生四类问题:

  1. 上下文丢失:长对话中早期重要信息被上下文窗口截掉;
  2. 个性化缺失:记不住用户偏好、习惯或特定需求;
  3. 学习能力受限:无法从过往成败中改进;
  4. 一致性问题:多轮对话中可能出现前后矛盾的回答。

原文给的直观例子是:第一次对 Agent 说"我叫张三,正在学习 Python",重开程序后新建一个 Agent 实例再问"你还记得我的学习进度吗",回答是"抱歉,我不知道您的学习进度"。要特别注意一个易混点:第 7 章的 SimpleAgent 会在同一个实例的 _history 里暂存当前对话,所以同进程同实例的连续对话是能带上最近上下文的;但这只是临时消息列表,不跨会话持久化,也不能做长期检索、遗忘与整合。

局限二:模型内置知识的局限。 模型知识完全来自训练数据,因此是静态的、有限的:

  1. 知识时效性:训练数据有时间截止点,拿不到最新信息;
  2. 专业领域知识:通用模型在垂直领域的深度不够;
  3. 事实准确性:通过检索验证来减少幻觉;
  4. 可解释性:给出信息来源,增强可信度。

RAG 的应对思路:在模型生成回答之前,先从一个外部知识库(文档、数据库、API)中检索出最相关的信息,把这些信息作为上下文一起提供给模型。

1.3 架构设计:四层记忆系统 + 五层 RAG 管道

原文的一个关键工程决策是:记忆与 RAG 都封装为标准工具,而不是新造 Agent 类(沿用第 7 章的设计原则)。其中 memory_tool 负责存储与维护对话过程中的交互信息,rag_tool 负责从用户知识库检索上下文,并可将重要的检索结果自动回写到记忆系统中。

记忆系统采用四层架构:

层 职责 代表组件
基础设施层 统一调度、数据结构、配置 MemoryManager、MemoryItem、MemoryConfig、BaseMemory
记忆类型层 按认知类型分工 WorkingMemory / EpisodicMemory / SemanticMemory / PerceptualMemory
存储后端层 向量 / 图 / 结构化持久化 QdrantVectorStore、Neo4jGraphStore、SQLiteDocumentStore
嵌入服务层 文本转向量 DashScopeEmbedding、LocalTransformerEmbedding、TFIDFEmbedding

RAG 系统的核心架构概括为"五层七步",自上而下为:用户层(RAGTool 统一接口)→ 应用层(智能问答、搜索、管理)→ 处理层(文档解析、分块、向量化)→ 存储层(向量数据库、文档存储)→ 基础层(嵌入模型、LLM、数据库)。分层的好处是每层可独立替换:可以把嵌入模型从 sentence-transformers 换成百炼 API,而不动上层业务逻辑。

1.4 30 秒上手:工具化接入骨架

from hello_agents import SimpleAgent, HelloAgentsLLM, ToolRegistry
from hello_agents.tools import MemoryTool, RAGTool

tool_registry = ToolRegistry()
tool_registry.register_tool(MemoryTool(user_id="user123"))
tool_registry.register_tool(RAGTool(knowledge_base_path="./knowledge_base"))

agent = SimpleAgent(name="智能助手", llm=HelloAgentsLLM(),
                    system_prompt="你是一个有记忆和知识检索能力的AI助手",
                    tool_registry=tool_registry)
agent.run("你好!请记住我叫张三,我是一名Python开发者")

这段在干什么:把两个工具注册进同一个 ToolRegistry,Agent 就同时有了"记事儿"和"查资料"两种能力——LLM 通过第 7 章的 Thought/Action 机制自己决定何时调 memory、何时调 rag。


2. 记忆系统:让智能体拥有记忆(8.2)

2.1 完整工作流:编码 → 存储 → 检索 → 整合 → 遗忘

记忆系统的流程直接抄自认知科学对人类记忆形成阶段的描述,每个认知阶段都被映射成具体的技术组件与操作——理解这张映射表,后面的 API 就自然记住了:

认知阶段 含义 对应技术实现
编码 Encoding 把感知信息转成可存储形式 生成嵌入向量 + 补充元数据
存储 Storage 把编码后信息存下来 写入 Qdrant / Neo4j / SQLite
检索 Retrieval 按需提取相关信息 向量检索 + 结构化过滤 + 综合评分排序
整合 Consolidation 短期记忆转为长期记忆 consolidate(按重要性阈值升格类型)
遗忘 Forgetting 删除不重要或过时信息 forget(重要性 / 时间 / 容量三种策略)

注意这是闭环:写入时打上的 importance、timestamp、session_id 正是后续检索调权、整合升格、遗忘淘汰的依据——写入阶段的元数据质量决定下游所有环节的上限。

2.2 四种记忆类型:分工与特征

类型 存什么 存储方案 关键参数
工作记忆 Working 当前会话上下文 纯内存 默认上限 50 条,TTL 60 分钟
情景记忆 Episodic 具体交互事件、学习经历 SQLite + Qdrant 时间序列 / 会话级检索
语义记忆 Semantic 抽象知识、概念、用户偏好 Qdrant + Neo4j 实体关系 + 图谱推理
感知记忆 Perceptual 图像、音频等多模态 SQLite + Qdrant(按模态分集合) 跨模态检索,动态生命周期

几点值得展开:

  • 工作记忆追求极速访问,所以刻意限制容量(默认 50 条)且生命周期与会话绑定,会话结束自动清理;代价是重启即丢失——正好符合"存临时、易变信息"的定位。
  • 情景记忆的价值在于复盘:它保留完整上下文,支持按时间序列或主题回顾,是智能体从过往经验中学的基础。
  • 语义记忆是最复杂的部分,回答"用户是谁、要长期遵守什么规则、某个领域知识点是什么",是智能体形成知识体系与做关联推理的核心。它会在写入时自动做实体与关系抽取,把知识组织成图谱。
  • 感知记忆为对齐日益丰富的多媒体交互而引入,采用模态分离存储——为不同模态创建独立向量集合,避免维度不匹配,同时保证检索准确性。

2.3 MemoryTool:九个操作的统一入口

MemoryTool 遵循"统一入口、分发处理"模式,所有能力都通过一个 execute(action, **kwargs) 暴露:

action 作用 关键参数
add 添加记忆(支持 4 种类型) content / memory_type / importance
search 搜索记忆 query / limit / memory_types / min_importance
summary 获取记忆摘要 limit
stats 获取统计信息 —
update 更新记忆 —
remove 删除记忆 —
forget 遗忘(多策略) strategy / threshold / max_age_days
consolidate 整合(短期→长期) from_type / to_type / importance_threshold
clear_all 清空所有记忆 —

add 在写入时自动完成三件事:会话 ID 自动管理(每个记忆都有明确会话归属,形如 session_20260315_101530)、多模态数据智能处理(感知记忆会根据文件路径自动推断 modality 并存 raw_data)、上下文自动补充(写入 timestamp 与会话信息)。

关键的 importance 参数默认 0.5、取值 0.0–1.0,模拟人脑对信息重要性的评估——它是后续整合与遗忘的唯一依据,所以调用方必须认真给分,而不是一律走默认值。

search 的设计细节:同时接受单数 memory_type 与复数 memory_types(前者会被标准化成后者),并用 min_importance(默认 0.1)过滤低质量记忆。

2.4 四种记忆的检索评分公式(全章最硬的知识点)

四种记忆类型各有自己的加权公式。符号先说明:向量相似度 来自嵌入检索得分,重要性 取 0–1,权重项 0.8 + 重要性 × 0.4 把重要性映射到 [0.8, 1.2]——重要性只做 ±20% 调节,不压过相似度主导排序。

工作记忆采用组合词法检索(TF-IDF 词项权重相似度 + 关键词匹配),最终得分:

基础相关性 base_relevance = 词法相似度 × 0.7 + 关键词匹配得分 × 0.3   # 词法得分>0 时
基础相关性 base_relevance = 关键词匹配得分                            # 词法得分不可用时
最终得分 = 基础相关性 × 时间衰减 × (0.8 + 重要性 × 0.4)

这里必须澄清一个高频易混点:工作记忆用的是 TF-IDF 稀疏词项向量,它来自词频与逆文档频率,不等同于基于稠密嵌入的语义检索——同义词、改写句并不必然匹配上。

情景记忆(结构化过滤 → 语义向量检索 → 综合评分):

最终得分 = (向量相似度 × 0.8 + 时间近因性 × 0.2) × (0.8 + 重要性 × 0.4)

语义记忆(向量检索 + 图检索两路,再融合):

基础相关性 = 向量相似度 × 0.7 + 图相似度 × 0.3
最终得分 = 基础相关性 × (0.8 + 重要性 × 0.4)

权重 0.7 / 0.3 的设计意图:语义相似度是主要因素(保证结果与查询在语义上相关),关系推理作为补充(发现概念间的隐含关联)。

感知记忆(同模态向量检索 + 时间/重要性融合):

最终得分 = (向量相似度 × 0.8 + 时间近因性 × 0.2) × (0.8 + 重要性 × 0.4)

情景记忆与感知记忆都强调"时间近因性",其计算使用指数衰减模型:

recency_score = max(0.1, exp(-0.1 × age_hours / 24))

即 24 小时内基本保持高分,之后随小时数指数衰减,但保底 0.1,不会归零。

为什么情景记忆要 0.2 的时间权重,而语义记忆换成 0.3 的图权重:情景记忆存的是"何时发生了什么",近期事件天然更相关,所以引入时间近因性;语义记忆存的是抽象知识与概念关系,概念本身不随时间失效,所以时间项换成图关系——用实体关联去补足向量检索发现不了的隐含联系。理解这个"权重差异背后的语义差异",是本章最常考的分析点。

2.5 遗忘与整合:记忆的"新陈代谢"

遗忘模拟人脑的选择性遗忘,提供三种策略:

策略 触发逻辑 典型参数
importance_based 删除重要性低于阈值的记忆 threshold,默认 0.1
time_based 删除超过指定天数的记忆 max_age_days,默认 30
capacity_based 存储接近上限时删最不重要的 threshold

整合(consolidate) 借鉴神经科学的记忆固化概念,把短期记忆升格为长期记忆:默认是把重要性超过 0.7 的工作记忆转为情景记忆,也支持把情景记忆转为语义记忆(阈值可调到 0.8)。整个过程自动化,用户不必手动挑记忆,系统会自己识别符合条件的条目并做类型转换。

这两者的配合构成了记忆系统的自净机制:写入时打分 → 高分者被 consolidate 升级存下来 → 低分者被 forget 清掉。如果没有遗忘,向量库会无限膨胀、检索噪声越来越大;如果没有整合,会话一结束工作记忆就全丢了,智能体等于永远学不到东西。

2.6 MemoryTool 与 MemoryManager 的分层

两者的分工体现"关注点分离":

class MemoryTool(Tool):
    def __init__(self, user_id="default_user", memory_config=None, memory_types=None):
        self.memory_types = memory_types or ["working", "episodic", "semantic"]
        self.memory_manager = MemoryManager(
            config=self.memory_config, user_id=user_id,
            enable_working="working" in self.memory_types,
            enable_episodic="episodic" in self.memory_types,
            enable_semantic="semantic" in self.memory_types,
            enable_perceptual="perceptual" in self.memory_types)

这段在干什么:MemoryTool 只管用户接口与参数处理,核心逻辑下沉给 MemoryManager;memory_types 列表按需启用记忆模块——默认只开 working / episodic / semantic,感知记忆因要加载 CLIP/CLAP 编码器而默认关闭(enable_perceptual=False)。典型"用配置换资源"。


3. RAG 系统:知识检索增强(8.3)

3.1 RAG 是什么:三个词拆解

检索增强生成(Retrieval-Augmented Generation, RAG) 结合了信息检索与文本生成。核心思想是:在生成回答之前,先从外部知识库检索相关信息,再把检索结果作为上下文提供给 LLM。拆成三个词看:

  • 检索:从知识库中查询相关内容;
  • 增强:把检索结果融入提示词,辅助模型生成;
  • 生成:输出兼具准确性与透明度的答案。

3.2 完整链路:两个阶段、一条管道

一个完整的 RAG 应用分两大环节:

数据准备阶段(离线):数据提取 → 文本分割 → 向量化,把外部知识构建成一个可检索的数据库。
应用阶段(在线):用户提问 → 检索 → 注入 Prompt → 驱动 LLM 生成答案。

HelloAgents 的端到端管道是一条直线:

任意格式文档 → MarkItDown转换 → Markdown文本 → 智能分块 → 向量化 → 存储索引
                                                                    ↓
用户提问 → (MQE/HyDE 查询扩展)→ 向量检索 → 去重与分数排序 → 拼进上下文 → LLM生成

其中文档载入由微软开源的 MarkItDown 承担,支持文档(PDF/Word/Excel/PowerPoint)、图像(JPG/PNG/GIF,走 OCR)、音频(MP3/WAV/M4A,走转录)、文本(TXT/CSV/JSON/XML/HTML)与代码;PDF 走增强处理分支,其余走统一转换,失败回退纯文本读取。统一成 Markdown 是整条管道的基石——让后续分块能依赖结构信息,而不是盲目按字符数切。

3.3 三代演进:朴素 RAG → 高级 RAG → 模块化 RAG

阶段 时间 检索方式 生成/优化方式
朴素 RAG 2020–2021 关键词匹配,TF-IDF / BM25 检索结果不加处理直接拼接进上下文
高级 RAG 2022–2023 稠密嵌入(Dense Embedding)语义检索 引入查询重写、文档分块、重排序
模块化 RAG 2023–至今 混合检索、多查询扩展、假设性文档嵌入 思维链推理、自我反思与修正

朴素 RAG 的痛点(高频考点):也叫"检索-读取"(Retrieve-Read)模式,用 TF-IDF / BM25 这类词频与文档频率统计来评估相关性,对字面匹配效果好,但难以理解语义上的相似性;同时把检索到的文档内容不加处理地直接拼接,噪声与冗余会一起进上下文,挤占窗口并干扰生成。高级 RAG 的改进正是针对这两点:检索端转向稠密向量以匹配语义,生成端引入重排、改写、分块等优化。

3.4 切分:结构感知分块 + Token 控制 + 重叠

分块是"看似最无聊、实际最影响效果"的一环。Markdown 结构感知分块流程为:

标准Markdown文本 → 标题层次解析 → 段落语义分割 → Token计算分块 → 重叠策略优化 → 向量化准备
       ↓                ↓              ↓            ↓           ↓            ↓
   统一格式          #/##/###        语义边界      大小控制     信息连续性    嵌入向量

实现分两步走:

  1. 按标题层次切段落,保持语义完整。维护一个 heading_stack:遇到标题行(# 开头,层级由 # 的个数决定)就先把缓冲区的内容 flush 成一个段落,并记录它的 heading_path(如 "8.3 RAG系统 > 8.3.4 架构设计"),再按层级回退/压栈。这样每个段落天然带着自己的位置坐标(start / end)与标题路径——标题路径后续还能作为检索过滤条件。
  2. 按 Token 数量合并成块,并做重叠。逐段累加估算 token 数,超过 chunk_tokens 就收口成一块;收口后从尾部倒着保留若干段作为下一块的开头,直到累加接近 overlap_tokens——这就是重叠(overlap)。
def _chunk_paragraphs(paragraphs, chunk_tokens, overlap_tokens):
    # 累加到超限就出块;出块后从尾部逆向保留 <= overlap_tokens 的段落
    if overlap_tokens > 0 and cur:
        kept = []
        kept_tokens = 0
        for x in reversed(cur):
            t = _approx_token_len(x["content"]) or 1
            if kept_tokens + t > overlap_tokens:
                break
            kept.append(x); kept_tokens += t
        cur = list(reversed(kept)); cur_tokens = kept_tokens

这段在干什么:重叠的作用是防止关键信息正好落在切缝上被切断——答案的上半句在上一块的尾部、下半句在下一块的开头,有重叠才能保证至少有一块完整包含它。代价是存储与嵌入成本上升(重叠部分被重复编码两次)。

为什么重叠要以 token 计而不是以段计:因为嵌入模型与 LLM 都按 token 计费计长,按段落保留会导致块大小不可控。工厂里的实际取值见 8.4 节案例:chunk_size=1000, chunk_overlap=200(约 20% 重叠)。

中英文混合场景的 token 估算是个坑:_approx_token_len 对 CJK 字符按 1 token/字计算,非 CJK 部分按空白分词计数;_is_cjk 覆盖 CJK 统一汉字、扩展 A–E 区与兼容汉字区。这解释了为什么中文文档的 token 数与字符数接近,而英文文档 token 数明显少于字符数。

3.5 嵌入(Embedding):三种方案与选型

嵌入模型负责把文本转成高维向量,RAG 的检索能力很大程度上取决于嵌入模型的质量。HelloAgents 实现了统一嵌入接口,提供三级方案:

方案 具体模型 特点与适用
云端 API(首选) 百炼 DashScope,默认 text-embedding-v3 效果好,需 API Key 与网络,按量计费
本地部署 sentence-transformers,默认 all-MiniLM-L6-v2 离线可用、免费,质量与硬件受限
轻量兜底 TF-IDF 嵌入 不依赖模型,纯词法,仅在两种方案都不可用时保底

选型规则:能联网且要求质量 → 云端 API;必须离线或数据不能出内网 → 本地模型;只是跑通流程或做冒烟测试 → TF-IDF 兜底。维度必须与向量库集合对齐:代码里用 get_dimension(384) 取默认维度(本地 all-MiniLM-L6-v2 对应 384),而云端 text-embedding-v3 在运行日志中给出的是 1024 维——维度不匹配时,索引代码会把向量截断或补零并打印警告,这是必须避免的隐性降质。

索引时的工程细节也值得记:文本先经 _preprocess_markdown_for_embedding 预处理(去掉 Markdown 语法噪声能提升嵌入质量),再按批次(batch_size=64)编码,单批失败有重试机制,单个向量转换失败则退化为零向量——失败降级而不是整批中断。

3.6 向量存储与相似度度量

存储层用 Qdrant 做向量检索,集合的关键配置为:QDRANT_COLLECTION(集合名)、QDRANT_VECTOR_SIZE(例如 384)、QDRANT_DISTANCE=cosine(距离度量)、QDRANT_TIMEOUT=30。距离度量是建集合时就要定死的参数,它决定了检索时"相似"如何计算。

三种常见度量及其公式(向量记为 a、b,维度 n):

余弦相似度  cos(a, b) = (a · b) / (‖a‖ ‖b‖),其中 a · b = Σᵢ aᵢbᵢ
内积/点积    dot(a, b) = Σᵢ aᵢbᵢ
欧氏距离    L2(a, b) = sqrt( Σᵢ (aᵢ - bᵢ)² )

符号与含义解释:aᵢ、bᵢ 是两个向量在第 i 维的分量;‖a‖ 是向量模长;Σᵢ 表示对全部维度求和。

  • 余弦相似度只比较方向、与向量长度无关,取值 [-1, 1],越大越相似。文本向量长度受篇幅影响很大,所以文本检索最常选余弦——本项目 QDRANT_DISTANCE=cosine 正是这一选择。
  • 内积同时受方向与模长影响,值域无上界;向量归一化后内积与余弦等价,所以很多系统先归一化再用内积(更快)。
  • 欧氏距离是距离而非相似度,越小越相似,值域 [0, +∞);它对模长敏感,适合关注绝对差异的场景。

易错点:检索结果里的 score 语义随度量方式而变——用余弦时是相似度(越大越好),用欧氏时是距离(越小越好)。本项目的评分公式(向量相似度 × 0.8 + 时间近因性 × 0.2)把 score 当作 [0,1] 的相似度使用,因此必须与余弦度量配套,换度量方式而不改评分公式,排序就会反过来。

3.7 高级检索:MQE、HyDE 与统一扩展框架

要解决的问题:用户的查询表述与文档实际内容常存在用词差异,导致相关文档检索不到——即查准/查全的漏检问题。HelloAgents 用三种互补策略应对:

(1)多查询扩展(Multi-Query Expansion, MQE)——"一个问题多种问法"。核心洞察是:同一问题可以有多种表述,不同表述可能匹配到不同的相关文档。例如"如何学习 Python"可扩展为"Python 入门教程""Python 学习方法""Python 编程指南"。实现上由 LLM 生成语义等价或互补的多样化查询,并行检索后合并结果。对模糊查询、专业术语查询效果显著,原文经验数据是可提升召回率 30%–50%。

def _prompt_mqe(query: str, n: int) -> List[str]:
    prompt = [
        {"role": "system", "content": "你是检索查询扩展助手。生成语义等价或互补的多样化查询。使用中文,简短,避免标点。"},
        {"role": "user", "content": f"原始查询:{query}\n请给出{n}个不同表述的查询,每行一个。"}
    ]
    return [ln.strip("- \t") for ln in (llm.invoke(prompt) or "").splitlines() if ln.strip()][:n] or [query]

这段在干什么:让 LLM 充当"改写器",把一个查询扩成 n 条不同说法,任何一条命中都算命中——用算力换召回。失败时回退为原查询,保证不阻塞主流程。

(2)假设文档嵌入(Hypothetical Document Embeddings, HyDE)——"用答案找答案"。传统方法用问题匹配文档,但问题是疑问句、文档是陈述句,二者在语义空间中分布存在差异(语义鸿沟)。HyDE 先让 LLM 生成一段假设性答案,再用它的向量去检索真实文档,从而缩小鸿沟。即使假设答案不完全正确,其中的关键术语、概念与表述风格也能有效引导检索,对专业领域查询尤为有效。

(3)统一扩展检索框架——"扩展-检索-合并"三步:

def search_vectors_expanded(store, query, top_k=8, rag_namespace=None,
                            enable_mqe=False, mqe_expansions=2, enable_hyde=False,
                            candidate_pool_multiplier=4):
    expansions = [query]
    if enable_mqe and mqe_expansions > 0:
        expansions.extend(_prompt_mqe(query, mqe_expansions))
    if enable_hyde:
        expansions.append(_prompt_hyde(query))
    # 去重 → 分配候选池 → 逐查询检索 → 按最高分聚合去重 → 排序取 top_k
    pool = max(top_k * candidate_pool_multiplier, 20)
    per  = max(1, pool // max(1, len(expansions)))

这段在干什么:把原始查询、MQE 查询、HyDE 假设文档合并成一个查询集合,去重后给每个查询分配配额(per),逐个独立检索,最后按 score 取每条结果的最高分做去重合并并排序。

两个关键参数的设计意图:

  • candidate_pool_multiplier(默认 4):候选池 = max(top_k × 4, 20)。先把池子挖大再筛,保证有足够候选做对比;
  • rag_namespace + 过滤器(is_rag_data / data_source=rag_pipeline):只召回 RAG 管道自己写入的数据,避免混入记忆数据。

选型规则(可直接照用):一般查询 → 只开 MQE;专业领域查询 → MQE + HyDE 同时开;性能敏感场景 → 只走基础检索或仅开 MQE。

3.8 RAGTool 的接口

RAGTool 是 RAG 系统的统一入口,初始化参数为 knowledge_base_path / qdrant_url / qdrant_api_key / collection_name / rag_namespace,内部按 rag_namespace 维护多个管道(self._pipelines[namespace] = pipeline),实现知识库的命名空间隔离。

action 作用
add_text 直接添加一段文本知识(带 document_id)
add_document 添加文件(按 chunk_size / chunk_overlap 分块入库)
search 纯检索(limit / min_score)
ask 检索 + LLM 增强问答(可开 enable_mqe / enable_hyde)
stats 知识库统计

4. 检索质量提升手段总览(工程对照)

把前面散落的技巧按"在链路的哪一步发力"整理成一张速查表:

手段 发力点 解决的问题 代价
结构感知分块 + 重叠 切分 语义被切断、块过大 存储与嵌入成本上升
混合检索(向量 + 结构化过滤 / 向量 + 图) 召回 纯语义召回的偏差与噪声 需要额外的存储后端
MQE 多查询扩展 查询 用词差异导致的漏检 多轮 LLM 调用与检索
HyDE 假设文档嵌入 查询 问句与陈述句的语义鸿沟 一次额外 LLM 生成
重排(Rerank) 排序 粗排 top-k 里混入不相关结果 增加一次精排开销
扩候选池 + 去重合并 排序 候选不足导致错失好文档 检索次数与延迟上升
上下文智能合并与截断 注入 窗口被噪声挤占 会有信息损失

混合检索在本章有两处落地:情景记忆是"结构化预过滤(时间范围、重要性)+ 向量检索",先缩候选再算语义;语义记忆是"向量检索 + 图检索"双路合并。共同点:单一信号不可靠,多信号加权才稳。


5. 选型对照:三个存储后端各管一段

后端 类型 在本章承担 不可替代的能力
Qdrant 向量数据库 语义检索、嵌入向量存储 高维向量的相似度检索
Neo4j 图数据库 知识图谱的实体与关系 多跳关系、路径查找等关系推理
SQLite 关系型(文档存储) 结构化数据与元数据持久化 复杂条件查询、事务与持久化

Qdrant 还提供集合隔离 + 命名空间隔离:记忆系统用 hello_agents_vectors、RAG 用 rag_knowledge_base、感知记忆按模态拆成 perceptual_text / perceptual_image / perceptual_audio 三个集合(各自对应不同向量维度)。隔离的作用有二:避免维度不匹配,避免不同用户/用途的数据互相污染检索结果。


6. 实战:智能文档问答助手(8.4)

案例把 MemoryTool 与 RAGTool 组合成基于 Gradio 的 Web 应用,围绕一份技术 PDF(Happy-LLM 公测文档)做交互式学习助手。应用分三部分:核心助手类 PDFLearningAssistant(封装两个工具的调用逻辑)、Gradio 界面、笔记 / 回顾 / 统计 / 报告等辅助功能。

五个步骤形成闭环:步骤 1 处理 PDF 并把信息记入记忆系统 → 步骤 2 检索结果也记入记忆系统 → 步骤 3 展示记忆系统完整能力(添加、检索、整合、遗忘)→ 步骤 4 整合 RAG 与 Memory 提供智能路由 → 步骤 5 汇总统计生成学习报告。

关键设计:用 ID 做两级隔离。 MemoryTool(user_id=...) 实现用户级记忆隔离(每个用户独享自己的工作/情景/语义/感知空间);RAGTool(rag_namespace=f"pdf_{user_id}") 实现知识库命名空间隔离(每人独立 PDF 知识库);session_id 追踪单次会话,便于回顾。

两个核心方法最能说明"工具协同"的写法:

def load_document(self, pdf_path):
    result = self.rag_tool.execute("add_document", file_path=pdf_path,
                                   chunk_size=1000, chunk_overlap=200)
    if result.get("success", False):
        self.memory_tool.execute("add", content=f"加载了文档《{self.current_document}》",
                                 memory_type="episodic", importance=0.9,
                                 event_type="document_loaded", session_id=self.session_id)

这段在干什么:一行 add_document 触发 MarkItDown 转换 → 增强处理 → 智能分块 → 向量化的完整链路;紧接着把"加载了某文档"这个事件写入情景记忆。为什么用情景记忆而不是工作记忆——因为它是一个具体的、有时间戳的事件,情景记忆保留事件完整性并支持按时间回顾;配上 session_id 就能回答"我之前加载过哪些文档"。

def ask(self, question, use_advanced_search=True):
    self.memory_tool.execute("add", content=f"提问: {question}",
                             memory_type="working", importance=0.6, session_id=self.session_id)
    answer = self.rag_tool.execute("ask", question=question, limit=5,
                                   enable_advanced_search=use_advanced_search,
                                   enable_mqe=use_advanced_search, enable_hyde=use_advanced_search)
    self.memory_tool.execute("add", content=f"关于'{question}'的学习",
                             memory_type="episodic", importance=0.7,
                             event_type="qa_interaction", session_id=self.session_id)

这段在干什么:同一个方法里三种记忆类型各司其职——提问本身是工作记忆(临时上下文,importance 0.6 偏低),回答由 RAG 高级检索产出,问答交互作为一个学习事件写入情景记忆(importance 0.7)。注意 importance 的梯度设计:加载文档 0.9 > 问答交互 0.7 > 单次提问 0.6,这个梯度直接决定了后续 consolidate 时谁能被升格为长期记忆。

其余落点:add_note → 语义记忆(importance 0.8);recall → memory_tool.search;get_stats → 会话时长 / 文档数 / 提问数 / 笔记数;generate_report → 汇总 memory_tool.summary 与 rag_tool.stats 输出 JSON。启动后访问 http://localhost:7860。

案例的教学价值在于演示了"RAG 出答案、Memory 记过程"的分工:RAG 面向外部客观知识,Memory 面向"我与用户之间发生的事"。答得准靠 RAG,越来越懂你靠 Memory。


7. 高频考点 & 易错点速查

  1. LLM 两大根本局限必须背全:无状态导致的对话遗忘、内置知识静态有限。第 7 章 SimpleAgent 的 _history 只是同实例临时消息列表,不是记忆系统(最易错)。
  2. 四种记忆类型的对应关系:工作=短期/TTL/纯内存;情景=事件/时间序列;语义=抽象知识/知识图谱;感知=多模态/跨模态。人脑分层里的"感觉记忆 0.5–3 秒""工作记忆 15–30 秒、7±2 个项目"是常考数字。
  3. 四类评分公式的权重差异是分析题重点:
    • 情景 (向量×0.8 + 时间近因性×0.2) × (0.8+重要性×0.4)
    • 语义 (向量×0.7 + 图相似度×0.3) × (0.8+重要性×0.4)
    • 感知 (向量×0.8 + 时间近因性×0.2) × (0.8+重要性×0.4)
    • 工作 基础相关性 × 时间衰减 × (0.8+重要性×0.4),其中基础相关性在词法分>0 时 = 词法×0.7 + 关键词×0.3
      权重区间 [0.8, 1.2] 的意义是重要性只做 ±20% 调节,不能压过相似度主导排序。
  4. TF-IDF 不是语义检索。工作记忆用的是稀疏词项向量(词频 + 逆文档频率),与稠密嵌入的语义相似度是两回事。
  5. 余弦 / 内积 / 欧氏三者不能混用:余弦比方向(越大越相似,文本首选,本项目 QDRANT_DISTANCE=cosine);内积在归一化后等价于余弦;欧氏是距离(越小越相似)。score 的语义随度量变化,换度量必须同步改评分公式。
  6. 切分两要素:结构感知(按 Markdown 标题层次,保留 heading_path)+ Token 控制与重叠。案例取值 chunk_size=1000, chunk_overlap=200。重叠的作用是防止答案被切缝截断。中文按 CJK 字符 1 token/字估算,英文按空白分词。
  7. 朴素 RAG 两大痛点:词法匹配(TF-IDF/BM25)不理解语义;检索结果不加处理直接拼接。三代演进的时间与关键词(2020–2021 朴素 / 2022–2023 高级 / 2023–至今 模块化)是常识题。
  8. MQE vs HyDE:MQE 靠"多种问法"提召回(原文经验值 +30%–50%);HyDE 靠"先生成假答案再检索"跨语义鸿沟补精度。统一框架的参数:mqe_expansions=2、candidate_pool_multiplier=4、候选池 max(top_k×4, 20)。
  9. 遗忘三策略 + 整合机制:importance_based(threshold 默认 0.1)/ time_based(max_age_days 默认 30)/ capacity_based;consolidate 默认 working→episodic、阈值 0.7。没有遗忘则库膨胀、噪声上升;没有整合则学不到东西。
  10. 章末习题考点映射:①四类记忆评分公式对比 + 为"个人健康管理助手"组合四类记忆 + consolidate 自动触发条件设计(考记忆类型学与生命周期);②无标题文档(小说/法条)的语义边界分块优化 + 基础检索/MQE/HyDE 效果对比 + 三种嵌入方案在准确性/速度/成本/离线四维的选型(考 RAG 全链路);③实现综合重要性、访问频率、时间衰减的"智能遗忘",并设计记忆归档与敏感信息彻底清除(考遗忘与合规);④RAG 与 Memory 的"智能路由"选择、学习报告升级为知识盲点识别、多用户下的 Qdrant/Neo4j 数据隔离(考工程集成);⑤自动抽取的实体关系质量评估、Neo4j 多跳与路径查找场景设计、向量+图混合检索相对纯向量检索的增益场景(考知识图谱)。

8. 与前后章节的衔接

第 7 章给了 Agent 的骨架与工具系统,但框架"缺少一个关键能力:记忆"。本章在第 7 章框架上做能力扩展,把 Memory 与 RAG 封装成标准工具挂进 ToolRegistry:输入是 Tool 抽象、ToolRegistry 与 SimpleAgent,输出是两个可复用的内置工具与一套可替换的分层架构。

本章也收回了第 1 章的伏笔:第 1 章说数字环境"部分可观察"所以需要记忆,本章给出工程答案;Thought/Action/Observation 轨迹累积出的"最朴素记忆"被升级为可持久化、可检索、可遗忘、可整合的正式系统。下一章转向上下文工程——本章写入上下文的"检索片段 + 记忆条目",正是下一章要精细调度的原料。

延伸(原文参考文献,仅此 1 条):Atkinson, R. C., & Shiffrin, R. M. (1968). Human memory: A proposed system and its control processes. In Psychology of Learning and Motivation (Vol. 2, pp. 89–195). Academic Press.


9. 课后练习

在线试卷:https://md-quiz-online.app.workbuddy.host/

posted @ 2026-09-28 17:08  测试小罡  阅读(9)  评论(0)    收藏  举报