Skip to content

10 — Memory & Retrieval Architecture

Status: 📋 Design proposal Extends: 03-agent-framework.md (§3.4 State and Memory) Inspired by: TencentDB Agent Memory design patterns (not direct integration)


1. Why This Document

当前架构(03-agent-framework)在 Memory 层面定义了两件事:

  • Session memory(会话记忆)— 对话内的临时数据,会话结束就清空
  • Per-user persistent memory(用户持久记忆)— 长期跟踪,但 MVP 默认关闭

这两层在实际运行中暴露了几个问题:

  1. MEMORY.md 全量加载 — 所有记忆每次对话都塞进 prompt,记忆多了占用 context 窗口
  2. Competences 硬编码 — 领域知识平铺在 competences.py 的 dict 里,手动注册,ID 硬编码
  3. Trace 不持久 — 工具调用记录只在内存里,会话结束后无法回溯 "上次为什么得出这个结论"
  4. 研报检索是空的search_reports() 返回 NotImplementedError,Agent 没有非结构化知识源

这份文档提出一个分层记忆 + 检索注入的架构来解决这些问题。核心思想借鉴了 TencentDB Agent Memory 的设计模式(L0-L3 分层、混合检索、上下文卸载),但不直接集成该库(技术栈不匹配:它是 TypeScript/OpenClaw 生态,我们是 Python/Hermes)。


2. 核心概念(非技术解释)

在深入技术细节之前,先用日常语言解释我们要解决的核心问题。

2.1 为什么 Agent 需要"记忆"?

想象你带一个研究助手。第一天上班,你告诉他:

"我只看白酒和新能源,不碰银行股。"

如果没有记忆:

  • 第二天他又给你推荐银行股 — 他忘了
  • 你每次都要重复同样的偏好 — 你烦了

如果有记忆:

  • 他记住了你的偏好 — 不用再重复
  • 他还能记住 "你上个月问过茅台的估值" — 有连续性

记忆让 Agent 从 "每次都是陌生人" 变成 "了解你的老搭档"。

2.2 为什么记忆要"分层"?

还是那个助手。如果你把所有信息混在一起:

"用户不吃辣、茅台 Q1 营收增长 12%、2023 年 3 月问过伊利、PE 计算公式是…"

助手每次回答问题前要从这一堆东西里翻找 — 慢、容易漏、还占地方。

分层就是把信息分类放好:

层级放什么类比加载方式
L3 画像用户偏好、风格、禁忌助手的"用户手册"每次都看(量少,几行)
L2 场景当前话题的上下文"你上周刚分析过白酒板块"需要时才翻
L1 事实具体数据、结论"茅台 Q1 营收 +12%"搜索后注入
L0 日志原始对话和操作记录工作日志,可回溯需要回溯时才查

分层的核心原则:不是所有信息每次都需要。按需提供,省空间、提质量。

2.3 什么是"检索注入"(Retrieval Injection)?

没有检索注入:
  用户: "茅台现在值得买吗?"
  Agent: 只靠训练数据回答 → 可能过时、可能编造 ❌

有检索注入:
  用户: "茅台现在值得买吗?"
  → 系统自动检索: 3 份最新研报 + 用户偏好(价值投资) + 最近财报数据
  → 把这些"参考资料"交给 Agent
  → Agent 基于资料回答 → 有依据、有时效 ✅

检索注入 = 让 Agent "查资料后回答",而不是"凭记忆瞎编"。

2.4 什么是"混合检索"(Hybrid Recall)?

这是本架构的关键技术点。假设文档库里有一堆研报,用户搜 "600519 2025 估值"

方式一:只看关键词(BM25)

就像在 Word 里 Ctrl+F 搜索 — 必须词面完全匹配

文档结果原因
"600519 2025 年 PE 预计 28 倍"✅ 命中词都对上了
"白酒行业估值合理"❌ 漏掉没有"600519"、"2025"这些词
"茅台 2024 年报分析"⚠️ 误匹配有"茅台"但年份不对

问题: 用户搜"便宜",文档写的是"估值低" — 意思一样但用词不同,搜不到。

方式二:只看语义(Vector / 向量检索)

把文字转成"语义指纹",比较意思相不相似。

