记忆与上下文
AggRoot 拥有多层记忆系统,从会话内的即时上下文到跨会话的持久记忆,确保 AI 助手在不同场景下都能保持对项目的理解。
上下文感知记忆
会话内文件追踪
在对话过程中,AggRoot 自动追踪所有读取和修改的文件:
- 读取追踪:每次 Read/Glob/Grep 工具调用都会记录文件路径
- 修改追踪:每次 Write/Edit 工具调用都会记录变更内容
- 子代理追踪:子代理读取的文件也会被追踪,主代理可据此避免重复读取
文件追踪的作用
- 避免重复读取:主代理知道子代理已经读过哪些文件
- 变更撤销:通过
/undo命令撤销最近的文件修改 - 上下文构建:为 LLM 提供当前操作涉及的文件信息
撤销文件修改
你> /undo
/undo 会撤销 AggRoot 在当前会话中对文件的最近一次修改。
Tips:
/undo只能撤销当前会话中的修改。如果修改已经 git commit,需要使用git revert来撤销。
项目记忆
项目记忆是 AggRoot 跨会话的持久知识库,帮助 AI 助手在新的对话中快速了解项目背景和用户偏好。
架构概览
LLM / Agent Loop
│
├── Memory Tool (add/replace/remove) ──→ MemoryService ──→ Memory Aggregate ──→ UnifiedMemoryStore (磁盘)
│
├── MemoryManager (编排层)
│ ├── 内置 MemoryService(始终活跃)
│ └── 外部 Provider(最多1个,如 Honcho)
│
├── MemoryFlushService (后台LLM驱动的记忆审查)
│
└── Session Search Tool (FTS5 会话搜索)
存储位置
~/.aggroot/projects/{projectKey}/memory/ # 用户私有记忆
{projectRoot}/.aggroot/memory/ # 团队共享记忆(只读)
- 用户私有记忆:存储在
~/.aggroot/下,仅当前用户可见 - 团队共享记忆:存储在项目
.aggroot/目录下,可提交到 Git 供团队共享 - 用户私有记忆优先级高于团队共享记忆(同名条目以私有为准)
其中 projectKey 由项目根路径派生,例如 C:\Users\zhuoz\Desktop\aggroot → c-Users-zhuoz-Desktop-aggroot。
记忆类型
| 类型 | 中文标签 | 用途 | 字符限额 | 示例 |
|---|---|---|---|---|
| user | 用户画像 | 用户的角色、偏好、知识背景 | 800 | "用户是后端工程师,熟悉 Go,React 新手" |
| feedback | 行为反馈 | 用户对 AI 行为的指导(应该做/不应该做) | 800 | "不要在每次回复后总结" |
| project | 项目状态 | 项目级的决策和背景 | 1000 | "认证中间件重写是合规需求驱动" |
| reference | 外部引用 | 指向外部系统的链接 | 600 | "Pipeline Bug 在 Linear INGEST 项目中追踪" |
Tips: 字符限额是同一类型所有条目内容的总字符数(含分隔符),不是单条限制。超限时需先 replace 或 remove 旧条目。
记忆命令
你> /memory # 查看记忆概览
你> /memory list # 列出所有记忆条目
你> /memory add # 添加新记忆
你> /memory delete <名称> # 删除指定记忆
你> /memory search <关键词> # 搜索记忆
记忆文件格式
每条记忆存储为独立的 Markdown 文件,带 YAML frontmatter:
---
name: 用户角色
description: 用户的角色和技术背景
type: user
---
用户是全栈工程师,熟悉 TypeScript 和 React,对 DDD 架构有深入了解。
**Why:** 知道用户技术背景后,可以跳过基础解释,直接讨论架构决策
**How to apply:** 在解释代码时使用架构术语,不需要解释 DDD 基础概念
name→ 人类可读标识,同时作为文件名基(特殊字符替换为_)description→ 一行摘要,用于 MEMORY.md 索引显示和相关性匹配type→ 4 种:user/feedback/project/reference- body → 完整记忆内容(Markdown 格式)
Tips: 对于
feedback和project类型,推荐使用 Why + How to apply 结构:Why 记录原因(通常是一次事故或强烈偏好),How to apply 记录何时/何处应遵循此规则。这样 AggRoot 在边缘场景下也能做出正确判断。
索引文件 MEMORY.md
所有记忆条目通过 MEMORY.md 索引文件管理:
# Memory Index
- [用户角色](用户角色.md) — 用户的角色和技术背景
- [行为反馈](行为反馈.md) — 回复要简洁,不要总结已做的事
- [项目状态](项目状态.md) — 认证中间件重写是合规驱动
索引在每次 add/replace/remove 后自动重建。MEMORY.md 会在对话开始时自动加载到上下文中(前 200 行),超出部分不加载。
写入磁盘
写入采用原子写入策略:先写 .tmp 临时文件,再 rename 覆盖目标文件,防止写一半断电导致损坏。同类型写操作通过 per-type 互斥锁串行化,防止并发会话间的竞态条件。
什么时候存(写入触发点)
记忆写入有 5 种触发方式:
| 触发时机 | 触发方 | 说明 |
|---|---|---|
| 模型主动调用 | memory 工具 |
模型调用 action: add/replace/remove,模型认为值得记住就调用 |
| 后台 nudge 审查 | MemoryFlushService.nudgeReview() |
每 10 轮用户对话触发一次,轻量 LLM agent 审查最近对话并调用 memory 工具保存 |
| 压缩前 flush | MemoryFlushService.flushBeforeCompact() |
上下文压缩前执行,保存即将被丢弃的重要信息,最少需 6 轮对话 |
| 会话结束 flush | MemoryFlushService.flushBeforeSessionEnd() |
会话归档前提取记忆,无轮次门槛 |
| 桥接到外部 | _bridgeMemoryWrite() |
内置 memory 工具写入后,自动通知外部 Provider 同步 |
写入流程
memory 工具 (action=add, type=feedback, content="...")
→ 安全扫描(不可见Unicode、提示注入、数据外泄)
→ MemoryService.add()
→ Memory 聚合根
→ per-type 互斥锁
→ 从磁盘重新加载该类型(并发安全)
→ 同名去重(同名+同内容=跳过,同名+不同内容=覆盖)
→ 同类型内容去重
→ 字符限额检查
→ 原子写入磁盘 (.tmp → rename)
→ 异步重建 MEMORY.md 索引
nudge 计数器
后台 nudge 审查每 10 轮触发一次,但以下情况会重置计数器:
- 模型主动使用
memory工具(add/replace/remove) - 模型使用外部记忆工具(如
honcho_*) - nudge 触发并完成
这避免了模型刚主动存过记忆又立即触发 nudge 审查的浪费。
什么时候召回(读取触发点)
记忆召回有 3 个层次:
第 1 层:系统提示快照(冻结)
- 触发时机:会话开始时
Memory.load()从磁盘加载所有条目,生成冻结快照注入 system prompt - 冻结策略:快照在会话内不更新(保护 LLM prefix cache),仅在上下文压缩后重拍
- 注入策略:
| 类型 | 注入方式 |
|---|---|
| user | 全量注入 |
| feedback | 全量注入 |
| project | Top-3(按类型加成排序) |
| reference | Top-3(按类型加成排序) |
第 2 层:外部 Provider 预取
- 触发时机:每轮对话开始前
MemoryManager.prefetchAll() - 注入方式:结果用
<memory-context>栅栏包裹,作为 user message 注入(非 system prompt) - 栅栏标记:
[System note: The following is recalled memory context, NOT new user input.],防止模型混淆 - 异步预取:当前轮结束后
queuePrefetchAll()为下一轮预取
第 3 层:工具主动搜索
session_search工具:FTS5 关键词搜索历史会话 + LLM 摘要honcho_*工具:外部 Provider 暴露的搜索工具
召回模式
| 模式 | 自动注入 | 工具暴露 | 适用场景 |
|---|---|---|---|
context |
有 | 无 | 只需自动注入,不需要手动搜索 |
tools |
无 | 有 | 只需手动搜索,不需要自动注入 |
hybrid(默认) |
有 | 有 | 两者兼需 |
相关性匹配
当 project/reference 类型条目超过注入上限时,使用 TF 词频匹配评分选择最相关的:
- name 匹配:权重最高(3x)
- description 匹配:权重中等(2x)
- content 匹配:基础权重(1x)
- 类型加成:user(1.0)> feedback(0.8)> project(0.5)> reference(0.3)
安全扫描
所有记忆写入前经过三层安全检测:
| 层级 | 检测内容 |
|---|---|
| 不可见字符 | 零宽空格、BOM、方向控制符等 10 种 |
| 提示注入 | "ignore previous instructions" 等 7 种模式 |
| 数据外泄 | URL、IP、文件路径等 4 种外泄模式 |
| 持久后门 | 自我引用、系统提示修改等 2 种模式 |
检测到威胁时写入被拒绝,返回 MemorySecurityError。
上下文压缩(Compaction)
当对话历史接近 Token 限制时,AggRoot 会自动压缩旧消息。
压缩触发条件
- 消息总 Token 数接近模型上下文窗口限制
- 保留系统提示词和最近几轮对话的 Token 空间
压缩流程
- 压缩前 flush:
flushBeforeCompact()提取重要信息写入记忆,避免丢失 - 消息链完整性修复:移除孤立的 tool 消息和缺少响应的 tool_calls
- 占位 tool result 插入:为仍缺少响应的 tool_calls 插入
[Tool execution was interrupted] - 旧消息摘要:将早期对话轮次压缩为摘要,保留关键信息
- 快照重拍:
invalidateSnapshot()从磁盘重新加载记忆并重拍快照
压缩后的影响
- 早期对话的详细内容可能被压缩为摘要
- 最近的对话轮次保持完整
- 重要的上下文信息应存入项目记忆,避免在压缩中丢失
Tips: 如果 AggRoot 在长对话中"遗忘"了之前的上下文,这通常是因为压缩。将关键信息写入项目记忆是避免遗忘的最好方法。
AGGROOT.md
AGGROOT.md 是项目根目录下的特殊文件,AggRoot 会自动将其内容注入到每个代理的系统提示词中。
用途
- 记录项目特定的约定和规则
- 定义代码风格偏好
- 说明项目架构和设计决策
- 提供项目背景信息
示例 AGGROOT.md
# 项目约定
## 代码风格
- 使用 TypeScript strict 模式
- API 响应统一使用 Result<T> 类型
- 错误处理使用自定义 AppError 类
## 架构
- DDD 分层架构:domain / application / infrastructure / interfaces
- 领域事件驱动解耦
- 依赖注入使用构造函数注入
## 测试
- 单元测试必须覆盖 domain 层
- 集成测试必须使用真实数据库
- 测试文件放在 `tests/` 目录下
## 禁止事项
- 不要在 domain 层引入基础设施依赖
- 不要使用 any 类型
- 不要跳过错误处理
AGGROOT.md vs 项目记忆
| 特性 | AGGROOT.md | 项目记忆 |
|---|---|---|
| 位置 | 项目根目录 | ~/.aggroot/projects/ |
| 可见性 | 团队共享 | 用户私有 |
| 加载方式 | 始终完整加载 | 按类型和相关性选择加载 |
| 适合内容 | 项目约定、架构规则 | 用户偏好、行为反馈 |
| 版本控制 | 应提交到 Git | 不应提交 |
| 更新频率 | 手动编辑 | 模型自动 + 手动 |
Tips: AGGROOT.md 保持简洁。它会被完整注入到每次 LLM 调用中,内容越多,消耗的 Token 越多。将详细文档放在项目记忆中,AGGROOT.md 只放最核心的规则。
子代理的文件追踪
子代理读取的文件也会被追踪,主代理可以据此避免重复读取。
工作机制
- 每个子代理创建时会注册一个
ForkContext,包含readFiles: Set<string> - 子代理的 Read 工具执行后,文件路径会自动添加到
readFiles - 主代理可以通过
ForkSubagentService.hasReadFile(forkId, filePath)检查子代理是否已读取某文件 - 子代理的输出缓存(OutputCache)与父代理共享,重复读取直接走缓存
最佳实践
你> 熟悉这个项目
AggRoot → DirectoryTree(depth=2)
→ SubagentExecutor(explore, "探索领域层")
→ SubagentExecutor(explore, "探索基础设施层")
→ [子代理返回文件列表和关键发现]
→ [主代理需要某文件细节时,直接 Read(走缓存,无磁盘 I/O)]
Tips: 子代理返回的是精简摘要,完整代码内容已缓存。需要查看细节时用 Read 工具读取,缓存会直接返回,不会重复消耗 API 额度。
外部记忆 Provider
AggRoot 支持接入外部记忆 Provider(如 Honcho),与内置记忆系统并行工作。
设计约束
- 单一外部 Provider:最多注册 1 个外部 Provider,防止工具 schema 膨胀和后端冲突
- 故障隔离:外部 Provider 调用全部 try/catch,失败不阻塞主流程
桥接机制
| 机制 | 说明 |
|---|---|
onMemoryWrite() |
内置 memory 工具写入后,自动通知外部 Provider 同步 |
onDelegation() |
子代理完成时通知外部 Provider |
onPreCompress() |
压缩前外部 Provider 可贡献文本到压缩摘要 |
syncAll() |
每轮结束后同步 user/assistant 内容对到外部 Provider |
prefetchAll() |
每轮开始前从外部 Provider 预取相关上下文 |
栅栏隔离
外部 Provider 召回的内容用 <memory-context> 栅栏包裹:
<memory-context>
[System note: The following is recalled memory context, NOT new user input.]
... recalled content ...
</memory-context>
这防止模型将召回的记忆误认为新的用户输入。sanitizeContext() 函数会剥离嵌套的栅栏标签,防止注入攻击。