跳到主要内容

快速开始:从「记住名字」到 Token 预算

七天实战

边学边练请访问 7 天学会 Memory Engineering:首页记忆分层可视化、/learn 七天课表、/lab 五个本地沙盒、社区与资源矩阵。

延伸阅读

概念全景见 入门介绍;写读路径与混合检索见 开发指南

本章目标

对齐子站 Day1(认知架构)Day2(上下文工程)。读完并动手后,你应能:

  1. 用五层组件(Sensory / Working / STM / LTM / Procedural)与四类 Taxonomy 画一张自己的 Agent 记忆草图。
  2. 实现可重启持久化的 FileMemory 键值记忆,并解释它与「硬编码变量」的差异。
  3. 实现带 Token 预算的 pack():System / History / Retrieved 分区裁剪,溢出时触发摘要替换。
  4. 对照验收清单自测:持久化、预算不超限、注入格式可审计。

本篇不上向量集群与图谱;那些是 Day3–Day4,见开发指南。

0. 环境准备

组件最低要求说明
Python3.11+本篇示例纯标准库即可跑 KV 与预算骨架
虚拟环境推荐python3 -m venv .venv && source .venv/bin/activate
LLM API(可选)任一聊天模型Day2 摘要步骤可用假摘要函数先跑通逻辑
编辑器任意能打印上下文长度即可
mkdir -p ~/memeng-day12 && cd ~/memeng-day12
python3 -m venv .venv
source .venv/bin/activate
# Day1–2 可不装第三方;Day3 再装 chromadb / openai 等
touch day01_memory.py day02_budget.py

可选:打开子站 /lab 的 Token 预算仪表盘,边改比例边看分布。

1. 是什么:五层记忆组件(Day1 Morning)

LLM 默认无状态。记忆工程把「超越窗口的持续智能」拆成五层,每层时间尺度与实现不同:

时间尺度是什么典型落地
Sensory 感知毫秒–秒原始输入缓冲消息/工具返回队列,脱敏截断
Working 工作秒–分钟当前激活上下文System + 近期对话 + 检索包
STM 短期/会话分钟–小时本会话完整历史滑动窗口、分段摘要
LTM 长期天–年跨会话持久化文件 / DB / 向量 / 图谱
Procedural 程序跨任务技能与策略工具定义、ReAct 模板、反思策略

为什么这样分层? Working 是稀缺资源(Token);Sensory 管脏数据入口;STM 管连贯;LTM 管跨会话;Procedural 管「下次怎么做得更好」。混成一个大字符串,三个月后几乎无法治理。

怎样发展而来? SOAR / ACT-R 已区分工作记忆与长期知识;Global Workspace 强调「被广播才进入意识」;MemGPT / Letta 把主上下文与外存做成 OS 分页。今日工程是把这些隐喻变成可测的预算与 API。

2. 是什么:四类记忆 Taxonomy(Day1 Afternoon)

类型一句话适合存什么不适合
Episodic 情景发生过什么对话事件、工具轨迹、决策路径稳定偏好(易重复)
Semantic 语义知道什么用户画像、领域事实一次性闲聊原文
Procedural 程序会怎么做工具链、反思模板、成功策略未验证的猜测步骤
Affective 偏好喜欢/忌讳语气、格式、禁忌与权限无关的杂讯

客服 Agent 练习:把「用户姓名、发票抬头、上次工单结论、忌用表情、escalation 话术」分别标到四类,并写「保留多久」。这就是 Day1 挑战的记忆需求清单。

3. 经典架构怎么映射到产品(Day1 Afternoon)

架构核心机制工程映射
ReAct + MemoryThought/Action/Observation 交错轨迹结构化写入情景记忆
Reflexion失败后生成反思再试反思进程序记忆,低分不上 LTM
Generative AgentsStream → Reflection → Planning定时巩固任务 + 重要性评分
MemGPT / Letta主上下文 / 外存分页与中断Core memory vs archival 分区

初学不要一次上 Letta 全集群:先用文件 KV 体会「进程重启仍记得」,再用预算体会「主上下文有限」。

4. 怎么落地:三种「记住名字」方案对比

