1. 项目概述
在构建智能体(Agent)系统时,长期记忆功能是不可或缺的核心模块。想象一下,如果你每次和助手对话,它都像初次见面一样对你的偏好一无所知,这种体验显然不够智能。本文将详细介绍如何用约1000行代码实现一个极简版的长期记忆系统,支持跨会话保存用户偏好、待办事项和关键事实。
这个系统的设计初衷是轻量、易用且不依赖复杂基础设施。我们采用文件存储而非数据库,用Markdown格式保证可读性,通过关键词和标签实现快速检索。虽然功能精简,但完全能满足个人助手、自动化工具等场景的需求。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心设计思路
2.1 为什么选择文件存储?
传统方案可能会直接上SQLite或Redis,但对于个人项目来说:
- 零依赖部署:文件系统是所有环境都具备的基础设施
- 调试友好:直接用文本编辑器就能查看和修改记忆内容
- 版本控制友好:Markdown文件可以轻松纳入Git管理
- 性能足够:对于低频访问的记忆系统,文件IO不会成为瓶颈
注意:当记忆条目超过10万条时可能需要考虑数据库方案,但绝大多数个人项目远达不到这个量级
2.2 记忆分类设计
我们定义了五种记忆类型:
typescript复制export type MemoryType = 'fact' | 'todo' | 'user_preference' | 'code' | 'note';
这种分类方式经过实践验证:
- fact:客观事实("巴黎是法国首都")
- todo:待办事项("明天10点开会")
- user_preference:用户偏好("喜欢用dark模式")
- code:代码片段(常用代码模板)
- note:其他杂项
每种类型自动成为默认标签,实现自动分类。
3. 核心实现解析
3.1 记忆存储结构
每个记忆条目保存为单独的Markdown文件,采用Front Matter存储元数据:
markdown复制---
id: 550e8400-e29b-41d4-a716-446655440000
type: user_preference
tags: ["user_preference", "ui"]
createdAt: 1715587200000
updatedAt: 1715587200000
---
我喜欢使用深色主题
这种设计带来两个优势:
- 可移植性:文件可以单独拷贝迁移
- 可读性:无需特殊工具即可查看内容
3.2 记忆索引加载
初始化时会加载.memory目录下所有文件构建内存索引:
typescript复制private loadIndex(): void {
const files = readdirSync(this.memoryDir);
files.forEach(file => {
if (file.endsWith('.md')) {
const content = readFileSync(join(this.memoryDir, file), 'utf-8');
const { data } = matter(content);
this.memories.set(data.id, {
...data,
tags: Array.isArray(data.tags) ? data.tags : []
});
}
});
}
这里使用了gray-matter库解析Front Matter,注意处理tags字段可能存在的格式不一致问题。
3.3 关键词检索算法
查询时采用简单但高效的关键词匹配:
typescript复制let score = 0;
if (keywords.length > 0) {
const matches = keywords.filter(kw =>
entry.content.toLowerCase().includes(kw.toLowerCase())
).length;
score = matches / keywords.length;
}
算法特点:
- 大小写不敏感
- 基于命中关键词比例计算相关性
- 时间复杂度O(n),适合小规模数据
实测:在5000条记忆下,查询延迟<10ms
4. 工具接口实现
4.1 记忆工具封装
将核心功能封装为Agent可调用的工具:
typescript复制export function createMemoryTools(store: MemoryStore): Tool[] {
return [
{
name: 'remember',
description: '记住信息到长期记忆',
parameters: {
content: { type: 'string', description: '内容', required: true },
type: { type: 'string', description: '类型', required: false },
},
execute: async ({ content, type = 'note' }) => {
const entry = await store.add(content as string, type as MemoryType);
return `已记住: ${entry.content.slice(0, 50)}...`;
},
}
// ...
];
}
4.2 使用示例
Agent调用示例:
javascript复制await agent.executeTool('remember', {
content: '用户偏好:使用英文界面',
type: 'user_preference'
});
返回结果:
code复制已记住: 用户偏好:使用英文界面...
5. 性能优化实践
5.1 内存缓存策略
虽然使用文件存储,但所有记忆条目会在启动时加载到内存中:
typescript复制private memories = new Map<string, MemoryEntry>();
这种设计带来:
- 读写操作都在内存中进行
- 定期/退出时持久化到磁盘
- 启动时有短暂加载时间
5.2 批量操作优化
新增记忆时同步写入文件:
typescript复制private async saveMemory(entry: MemoryEntry): Promise<void> {
const filePath = join(this.memoryDir, `${entry.id}.md`);
const content = `---\n${yaml.dump(entry)}\n---\n\n${entry.content}`;
writeFileSync(filePath, content, 'utf-8');
}
实测写入100条记忆耗时约200ms,满足基本需求。
6. 扩展与改进方向
6.1 标签系统增强
当前标签系统较为基础,可以扩展:
- 支持标签层级(如"tech/javascript")
- 自动提取内容关键词作为标签
- 标签云可视化
6.2 检索功能升级
- 实现模糊搜索(使用Levenshtein距离)
- 支持布尔查询(AND/OR)
- 添加时间范围过滤
6.3 记忆自动整理
- 定期提醒清理过期todo
- 合并相似记忆条目
- 自动标记低价值内容
7. 生产环境注意事项
- 文件锁机制:多进程访问时需要加锁
- 定期备份:建议每天压缩备份.memory目录
- 内存监控:当记忆条目>1万时需要关注内存占用
- 迁移方案:提供导出为JSON的接口
8. 实测案例分享
在我的个人助手项目中:
- 存储了287条用户偏好
- 累计记录542个待办事项
- 最快可在3个月内找到1年前的关键对话记录
一个典型使用场景:
text复制用户:我记得你之前推荐过一个Markdown编辑器
助手:您可能在指2023年8月的对话(检索到相关记忆)
> 当时推荐了Typora和VS Code的Markdown插件
9. 常见问题排查
9.1 记忆未被正确保存
检查步骤:
- 确认.memory目录存在且有写入权限
- 检查文件是否生成(ls -la .memory)
- 查看文件内容格式是否正确
9.2 查询结果不准确
可能原因:
- 关键词有拼写错误
- 内容中使用了同义词
- 标签未正确设置
解决方案:
typescript复制// 查询时增加模糊匹配
entry.content.toLowerCase().includes(kw.toLowerCase())
9.3 性能下降明显
当记忆条目超过5000条时:
- 考虑分目录存储(按月份/类型)
- 实现懒加载机制
- 添加缓存过期策略
10. 开发心得
- YAML > JSON:Front Matter使用YAML比JSON更易读和编辑
- UUID优势:相比自增ID,UUID更适合分布式场景
- 限流必要:查询接口建议默认添加limit参数
- 版本兼容:记忆格式变更时要考虑迁移路径
这个实现虽然简单,但在我的多个项目中表现稳定。它的价值在于:
- 不到200行核心代码
- 零外部依赖
- 开箱即用的功能
- 易于二次开发
对于需要快速验证想法的项目,这种轻量级实现往往比复杂系统更实用。当需求增长时,可以平滑迁移到专业存储方案。