文档结果原因
"白酒行业估值分析"✅ 命中语义相关
"600887 伊利最新行情"⚠️ 可能误匹配都是"行情类"文档,向量可能混淆
"茅台 2024 年报"⚠️ 可能排前面向量对数字(2024 vs 2025)不敏感

问题: 股票代码、精确年份这种需要"精确匹配"的东西,向量检索容易混淆。

方式三:混合检索(Hybrid = BM25 + Vector + 排名融合)

两路都跑,然后合并结果:

用户搜 "600519 2025 估值"

BM25 找到: "600519 2025 年 PE 28 倍" (精确匹配了代码和年份) ✅
Vector 找到: "白酒行业估值分析"     (语义上相关)                ✅

混合后:
  1. "600519 2025 年 PE 28 倍" ← 两路都命中,排第一
  2. "白酒行业估值分析"        ← 语义补上了 BM25 漏掉的
  3. "600887 伊利行情"        ← BM25 没命中(代码不对),排名靠后

为什么混合更好: BM25 负责精确性(代码、年份必须对),Vector 负责召回性(同义词、相关话题也能找到)。两者互补,不是重复。

排名融合用 RRF(Reciprocal Rank Fusion / 倒排排名融合)算法: 不关心各自的绝对分数,只看排名。一个文档在两路检索中都出现,它的排名就会靠前。数学上很简洁,工程上 k=60 是经验最优值。

2.5 术语速查表

术语一句话解释类比
RAG先检索资料,再让 AI 回答开卷考试 vs 闭卷考试
BM25关键词搜索算法Ctrl+F 的加强版
Embedding把文字转成"语义向量"把一句话压缩成"语义指纹"
Vector Recall用语义向量找相关文档"意思相近"的搜索
Hybrid RecallBM25 + Vector 合并结果"字面对 + 意思对"都照顾到
RRF合并两路排名的算法两个评审都打高分的选手排前面
Context WindowAI 一次能"看到"的文本量人的短期记忆容量
Context Offloading把详细内容移出 AI 视线,只留摘要笔记本放旁边,需要时翻开
Citation每个断言标注来源论文里的参考文献标注
Competence注册在案的领域常识"A 股财年 12/31" 这种事实

3. 现状诊断

3.1 现有架构的 Memory 层

┌─────────────────────────────────┐
│  Hermes Gateway                 │
│  ├── Agent Loop                 │
│  ├── Tool Dispatch              │
│  └── Context Loading:           │
│      SOUL.md (全量)              │
│      AGENTS.md (全量)            │
│      SKILL.md (按需)             │
│      MEMORY.md (全量) ← 问题在这  │
│      USER.md (全量)  ← 问题在这   │
└─────────────────────────────────┘

MEMORY.md / USER.md 的问题:

  • 全量加载,不论当前话题是否相关
  • 没有层次 — 用户偏好和某个股票的历史分析混在一起
  • 记忆增多后占用大量 context 窗口
  • 无法检索 — 只能靠 Hermes 自己的摘要机制

3.2 Competences 的问题

python
# core/core/competences.py — 现状
registry.register(Competence(
    id="a_share_fiscal_year_end",
    statement="A-share fiscal year ends December 31",
    source="CSRC accounting standards"
))
  • 硬编码注册,手动维护
  • 所有知识平铺在一个 dict 里,没有层次
  • 新知识必须改代码、重新部署
  • 无法从文档/研报中自动提取

3.3 ToolTrace 的问题

  • 仅在内存中,会话结束即丢失
  • 无法跨会话回溯 "上次分析为什么得出这个结论"
  • Verifier 每次只能验证当前会话的 claim

3.4 研报检索的问题

python
# core/core/data/schemas.py — 现状
def search_reports(query: str, code: str | None = None) -> list[ReportChunk]:
    raise NotImplementedError("report search is Phase 2 work")
  • 完全未实现
  • Agent 没有卖方研报知识源
  • 投研推理只靠价格和财报数据,缺"观点面"

4. 目标架构

4.1 整体拓扑

                        用户