方案做法优点缺点
硬编码变量user_name = "Kimi"零依赖重启即忘,无法多用户
文件 KVJSON 落盘可持久、可审计无语义检索、无并发锁
数据库SQLite/Postgres多用户、事务Day1 过重时可后移

Day1 实战要求你亲手走完「变量 → 文件」这一步,建立「外挂存储器」直觉。

4.1 可运行:FileMemory

# day01_memory.py
import json
import os
from typing import Any


class FileMemory:
"""最小长期语义记忆:键值 + JSON 落盘。"""

def __init__(self, path: str = "memory.json") -> None:
self.path = path
if os.path.exists(path):
with open(path, "r", encoding="utf-8") as f:
self.store: dict[str, Any] = json.load(f)
else:
self.store = {}

def remember(self, key: str, value: Any) -> None:
self.store[key] = value
with open(self.path, "w", encoding="utf-8") as f:
json.dump(self.store, f, ensure_ascii=False, indent=2)

def recall(self, key: str, default: str = "(还没有相关记忆)") -> Any:
return self.store.get(key, default)

def forget(self, key: str) -> bool:
if key not in self.store:
return False
del self.store[key]
with open(self.path, "w", encoding="utf-8") as f:
json.dump(self.store, f, ensure_ascii=False, indent=2)
return True


if __name__ == "__main__":
mem = FileMemory()
mem.remember("user_name", "Kimi")
mem.remember("tone", "简洁中文,少用表情")
print(mem.recall("user_name"))
# 重启进程再运行本文件的 recall,应仍得到 Kimi

验收点:杀掉终端再开,再次 recall("user_name") 仍正确;forget 后文件中键消失。

4.2 升级:带命名空间与元数据

生产不会只有一个全局 dict。最小升级:

def remember_ns(self, user_id: str, key: str, value: Any, kind: str = "semantic") -> None:
bucket = self.store.setdefault(user_id, {})
bucket[key] = {"value": value, "kind": kind, "ts": __import__("time").time()}
self._flush()

索引前缀 user_id 是多租户隔离的第一道门,Day6–Day7 会强化;现在养成习惯即可。

5. Day2:上下文工程是什么、为什么

是什么:在固定 Token 上限内,决定 System、历史、检索记忆、本轮用户话如何分配与压缩。

为什么:窗口溢出会导致截断乱序;全量塞历史会噪声淹没与成本爆炸;无分区则无法解释「这次答错是因为检索还是历史」。

怎样发展:早期 ChatBot 用固定最近 N 轮;LangChain Memory / LangGraph MessagesState 引入可追加状态;今日主流是预算驱动的拼装器 + 溢出摘要。

5.1 Token 预算表(建议起步比例)

分区建议占比内容溢出时
System15–20%角色、安全、记忆锚点禁止挤占,先减其它
History50–60%近期对话早期轮次摘要替换
Retrieved15–25%从 LTM 召回的片段降 TopK 或提阈值
User turn余量本轮输入过长则截断/要求精简

子站课表示例:system 20% / history 60% / retrieved 20%。比例不是教条,应用评测集调。

5.2 滑动窗口 vs 摘要

策略做法信息损失适用
滑动窗口只留最近 N 轮早期细节全丢闲聊、短任务
触发摘要超预算时压缩头部细节变概括长会话客服
分层摘要摘要的摘要可无限延长「无限上下文」模拟

5.3 MessagesState 与多会话

LangGraph 风格心智模型:消息列表是 STM 的载体,支持 append;会话用 thread_id 隔离;用户级画像用 user_id 跨线程共享。临时 Scratchpad(CoT 草稿)默认不写 LTM,除非反思打分通过。

6. 怎么落地:可运行的 pack()

下面示例用「字符数 / 4 ≈ token」做教学近似;生产请换成 tiktoken 或厂商 tokenizer。

# day02_budget.py
from __future__ import annotations

from typing import Callable


