AstroResearch/docs/architecture/agent/memory.md
Asfmq cec4b8cf7b feat: Docker 容器化、Cookie 鉴权、Coordinator 编排、FTS5 搜索与 P1-P3 全面收尾
Docker 容器化部署
  - 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
    两种镜像,Docker Compose 一键启动
  - build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
  - 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制

  安全:Cookie-Based 鉴权系统
  - HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
  - 登录/登出/验证接口 + 中间件注入
  - 前端登录页面 + 退出按钮
  - 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
  - 书签脚本 fetch 添加 credentials:'include'

  Coordinator 模式 (P2)
  - 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
  - WorkerPool + Semaphore 并发控制 + 超时保护
  - 前端协调者模式开关

  Hook 系统:UserPromptSubmit 事件 (P2)
  - 第 13 个生命周期事件,fire-and-forget 审计

  FTS5 全文搜索 (P3)
  - agent_sessions_fts + agent_messages_fts 虚拟表
  - search_history Agent 工具 + /api/search/history HTTP 接口
  - 前端防抖搜索框 + 仅当前会话筛选

  工具加载优化 (P3)
  - defer_loading 延迟加载 (7 个重型工具)
  - is_readonly 只读标记 (9 个查询工具)
  - classifier_summary 工具目录供 LLM 按需判断

  模型回退策略 (P3)
  - LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
  - LlmClient model 改为 Arc<RwLock> 支持运行时切换
  - 连续 3 次过载后自动切换

  压缩记忆桥接 (P3)
  - 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)

  git2 依赖修复
  - 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
2026-06-23 20:22:06 +08:00

23 KiB
Raw Permalink Blame History

记忆系统 (memory/)

参考 Claude Code memdir/ 设计,提供完全基于文件系统(非数据库)的项目级持久化记忆管理。核心代码位于 src/agent/memory/8 个文件)和 src/agent/tools/memory.rssave_memory 工具)。

整体架构

graph TD
    subgraph Storage["文件存储 ({library_dir}/memory/)"]
        MEMORY_MD["MEMORY.md<br/>索引文件 (≤200行, ≤25KB)"]
        Files["{slug}.md × N<br/>每个记忆一个 Markdown 文件"]
        Archive["{slug}_v1.md<br/>旧版本归档(永不删除)"]
    end

    subgraph Manager["MemoryManager (mod.rs)"]
        Load["reload()<br/>扫描目录 → 解析 frontmatter → 按 mtime 排序"]
        Save["save_memory()<br/>写文件 → 归档旧版 → 更新索引 → reload"]
        Reminder["build_system_reminder(N)<br/>构建注入 system prompt 的 XML 块"]
        SelectRel["select_relevant_memories()<br/>LLM 语义选择 + 指数衰减排序"]
    end

    subgraph Tool["save_memory 工具 (tools/memory.rs)"]
        Validate["参数校验<br/>slug 格式 + memory_type 枚举"]
        QualityGate["写入时门控<br/>质量检查 + Jaccard 去重"]
        Execute["执行写入<br/>MemoryManager.save_memory()"]
    end

    subgraph Pipeline["记忆生命周期"]
        Extract["extraction.rs<br/>会话结束时自动提取(默认关闭)"]
        Dedup["dedup.rs<br/>Jaccard 相似度去重 (≥70%) + 内容质量门控"]
        Decay["decay.rs<br/>指数时间衰减 + Hebbian 激活层级"]
        Age["age.rs<br/>时效标签 + freshness 警告"]
        Guardrails["guardrails.rs<br/>WHAT_NOT_TO_SAVE + VERIFY_BEFORE_RECOMMENDING"]
    end

    subgraph Types["types.rs — 数据模型"]
        MemoryEntry["MemoryEntry<br/>{ slug, name, description, memory_type, mtime, content, path, status }"]
        MemoryType["MemoryType: User | Feedback | Project | Reference"]
        MemoryStatus["MemoryStatus: Active | Historical { superseded_by }"]
    end

    Tool --> Manager
    Manager --> Storage
    Reminder -->|"try_lock() 非阻塞"| Runtime["AgentRuntime System Prompt"]
    Extract --> Dedup --> Tool
    Decay --> SelectRel
    Guardrails --> Reminder
    Guardrails --> Tool

