AggRoot
文档目录

记忆与上下文

AggRoot 拥有多层记忆系统,从会话内的即时上下文到跨会话的持久记忆,确保 AI 助手在不同场景下都能保持对项目的理解。

上下文感知记忆

会话内文件追踪

在对话过程中,AggRoot 自动追踪所有读取和修改的文件:

  • 读取追踪:每次 Read/Glob/Grep 工具调用都会记录文件路径
  • 修改追踪:每次 Write/Edit 工具调用都会记录变更内容
  • 子代理追踪:子代理读取的文件也会被追踪,主代理可据此避免重复读取

文件追踪的作用

  1. 避免重复读取:主代理知道子代理已经读过哪些文件
  2. 变更撤销:通过 /undo 命令撤销最近的文件修改
  3. 上下文构建:为 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\aggrootc-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: 对于 feedbackproject 类型,推荐使用 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 空间

压缩流程

  1. 压缩前 flushflushBeforeCompact() 提取重要信息写入记忆,避免丢失
  2. 消息链完整性修复:移除孤立的 tool 消息和缺少响应的 tool_calls
  3. 占位 tool result 插入:为仍缺少响应的 tool_calls 插入 [Tool execution was interrupted]
  4. 旧消息摘要:将早期对话轮次压缩为摘要,保留关键信息
  5. 快照重拍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 只放最核心的规则。

子代理的文件追踪

子代理读取的文件也会被追踪,主代理可以据此避免重复读取。

工作机制

  1. 每个子代理创建时会注册一个 ForkContext,包含 readFiles: Set<string>
  2. 子代理的 Read 工具执行后,文件路径会自动添加到 readFiles
  3. 主代理可以通过 ForkSubagentService.hasReadFile(forkId, filePath) 检查子代理是否已读取某文件
  4. 子代理的输出缓存(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() 函数会剥离嵌套的栅栏标签,防止注入攻击。