让 AI Agent 不再「失忆」:blade-code Auto Memory 系统设计与实现
每次开新会话,Agent 都忘了你的项目用 pnpm 不用 npm,忘了那个诡异的 bug 要加
--legacy-peer-deps才能绕过,忘了你上次花 20 分钟教它的项目架构。本文介绍 blade-code 的 Auto Memory 系统——让 Agent 跨会话记住项目知识,从「每次从零开始」变成「越用越懂你」。
目录
问题:Agent 的金鱼记忆
用过 Coding Agent 的人都有这个体验:
# 第 1 次会话
你:项目怎么构建?
Agent:让我看看... 找到了,用 pnpm build
你:对,记住了
# 第 2 次会话
你:构建一下
Agent:让我试试 npm run build...
你:不是 npm,是 pnpm!上次不是说过了吗?
Agent:抱歉,让我用 pnpm build...
问题的根源很简单:会话结束,上下文清空,一切归零。
现有的 Coding Agent 通常有两种「记忆」:
- 会话内上下文 — 当前对话的消息历史,会话结束就没了
- 项目配置文件 — 比如
CLAUDE.md或BLADE.md,需要用户手写
第一种是短期记忆,活不过一个会话。第二种是外部记忆,但要用户自己维护——大多数人写了个开头就再也不更新了。
缺的是中间那层:Agent 自己在工作中积累的项目知识,自动记录,跨会话持久化。
Claude Code 怎么做的
Claude Code 在 2 月上线了 Auto Memory 功能,思路很直接:
- 项目根目录下有个
~/.claude/CLAUDE.md - Agent 工作时发现有价值的信息,自动写入
- 新会话启动时,前 200 行注入 system prompt
- 用户可以用
/memory命令查看和编辑
简单粗暴,但有效。它解决了「Agent 失忆」的核心问题,而且几乎零配置。
不过也有一些局限:
- 单文件 — 所有知识塞在一个 CLAUDE.md 里,项目复杂了会很乱
- 200 行硬限制 — 超了就截断,没有溢出机制
- 用标准文件工具读写 — 没有敏感数据过滤,Agent 可能把 token 写进去
blade-code 的方案在这个基础上做了改进。
blade-code 的方案
存储设计
不用单文件,用索引 + 主题文件的结构:
~/.blade/projects/{escaped-path}/memory/
├── MEMORY.md # 索引文件(启动时加载前 200 行)
├── patterns.md # 代码模式和约定
├── debugging.md # 调试洞察
├── architecture.md # 架构笔记
├── build.md # 构建和部署
└── ... # Agent 按需创建
MEMORY.md 是入口索引,保持精简。详细内容分散到主题文件里,Agent 需要时按需读取。
这样做的好处:
- 索引不会膨胀到 200 行限制
- 主题文件可以很详细,不受行数限制
- 用户管理起来更清晰
路径复用了 blade-code 现有的 getProjectStoragePath() 体系,memory 目录和 session JSONL 文件同级,不需要额外的路径映射。
加载机制
会话启动时,buildSystemPrompt() 自动注入 MEMORY.md:
构建顺序:
1. 环境上下文(OS、Node 版本等)
2. 默认系统提示
3. BLADE.md(用户手写的项目配置)
4. ★ Auto Memory(MEMORY.md 前 200 行) ← 新增
5. append(用户追加的提示)
6. 模式特定提示(Plan/Spec)
注入格式:
<auto-memory>
# Project Memory
## Build
- Package manager: pnpm
- Build: pnpm build
- Test: pnpm vitest run
## Architecture
- Monorepo: packages/cli + packages/web
- Entry: src/agent/Agent.ts
</auto-memory>
放在 BLADE.md 之后、append 之前,优先级适中——不会覆盖用户的显式配置,但比默认提示更具体。
工具设计
给 Agent 两个专用工具,而不是复用 Read/Write:
MemoryRead — 读取记忆文件
{
topic: string // "debugging" → debugging.md
// "MEMORY" → MEMORY.md
// "_list" → 列出所有文件
}
MemoryWrite — 写入记忆文件
{
topic: string // 主题名
content: string // 内容
mode: 'append' | 'overwrite' // 默认 append
}
为什么不直接用 Read/Write 工具?
- 安全隔离 — Memory 工具有敏感数据过滤,通用文件工具没有
- 路径封装 — Agent 不需要知道
~/.blade/projects/...的完整路径 - 语义清晰 —
MemoryWrite({ topic: "debugging", ... })比Write({ file_path: "~/.blade/projects/-root-my-project/memory/debugging.md", ... })意图明确得多
安全机制
三层防护:
1. 敏感数据过滤
const SENSITIVE_PATTERNS = [
/password\s*[=:]/i,
/api_key\s*[=:]/i,
/token\s*[=:]/i,
/secret\s*[=:]/i,
/private_key/i,
];
Agent 尝试写入包含密码、token、API key 的内容时,直接拒绝并返回错误提示。注意是匹配 password = xxx 这种赋值模式,而不是简单的关键词——「用户忘记了密码重置流程」这种描述性文本不会被误杀。
2. 路径遍历防护
if (topic.includes('..') || topic.includes('/') || topic.includes('\\')) {
throw new Error('Invalid topic name');
}
防止 Agent 通过 ../../etc/passwd 之类的 topic 名写入任意路径。
3. 索引行数限制
MEMORY.md 加载上限 200 行。超出的部分不会注入 system prompt,避免上下文膨胀。Agent 被指示把详细内容放到主题文件里,MEMORY.md 只做索引。
核心实现
1. AutoMemoryManager
核心管理器,约 150 行:
export class AutoMemoryManager {
private memoryDir: string;
private maxIndexLines: number;
constructor(projectPath: string, memoryDirOverride?: string) {
// 复用现有路径体系,或测试时注入自定义路径
this.memoryDir = memoryDirOverride
?? path.join(getProjectStoragePath(projectPath), 'memory');
this.maxIndexLines = 200;
}
async initialize(): Promise<void> {
await fs.mkdir(this.memoryDir, { recursive: true, mode: 0o755 });
}
async loadIndex(): Promise<string> {
const indexPath = path.join(this.memoryDir, 'MEMORY.md');
try {
const content = await fs.readFile(indexPath, 'utf-8');
const lines = content.split('\n');
if (lines.length > this.maxIndexLines) {
return lines.slice(0, this.maxIndexLines).join('\n')
+ `\n\n<!-- Truncated: ${lines.length - this.maxIndexLines} more lines -->`;
}
return content;
} catch {
return '';
}
}
async writeTopic(topic: string, content: string, mode: 'append' | 'overwrite'): Promise<void> {
this.validateTopic(topic);
await this.initialize();
const filePath = this.getTopicPath(topic);
if (mode === 'append') {
await fs.appendFile(filePath, content);
} else {
await fs.writeFile(filePath, content);
}
}
private validateTopic(topic: string): void {
if (topic.includes('..') || topic.includes('/') || topic.includes('\\')) {
throw new Error('Invalid topic name: path traversal not allowed');
}
}
private getTopicPath(topic: string): string {
const filename = topic === 'MEMORY' ? 'MEMORY.md' : `${topic}.md`;
return path.join(this.memoryDir, filename);
}
}
设计上有个细节:memoryDirOverride 参数。这是为了测试隔离——不加这个参数的话,测试会写入 ~/.blade/projects/ 下的真实目录,导致测试之间互相污染。
2. System Prompt 注入
修改 buildSystemPrompt(),在 BLADE.md 之后插入:
// builder.ts
const memoryEnabled = process.env.BLADE_AUTO_MEMORY !== '0';
if (memoryEnabled) {
const manager = new AutoMemoryManager(projectPath);
const memoryContent = await manager.loadIndex();
if (memoryContent.trim()) {
parts.push(`<auto-memory>\n${memoryContent}\n</auto-memory>`);
}
}
同时在 DEFAULT_SYSTEM_PROMPT 里加了记忆指令,告诉 Agent 什么时候该记、记什么、怎么记:
## Auto Memory
You have a persistent memory system that survives across sessions.
**What to remember:**
- Build/test/lint commands that work for this project
- Code patterns and conventions you discover
- Debugging insights and solutions to tricky problems
- Architecture decisions and key file relationships
**When to save:**
- After solving a non-trivial problem
- When you discover project-specific patterns
- When the user tells you to remember something
**Rules:**
- Don't save trivial or obvious information
- Don't save sensitive data (passwords, tokens, keys)
- Keep MEMORY.md concise — overflow into topic files
3. Memory 工具
MemoryWrite 的核心逻辑——敏感数据检查:
const SENSITIVE_PATTERNS = [
/password\s*[=:]/i,
/api_key\s*[=:]/i,
/token\s*[=:]/i,
/secret\s*[=:]/i,
/private_key/i,
];
function containsSensitiveData(content: string): boolean {
return SENSITIVE_PATTERNS.some(pattern => pattern.test(content));
}
// execute 方法中
if (containsSensitiveData(content)) {
return {
success: false,
llmContent: 'Rejected: content appears to contain sensitive data (password/token/secret/api_key/private_key). Do not store credentials in memory.',
displayContent: '❌ Sensitive data detected',
};
}
这里有个实现细节值得说:工具的 execute 签名。
blade-code 的 createTool 工厂函数有个问题——快捷 execute(params, signal) 方法不传 ExecutionContext,导致工具内部拿不到 context.workspaceRoot。我们在实现 Memory 工具时发现了这个 bug,顺手修了(下一节详述)。
4. /memory 命令
斜杠命令支持 4 个子命令:
/memory # 等同于 /memory list
/memory list # 列出所有记忆文件及大小
/memory show [topic] # 显示文件内容
/memory edit [topic] # 用 $EDITOR 打开编辑
/memory clear # 清空所有记忆
/memory edit 的实现调用 $EDITOR 环境变量(默认 vi),用 execSync 同步执行,编辑器关闭后返回会话:
const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
execSync(`${editor} ${filePath}`, { stdio: 'inherit' });
顺手修的两个架构问题
写 Auto Memory 的过程中,发现了两个 blade-code 的架构问题,一并修了。
问题 1:createTool.execute 不传 ExecutionContext
这是最大的问题。createTool 包装后的快捷 execute 方法签名是 (params, signal?),但底层 config.execute 期望的是 (params, context: ExecutionContext)。
// 修复前
async execute(params, signal?) {
const invocation = this.build(params);
return invocation.execute(signal || new AbortController().signal);
// ← context 丢了!
}
// 修复后
async execute(params, signal?, context?) {
const invocation = this.build(params);
return invocation.execute(signal || new AbortController().signal, undefined, context);
}
这意味着之前所有通过 tool.execute() 快捷调用的工具,context.workspaceRoot、context.sessionId 等全是 undefined。只有走完整 Pipeline 的调用才正常。
修复是向后兼容的——不传 context 的调用方不受影响。
问题 2:ContextManager/PersistentStore 职责模糊
PersistentStore 有三个废弃方法还在代码里:
saveContext()— 把 ContextData 转成 JSONL,但丢失了 tool calls 和 workspace 信息saveSession()/saveConversation()— 标记废弃但没删,只打 console.warn
同时 ContextManager.loadSession() 从 JSONL 重建 ContextData 的逻辑散落在多个地方,重建时丢失了 tool calls 和 compaction summary。
修复方案:
- 清理废弃方法,新增
initSession()和loadEvents() - 新建
ContextAssembler,集中 JSONL → ContextData 的重建逻辑 - 修复
saveCurrentSession()重复写入 — Agent.ts 已经通过saveMessage写了 JSONL,saveCurrentSession又写一遍
export class ContextAssembler {
assemble(events: SessionEvent[]): AssembledSession | null {
return {
session: this.assembleSession(events),
conversation: this.assembleConversation(events), // 现在包含 summary
toolCalls: this.assembleToolCalls(events), // 之前完全丢失
};
}
}
效果
实际使用效果:
# 第 1 次会话
你:项目怎么构建?
Agent:让我看看... pnpm build。我记下来了。
[MemoryWrite: topic="MEMORY", content="## Build\n- pnpm build\n- pnpm vitest run"]
# 第 2 次会话(Agent 启动时自动加载 MEMORY.md)
你:跑一下测试
Agent:好的,pnpm vitest run
# 不再问「用 npm 还是 pnpm」了
几次会话后,Agent 积累的记忆可能长这样:
# Project Memory
## Build
- Package manager: pnpm (monorepo)
- Build: pnpm build
- Test: pnpm vitest run
- Lint: pnpm eslint .
## Architecture
- Monorepo: packages/cli (core) + packages/web (Web UI)
- Agent entry: src/agent/Agent.ts
- Tool system: src/tools/core/createTool.ts
- Context: src/context/ContextManager.ts
## Patterns
- Use createTool() factory for all tools
- ToolResult: { success, llmContent, displayContent }
- Tests: vitest, co-located in tests/unit/
总结
Auto Memory 的核心思路很简单:
- 给 Agent 一个持久化的笔记本(
~/.blade/projects/{path}/memory/) - 启动时自动加载索引(MEMORY.md 前 200 行 → system prompt)
- 工作中自动记录(MemoryWrite 工具 + 敏感数据过滤)
- 用户可管理(
/memory命令)
实现上的关键决策:
| 决策 | 选择 | 原因 |
|---|---|---|
| 存储结构 | 索引 + 主题文件 | 避免单文件膨胀 |
| 工具设计 | 专用 MemoryRead/Write | 安全隔离 + 语义清晰 |
| 加载位置 | BLADE.md 之后 | 不覆盖用户配置 |
| 敏感数据 | 正则模式匹配 | 平衡安全和可用性 |
| 环境变量 | BLADE_AUTO_MEMORY=0 | 一键禁用 |
代码已随 blade-code v0.2.6 发布:
- 📦 npm: blade-code
- 🔗 GitHub: echoVic/blade-code
如果你也在做 Coding Agent,Auto Memory 是一个投入产出比很高的功能——实现不复杂(核心代码 ~300 行),但用户体验提升明显。
blade-code 技术深度系列第 5 篇。前几篇:MCP 客户端实现 | 安全权限设计 | 多模型架构 | STOP 可观测性协议