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——中间多了一层转换,能力上没有多出任何东西。等到你真的需要按关键词检索、按时间范围过滤、给不同类型的记忆打不同权重的时候,再加数据库。