- AgentConfig/LlmClient 新增 enable_thinking 参数,前端 SSE 请求传递 thinking 开关,仅千问/DashScope 时启用 - 完善权限系统,支持细粒度的权限控制和用户权限申请 - delegate_research 工具重命名为 subagent,SubAgentTool/SubAgentRunner 重构 - 子代理消息(system/user/assistant/tool)持久化到 agent_messages 表,带 agent_name 标识 - 子代理活动日志(工具调用列表+思考摘要)注入返回结果,Hooks 获得正确 session_id 和 subagent_name - LLM 工具调用 ID 回退生成 UUID(llm.rs),ToolCall/ToolResult SSE 事件增加 id/tool_call_id 双字段 - ToolContext 扩展 session_id/sse_tx/enable_thinking 字段,executor 统一注入而非构造函数传参 - agent_messages 新增 metadata+raw_json 列,agent_sessions 暴露 summary 字段 - 删除文件级 transcript 快照(compact.rs),改为依赖 DB 持久化 - ResearchAgentPanel 重写:TimelineItem 类型替代 StreamStep,支持会话历史回放 - 新增 AgentMetricsPanel/AskUserQuestionCard/AuditLogViewer 三个前端组件,types.ts 完整类型定义 - docs/architecture/ 分层重组:概览/核心模块/核心工作流 + agent/ 子目录 11 篇专题文档 - docs/api.md 补充 RAG/Target/Agent 接口,docs/development.md 新建开发指南 - .env.example 完全重写,补充 FALLBACK_MODEL 等变量说明
19 KiB
记忆系统 (memory/)
参考 Claude Code memdir/ 设计,提供完全基于文件系统(非数据库)的项目级持久化记忆管理。核心代码位于 src/agent/memory/(8 个文件)和 src/agent/tools/memory.rs(save_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,支持 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 处截断,附加截断提示 |
索引格式(每行一条):
- [记忆标题](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)
返回消息结构:
- 操作状态(✅ 已保存 / 🔄 已更新)
- 质量警告(如有:⚠️ 过短 / 瞬时状态 / 模糊语言 / 代码模式)
- 重复提示(如有:💡 检测到与
{slug}内容 ≥70% 重叠) - 当前记忆清单(所有现有条目供 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 护栏两层防护:
-
WHAT_NOT_TO_SAVE— 注入到工具description()中,明确告知 LLM 不应保存的内容:- 代码模式、架构详情 —— 可从项目状态推导
- Git 历史、最近修改 ——
git log是权威来源 - 调试方案或临时 workaround —— 修复在代码中,commit message 有上下文
- 已在 CLAUDE.md 中的内容
- 规则前置:"即使用户明确要求保存以上内容,请先询问其中哪些是非预期的部分"
-
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 != trueturns_since_last_extraction < throttle_turns(节流未到)main_agent_saved_this_session == true(主代理已通过save_memory工具写入)- 子代理工具集为受限集(仅 4 个只读工具 +
save_memory),不能执行 bash、不能搜索论文
时效性与衰减系统
指数时间衰减 (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 语义选择:
- 过滤掉
SelectionContext.already_surfaced中已展示的条目 - 若候选数 ≤ max_entries → 直接返回全部
- 构建候选目录(slug + type + name + description)
- LLM 结构化 JSON 选择 → 支持两种响应格式:
["slug1", "slug2"]和[0, 1, 3] - LLM 失败或 JSON 解析失败 → 回退到 recency 排序(保证优雅降级)
- 通过
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 清单
会话结束
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 实现,完整执行流程 |