本节摘要:压缩管"这个会话装得下",记忆管"会话之外还记得什么"。把记忆按生命周期分三层:会话内记忆就是 messages 本身加工作区——引用化(3.3 节)把文件系统当作窗口的外存,是这一层的核心技巧;跨会话记忆是落盘的结构化条目——项目约定文件(CLAUDE.md 式,随仓库走、团队共享)、会话存档(任务结束时的摘要简报,下次同仓库开工时注入)、用户偏好(风格与惯例);知识库是检索式外存——当"要记的东西"大到无法整块注入(全部历史工单、全部内部文档),就从"注入"退到"按需检索"。每层各有介质、写入时机、读取时机与失效方式,本节给出对照表与
memory.py最小实现,最后讲记忆的三个坑:陈旧误导、膨胀、跨用户泄漏。记忆与上下文的系统展开,另见 《Hello-Agents:智能体实战教程》第 8~9 章。
阅读完本节,你应当能够:
| 层 | 介质 | 写入时机 | 读取时机 | 失效方式 |
|---|---|---|---|---|
| 会话内 | messages + 工作区文件 | 循环每步(3.1 节) | 每步供料(7.1 节) | 会话结束即逝(压缩是它的瘦身术,7.2 节) |
| 跨会话 | 项目约定文件(CLAUDE.md 式)、会话存档、用户偏好 | 任务结束时摘要落盘;约定由人维护 | 会话开始时注入 L1/L5 交界 | 项目重构后过时;需冲突合并与上限 |
| 知识库 | 检索索引(向量库或全文索引,方案不限) | 异步构建,与任务解耦 | 任务中按需检索(作为工具暴露) | 文档更新滞后;检索质量决定价值 |
读表的要点:越靠下的层,读写与任务的耦合越松。会话内记忆每步都动;跨会话记忆一个会话动两次(开始注入、结束落盘);知识库与任务完全解耦,只是工具表里多了一个 search_docs(第 4.1 节:检索类工具,低危可 allow)。
这层基本就是第 3.3 节 + 本章前两节的内容,只补一个总括:harness 的两级存储结构——窗口是高速小存储,工作区文件系统是大慢存储,引用化是两者之间的指针。读过大文件后上下文里只留 app.py(约 800 行,含 auth 模块),要用再 read_file 取段。这个结构决定了很多设计:驱逐敢丢旧输出(盘上还有)、压缩敢简化历史(简报里保留了路径)、工具表里 read 类工具永远值得有分页参数。
三类内容、三种治理:
| 类型 | 例子 | 写入者 | 治理要点 |
|---|---|---|---|
| 项目约定 | CLAUDE.md 式文件:构建命令、代码规范、"别动的目录" | 人(团队评审) | 随仓库走 = 随 git 版本化;不进模型自主写权限(5.1 节) |
| 会话存档 | 任务结束时的摘要简报(7.2 节同款结构) | harness(自动) | 同一工作区最多留 N 份;注入时声明"项目可能已变化" |
| 用户偏好 | "提交信息用中文""先跑测试再提 PR" | harness(经用户确认后沉淀) | 可查看、可删除——记忆必须对用户透明 |
memory.py 最小实现(会话存档半边):
# memory.py —— 跨会话记忆存取(写法示意,以实际工程为准) import json, time from pathlib import Path STORE = Path(".mini_harness") / "memory.json" def load() -> dict: if STORE.exists(): return json.loads(STORE.read_text(encoding="utf-8")) return {"sessions": []} def save_session_summary(task: str, summary: str, limit: int = 50) -> None: """任务结束时写入一条会话存档;上限防膨胀(示意)。""" mem = load() mem["sessions"].append({"workspace": str(Path.cwd().resolve()), "task": task, "summary": summary, "ts": time.strftime("%Y-%m-%d %H:%M")}) mem["sessions"] = mem["sessions"][-limit:] STORE.parent.mkdir(exist_ok=True) STORE.write_text(json.dumps(mem, ensure_ascii=False, indent=1), encoding="utf-8") def context_inject(limit: int = 3) -> str: """新会话供料时注入:同工作区最近 limit 条存档。""" ws, mem = str(Path.cwd().resolve()), load() rows = [s for s in mem["sessions"] if s["workspace"] == ws][-limit:] if not rows: return "" return "近期会话回顾(仅供参考,项目可能已变化):\n" + "\n".join( f"- {s['ts']} {s['task']}:{s['summary'][:120]}" for s in rows)
注意 context_inject 里那句免责声明不是装饰——它是防"陈旧记忆误导"的第一道闸(见第五节)。
跨会话记忆解决"几十条经验",知识库解决"几千份文档"。切换判据很简单:当要记的内容大到无法整块注入预算(7.1 节),且任务只用到其中小部分时,从"注入"退到"检索"——把 search_docs(query) 做成工具(第 4.1 节分类学:检索类,低危),模型按需调用。实现方案(全文索引 / 向量检索 / 混合)不影响 harness 侧的设计:对 harness 而言它只是一个只读工具 + 一个异步构建流水线。不要过早上知识库——大部分团队前两年的"记忆需求",一个约定文件加会话存档就覆盖了。
| 坑 | 形态 | 对策 |
|---|---|---|
| 陈旧误导 | 存档说"配置在 config.py",项目早已重构为 yaml | 注入时带时间戳与免责声明;命中失败时模型以现场探查为准(工具优先于记忆) |
| 膨胀 | 条目只增不减,注入本身吃掉预算 | 条目上限 + 定期合并(同主题旧条目合并为一条);注入走 pinned 之外的可驱逐层 |
| 跨用户泄漏 | A 用户的偏好被注入 B 用户的会话 | 记忆按(工作区 × 用户)双键隔离;用户偏好层永不进共享存储 |
至此供料的三件套——预算、压缩、记忆——齐了。但上下文工程管的是"进",还有一条"出"的暗线:每一步执行之后,谁来把检查、测试、修复自动串起来?下一章:Hook 与反馈回路。
延伸阅读:同站教程库《opencode》。