Agent 开发进阶(九):让 Agent 拥有长期记忆,Memory 系统的设计与实现

4 阅读8分钟

Agent 开发进阶(九):让 Agent 拥有长期记忆,Memory 系统的设计与实现

本文是「从零构建 Coding Agent」系列的第九篇,适合想让 Agent 跨会话保持"记忆"的开发者。

先问一个问题

你有没有遇到过这样的情况:

  • 每次新会话,Agent 都忘记你的偏好
  • 明明上次纠正过的问题,这次又犯了
  • 每次都要重新解释项目的特殊约定
  • 感觉 Agent "每次都像第一次合作"

如果这些问题困扰着你,那么你需要一个记忆系统。

但先立一个边界:Memory 不是什么都存

这是最容易讲歪的地方。

Memory 不是「把一切有用信息都记下来」。如果你这样做,很快就会出现两个问题:

  1. Memory 变成垃圾堆,越存越乱
  2. 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 系统让你能够:

  1. 保持连续性:跨会话记住用户的偏好和重要约定
  2. 避免重复犯错:不再犯同样的错误
  3. 提高效率:不需要每次都重新解释项目背景
  4. 个性化体验:让 Agent 更了解你和你的项目

推荐的实现步骤

  1. 第一步:定义 4 种 memory 类型(user、feedback、project、reference)
  2. 第二步:实现基于文件的 memory 管理器
  3. 第三步:创建 save_memory、get_memory 等工具
  4. 第四步:在会话开始时加载 memory 到上下文
  5. 第五步:添加验证逻辑,防止使用过时的 memory

下一章预告

有了 Memory 系统,你的 Agent 已经具备了跨会话的记忆能力。下一章我们将探讨 Prompt 系统,让你能更灵活地组装系统提示词,精确控制 Agent 的行为。


一句话总结:Memory 保存的是「以后还可能有价值、但当前代码里不容易直接重新看出来」的信息。


如果觉得有帮助,欢迎关注,我会持续更新「从零构建 Coding Agent」系列文章。