┌─────────────────────────────────────────────┐
│  Hermes(不变)                                │
│  Agent Loop / Tool Dispatch / Context Load   │
│  SOUL.md → AGENTS.md → SKILL.md             │
└──────────────────┬──────────────────────────┘

         ┌─────────┼─────────┐
         │         │         │
         ▼         ▼         ▼
  ┌──────────┐ ┌────────┐ ┌───────────┐
  │ L3 画像   │ │ 检索    │ │ 工具调用   │
  │ MEMORY.md│ │ 注入点  │ │ Trace →   │
  │ (全量加载)│ │(新增)   │ │ L0 持久化  │
  │          │ │        │ │ (新增)     │
  └──────────┘ └───┬────┘ └─────┬─────┘
                   │            │
             ┌─────┴─────┐ ┌────┴──────┐
             │ L1 事实    │ │ L0 日志    │
             │ + L2 场景  │ │ 持久存储   │
             │ DuckDB    │ │ DuckDB    │
             └───────────┘ └───────────┘

关键原则:Hermes 不动,Memory Layer 作为插件叠加在它上面。

4.2 四层存储定义

层级名称内容存储加载方式日常类比
L3Persona(画像)用户偏好、风格、禁忌MEMORY.md全量加载(量少)用户的"使用说明书"
L2Scenario(场景)当前话题的上下文memory/scenarios/*.md按话题检索注入"上次聊过这个的背景"
L1Fact(事实)具体数据、结论memory/facts.db (DuckDB)按 ticker/指标检索注入"数据卡片"
L0Log(日志)原始对话、工具调用记录memory/logs.db (DuckDB)按 ID 回溯"工作日志"

4.3 四步流程详解

Step 1: L3 Persona — "了解你是谁"

什么时候发生: 每次会话启动时。

具体做什么:

  • Hermes 加载 MEMORY.md(已存在的机制,不变)
  • MEMORY.md 里只放 L3 级别的画像信息(用户偏好、风格、禁忌)
  • 通常 5–10 条,全量加载也不占多少 token

实际效果:

MEMORY.md:
- 用户偏好:价值投资为主,关注白酒和新能源板块
- 不碰:ST 股、银行股
- 报告风格:简洁,先结论后论据
- 语言:中文沟通,英文输出(项目偏好)

→ Agent 从第一句话就知道 "这个人不要银行股推荐"

Step 2: L2 Scenario — "想起上次聊过这个"

什么时候发生: 用户提出一个问题,Agent 准备开始分析之前。

具体做什么:

  • 从用户问题中提取话题(如 "白酒板块"、"600519")
  • memory/scenarios/ 目录中检索相关场景文件
  • 如果有匹配的场景,把场景摘要注入 context

实际效果:

用户: "茅台最近怎么样?"
→ 话题提取: ticker=600519, sector=白酒
→ 检索 memory/scenarios/ 目录:
  ✅ consumer_sector_q1_analysis.md
     "2026Q1 白酒板块整体承压,龙头估值回调"
  ✅ moutai_valuation_context.md
     "上次(4月)估值讨论,结论:PE 28x 略高"
→ 注入这两个场景摘要(每个约 200 字)
→ Agent 知道 "哦上次讨论过,结论是估值略高"

Step 3: L1 Fact — "查到相关数据"

什么时候发生: 和 L2 一起,在分析开始前检索。

具体做什么:

  • 按 ticker、指标类型在 facts.db 中检索
  • 返回最相关的 N 条事实
  • 注入 context 供 Agent 引用

实际效果:

用户: "茅台最近怎么样?"
→ L1 检索: ticker=600519
→ 返回:
  [1] "600519 2026Q1 营收同比 +12%,净利润 +9%"
      (来源: Tushare 2026-04-28)
  [2] "600519 最新 PE 26x"
      (来源: 工具调用 tc_xyz789)
→ 注入这 2 条事实
→ Agent 回答时可以引用具体数字

Step 4: L0 Log — "回溯上次为什么这么说"

什么时候发生: 用户问 "你上次为什么得出这个结论?" 或 Agent 需要自查时。

具体做什么:

  • tool_call_id 或时间范围在 logs.db 中回查
  • 返回原始工具调用记录和结果
  • 用于审计、debug、回溯

实际效果:

用户: "你上个月说茅台 PE 28 倍,怎么算的?"
→ Agent 通过 cite 中的 tool_call_id 回查 L0
→ 找到:
  tool_call_id: tc_abc123
  tool: get_fundamentals
  args: code=600519, metrics=[pe]
  result: {"pe": 28.3}
  timestamp: 2026-04-15T10:30:00Z
→ Agent 展示计算过程,完全可回溯

5. 与现有系统的集成点

5.1 和 Hermes MEMORY.md 的关系

现在:
  MEMORY.md — 什么记忆都往里塞,全量加载

集成后:
  MEMORY.md — 只放 L3 Persona(用户画像)
  memory/   — L0/L1/L2 的新家

不需要改 Hermes: Hermes 加载 MEMORY.md 的机制不变。我们只是改变 MEMORY.md 的内容 — 从"什么都有"变成"只有用户画像"。

5.2 和 Competences 的关系

现在:
  competences.py 硬编码注册:
    register(Competence(id="xxx", statement="..."))
  → 平铺 dict,ID 硬编码

集成后:
  Competences = L1 Facts 的一个子集 (type="competence")

  memory/facts.db:
    id            | type         | statement            | source
    --------------|-------------|----------------------|-------------
    fiscal_end    | competence  | A股财年截止12/31      | 上交所规则
    pe_formula    | competence  | PE = 股价 / EPS       | 金融常识
    600519_q1_rev | fact        | 600519 Q1 营收+12%    | Tushare

好处:

  • Competences 不再需要改代码注册 — 写入 facts.db 即可
  • 可以按 topic/ticker 检索 competence,不再是全量加载
  • 新的领域知识可以从文档中自动提取(未来)

5.3 和 ToolTrace 的关系

现在:
  ToolTrace → 内存 list → 会话结束丢失

集成后:
  ToolTrace → 写入 L0 logs.db (DuckDB)

  memory/logs.db:
    tool_call_id | tool_name   | args            | result  | timestamp
    tc_abc123    | get_daily   | ts_code=600519   | {...}   | 2026-05-13

解决了 03-agent-framework §7 开放问题 #3: "Trace storage. Default: memory for MVP, persisted for production."

5.4 和 search_reports() 的关系

现在:
  search_reports() → NotImplementedError

Phase 1 (FTS only):
  研报 PDF → pdfplumber 解析 → DuckDB 全文搜索
  → BM25 关键词匹配

Phase 2 (Hybrid):
  研报 chunk → bge-m3 embedding → 向量检索
  → BM25 + Vector + RRF 混合排序

6. 实施阶段

Phase 0: 基础设施(1–2 周)

目标: 搭好 L0-L3 的骨架,先不接检索。

任务具体做什么产出
创建 memory/ 目录facts.db, logs.db, scenarios/目录结构
L0 日志持久化每次工具调用写入 logs.dblogs.db 有数据
L3 画像整理把现有 MEMORY.md 精简为纯 Persona精简后的 MEMORY.md
L1 Schema 设计facts.db 的表结构CREATE TABLE facts (...)

不涉及: 检索、embedding、任何智能注入。

Phase 1: 全文搜索 + 手动注入(2–3 周)

目标:search_reports()NotImplementedError 变成能返回结果。

任务具体做什么产出
研报入库手工收集 50–100 份 PDF → pdfplumber 解析 → DuckDBreports 表有数据
DuckDB FTS利用 DuckDB 的全文搜索功能search_reports() 可调用
Skill 集成在 stock-research workflow 中调用 search_reportsAgent 开始引用研报
L1 事实写入工具调用结果自动存入 facts.dbfacts.db 有数据
  • 检索方式: 纯 BM25 全文搜索。不用 embedding。
  • 验证标准: 给一个股票,Agent 能引用至少 1–2 份研报的观点。

Phase 2: 混合检索 + 自动注入(2–3 周)

目标: 从关键词搜索升级到语义搜索 + 自动 context 注入。

任务具体做什么产出
Embedding 管线bge-m3 路由 → 研报 chunk → 向量入库向量索引就绪
Hybrid RecallBM25 + Vector + RRF 融合混合搜索 API
自动检索注入SKILL.md workflow 中加 memory_retrieval 步骤自动注入 L1/L2
L2 场景提取LLM 从对话中自动提取场景 → 存为 Markdownscenarios/ 有数据
  • 检索方式: BM25 + Vector + RRF
  • 验证标准: 搜 "便宜" 能匹配 "估值低"(语义),搜 "600519" 能精确匹配代码(关键词)。

Phase 3: Context Offloading + 自维护(持续迭代)

目标: 长对话不爆 context,记忆自动维护。

任务具体做什么产出
Mermaid 上下文卸载工具日志 offload 到 refs/*.md,context 只放摘要图长对话 token 降低
记忆 TTL 管理L0/L1 自动清理过期数据存储不无限增长
自维护循环Agent 自动从对话中提取新事实 → 写入 L1记忆自动丰富

7. 技术选型

存储层

组件选型原因
L0/L1 存储DuckDB(已有)项目已有,零新依赖,支持 FTS
L2 场景Markdown 文件人类可读、可编辑、可 git 追踪
L3 画像MEMORY.md(Hermes 原生)零集成成本
向量索引(Phase 2)sqlite-vecLanceDB嵌入式、Python-native、零运维

为什么不用 Qdrant / pgvector / Milvus: 这些都需要独立服务部署。项目现有基础设施是 DuckDB + SQLite,引入一个独立向量服务增加了运维复杂度。Phase 2 优先用嵌入式方案。

检索层

组件Phase 1Phase 2
关键词搜索DuckDB FTS (BM25)DuckDB FTS (BM25)
语义搜索bge-m3 embedding + 向量库
排名融合RRF (k=60)
中文分词DuckDB 内置jieba(bge-m3 自带)

注入层

组件实现方式
L2/L1 检索触发SKILL.md workflow 中加 memory_retrieval 步骤
Context 注入格式Markdown block,标注来源
注入量控制最多 N 条 L1 + M 个 L2,总量 ≤ context 窗口的 15%

8. 风险控制

风险影响缓解措施
注入的记忆污染 Agent 判断错误的上下文导致错误结论注入的记忆带 cite/来源,verifier 可验证
记忆过时旧数据误导 AgentTTL 机制,过期自动清理
Context 窗口仍然不够即使检索注入,相关记忆太多限制注入比例 ≤ 15%,超过则截断
研报版权问题入库的研报是否有 redistribution 权利Phase 1 仅用公开免费研报,标注来源
Phase 2 embedding 成本bge-m3 本地模型有计算开销批量离线计算,增量更新

9. 和 03-agent-framework 的对照

03 的设计本文档的演进变化类型
Session memory (always on)→ L1 + L2 检索注入重定义 — 从"全量"到"按需"
Per-user memory (opt-in)→ L3 Persona (MEMORY.md)细化 — 明确只放画像
Trace: memory for MVP→ L0 logs.db 持久化升级 — 从内存到持久化
Competences: registered dict→ L1 facts (type=competence)迁移 — 从硬编码到可检索
search_reports: deferred→ Phase 1/2 核心交付激活 — 从 deferred 到执行中
Multi-model routing不变保持 — bge-m3 路由已存在

10. 开放问题

  1. Competences 迁移策略: 现有 competences.py 中硬编码的 competence 是自动迁移到 facts.db,还是保持双轨运行一段时间?
  2. Hermes 钩子: 是否需要在 Hermes 的 after-tool-call 钩子中自动触发 L0 日志写入?还是需要 Hermes 支持自定义 hook 机制?
  3. L2 场景自动提取: 是手动创建场景 Markdown,还是让 LLM 在会话结束时自动提取并写入 scenarios/?自动提取的质量如何保证?
  4. 多用户隔离: 当前项目是单用户(profile 级)。如果未来多用户,memory 是按用户隔离还是共享?
  5. Embedding 模型部署: bge-m3 是本地跑还是调 API?本地需要计算资源,API 有延迟和成本。

11. 决策记录

决策选择拒绝的替代方案原因
Memory 分层L0-L3 四层扁平向量库;全量 summary分层支持渐进式信息暴露,省 token
检索策略Phase 1: BM25 only → Phase 2: Hybrid直接上 Vector;只用 BM25先验证价值再投入,混合效果最优
向量库选型嵌入式 (sqlite-vec / LanceDB)Qdrant / pgvector / Milvus零运维,与 DuckDB 生态一致
L3 存储保持 MEMORY.md (Hermes 原生)迁移到 DB零集成成本,量少不需检索
L0/L1 存储DuckDB (已有)引入新 DB零新依赖,已有 FTS 能力
是否集成 TencentDB 库不直接集成,借鉴设计直接安装使用TypeScript 生态不匹配,Python 重写更轻
Competences 去向迁移到 L1 facts (type=competence)保持硬编码可检索、可扩展、可自动提取
ToolTrace 去向持久化到 L0 logs.db保持内存解决 03-framework 开放问题 #3

团队内部文档