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 系统依赖
491 lines
23 KiB
Markdown
491 lines
23 KiB
Markdown
# 记忆系统 (`memory/`)
|
||
|
||
参考 Claude Code `memdir/` 设计,提供**完全基于文件系统**(非数据库)的项目级持久化记忆管理。核心代码位于 `src/agent/memory/`(8 个文件)和 `src/agent/tools/memory.rs`(`save_memory` 工具)。
|
||
|
||
## 整体架构
|
||
|
||
```mermaid
|
||
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 # 旧版本归档(原文件重命名,永不删除)
|
||
└── ...
|
||
```
|
||
|
||
**每个记忆文件**格式:
|
||
|
||
```markdown
|
||
---
|
||
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,支持 `name`、`description`、`type`/`memory_type`、`status`、`superseded_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 处截断,附加截断提示 |
|
||
|
||
索引格式(每行一条):
|
||
```markdown
|
||
- [记忆标题](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` | 写入磁盘不可中途中断 |
|
||
|
||
**完整执行流程**:
|
||
|
||
```mermaid
|
||
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 条)
|
||
⑤ 核心原则
|
||
```
|
||
|
||
注入格式示例:
|
||
|
||
```xml
|
||
<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`),不阻塞会话关闭。
|
||
|
||
```mermaid
|
||
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()` 在压缩后自动从丢弃的消息中提取持久记忆,将"即将消失的上下文"转化为"跨会话可用的记忆"。
|
||
|
||
```mermaid
|
||
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`(默认 5)时,`select_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}.md(frontmatter + 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` 核心:构造、`reload`、`save_memory`、`build_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 实现,完整执行流程 |
|
||
|
||
---
|
||
|