def approx_tokens(text: str) -> int:
return max(1, len(text) // 4)


def trim(text: str, budget: int) -> str:
"""按近似 token 预算截断;真实系统应对齐 tokenizer。"""
if approx_tokens(text) <= budget:
return text
keep = max(16, budget * 4)
return text[:keep] + "…"


def pack(
system: str,
history: list[str],
retrieved: list[str],
budget: int = 8192,
summarize: Callable[[list[str]], str] | None = None,
) -> list[str]:
"""按 20/60/20 拼装上下文;历史溢出则摘要头部。"""
b_sys = int(budget * 0.2)
b_his = int(budget * 0.6)
b_ret = int(budget * 0.2)

ctx: list[str] = [trim(system, b_sys)]

hist_text = "\n".join(history)
if approx_tokens(hist_text) > b_his:
if len(history) > 6:
head, tail = history[:-6], history[-6:]
else:
head, tail = history[:1], history[1:]
if summarize is None:
summary = ";".join(head)[:400]
else:
summary = summarize(head)
history = [f"前情提要:{summary}"] + tail
hist_text = "\n".join(history)

ctx.append(trim(hist_text, b_his))
ctx.append(trim("\n".join(retrieved), b_ret))
return ctx


def fake_summarize(msgs: list[str]) -> str:
return "用户曾讨论:" + " / ".join(m[:40] for m in msgs)


if __name__ == "__main__":
system = "你是个人助理。优先使用【已检索记忆】中的事实。"
history = [f"用户:第{i}轮闲聊内容……" for i in range(20)]
retrieved = ["[semantic] 用户姓名=Kimi", "[affective] 语气=简洁"]
packed = pack(system, history, retrieved, budget=2000, summarize=fake_summarize)
total = sum(approx_tokens(x) for x in packed)
print("parts", len(packed), "approx_tokens", total)
for i, p in enumerate(packed):
print(f"--- part {i} ({approx_tokens(p)}) ---\n{p[:120]}…")

调试习惯:每次 LLM 调用前打印三区 token 与原文前 200 字。答错时先看是检索噪声、历史淹没,还是 System 锚点缺失。

6.1 把 FileMemory 注入 Retrieved

from day01_memory import FileMemory

mem = FileMemory()
retrieved = [
f"[semantic] user_name={mem.recall('user_name')}",
f"[affective] tone={mem.recall('tone')}",
]
# 再交给 pack(...)

注入格式建议固定:[类型][日期] 陈述,便于审计与红队抽检。

7. 写入门禁(Day1–2 就要有)

即便只有文件 KV,也要拒绝垃圾写入:

规则示例动作
用户明确偏好「以后用中文简短回复」写入 affective
长期事实「我叫 Kimi」写入 semantic,可覆盖同键
一次性闲聊「今天好热」不写 LTM
敏感原文身份证号、密码拒绝或脱敏后再写
未验证猜测Agent 自行推断偏好标低置信度或不写
WRITE_DENY_KEYWORDS = ("密码", "验证码", "身份证")

def should_remember(utterance: str) -> bool:
if any(k in utterance for k in WRITE_DENY_KEYWORDS):
return False
triggers = ("我叫", "请记住", "以后都", "不要再用")
return any(t in utterance for t in triggers)

8. 验收标准(Checklist)

完成下列全部项,再在子站勾选 Day1 / Day2:

  1. 持久化:重启进程后 user_name 仍可 recall。
  2. 隔离雏形:至少两个 user_id 桶互不串读。
  3. 预算pack() 在故意超长历史上触发摘要,且近似 token ≤ budget。
  4. 可观测:打印 System / History / Retrieved 三区长度。
  5. 门禁:敏感词与闲聊不会污染 memory.json
  6. 架构图:手绘或 Mermaid 画出 Sensory → Working → STM/LTM。
  7. 需求清单:客服场景写出「记什么 / 多久 / 形态(KV)」一页纸。

9. 常见坑

现象修正
把全部历史当 System角色漂移、费用高System 只放稳定锚点
摘要无版本前情提要互相矛盾摘要带时间戳,旧摘要归档
字符近似当生产 tokenizer真实超窗截断换官方 tokenizer
JSON 无锁多进程写文件损坏单写者或换 SQLite
忘记 forget API无法合规擦除Day1 就实现删除

10. 下一步

  1. 打开 七天学会 · Day1/Day2 完成挑战。
  2. 进入 开发指南:写路径、混合检索、图谱、压缩漏斗。
  3. 选型前浏览 GitHub 项目
  4. 想一次做完个人助理记忆系统:跟 从零到一

口诀:Working 是稀缺资源;KV 先跑通持久化;预算先于模型;写入先于检索调参。