2. 记忆,让 Agent 记得发生了什么 - Memory

上一篇文章搭了一个能跑通的 Agent Loop。但它有个问题:每次启动都是全新的。

你让它”继续上次的工作”,它一脸茫然。你让它”跟昨天一样处理”,它不知道昨天发生了什么。

没有记忆的 Agent,就像电影《记忆碎片》里的主角——每一轮醒来,世界都是崭新的。

从最简单的东西开始:一个 Markdown 文件

说到记忆系统,很容易条件反射地想到数据库、向量检索、RAG 管道。但 prototype 阶段不需要这些。

Agent 的记忆,说穿了就是把”我干了什么”记下来,下次启动的时候能读到。一个文件就够。

function saveMemory(task: string, result: string) {
  const entry = `## ${new Date().toISOString()}\n\n**Task:** ${task}\n\n**Result:**\n${result}\n\n---\n\n`;
  const file = "memory.md";
  if (fs.existsSync(file)) {
    fs.appendFileSync(file, entry, "utf-8");
  } else {
    fs.writeFileSync(file, `# Agent Memory\n\n${entry}`, "utf-8");
  }
}

每次 Agent 完成任务,往 memory.md 追加一条记录。长这样:

# Agent Memory
 
## 2026-03-30T07:51:28.740Z
 
**Task:** 在当前目录下,创建一个 HelloWorld.txt 文件,写入 HelloWorld
 
**Result:**
任务已完成!已在当前目录下创建 HelloWorld.txt 文件,并写入内容 "HelloWorld"。

没什么黑科技。追加写、纯文本、人类可读。调试的时候直接打开文件就能看到 Agent 干了什么,比翻数据库表舒服。

记忆读取:滑动窗口

Agent 跑得越久,memory.md 越大。把整个文件塞进 LLM 上下文是不现实的——token 有上限,而且塞得越多,LLM 越容易在旧信息里分散注意力。

所以只读最近的一小段。

function loadMemory(windowSize = 1000) {
  const file = "memory.md";
  if (!fs.existsSync(file)) return "";
 
  const content = fs.readFileSync(file, "utf-8");
  if (content.length <= windowSize) return content;
 
  const start = content.length - windowSize;
  const prevHeader = content.lastIndexOf("## ", start);
  return content.slice(prevHeader !== -1 ? prevHeader : start);
}

逻辑:文件不大就全读;太大了就从末尾往前取,但取之前找到最近的 ## 标题,保证返回的内容从一条完整记录开始,不拦腰截断。

这就是滑动窗口。Agent 只记得最近的事,旧的自然忘了。

记忆注入:拼进 System Prompt

读出来的记忆往哪放?在每轮对话开始前,拼到 System Prompt 末尾。

function getSystemMessage() {
  const memory = loadMemory();
  let prompt = "You are a powerful code assistant. Be concise and helpful.";
 
  if (memory) {
    prompt += `\n\nPrevious context:\n${memory}`;
  }
 
  return [{ role: "system", content: prompt }];
}

然后把返回的 messages 传给 agentLoop,替换上一篇里写死的 system prompt。Agent 在开始干活之前,就能看到之前发生过什么。

Agent 的记忆,到底是个什么东西

做过后端开发的话,直觉反应可能是建一张 agent_memory 表:id, task, result, created_at,加索引,写 CRUD。

但 Agent 的记忆用不着这样。

LLM 没有状态。每次调用,你给它一段文本,它返回一段文本。所谓的”记忆”,就是下一次调用时,把上一次的结果塞进 Prompt。

不管是用一个 Markdown 文件,还是上 RAG + 向量数据库 + 摘要分层,底层都是同一件事:把历史信息注入上下文。区别只在于注入什么、怎么挑。

Markdown 方案不是简陋。你用数据库存,最终还是要序列化成文本才能塞给 LLM——中间多了一层转换,能力上没有多出任何东西。等到你真的需要按关键词检索、按时间范围过滤、给不同类型的记忆打不同权重的时候,再加数据库。


下一篇3. 规划,让 Agent 先想后做 - Plan