关于 agent-memory
为编码 Agent 提供跨会话持久记忆的指南与示例实现。先使用现有指令文件,再介绍文件式记忆及适用于大量事实和历史对话的可搜索存储。
主要功能
- 01优先利用 Agent 指令文件
- 02介绍文件式记忆组织方式
- 03提供 SQLite 向量嵌入和 ChromaDB 方案
- 04说明检索并注入相关上下文
- 05涵盖记忆更新与敏感数据处理
适用场景
- 01在编码会话之间保留项目约定
- 02按语义搜索历史支持工单
- 03向 Agent 提示词添加相关历史上下文
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ terminalskills/ skills --skill agent-memory安装后在 Claude Code 中输入/agent-memory确认已加载。
附带文件_scores.json
详细介绍
Agent Memory
概述
编码 Agent 每次会话开始时,上下文窗口都是空的。此技能让它们拥有可持续保留的记忆:首先是 Agent 自己会读取的基于文件的记忆(CLAUDE.md、AGENTS.md、GEMINI.md),然后是由你管理的小型记忆模块(Markdown 文件、带嵌入向量的 SQLite 或 ChromaDB),用于存储指令文件无法容纳的内容,例如数千条可按含义搜索的过往工单或对话。
说明
步骤 0:使用 Agent 已有的记忆
在构建任何内容之前先检查这一点。每次会话开始时都会加载指令文件:
| Agent | 持久指令 | 备注 |
|---|---|---|
| Claude Code | CLAUDE.md(项目、~/.claude/CLAUDE.md)、CLAUDE.local.md、.claude/rules/*.md | 可通过 @docs/api.md 导入。自动记忆默认开启:Claude 会将笔记写入 ~/.claude/projects/<project>/memory/,并在启动时加载 MEMORY.md(前 200 行或 25KB)。使用 /memory 查看或切换;使用 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 禁用。 |
| OpenAI Codex | AGENTS.md(~/.codex 以及从仓库根目录到下级的每个目录)、AGENTS.override.md | 总大小上限由 project_doc_max_bytes 限制(32 KiB)。 |
| Gemini CLI | GEMINI.md(~/.gemini/ 和项目目录) | 使用 /memory show 和 /memory reload;可通过 context.fileName 更改文件名(例如 AGENTS.md)。 |
如果请求是“让 Agent 记住我们的约定”,就编写或扩展这些文件,然后停止。仅当记忆过于庞大、变化过于频繁或必须进行语义搜索时,才采用下面的策略。
策略 1:基于文件的记忆(无依赖项)
memory/
MEMORY.md # curated long-term facts, kept short
2026-10-02.md # daily session logs
decisions.md # key decisions and the reasoning
# MEMORY.md
## Projects
- Billing API: Node 22, Fastify, Postgres 16; deploys from the release branch
## Preferences
- TypeScript strict mode; Vitest, not Jest; pnpm, not npm
## Lessons
- Integration tests need Redis on localhost:6379 (docker compose up redis)
# agent_memory.py
from datetime import datetime, timedelta
from pathlib import Path
class FileMemory:
def __init__(self, memory_dir: str = "memory"):
self.dir = Path(memory_dir)
self.dir.mkdir(parents=True, exist_ok=True)
self.long_term = self.dir / "MEMORY.md"
def log_today(self, content: str, section: str = "Notes") -> Path:
daily = self.dir / f"{datetime.now():%Y-%m-%d}.md"
if not daily.exists():
daily.write_text(f"# {daily.stem}\n")
with daily.open("a") as f:
f.write(f"\n## {section}\n{content}\n")
return daily
def remember(self, key: str, value: str, category: str = "General") -> None:
text = self.long_term.read_text() if self.long_term.exists() else "# MEMORY.md\n"
header = f"## {category}"
if header not in text:
text += f"\n{header}\n"
pos = text.index(header) + len(header) + 1
self.long_term.write_text(text[:pos] + f"- **{key}**: {value}\n" + text[pos:])
def search(self, query: str, limit: int = 10) -> list[dict]:
terms = query.lower().split()
hits = []
for path in self.dir.rglob("*.md"):
for n, line in enumerate(path.read_text().splitlines(), 1):
score = sum(t in line.lower() for t in terms) / len(terms)
if score:
hits.append({"file": path.name, "line": n, "text": line.strip(), "score": score})
return sorted(hits, key=lambda h: h["score"], reverse=True)[:limit]
def recent_context(self, days: int = 3) -> str:
parts = []
for i in range(days):
day = datetime.now() - timedelta(days=i)
f = self.dir / f"{day:%Y-%m-%d}.md"
if f.exists():
parts.append(f.read_text())
return "\n---\n".join(parts)
整理:每周一次,将 recent_context(days=7) 和当前的 MEMORY.md 提供给 Agent(或本地模型),让它合并持久有效的信息、删除过时内容,并保持文件简短,以便每次会话都能完整加载。
策略 2:SQLite 加嵌入向量(语义搜索,单文件)
npm install better-sqlite3 openai
export OPENAI_API_KEY="sk-proj-..." # from your secret manager, never committed
// memory-store.ts
import Database from "better-sqlite3";
import OpenAI from "openai";
export class MemoryStore {
private db = new Database("agent-memory.db");
private openai = new OpenAI(); // reads OPENAI_API_KEY
private model = "text-embedding-3-small";
constructor() {
this.db.exec(`CREATE TABLE IF NOT EXISTS memories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content TEXT NOT NULL,
category TEXT DEFAULT 'general',
embedding BLOB NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`);
}
private async embed(text: string): Promise<Float32Array> {
const res = await this.openai.embeddings.create({ model: this.model, input: text });
return new Float32Array(res.data[0].embedding);
}
async store(content: string, category = "general"): Promise<number> {
const v = await this.embed(content);
const blob = Buffer.from(v.buffer, v.byteOffset, v.byteLength);
return Number(this.db.prepare(
"INSERT INTO memories (content, category, embedding) VALUES (?, ?, ?)"
).run(content, category, blob).lastInsertRowid);
}
async search(query: string, limit = 5, minScore = 0.3) {
const q = await this.embed(query);
const rows = this.db.prepare(
"SELECT content, category, embedding, created_at FROM memories ORDER BY id DESC LIMIT 5000"
).all() as { content: string; category: string; embedding: Buffer; created_at: string }[];
return rows
.map((r) => {
// a Buffer from SQLite can sit at a non-aligned offset inside a shared pool: copy the exact bytes
const bytes = r.embedding.buffer.slice(r.embedding.byteOffset, r.embedding.byteOffset + r.embedding.byteLength);
return { content: r.content, category: r.category, created_at: r.created_at, score: cosine(q, new Float32Array(bytes)) };
})
.filter((r) => r.score >= minScore)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
}
}
function cosine(a: Float32Array, b: Float32Array): number {
let dot = 0, na = 0, nb = 0;
for (let i = 0; i < a.length; i++) { dot += a[i] * b[i]; na += a[i] ** 2; nb += b[i] ** 2; }
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
这会在 JavaScript 中扫描行,适用于多达几千条记忆。超过这个规模后,请使用索引(sqlite-vec 扩展或策略 3)。仅存储来自一个模型的向量;更换嵌入模型意味着需要重新嵌入所有内容。
策略 3:ChromaDB(筛选与扩展)
python3 -m pip install chromadb # 1.x; the default embedder downloads a small ONNX model on first use
# chroma_memory.py
from datetime import datetime, timezone
import chromadb
class ChromaMemory:
def __init__(self, path: str = "./chroma_db", name: str = "agent_memory"):
self.client = chromadb.PersistentClient(path=path)
self.col = self.client.get_or_create_collection(
name, configuration={"hnsw": {"space": "cosine"}} # older code used metadata={"hnsw:space": ...}
)
def store(self, memory_id: str, content: str, category: str = "general") -> None:
# upsert: the same id overwrites instead of duplicating
self.col.upsert(ids=[memory_id], documents=[content], metadatas=[
{"category": category, "created_at": datetime.now(timezone.utc).isoformat()}])
def recall(self, query: str, n: int = 5, category: str | None = None) -> list[dict]:
r = self.col.query(query_texts=[query], n_results=n,
where={"category": category} if category else None,
include=["documents", "metadatas", "distances"])
return [{"content": d, "category": m["category"], "similarity": 1 - dist}
for d, m, dist in zip(r["documents"][0], r["metadatas"][0], r["distances"][0])]
def forget(self, memory_id: str) -> None:
self.col.delete(ids=[memory_id])
使用余弦空间时,distance = 1 - similarity。如果需要多台机器或多个服务共享一份记忆,可选择托管式向量服务(Pinecone、Qdrant Cloud);store 和 recall 的形式保持不变。
注入记忆
在会话开始时加载 MEMORY.md 和 recent_context();执行任务前,运行 recall(task_description),并将排名前三或前四的结果添加到系统提示词或 Agent 的指令文件中,而不是用户消息中。显示每条记忆的来源,以便 Agent 判断其时间新旧。
示例
示例 1:跨会话记住项目决策
用户提示词:“我的 Codex 和 Claude Code Agent 总是忘记我们使用 pnpm,而且集成测试需要 Redis。让它们记住。”
Agent 会将这两条信息添加到 AGENTS.md(供 Codex 读取)和通过 @AGENTS.md 导入的 CLAUDE.md 中,或者运行 /memory 确认 Claude 的自动记忆已开启。它不会构建数据库。在新会话中,询问“如何运行测试?”时,它会回答 pnpm test,并指出 Redis 是前置条件,无需再次提醒。
示例 2:按含义搜索过往支持工单
用户提示词:“我们的支持机器人有 40,000 条已解决工单。客户写下‘我的发票 PDF 是空白的’时,它应该能找到之前那些‘账单导出为空’的工单。”
Agent 会创建一个 ChromaDB 集合,使用 store(f"ticket-{id}", text, category="billing") 加载每条工单,并调用 recall("my invoice PDF is blank", n=5, category="billing")。排名靠前的结果会是三条之前关于导出内容空白的工单,并附有各自的相似度分数和解决方案文本,机器人会在回复中引用它们。
指南
- 优先使用 Agent 自带的指令文件;它们会免费加载,且人们会在拉取请求中审核这些文件。
- 始终加载的记忆应保持简短(Claude Code 会加载约 200 行
MEMORY.md,Codex 会加载 32 KiB 的 AGENTS.md);详细内容应放在主题文件中,并按需搜索。 - 切勿在记忆文件或向量存储中保存机密、令牌或个人数据;记忆是纯文本,通常会提交或同步。
- 记忆会过时:为条目添加日期,每周整理一次,并删除不再准确的内容。错误的记忆比没有记忆更糟。
- 使用真实查询测试召回效果,并设定最低相似度,避免注入匹配度较低的内容。
- 嵌入成本很低(text-embedding-3-small 按每百万个令牌计价;请查看当前价格),但每条嵌入的记忆都会发送给提供商;敏感内容请使用本地嵌入器。
- 记忆少于几百条时,不要使用向量搜索;用 grep 搜索 Markdown 更便于调试。