存储结构

记忆不使用 SQLite,全部存储在文件系统 {library_dir}/memory/ 目录下:

{library_dir}/memory/
├── MEMORY.md              # 索引文件(最多 200 行 / 25KB
├── user-prefs.md          # 活跃记忆YAML frontmatter + Markdown 内容)
├── project-goals.md
├── user-prefs_v1.md       # 旧版本归档(原文件重命名,永不删除)
└── ...

每个记忆文件格式:

---
name: user-role
description: 用户的科研角色和偏好
type: user
status: active
---

用户是天体物理学博士后,主要研究恒星演化与双星系统。
**Why:** 在对话开始时用户明确说明了研究领域。
**How to apply:** 默认使用天体物理学术语,优先推荐恒星演化相关文献。

版本管理机制:当更新已有 slug 时,旧文件重命名为 {slug}_v1.md,其 frontmatter 中 status 改为 historical 并记录 superseded_by 指向新版本。旧版本永不删除——保留完整的事实演进历史。MemoryStatus 枚举控制此生命周期:

Active ──(更新)──→ Historical { superseded_by: Some("new-slug") }

核心数据结构 (types.rs)

结构体/枚举 字段/变体 说明
MemoryType User 用户角色、偏好、知识背景
Feedback 用户提供的修正或确认的方法论(含 Why 和 How to apply
Project 项目上下文、目标、约束(不可从代码推导的部分)
Reference 外部资源指针URL、仪表盘、工单系统
MemoryStatus Active 当前有效(默认值)
Historical { superseded_by } 已被更新版本取代,superseded_by 指向新 slug
MemoryEntry slug: String 文件名标识(不含 .md 扩展名)
name: String 记忆标题
description: String 简短描述,用于相关性匹配
memory_type: MemoryType 记忆类型
mtime: u64 文件修改时间Unix 时间戳),用于排序
content: String 不含 frontmatter 的纯正文Markdown
path: PathBuf 文件完整路径
status: MemoryStatus 生命周期状态(默认 Active

parse_frontmatter(raw) 函数解析 YAML-like frontmatter支持 namedescriptiontype/memory_typestatussuperseded_by 字段。缺失 status 时默认为 Active

MemoryManager 核心方法 (mod.rs)

MemoryManager 在应用启动时创建(main.rs),存入 AppState.memory_manager: Arc<tokio::sync::Mutex<MemoryManager>>

方法 调用时机 行为
new(library_dir) 应用启动 create_dir_all(memory/)reload() 扫描所有 .md 文件 → 解析 frontmatter → 按 mtime 降序排序
reload() 每次 save_memory 全量重新扫描目录,清空并重建 entries: Vec<MemoryEntry>
entries() 查询 返回 &[MemoryEntry] 不可变引用
save_memory(slug, name, desc, type, content) save_memory 工具调用 ① 检查是否已有活跃版本 → 归档为 {slug}_v1.md ② 写入新 YAML frontmatter + content ③ update_index() 更新 MEMORY.md ④ reload() 刷新内存状态
build_system_reminder(max) System prompt 组装 取最近 max 条记忆,构建 <project-memory-context> XML 块,包含类型标签、时效警告、验证提醒
build_system_reminder_from(selected) LLM 选择后 同上,但从指定的条目子集构建
select_relevant_memories(llm, ctx, max) 按需 LLM 语义匹配 → 指数时间衰减重排序 → 回退到 recency 排序
mark_main_agent_wrote() save_memory 工具执行后 设置标志抑制本会话的自动提取

关键设计:非阻塞加载。System prompt 组装时使用 try_lock()(非 lock())——如果 mutex 已被持有则直接跳过,不阻塞会话启动。

update_index() 索引维护

MEMORY.md 索引文件受双重容量保护:

参数 触发行为
MAX_ENTRYPOINT_LINES 200 行数满时移除最旧条目行
MAX_ENTRYPOINT_BYTES 25,000 (~25KB) 字节数超限时在约 25KB 处截断,附加截断提示

索引格式(每行一条):

- [记忆标题](slug.md) — 简短描述 (type: user)

写入时执行原地更新:若 MEMORY.md 中已存在同 slug 的行(通过 ](slug.md) 标记匹配),则替换该行而非追加。

