L0–L3

分层记忆架构

给 agent 的一套文件约定:每轮只读 1.6k token,需要时才翻全库
4层
6条规则
4条禁令
5份可复制件

四层怎么分工

不是四种存储,是四种读取时机。越靠上越常读、越贵,所以只允许放「路由键」;越靠下越少读、越便宜,所以允许放「全部细节」。 判断一条信息该放哪层,只看一个问题:下一次执行任务时,它必须不必须在场?

读得越往下 → 发生得越少 L0 常驻规则 AGENTS.md ≤ 2.6 KB · 只放不可违反的规则 + 指针 L1 索引 memory/MEMORY.md ≤ 15 KB · 每行 ≤ 200 字,只放路由键 L2 主题记忆 memory/(标签)名字.md ≤ 4k token / 篇 · 一篇一个主题 L3 冷档 memory/(标签)xxx-detail/ 不限 · 正文都在这里,主文件只留结论 只给指针 命中关键词 要细节 / 沿革 回指 每轮自动注入 ≈ 0.5k tok · 最贵的地方 任何一句废话都在每轮计费 会话开场读一次 ≈ 1.2k tok · 只放「去哪找」 细节写进这里 = 每会话都付一次 命中才读 按篇计费 · 平均 1–2 篇/任务 超 4k 就拆 L3,别硬塞 几乎不读 只在追查历史/踩坑时翻 不校验预算,但要能被索引找到 闸门 memory-budget.py:L0/L1 超预算 · 索引漏登 · 死链 · L2 超限无摘要 → 一律不过 闸门只做四件事:量体积、查登记、查链接、查摘要;改完索引再跑一次,规则越少越有人真跑。

关键不在层数,在「上行只留指针,下行只放正文」。一旦有人把细节写进 L0/L1,整个成本模型就崩了—— 而这几乎总是从「就加一行,应该没关系」开始的。

为什么值得分四层

成本模型很简单:每轮注入的内容 × 轮数。一个 90k token 的记忆库如果每轮全塞,一天几十轮就是几百万 token; 分成四层后,每轮只付 L0+L1 的固定开销,L2/L3 只在真的用到时才读。

≈ 1.6k

每轮固定开销(L0 + L1)要盯的数

这两个文件每轮/每会话都要重发,所以它们是唯一值得反复瘦身的地方。

≈ 86k

全库文本(L2 + L3)不用盯

总量涨十倍也不影响每轮成本——只要 L2 没被塞进 L1,L3 没被塞进 L2。

≈ 2%

每轮成本 / 全库体积

这就是分层的全部收益:把「全库」换成「索引 + 命中页」。

各层体积的量级(对数刻度)

L0 规则2.3 KB
L1 索引5.3 KB
L2 主题396 KB
L3 冷档469 KB

条宽按 log(体积) 画,否则前两层会看不见。数字是一份 50 条左右记忆库的量级,用来标定你自己的预算。

让这套结构不塌的六条规则

预算只是结果,规则才是原因。下面每一条都对应一个真实踩过的坑。

1 · 分类前置命名

(标签)名字.md,文件名与 frontmatter 的 name 同形。冷档目录同样前置:(技术)xxx-detail/。目录名决定排序与扫读速度,比标签系统可靠。

2 · 索引只放路由键

一行 = 「标题 + 链接 + ≤ 60 字提示」。写细节就是给未来的自己加税——那条细节会被每会话读一次,而正文在 L2 里本来就是懒加载的。

3 · L0 只放规则和指针

环境清单、目录结构、技能统计这类「资料」一律进 L2。L0 每轮计费,多一段就多付一辈子。

4 · 超预算先加头部摘要

L2 超过 4k token 或 200 行,就在前 45 行内补 ## 当前状态(先读这里):当前状态、该跳读哪几节、最新变化在哪。用 read limit=45 就能干活。

5 · 拆冷档而不是删正文

沿革、勘误、复现细节搬进 *-detail/,主文件只留结论 + 一行指针。删掉的信息三个月后一定有人来问。

6 · 改名要全库扫链接

改 frontmatter 的 name 会让按裸名寻址的 [[wikilink]] 全部失效。改名前后各跑一次闸门,并先整包备份。

四条禁令

  1. 禁止把细节写进索引 · 索引行只是路由键
  2. 禁止把资料写进常驻规则 · 环境/路径/清单都进 L2
  3. 禁止把详情堆回速查表 · 正文进冷档,主表留一行
  4. 禁止为同一主题再开新文件 · 先合并,再新建

子架构:领域大了怎么办

某个领域(比如某个长期项目)记忆变多时,在它下面再套一层,但预算层级不变:

子层落点在总架构里算
子 L1该领域总索引总 L2
子 L2领域内各主题总 L2
子 L3(标签)xxx-detail/总 L3

