四层怎么分工
不是四种存储,是四种读取时机。越靠上越常读、越贵,所以只允许放「路由键」;越靠下越少读、越便宜,所以允许放「全部细节」。 判断一条信息该放哪层,只看一个问题:下一次执行任务时,它必须不必须在场?
关键不在层数,在「上行只留指针,下行只放正文」。一旦有人把细节写进 L0/L1,整个成本模型就崩了—— 而这几乎总是从「就加一行,应该没关系」开始的。
为什么值得分四层
成本模型很简单:每轮注入的内容 × 轮数。一个 90k token 的记忆库如果每轮全塞,一天几十轮就是几百万 token; 分成四层后,每轮只付 L0+L1 的固定开销,L2/L3 只在真的用到时才读。
每轮固定开销(L0 + L1)要盯的数
这两个文件每轮/每会话都要重发,所以它们是唯一值得反复瘦身的地方。
全库文本(L2 + L3)不用盯
总量涨十倍也不影响每轮成本——只要 L2 没被塞进 L1,L3 没被塞进 L2。
每轮成本 / 全库体积
这就是分层的全部收益:把「全库」换成「索引 + 命中页」。
各层体积的量级(对数刻度)
条宽按 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]] 全部失效。改名前后各跑一次闸门,并先整包备份。
四条禁令
- 禁止把细节写进索引 · 索引行只是路由键
- 禁止把资料写进常驻规则 · 环境/路径/清单都进 L2
- 禁止把详情堆回速查表 · 正文进冷档,主表留一行
- 禁止为同一主题再开新文件 · 先合并,再新建
子架构:领域大了怎么办
某个领域(比如某个长期项目)记忆变多时,在它下面再套一层,但预算层级不变:
| 子层 | 落点 | 在总架构里算 |
|---|---|---|
| 子 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 步是让它半年后还站得住。
- 建两个文件 ·
AGENTS.md+memory/MEMORY.md,先写十行以内。 - 定预算 · 先抄 2.6 KB / 15 KB / 4k tok / 200 行;跑一周再按自己实际情况调。
- 定命名 ·
(标签)名字.md,文件名与name同形;标签固定成一小撮,别每天起新的。 - 写第一条记忆 · 正文进 L2,索引只加一行。写完立刻跑一次闸门。
- 接闸门 · 放进收工步骤或 CI;不过就不算写完,这条要写进 L0。
- 第一次拆分 · 某篇超 4k tok 时,正文搬
*-detail/,主文件留结论 + 指针 + 头部摘要。 - 第一次瘦身 · 每周只动 L0/L1:删形容词、合并重复、把资料踢去 L2。L2/L3 别动。
适用边界(写给要用它的人)
它成立的前提
① agent 每轮会把一份固定的 system / instructions 文件注入上下文;② 能读写本地文件;③ 检索靠「读索引 + 关键词命中」,不依赖向量库。
满足这三条,L0–L3 就直接可用。
它不解决的问题
· 语义检索:这是关键词路由,找不到「意思相近但词不同」的记忆,要么补同义词进索引,要么另加检索层。
· 多人协作:改名会打断链接,多人同时改索引要先约定写入者。
· 跨设备同步:文件树会漂,得靠版本库或同步盘。