mcpField
A
agent-memory

agent-memory

github.com/terminalskillsAgent 工作流18440 次安装今天更新
代码仓库

关于 agent-memory

为编码 Agent 提供跨会话持久记忆的指南与示例实现。先使用现有指令文件,再介绍文件式记忆及适用于大量事实和历史对话的可搜索存储。

主要功能

  1. 01优先利用 Agent 指令文件
  2. 02介绍文件式记忆组织方式
  3. 03提供 SQLite 向量嵌入和 ChromaDB 方案
  4. 04说明检索并注入相关上下文
  5. 05涵盖记忆更新与敏感数据处理

适用场景

  1. 01在编码会话之间保留项目约定
  2. 02按语义搜索历史支持工单
  3. 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 CodeCLAUDE.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 CodexAGENTS.md(~/.codex 以及从仓库根目录到下级的每个目录)、AGENTS.override.md总大小上限由 project_doc_max_bytes 限制(32 KiB)。
Gemini CLIGEMINI.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 更便于调试。

评论

支持 Markdown