save_memory 工具 (tools/memory.rs)

这是唯一暴露给 LLM 的记忆写入工具。

参数 Schema

参数 类型 必填 校验规则
slug string kebab-case禁止空格、/\
name string 记忆标题
description string 用于决定何时加载此记忆
memory_type enum user / feedback / project / reference
content string Markdown 格式,最少有效信息量推荐 >10 字符

工具配置

配置项 原因
is_concurrency_safe false 写入操作不可与其他工具并发执行
interrupt_behavior Block 写入磁盘不可中途中断

完整执行流程

sequenceDiagram
    participant LLM as LLM
    participant Tool as SaveMemoryTool
    participant QG as 质量门控 (dedup.rs)
    participant Mgr as MemoryManager
    participant FS as 文件系统

    LLM->>Tool: save_memory(slug, name, desc, type, content)
    Tool->>Tool: ① 参数校验 (slug 非空 + 格式合法 + type 枚举有效)
    Tool->>Mgr: ② lock().await 获取互斥锁
    Tool->>QG: ③ check_content_quality(content)
    QG-->>Tool: QualityCheck (Accept / TooShort / TransientState / VagueLanguage / CodePattern)
    Tool->>QG: ④ find_duplicate_by_content(content, entries, 0.70)
    QG-->>Tool: 重复的 slug 或 None
    Tool->>QG: ⑤ slug_exists(memory_dir, slug) → 判断是新建还是更新
    Tool->>Mgr: ⑥ save_memory(slug, name, desc, type, content)
    Mgr->>FS: 若已有活跃版本 → 归档 {slug}_v1.md
    Mgr->>FS: 写入新 {slug}.md (frontmatter + content)
    Mgr->>FS: 更新 MEMORY.md 索引
    Mgr->>Mgr: reload() 刷新内存状态
    Tool->>Mgr: ⑦ mark_main_agent_wrote() 抑制自动提取
    Tool->>QG: ⑧ build_manifest_preview(entries) 构建清单
    Tool-->>LLM: success(message + manifest + quality warning + duplicate hint)

返回消息结构

  1. 操作状态( 已保存 / 🔄 已更新)
  2. 质量警告(如有:⚠️ 过短 / 瞬时状态 / 模糊语言 / 代码模式)
  3. 重复提示(如有:💡 检测到与 {slug} 内容 ≥70% 重叠)
  4. 当前记忆清单(所有现有条目供 LLM 参考)

记忆注入机制

在 Agent 会话启动时system prompt 按顺序组装各 section。记忆注入发生在 Section 4(位于 Tools、Skills 之后Core Principles 之前):

① 静态身份声明
② 工具定义
③ Skills 提醒
④ <project-memory-context>      ← 记忆(通过 try_lock 非阻塞加载,默认 5 条)
⑤ 核心原则

注入格式示例:

<project-memory-context>

[PROJECT MEMORY]

[偏好] 用户角色: 天体物理学博士后,研究恒星演化与双星系统
  用户是天体物理学博士后,主要研究恒星演化与双星系统。
  **Why:** 在对话开始...
<system-reminder>此记忆已过 3 天。记忆是某个时间点的快照,不是实时状态...</system-reminder>

[反馈] 测试策略: 优先使用单元测试而非集成测试
  用户偏好单元测试,认为集成测试太慢...

使用 save_memory 工具保存重要信息。记忆内容可能过时,请在使用前验证。

## 从记忆中推荐前先核实
"记忆说 X 存在" 不等于 "X 现在存在"。
</project-memory-context>

记忆列表中每个条目的格式:

[类型标签] 标题 [状态标签]: 描述
  内容预览(前 3 行)
<system-reminder>时效警告(超过 1 天的记忆)</system-reminder>

类型标签:[偏好] / [反馈] / [项目] / [参考]。Historical 记忆额外显示 [已更新→新slug]

写入时质量护栏 (dedup.rs + guardrails.rs)