注意:子 L2 也必须在总索引里占一行,否则闸门会判「登记缺失」。

可复制件

五块东西:目录树、两段骨架、一个能跑的校验脚本、一份 Mermaid 源码。按钮复制的是纯文本,直接粘进编辑器即可。

① 目录树

最小可用结构
~/.agent-home/                # 或任意 agent 的 home
├── AGENTS.md                 # L0 常驻规则(每轮注入,≤2.6 KB)
└── memory/
    ├── MEMORY.md             # L1 索引(每会话一次,≤15 KB)
    ├── (环境)runtime.md      # L2 主题记忆(命中才读,≤4k tok)
    ├── (技术)build-issues.md
    ├── (项目)alpha.md
    ├── (规则)review.md
    ├── (技术)build-issues-detail/   # L3 冷档(不限)
    │   ├── 2026-01-04-timeout.md
    │   └── 2026-02-11-race.md
    └── (环境)runtime-inventory.md   # 顶层冷档文件(不计 L2 预算)

② AGENTS.md 骨架

L0:只放规则和指针,控制在 2.6 KB 内
# 常驻规则(L0)

## 硬规则(不可违反)
- 动手前先查记忆索引;命中就读对应主题记忆,不要凭印象。
- 同一件事失败 2 次必须上报,不自行绕过。
- 新建记忆条目 / 新目录前先说清「为什么建、放哪、叫什么」,得到确认再写。
- 任何删除、覆盖、跨目录移动,先确认目标路径。

## 去哪查(本文件只留指针)
- 环境 / 依赖 / 路径 → memory/(环境)runtime.md
- 报错与踩坑 → memory/(技术)build-issues.md
- 项目进度 → memory/(项目)alpha.md
- 记忆库自身的规矩 → memory/(规则)memory.md

## 开场动作
1. 读 memory/MEMORY.md(≤15 KB 的索引)。
2. 任务涉及哪个关键词,再读对应的那一篇,不要全库通读。
3. 收尾跑 python memory-budget.py,必须 [OK]。

③ MEMORY.md 骨架

L1:一行一个路由键
# 记忆索引(只放路由键,细节在对应文件里)

## 行动优先(动手前必读)
- [运行环境]((环境)runtime.md) — 解释器 / 依赖 / 路径速查
- [常见问题]((技术)build-issues.md) — 报错先查表,命中读详情
- [记忆规矩]((规则)memory.md) — 四层分级与预算

## 项目
- [Alpha 服务]((项目)alpha.md) — 进度 / 口径 / 待办

## 知识与工具
- [构建链路]((技术)build-chain.md) — 脚本化构建与校验


④ memory-budget.py

闸门:四件事——量体积、查登记、查链接、查摘要
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""分层记忆库预算校验(L0–L3)。

