Agent 开发进阶(九):让 Agent 拥有长期记忆,Memory 系统的设计与实现
本文是「从零构建 Coding Agent」系列的第九篇,适合想让 Agent 跨会话保持"记忆"的开发者。
先问一个问题
你有没有遇到过这样的情况:
- 每次新会话,Agent 都忘记你的偏好
- 明明上次纠正过的问题,这次又犯了
- 每次都要重新解释项目的特殊约定
- 感觉 Agent "每次都像第一次合作"
如果这些问题困扰着你,那么你需要一个记忆系统。
但先立一个边界:Memory 不是什么都存
这是最容易讲歪的地方。
Memory 不是「把一切有用信息都记下来」。如果你这样做,很快就会出现两个问题:
- Memory 变成垃圾堆,越存越乱
- Agent 开始依赖过时记忆,而不是读取当前真实状态
所以这一章必须先立一个原则:
只有那些跨会话仍然有价值,而且不能轻易从当前仓库状态直接推出来的信息,才适合进入 memory。
Memory 的核心设计:选择性持久化
用一个图来表示 Memory 系统的工作流程:
conversation
|
| 用户提到一个长期重要信息
v
save_memory
|
v
.memory/
├── MEMORY.md # 索引
├── prefer_tabs.md # 用户偏好
├── feedback_tests.md # 用户反馈
└── incident_board.md # 外部资源
|
v
下次新会话开始时重新加载
关键点只有一个:
Memory 保存的是「以后还可能有价值、但当前代码里不容易直接重新看出来」的信息。
最适合先教的 4 类 Memory
1. user - 用户偏好
例如:
- 喜欢什么代码风格(tab vs space)
- 回答希望简洁还是详细
- 更偏好什么工具链
2. feedback - 用户反馈
用户明确纠正过的地方:
- "不要这样改"
- "这个判断方式之前错过"
- "以后遇到这种情况要先做 X"
3. project - 项目约定
不容易从代码直接重新看出来的项目约定或背景:
- 某个设计决定是因为合规而不是技术偏好
- 某个目录虽然看起来旧,但短期内不能动
- 某条规则是团队故意定下来的,不是历史残留
4. reference - 外部资源
外部资源指针:
- 某个问题单在哪个看板里
- 某个监控面板在哪里
- 某个资料库在哪个 URL
哪些东西不要存进 Memory
这是比「该存什么」更重要的一张表:
| 不要存的东西 | 为什么 |
|---|---|
| 文件结构、函数签名、目录布局 | 这些可以重新读代码得到 |
| 当前任务进度 | 这属于 task / plan,不属于 memory |
| 临时分支名、当前 PR 号 | 很快会过时 |
| 修 bug 的具体代码细节 | 代码和提交记录才是准确信息 |
| 密钥、密码、凭证 | 安全风险 |
这条边界一定要稳。
否则 memory 会从「帮助系统长期变聪明」变成「帮助系统长期产生幻觉」。
最小实现
1. 定义 Memory 类型
MEMORY_TYPES = ("user", "feedback", "project", "reference")
2. Memory 数据结构
from dataclasses import dataclass
from datetime import datetime
@dataclass
class MemoryItem:
"""单条记忆项"""
name: str
description: str
mem_type: str # user, feedback, project, reference
content: str
created_at: str = None
updated_at: str = None
def __post_init__(self):
if self.created_at is None:
self.created_at = datetime.now().isoformat()
if self.updated_at is None:
self.updated_at = self.created_at
3. Memory 管理器
import os
import re
from pathlib import Path
class MemoryManager:
"""记忆系统管理器"""
def __init__(self, memory_dir=".memory"):
self.memory_dir = Path(memory_dir)
self.memory_dir.mkdir(exist_ok=True)
self.index_file = self.memory_dir / "MEMORY.md"
self.memories = {}
self._load_all()
def save_memory(self, name: str, description: str, mem_type: str, content: str) -> str:
"""保存一条记忆"""
# 验证类型
if mem_type not in MEMORY_TYPES:
return f"错误:无效的记忆类型 '{mem_type}',可选值: {MEMORY_TYPES}"
# 创建记忆项
memory = MemoryItem(
name=name,
description=description,
mem_type=mem_type,
content=content
)
# 保存到文件
safe_name = re.sub(r'[^\w\-]', '_', name.lower())
file_path = self.memory_dir / f"{safe_name}.md"
# 构建 frontmatter + content
file_content = f"""---
name: {name}
description: {description}
type: {mem_type}
created_at: {memory.created_at}
updated_at: {memory.updated_at}
---
{content}
"""
file_path.write_text(file_content, encoding="utf-8")
# 更新内存和索引
self.memories[name] = memory
self._rebuild_index()
return f"记忆 '{name}' 已保存到 {file_path}"
def get_memory(self, name: str) -> str:
"""获取单条记忆内容"""
memory = self.memories.get(name)
if not memory:
return f"未找到记忆 '{name}'"
return f"""## {memory.name}
**类型**: {memory.mem_type}
**描述**: {memory.description}
{memory.content}
"""
def list_memories(self, mem_type: str = None) -> str:
"""列出所有记忆"""
if not self.memories:
return "暂无记忆"
lines = ["# Memory Index\n"]
for name, memory in self.memories.items():
if mem_type and memory.mem_type != mem_type:
continue
lines.append(f"- **{name}**: {memory.description} [{memory.mem_type}]")
return "\n".join(lines)
def delete_memory(self, name: str) -> str:
"""删除一条记忆"""
if name not in self.memories:
return f"未找到记忆 '{name}'"
# 删除文件
safe_name = re.sub(r'[^\w\-]', '_', name.lower())
file_path = self.memory_dir / f"{safe_name}.md"
if file_path.exists():
file_path.unlink()
# 更新内存和索引
del self.memories[name]
self._rebuild_index()
return f"记忆 '{name}' 已删除"
def _load_all(self):
"""加载所有记忆文件"""
for file_path in self.memory_dir.glob("*.md"):
if file_path.name == "MEMORY.md":
continue
try:
content = file_path.read_text(encoding="utf-8")
memory = self._parse_memory_file(content)
if memory:
self.memories[memory.name] = memory
except Exception as e:
print(f"加载记忆文件失败 {file_path}: {e}")
def _parse_memory_file(self, content: str) -> MemoryItem:
"""解析记忆文件"""
# 解析 frontmatter
frontmatter_match = re.match(r'^---\n(.*?)\n---\n\n?(.*)$', content, re.DOTALL)
if not frontmatter_match:
return None
# 解析 frontmatter
meta = {}
for line in frontmatter_text.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
meta[key.strip()] = value.strip()
return MemoryItem(
name=meta.get('name', ''),
description=meta.get('description', ''),
mem_type=meta.get('type', ''),
content=body.strip(),
created_at=meta.get('created_at', ''),
updated_at=meta.get('updated_at', '')
)
def _rebuild_index(self):
"""重建索引文件"""
lines = ["# Memory Index\n"]
for name, memory in self.memories.items():
lines.append(f"- **{name}**: {memory.description} [{memory.mem_type}]")
self.index_file.write_text("\n".join(lines), encoding="utf-8")
def get_all_for_context(self) -> str:
"""获取所有记忆,用于拼接到系统提示"""
if not self.memories:
return ""
sections = ["# Long-term Memory\n"]
# 按类型分组
by_type = {}
for name, memory in self.memories.items():
if memory.mem_type not in by_type:
by_type[memory.mem_type] = []
by_type[memory.mem_type].append(memory)
for mem_type in MEMORY_TYPES:
if mem_type not in by_type:
continue
sections.append(f"\n## {mem_type.upper()}\n")
for memory in by_type[mem_type]:
sections.append(f"### {memory.name}")
sections.append(f"{memory.description}\n")
sections.append(f"{memory.content}\n")
return "\n".join(sections)
4. 集成到 Agent Loop
def create_memory_tools(memory_manager):
"""创建 memory 相关的工具"""
def save_memory(name, description, mem_type, content):
return memory_manager.save_memory(name, description, mem_type, content)
def get_memory(name):
return memory_manager.get_memory(name)
def list_memories(mem_type=None):
return memory_manager.list_memories(mem_type)
def delete_memory(name):
return memory_manager.delete_memory(name)
return {
"save_memory": save_memory,
"get_memory": get_memory,
"list_memories": list_memories,
"delete_memory": delete_memory,
}
def agent_loop_with_memory(state, memory_manager):
"""带记忆功能的 Agent Loop"""
# 会话开始时加载记忆
memory_context = memory_manager.get_all_for_context()
if memory_context:
# 将记忆拼接到系统提示中
state["system"] = state.get("system", "") + "\n\n" + memory_context
while True:
response = call_model(state["messages"])
if response.stop_reason != "tool_use":
return response.content
results = []
for block in response.content:
if block.type == "tool_use":
# 执行工具(包括 memory 工具)
output = run_tool(block.name, block.input)
results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": output
})
if results:
state["messages"].append({"role": "user", "content": results})
Memory、Task、Plan、CLAUDE.md 的边界
这是最值得反复区分的一组概念。
| 概念 | 负责什么 | 示例 |
|---|---|---|
| memory | 跨会话仍有价值的信息 | 用户偏好、项目约定、外部资源 |
| task | 当前工作要做什么、依赖关系如何、进度如何 | 当前任务列表、进度状态 |
| plan | 这一轮我要怎么做 | 执行步骤、策略选择 |
| CLAUDE.md | 更稳定、更像长期规则的说明文本 | 项目规范、系统配置 |
一个简单判断法:
- 只对这次任务有用:
task / plan - 以后很多会话可能都还会有用:
memory - 属于长期系统级或项目级固定说明:
CLAUDE.md
新手最容易犯的 5 个错
1. 把代码结构也存进 memory
# ❌ 错误
memory_manager.save_memory(
name="file_structure",
description="项目文件结构",
mem_type="project",
content="这个项目有 src/ 和 tests/ 目录"
)
# ✅ 正确
# 这些信息可以通过重新读代码获得,不需要存
2. 把当前任务状态存进 memory
# ❌ 错误
memory_manager.save_memory(
name="current_task",
description="当前任务",
mem_type="project",
content="我现在正在改认证模块,这个 PR 还有两项没做"
)
# ✅ 正确
# 这些属于 task,不属于 memory
3. 把 memory 当成绝对真相
# ❌ 错误
# 直接使用 memory 里的信息,不做验证
if "auth_module_path" in memory:
path = memory["auth_module_path"]
# ✅ 正确
# memory 用来提供方向,使用前先验证当前状态
if memory.get("auth_module_path"):
# 先验证路径是否还存在
if Path(memory["auth_module_path"]).exists():
path = memory["auth_module_path"]
else:
# 路径已变,重新查找
path = find_auth_module()
4. 不存正反馈,只存负反馈
# ❌ 错误
# 只在用户纠正时保存 feedback
if user_corrects_me():
save_memory(..., mem_type="feedback", content="这样做是错的")
# ✅ 正确
# feedback 不仅来自负反馈,也来自被验证的正反馈
if user_approves_my_approach():
save_memory(..., mem_type="feedback", content="这样做是对的,已经被用户确认")
5. 用户说「忽略 memory」时继续用
# ❌ 错误
if user_says_ignore_memory():
print("好的,我知道了")
# 但实际上还在用 memory
# ✅ 正确
ignore_memory = user_says_ignore_memory()
if ignore_memory:
memory_context = "" # 当 memory 为空来工作
else:
memory_context = memory_manager.get_all_for_context()
为什么这很重要
因为一个真正智能的系统,不应该每次都从零开始。
Memory 系统让你能够:
- 保持连续性:跨会话记住用户的偏好和重要约定
- 避免重复犯错:不再犯同样的错误
- 提高效率:不需要每次都重新解释项目背景
- 个性化体验:让 Agent 更了解你和你的项目
推荐的实现步骤
- 第一步:定义 4 种 memory 类型(user、feedback、project、reference)
- 第二步:实现基于文件的 memory 管理器
- 第三步:创建 save_memory、get_memory 等工具
- 第四步:在会话开始时加载 memory 到上下文
- 第五步:添加验证逻辑,防止使用过时的 memory
下一章预告
有了 Memory 系统,你的 Agent 已经具备了跨会话的记忆能力。下一章我们将探讨 Prompt 系统,让你能更灵活地组装系统提示词,精确控制 Agent 的行为。
一句话总结:Memory 保存的是「以后还可能有价值、但当前代码里不容易直接重新看出来」的信息。
如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。