2. 记忆,让 Agent 记得发生了什么 - Memory
最简单的记忆存储 - 一个 Markdown 文件
没有数据库、没有RAG向量检索,先做一个最简单的记忆存储,把每次Agent的执行时间、任务和结果追加到一个Markdown文件中。
/**
* 保存记忆到 memory.md 文件
* @param task 用户输入
* @param result 执行结果
*/
export function saveMemory(task: string, result: string): void {
const timestamp = new Date().toISOString();
const entry = `## ${timestamp}\n\n**Task:** ${task}\n\n**Result:**\n${result}\n\n---\n\n`;
if (fs.existsSync(MEMORY_FILE)) {
fs.appendFileSync(MEMORY_FILE, entry, "utf-8");
} else {
fs.writeFileSync(MEMORY_FILE, "# Agent Memory\n\n" + entry, "utf-8");
}
}# Agent Memory
## 2026-03-30T07:51:28.740Z
**Task:** -- 在当前目录下,创建一个HelloWorld.txt文件,然后在里面写入HelloWorld
**Result:**
任务已完成!已在当前目录下创建HelloWorld.txt文件,并写入内容"HelloWorld"。记忆读取
通过滑动窗口的方式读取该Markdown文件的最新记录,因为上下文的容量是有限的,记录的memory会随着执行次数的增多越来越多,必须读取有限的最新记忆。
/**
* 通过滑动窗口方式加载 memory.md 文件
* @param windowSize 窗口大小(字符数),默认 1000
* @returns 记忆内容字符串
*/
export function loadMemory(windowSize: number = 1000): string {
if (!fs.existsSync(MEMORY_FILE)) {
return "";
}
const content = fs.readFileSync(MEMORY_FILE, "utf-8");
if (content.length <= windowSize) {
return content;
}
// 从末尾截取指定大小的内容
const startIndex = content.length - windowSize;
// 尝试从最近的标题开始,避免截断中间的内容
const headerMatch = content.lastIndexOf("## ", startIndex);
const finalStartIndex = headerMatch !== -1 ? headerMatch : startIndex;
return content.slice(finalStartIndex);
}
记忆注入
把加载出来的记忆拼接到system prompt的末尾,并告诉LLM这是之前的内容,这样LLM在处理新任务的时候就能看到之前的历史了。
async function getSystemMessage(planText?: string): Promise<ChatMessage[]> {
const memory = loadMemory();
let systemPrompt = `You are a powerful code assistant. First, figure out what kind of project & system this is. Last, Be concise and helpful.`;
if (planText) {
systemPrompt += `\n\nTask Steps:\n${planText}`;
}
if (memory) {
systemPrompt += `\n\nPrevious context:\n${memory}`;
}
return [
{
role: "system",
content: systemPrompt,
},
];
}Agent 记忆的本质
使用Markdown记录记忆的方式揭露了记忆机制的根本原理:LLM 本身没有真正的记忆,所有的记忆都是通过在 Prompt 中注入历史信息来实现的。无论是 Openclaw 的 Memory.md 还是更复杂的 RAG 系统,底层都是通过上下文的注入来实现的。