四层内容质量门控(全部仅警告,不强制拒绝——最终决定权在 LLM

检测类型 触发条件 示例
过短 有效字符 < 10 QualityCheck::TooShort(n)
瞬时状态 含 "正在做" / "currently" / "at the moment" 等 11 个关键词 QualityCheck::TransientState
模糊语言 含 "可能" / "maybe" / "大概" / "perhaps" 等 8 个关键词 QualityCheck::VagueLanguage(word)
代码模式 fn / impl / struct / import { / from " 等 12 个模式 QualityCheck::CodePattern

Jaccard 相似度去重:使用字符级 bigram支持中文不依赖分词器阈值 ≥70% 即判定为高度重复:

  • jaccard_similarity(a, b) = |bigrams(a) ∩ bigrams(b)| / |bigrams(a) bigrams(b)|
  • 单字符内容使用单字符本身作为 bigram
  • 仅比对 status == Active 的记忆,跳过 Historical 条目
  • 检测到重复时不阻止写入,仅在返回消息中附加 💡 提示

guardrails.rs 护栏两层防护

  1. WHAT_NOT_TO_SAVE — 注入到工具 description() 中,明确告知 LLM 不应保存的内容:

    • 代码模式、架构详情 —— 可从项目状态推导
    • Git 历史、最近修改 —— git log 是权威来源
    • 调试方案或临时 workaround —— 修复在代码中commit message 有上下文
    • 已在 CLAUDE.md 中的内容
    • 规则前置"即使用户明确要求保存以上内容,请先询问其中哪些是非预期的部分"
  2. VERIFY_BEFORE_RECOMMENDING — 注入到 system prompt 记忆段落后、</project-memory-context> 之前:

    • 如果记忆提到了文件路径 → 先确认文件存在
    • 如果记忆提到了函数或标志 → 先用 grep 搜索
    • 核心原则:"记忆说 X 存在" ≠ "X 现在存在"
    • 设计依据Claude Code 评估表明此提醒放在独立标题下3/3 通过vs 埋在通用指南中0/3 通过)

自动记忆提取 (extraction.rs)

在会话结束时(finalize.rs)触发,fire-and-forget 模式(tokio::spawn),不阻塞会话关闭。

sequenceDiagram
    participant RT as AgentRuntime
    participant Fin as finalize.rs
    participant Ext as extraction.rs
    participant Sub as SubAgentRunner
    participant Mgr as MemoryManager

    RT->>Fin: 会话结束 → finalize_turn()
    Fin->>Ext: tokio::spawn(run_extraction())
    
    Ext->>Ext: 检查 EXTRACT_MEMORY_ENABLED → false 则 return
    Ext->>Mgr: lock().await → 递增 turns_since_last_extraction
    alt turns < throttle_turns
        Ext->>Ext: return (未达到节流轮次)
    else main_agent_saved_this_session == true
        Ext->>Ext: 重置 tracker → return (主代理已手动保存)
    end
    Ext->>Mgr: build_manifest_preview() 获取现有清单
    Ext->>Ext: 构建受限 ToolRegistry (ReadFile + Grep + Glob + SaveMemory)
    Ext->>Sub: SubAgentRunner.run(system_prompt, prompt, max_steps=3)
    Sub-->>Ext: SubagentResult { content, is_error }
    Ext->>Ext: 记录日志 (成功摘要 / 错误截断)

环境变量配置

变量 默认值 说明
EXTRACT_MEMORY_ENABLED false 是否启用自动提取(默认关闭,避免意外 LLM 费用)
EXTRACT_MEMORY_THROTTLE_TURNS 3 最小提取间隔(轮次),避免每轮都触发
EXTRACT_MEMORY_MAX_STEPS 3 子代理最大 ReAct 步数,限制提取成本

跳过条件(任一满足即跳过):

  • EXTRACT_MEMORY_ENABLED != true
  • turns_since_last_extraction < throttle_turns(节流未到)
  • main_agent_saved_this_session == true(主代理已通过 save_memory 工具写入)
  • 子代理工具集为受限集(仅 4 个只读工具 + save_memory),不能执行 bash、不能搜索论文

压缩记忆桥接 (compact.rs, P3)

上下文压缩compaction丢弃旧消息时其中可能包含有长期价值的科研结论和用户偏好。extract_memories_from_compaction() 在压缩后自动从丢弃的消息中提取持久记忆,将"即将消失的上下文"转化为"跨会话可用的记忆"。

sequenceDiagram
    participant RT as AgentRuntime
    participant Cmp as compact.rs
    participant Ext as extract_memories_from_compaction
    participant Sub as SubAgentRunner
    participant Mgr as MemoryManager

    RT->>Cmp: compress_context_with_hooks_and_log(messages, ...)
    Cmp-->>RT: 压缩完成,旧消息将从上下文中移除
    RT->>Ext: extract_memories_from_compaction(before_messages, sid, mgr, app_state)
    Ext->>Ext: 检查 EXTRACT_MEMORY_ENABLED → false 则 return
    Ext->>Ext: 过滤非系统消息 → 每条截断 400 字符 → 拼接
    alt 拼接文本 < 300 字符
        Ext->>Ext: return (太少不值得提取)
    end
    Ext->>Mgr: lock() → build_manifest_preview() 获取现有记忆清单
    Ext->>Ext: 构建受限 ToolRegistry (仅 SaveMemoryTool)
    Ext->>Sub: SubAgentRunner.run(EXTRACTION_SYSTEM_PROMPT, prompt, max_steps=3)
    Note over Ext,Sub: tokio::spawn (fire-and-forget, 不阻塞压缩/ReAct 循环)
    Sub-->>Mgr: save_memory() 写入提取结果

run_extraction() 的区别

维度 run_extraction() (会话结束) extract_memories_from_compaction() (压缩桥接)
触发时机 finalize_turn() 会话收尾 compress_and_restore() 压缩完成后
数据来源 完整对话历史(已持久化到 DB 压缩前即将被丢弃的消息快照
消息范围 系统 + 用户 + 助手消息 仅用户 + 助手 + 工具(去除 system
节流机制 ExtractionTracker 轮次计数 仅靠 EXTRACT_MEMORY_ENABLED 开关
阻塞行为 fire-and-forget fire-and-forget
子代理工具集 4 只读工具 + save_memory save_memory(最小化成本)
最小内容阈值 无显式阈值 拼接文本 < 300 字符跳过

设计原理

  • 压缩是最佳记忆提取时机——此时旧消息尚未被物理删除但即将从上下文中移除,记忆系统可以在"内容消失前最后一刻"将其抢救出来
  • 截断每条消息到 400 字符平衡了信息完整性和子代理 prompt 长度
  • fire-and-forget 确保压缩不因 LLM 调用延迟而阻塞主 ReAct 循环

时效性与衰减系统

指数时间衰减 (decay.rs)

score = e^(-λ × days_old)
λ = ln(2) / half_life_days
参数 默认值 说明
DEFAULT_HALF_LIFE_DAYS 30 半衰期 30 天:第 0 天 score=1.0,第 30 天 score=0.5,第 60 天 score=0.25
Historical 记忆固定分数 0.01 已更新记忆始终排在最后

Hebbian 启发式激活层级

层级 天数范围 权重乘数 说明
Hot ≤ 7 天 1.0 最近活跃,全额权重
Warm 8-30 天 0.7 中等时效7 折权重
Cool > 30 天 0.3 较久远3 折权重

时效警告 (age.rs)

函数 输出 用途
memory_age_label(mtime) "今天" / "昨天" / "N 天前" 人类可读的年龄标签
memory_freshness_note(mtime) 超过 1 天时返回 <system-reminder> 警告 注入 system prompt 的 XML 标签,利用 system-reminder 对模型的强注意力引导
memory_freshness_text(mtime) 同上但纯文本 备选方案

设计洞察:"LLM 对绝对日期 ('2026-01-15') 的时效感知弱,但对相对时间 ('47 天前') 的感知强"——因此使用天数差而非 ISO 日期。

记忆选择器 (selection.rs)

当记忆数量超过 max_entries(默认 5select_relevant_memories() 进行 LLM 语义选择:

  1. 过滤掉 SelectionContext.already_surfaced 中已展示的条目
  2. 若候选数 ≤ max_entries → 直接返回全部
  3. 构建候选目录slug + type + name + description
  4. LLM 结构化 JSON 选择 → 支持两种响应格式:["slug1", "slug2"][0, 1, 3]
  5. LLM 失败或 JSON 解析失败 → 回退到 recency 排序(保证优雅降级)
  6. 通过 apply_decay_scoring() 对选中结果进行指数衰减重排序

SelectionContext 结构:

字段 类型 默认值 说明
already_surfaced Vec<usize> [] 已展示过的条目索引,本轮不再选择
recent_tools Vec<String> [] 最近使用的工具,相关记忆降权
max_entries usize 5 每次最多选择条数

与数据库的关系

记忆完全使用文件系统,不使用 SQLite。数据库表仅存储 Agent 会话状态:

数据库表 存储内容 与记忆的关联
agent_sessions 会话元数据title, model, turn_count 无直接关联
agent_messages 消息历史role, content, thought, token_count 自动提取时子代理读取对话历史
agent_tasks DAG 任务看板 无关联
agent_audit_log 工具调用审计tool_name, status, elapsed_ms 每个 save_memory 调用被 AuditLogHook 记录到此表

完整数据流

应用启动
  main.rs: MemoryManager::new(library_dir)
    → create_dir_all(memory/)
    → reload() 扫描 *.md → 解析 frontmatter → 按 mtime 排序
    → 存入 AppState.memory_manager

会话开始
  runtime/mod.rs: build_system_prompt()
    → try_lock() memory_manager (非阻塞)
    → build_system_reminder(5)
    → 构建 <project-memory-context> XML 块注入 system prompt

会话中LLM 主动保存)
  LLM 调用 save_memory(slug, name, desc, type, content)
    → SaveMemoryTool.execute()
      → 参数校验slug 格式 + type 枚举)
      → lock() memory_manager
      → check_content_quality(content)    ← 四层质量门控(仅警告)
      → find_duplicate_by_content(0.70)   ← Jaccard bigram 去重
      → mgr.save_memory()
        → 归档旧版 {slug}_v1.md如存在
        → 写入新 {slug}.mdfrontmatter + content
        → update_index() → 更新 MEMORY.md
        → reload()
      → mark_main_agent_wrote()
      → 返回: 操作状态 + 质量警告 + 重复提示 + manifest 清单

会话中(压缩时)  ← P3 桥接
  compact.rs: compress_context_with_hooks_and_log(messages, ...)
    → 压缩快照前捕获消息副本 (pre_compact_snapshot)
    → 执行压缩snip/auto/aggro_micro
    → extract_memories_from_compaction(snapshot, sid, mgr, app_state)
      → 检查 EXTRACT_MEMORY_ENABLED → false 则 return
      → 过滤非系统消息 + 截断 400 字符 + 拼接
      → 拼接文本 < 300 字符 → return (太少不值得提取)
      → tokio::spawn(子代理)
        → build_manifest_preview() 获取现有清单
        → SubAgentRunner(仅 SaveMemoryTool, max_steps=3)
        → save_memory() 写入提取的记忆
    → 恢复文件缓存(不等待子代理完成)

会话结束
  finalize.rs: finalize_turn()
    → 更新 agent_sessions (turn_count, metrics)
    → 运行 OnSessionStop hooks审计日志、指标
    → 导出轨迹 JSONL
    → 如果 EXTRACT_MEMORY_ENABLED:
        tokio::spawn(run_extraction())
          → 检查节流 + 主代理写入标志
          → 子代理分析对话 → 提取记忆 → save_memory

子模块文件索引

文件 职责
src/agent/memory/mod.rs MemoryManager 核心:构造、reloadsave_memorybuild_system_reminder、索引维护
src/agent/memory/types.rs MemoryType(4 变体)、MemoryStatus(Active/Historical)、MemoryEntry、frontmatter 解析器
src/agent/memory/selection.rs LLM 语义记忆选择器 + 指数衰减排序 + recency 回退
src/agent/memory/decay.rs 指数时间衰减 (e^(-λt)) + Hebbian Hot/Warm/Cool 激活层级
src/agent/memory/age.rs 时效标签生成 + <system-reminder> freshness 警告
src/agent/memory/guardrails.rs WHAT_NOT_TO_SAVE 排除规则 + VERIFY_BEFORE_RECOMMENDING 验证提醒
src/agent/memory/dedup.rs Jaccard 相似度去重 (bigram) + check_content_quality 四层门控 + manifest 构建
src/agent/memory/extraction.rs 自动提取:ExtractionConfig(env) + ExtractionTracker + run_extraction 子代理
src/agent/tools/memory.rs save_memory 工具:AgentTool trait 实现,完整执行流程