四层的约定:
  L0  常驻规则   AGENTS.md        每轮随系统提示注入,最贵
  L1  索引       MEMORY.md        每会话开场读一次,只放路由键
  L2  主题记忆   memory/*.md      命中才读
  L3  冷档       memory/*-detail/ 几乎不读,不限预算

检查:
  1. L0 体积是否超预算
  2. L1 体积是否超预算、索引行是否过长
  3. L1 里的每条链接是否指向真实文件
  4. 每个 L2 文件是否都能从 L1 找到(登记完整性)
  5. L2 超预算时,是否带「先读这里」头部摘要
  6. 汇总每轮固定开销(L0 + L1)

用法:
  python memory-budget.py --home ~/.agent-home
  python memory-budget.py --home /path/to/home --agents AGENTS.md --memory memory
退出码 0 = 全部通过,1 = 有违规。
"""
import argparse
import fnmatch
import os
import re
import sys

# 中文 Windows 的控制台常是 GBK:先尝试切到 UTF-8,失败也不影响判定
try:
    sys.stdout.reconfigure(encoding="utf-8", errors="replace")
except Exception:
    pass

# ── 预算:改这里,或改成从配置文件读 ────────────────────────────────
L0_BUDGET_BYTES = 2600          # L0 上限
L1_BUDGET_BYTES = 15360         # L1 上限
INDEX_LINE_MAX = 200            # 单条索引行的最大字符数
L2_MAX_TOKENS = 4000            # 单条 L2 的 token 上限
L2_MAX_LINES = 200              # 单条 L2 的行数上限
HEADER_SCAN_LINES = 45          # 在前多少行内找头部摘要
HEADER_MARK = "先读这里"         # 头部摘要的标记

LINK_RE = re.compile(r"\[[^\]]*\]\(([^)]+)\)")
CJK_RE = re.compile(r"[\u4e00-\u9fff]")


def est_tokens(text):
    """粗估 token:CJK 约 1.6 字/token,其余约 4 字符/token。"""
    cjk = len(CJK_RE.findall(text))
    return int(cjk / 1.6 + (len(text) - cjk) / 4)


def read(path):
    with open(path, "r", encoding="utf-8", errors="replace") as fh:
        return fh.read()


def human(nbytes):
    return f"{nbytes} B" if nbytes < 1024 else f"{nbytes / 1024:.1f} KB"


def main():
    ap = argparse.ArgumentParser(description="校验 L0–L3 分层记忆库")
    ap.add_argument("--home", default=os.path.expanduser("~/.agent-home"), help="记忆库所在的 home 目录")
    ap.add_argument("--agents", default="AGENTS.md", help="L0 文件名(相对 home)")
    ap.add_argument("--memory", default="memory", help="记忆库目录名(相对 home)")
    ap.add_argument(
        "--cold",
        default="*-inventory.md,*-catalog.md",
        help="放在 memory/ 顶层但按冷档对待的文件(逗号分隔的 glob)",
    )
    args = ap.parse_args()

    home = os.path.abspath(os.path.expanduser(args.home))
    mem = os.path.join(home, args.memory)
    l0 = os.path.join(home, args.agents)
    l1 = os.path.join(mem, "MEMORY.md")

    problems = []
    notes = []

    if not os.path.isdir(mem):
        print(f"[FAIL] 找不到记忆库目录:{mem}")
        return 1

    # ── L0 ────────────────────────────────────────────────────────────
    if not os.path.isfile(l0):
        problems.append(f"L0 缺失:{l0}")
        l0_size = 0
        l0_tok = 0
    else:
        text0 = read(l0)
        l0_size = os.path.getsize(l0)
        l0_tok = est_tokens(text0)
        if l0_size > L0_BUDGET_BYTES:
            problems.append(f"L0 超预算:{human(l0_size)} > {human(L0_BUDGET_BYTES)}")
        print(f"L0  {human(l0_size):>8}  ~{l0_tok:>5} tok   {os.path.relpath(l0, home)}")

    # ── L1 ────────────────────────────────────────────────────────────
    if not os.path.isfile(l1):
        print(f"[FAIL] 找不到索引:{l1}")
        return 1

    text1 = read(l1)
    l1_size = os.path.getsize(l1)
    l1_tok = est_tokens(text1)
    if l1_size > L1_BUDGET_BYTES:
        problems.append(f"L1 超预算:{human(l1_size)} > {human(L1_BUDGET_BYTES)}")
    print(f"L1  {human(l1_size):>8}  ~{l1_tok:>5} tok   {os.path.relpath(l1, home)}")

    links = []
    long_lines = 0
    for i, line in enumerate(text1.splitlines(), 1):
        for target in LINK_RE.findall(line):
            links.append((i, target))
        if line.strip().startswith("- [") and len(line) > INDEX_LINE_MAX:
            long_lines += 1
            problems.append(f"L1 索引行过长(第 {i} 行,{len(line)} 字 > {INDEX_LINE_MAX})")
    if long_lines:
        notes.append(f"{long_lines} 条索引行超过 {INDEX_LINE_MAX} 字")

    # ── 链接有效性 ────────────────────────────────────────────────────
    linked_files = set()
    dead = 0
    for lineno, target in links:
        target = target.split("#")[0].strip()
        if not target or "://" in target:
            continue
        path = os.path.normpath(os.path.join(mem, target))
        if not os.path.exists(path):
            dead += 1
            problems.append(f"死链(第 {lineno} 行):{target}")
        else:
            linked_files.add(os.path.basename(path))

    # ── L2 登记完整性 + 单条预算 ──────────────────────────────────────
    cold_globs = [x.strip() for x in args.cold.split(",") if x.strip()]
    top_md = [
        f for f in os.listdir(mem)
        if f.endswith(".md") and f != "MEMORY.md" and os.path.isfile(os.path.join(mem, f))
    ]
    cold_files = [f for f in top_md if any(fnmatch.fnmatch(f, g) for g in cold_globs)]
    l2_files = sorted(f for f in top_md if f not in cold_files)
    l2_bytes = 0
    l2_tok = 0
    for name in l2_files:
        path = os.path.join(mem, name)
        body = read(path)
        size = os.path.getsize(path)
        tok = est_tokens(body)
        l2_bytes += size
        l2_tok += tok
        if name not in linked_files:
            problems.append(f"L2 未在 L1 登记:{name}")
        over_tok = tok > L2_MAX_TOKENS
        over_lines = len(body.splitlines()) > L2_MAX_LINES
        if over_tok or over_lines:
            head = "\n".join(body.splitlines()[:HEADER_SCAN_LINES])
            if HEADER_MARK not in head:
                why = []
                if over_tok:
                    why.append(f"~{tok} tok > {L2_MAX_TOKENS}")
                if over_lines:
                    why.append(f"{len(body.splitlines())} 行 > {L2_MAX_LINES}")
                problems.append(f"L2 超预算且缺头部摘要:{name}({','.join(why)})")

    # ── L3 只统计,不校验预算 ─────────────────────────────────────────
    l3_dirs = sorted(
        d for d in os.listdir(mem) if os.path.isdir(os.path.join(mem, d))
    )
    l3_files = 0
    l3_bytes = 0
    for d in l3_dirs:
        for root, _dirs, files in os.walk(os.path.join(mem, d)):
            for f in files:
                l3_files += 1
                l3_bytes += os.path.getsize(os.path.join(root, f))

    # ── 汇总 ──────────────────────────────────────────────────────────
    print(f"L2  {human(l2_bytes):>8}  ~{l2_tok:>5} tok   {len(l2_files)} 个主题记忆")
    if cold_files:
        print(f"    (另有 {len(cold_files)} 个顶层冷档文件按约定不计预算:{', '.join(cold_files[:3])}"
              f"{' …' if len(cold_files) > 3 else ''})")
    print(f"L3  {human(l3_bytes):>8}  {'':>7}     {len(l3_dirs)} 个冷档目录 / {l3_files} 个文件(不校验预算)")
    print("-" * 62)
    print(f"每轮固定开销 ≈ {l0_tok + l1_tok} tok(L0 {l0_tok} + L1 {l1_tok})")
    print(f"全库(L2+L3)≈ {l2_tok} tok 文本 / {human(l2_bytes + l3_bytes)}")
    if dead:
        notes.append(f"{dead} 条死链")

    if problems:
        print()
        for p in problems:
            print("[FAIL] " + p)
        print(f"\n[FAIL] 共 {len(problems)} 项违规")
        return 1

    print("\n[OK] 全部通过")
    return 0


if __name__ == "__main__":
    sys.exit(main())

⑤ Mermaid 源码

贴进 README / Wiki / 支持 Mermaid 的编辑器
flowchart TD
  U["一次请求"] -->|每轮注入| L0["L0 常驻规则
AGENTS.md · ≤2.6KB"] L0 -->|只给指针| L1["L1 索引
MEMORY.md · ≤15KB"] L1 -->|命中关键词| L2["L2 主题记忆
memory/*.md · ≤4k tok/篇"] L2 -->|要细节/沿革| L3["L3 冷档
*-detail/ · 不限"] L3 -.->|回指| L2 N["新事实"] --> D{"先判归宿"} D -->|每轮都要遵守| L0 D -->|需要被检索| L1 D -->|独立主题| L2 D -->|只是细节| L3 L2 --> G["闸门:memory-budget.py"] L1 --> G G -->|不过| R["删细节 / 补摘要 / 拆冷档"] R --> L2

落地顺序(半天能跑起来)

别先写满记忆库再分层——从零开始反而快。第 1–4 步做完就有收益,5–7 步是让它半年后还站得住。

  1. 建两个文件 · AGENTS.md + memory/MEMORY.md,先写十行以内。
  2. 定预算 · 先抄 2.6 KB / 15 KB / 4k tok / 200 行;跑一周再按自己实际情况调。
  3. 定命名 · (标签)名字.md,文件名与 name 同形;标签固定成一小撮,别每天起新的。
  4. 写第一条记忆 · 正文进 L2,索引只加一行。写完立刻跑一次闸门。
  1. 接闸门 · 放进收工步骤或 CI;不过就不算写完,这条要写进 L0。
  2. 第一次拆分 · 某篇超 4k tok 时,正文搬 *-detail/,主文件留结论 + 指针 + 头部摘要。
  3. 第一次瘦身 · 每周只动 L0/L1:删形容词、合并重复、把资料踢去 L2。L2/L3 别动。

适用边界(写给要用它的人)

它成立的前提

① agent 每轮会把一份固定的 system / instructions 文件注入上下文;② 能读写本地文件;③ 检索靠「读索引 + 关键词命中」,不依赖向量库。

满足这三条,L0–L3 就直接可用。

它不解决的问题

· 语义检索:这是关键词路由,找不到「意思相近但词不同」的记忆,要么补同义词进索引,要么另加检索层。

· 多人协作:改名会打断链接,多人同时改索引要先约定写入者。

· 跨设备同步:文件树会漂,得靠版本库或同步盘。

一句话总结:把「每轮都要在场的东西」压到两三个文件,其余全部做成「能被索引找到的懒加载正文」。 预算不是洁癖,是每轮都在付的账单。
本页是纯静态单文件:无外部字体、无脚本依赖、可直接发给别人或打印成 PDF。 结构来自一套实际运行半年以上的 agent 记忆库约定,数字是可替换的量级参考,不是标准答案。
已复制