Skip to content

第 8 章 记忆系统 ​

这一章把“记住”做成系统里边界清晰的子系统,状态的三层去处各有专门部件:窗口里的会话内状态、窗口外的跨会话记忆和另一份窗口里的子智能体。8.1 给出短期记忆三件套,会话、草稿区和检查点,管会话内的保存与断点续跑;8.2 讲长期记忆的载体选型,文件、数据库和向量库各自的适用边界;8.3 展开写入、遗忘和压缩三套生命周期机制,写什么由谁决定有应用管理与智能体自管理两条路线;8.4 讲子智能体,隔离策略的最终形态,专项任务进独立上下文,只回传结构化摘要。全章案例是一家电商的客服智能体:会话内记工单进度,跨会话记用户偏好与历史,该记该忘自己判断,纠纷时把二十万字聊天记录交给子智能体。

8.1 短期记忆 ​

会话之内的记忆叫短期记忆(short-term memory),默认载体是那份消息历史:用户消息、助手回复、工具调用与结果,在列表里依次追加。拿客服窗口里的一段场景,就能看清它管的是什么:用户上午报出订单 8899001,说包裹迟迟没到,智能体调用物流工具、核对订单,回复了预计送达时间;半小时后用户回来追问赔付怎么算,要回答这句话,模型得还记得那个订单号,记得自己查过什么、承诺过什么。支撑这份“记得”的,就是消息列表里那份依次追加的历史。

消息历史为什么能当记忆用,这个前提第 1 章已经交代过。API 不带状态,服务端不保留任何会话,多轮对话要衔接,靠客户端每次把历史完整带上。这也是所有记忆话题共同的出发点:每一次 API 调用都是独立的,对上一次调用没有任何印象,对话想要连续,只能由开发者手工维护。第 2 章手写 run_agent 时的 messages 列表,其实就是一份合格的短期记忆,它完整记录了会话里发生的事,模型每一轮都能看到全部内容。

那为什么不能只靠它?有三个缺口,最明显的是记忆容易丢失。列表放在进程自己的内存里,服务一重启、一发版、一崩溃,记忆就没了。多副本部署时更麻烦,每个副本各自记录,用户上一句话由 A 容器处理,下一句话轮到 B 容器处理,B 对之前发生的事一无所知。

Web 服务本来就是按请求处理的。每个 HTTP 请求相互独立,请求一结束内存就释放,用户十分钟后发来的那句话,对服务来说是一个全新的请求。

想让服务记住这些内容,就要显式管理状态:每次对话分配一个 session_id,执行完把状态存到外部,下一次执行开始时再加载。具体做法是把一段连续对话放进会话(session),这是一个可以跨多次执行存活的容器。里面放两部分内容:events 保存对话与工具调用的流水,state 保存任意的运行时状态。一次执行对应一个 ExecutionContext,会话比它存活得更久,执行结束时把必要信息写进会话,下次执行再取出来使用。

除了容易丢失,第二个缺口是消息历史只有流水,没有任务的结构化状态。流水能回答“发生过什么”,回答不了“现在进行到哪一步”。客服工单里有一批随时要查的信息:订单号、已核实的凭证、给出过几个方案、用户接受了哪个。这些信息分散在几十条消息里,模型每一轮都要重新翻一遍流水才能找到,既浪费注意力,也容易出错。

解决办法是给会话增加一块草稿区(scratchpad),也就是 Session 的 state 字段,动态变化的任务状态都放在这里。第 7 章实现 ExecutionContext 时,state 承担的就是这个作用:任务进度、中间结果和后面工具要引用的参数,都保存在这里。

第 7 章讲写入策略时说过一句话:中间产物写到上下文外面,窗口里只放引用。草稿区正是这条策略在执行循环里的具体位置,区别只是它放在内存或会话存储里,而不是文件里。

第三个缺口是断点续跑,平时不明显,一出故障就暴露出来。客服工单里常有敏感操作,比如修改收货地址、发起赔付,按第 15 章的要求,这类操作要经过人工审批。审批可能几分钟就到,也可能三天后才通过邮件送达,而进程在这期间早已退出。

要让智能体三天后还能接着执行,消息历史靠不住,要把执行进度保存成检查点(checkpoint),也就是一份供失败或暂停后恢复执行的进度快照。恢复一次执行需要三类信息:哪些步骤已经完成、每一步产出了什么、当前共享状态是什么。检查点里保存的正是这三类。

这里有一个容易出错的地方,我自己也遇到过:检查点保存之后,代码可能又改动了,多了一个步骤,或者某个类型变了。用旧检查点在改过的代码上恢复,得到的结果就不可信。因此要把有哪些步骤、各是什么类型这类结构信息算成一个哈希值,随检查点一起保存,恢复前先比对一遍,对不上就拒绝恢复。

检查点存放在哪里,可以按场景分层选择:测试时放内存,单机用文件,分布式用数据库或对象存储。文件后端的优点是明文可读、能纳入版本库,还不需要另外搭建基础设施。

把会话、草稿区、检查点组合起来,代码骨架如下。这是示意代码,模型调用和真实工具都用桩代替,但会话的持久化、草稿区里的待审批调用、跨“进程”的恢复,用的都是真实的语义。

python
"""示意代码:Session 与 SessionManager 的最小骨架,python3 可直接运行。"""

import json
from dataclasses import dataclass, field, asdict
from pathlib import Path

@dataclass
class Session:
    """跨多次执行存活的容器:events 是流水,state 是草稿区。"""

    session_id: str
    events: list = field(default_factory=list)
    state: dict = field(default_factory=dict)   # 工单状态、待审批调用都住这

    def add(self, role: str, content: str) -> None:
        self.events.append({"role": role, "content": content})

class FileSessionManager:
    """每个会话落成一个 JSON 文件,进程重启后仍在。
    整文件覆写、无并发控制,仅作示意;生产场景换数据库后端。"""

    def __init__(self, base_dir: str = "./sessions"):
        self.base = Path(base_dir)
        self.base.mkdir(parents=True, exist_ok=True)

    def get_or_create(self, session_id: str) -> Session:
        path = self.base / f"{session_id}.json"
        if path.exists():
            data = json.loads(path.read_text(encoding="utf-8"))
            return Session(data["session_id"], data["events"], data["state"])
        return Session(session_id)

    def save(self, session: Session) -> None:
        path = self.base / f"{session.session_id}.json"
        path.write_text(
            json.dumps(asdict(session), ensure_ascii=False, indent=2),
            encoding="utf-8",
        )

def run_turn(session: Session, user_input: str | None,
             approval: str | None = None) -> str:
    """一次交互。新输入与审批决定至少给一个。"""

    if user_input is None and approval is None:
        return "既没有新输入,也没有审批决定,会话原样保留。"

    if approval is not None:
        # 断点续跑:从草稿区取回待审批调用,相当于从检查点恢复
        pending = session.state.pop("pending_tool_call")
        if approval == "yes":
            session.add("assistant", f"[执行 {pending['tool']},参数 {pending['args']}]")
            session.add("assistant", "已登记新的收货地址,工单关闭。")
        else:
            session.add("assistant", "用户拒绝了地址修改,工单挂起。")
        return session.events[-1]["content"]

    if user_input:
        session.add("user", user_input)

    # 模型决策(桩):改地址不直接执行,写进草稿区等审批
    if "地址" in user_input and "改" in user_input:
        session.state["pending_tool_call"] = {
            "tool": "update_address",
            "args": {"order_id": "8899001", "city": "杭州"},
        }
        return "地址修改需要人工确认,执行已暂停。"

    session.add("assistant", "好的,已记录。")
    return "好的,已记录。"

if __name__ == "__main__":
    manager = FileSessionManager()
    # 演示每次从头开始,清掉上次运行留下的状态
    (manager.base / "ticket-8899001.json").unlink(missing_ok=True)
    s1 = manager.get_or_create("ticket-8899001")
    print(run_turn(s1, "帮我把订单 8899001 的收货地址改到杭州"))
    manager.save(s1)                                # 进程可以在这里退出

    s2 = manager.get_or_create("ticket-8899001")    # 模拟新进程重新取出
    print(run_turn(s2, None, approval="yes"))
    print(f"事件 {len(s2.events)} 条,草稿区剩余 {s2.state}")
    manager.save(s2)

实际运行一遍,暂停和恢复都能正常走通:第二次 get_or_create 取回的会话,带着第一次留下的全部流水和那笔待审批调用。代码的关键是 state 里的 pending_tool_call,暂停和恢复这套机制都依赖它。执行过程中遇到需要审批的工具,先不执行,把这次调用序列化保存到会话状态,立即回复一句提示。等审批到了,带着审批决定重新执行:批准的就执行,驳回的也如实记成结果回填给模型。

会话状态就是一份任意的字典,存放待审批调用、长任务进度和检查点数据,都是同一个模式。

这套东西不必每次都自己写。OpenAI 的 Agents SDK 把会话做成了内建原语,自动在每次运行前读取历史、运行后保存增量,会话存储有 SQLite、Redis、MongoDB、SQLAlchemy 等多种实现。Anthropic 的 Messages API 走另一条路:接口保持无状态,记忆由客户端自己实现,使用文件式的记忆工具,8.3 再展开。

会话保证连续性,草稿区保存任务状态,检查点支持断点续跑,但三者管理的都只是会话内这一段。它们还共同留下一个问题:每次请求都要把全部历史重放一遍,会话越长越花钱,第 7 章的压缩手段在这里同样适用。而且记忆出不了会话:用户上个月说过不要电话回访,这个月新开一个会话,再多的 session_id 也帮不上忙。跨会话记忆要把记忆移到会话生命周期之外,交给外部存储。

8.2 长期记忆 ​

跨会话的“记得”靠的是长期记忆(long-term memory):一份在会话结束后依然保留、新会话需要时再取出使用的存储。它要接的场景,短期记忆帮不上忙:用户在新会话里说“上次那个赔付方案挺好,这次物流又延误了,照旧处理”,仅凭“上次”两个字,任何会话内的消息历史都对不上号,能对上这句话的,只有上一份会话之外存下来的内容。上下文工程里的写入和检索两条策略,在长期记忆上合到了一起:会话结束时,把值得保留的信息写入外部存储;新会话开始时,检索回来注入上下文。

先把“记什么”分类清楚,再谈载体。分类不是一次就能分完的:最粗的一层按时长划分,短期记忆保存会话内的临时内容,长期记忆保存跨会话仍有价值的稳定信息,这一层确定了整节的框架。再往细分还有好几层:长期记忆里分情景、程序和语义三类,工程上还单独列出工具与接口记忆;内容分完之后,还要划清两条边界,哪些算知识、哪些算记忆,哪些是公共的、哪些是私有的;最后还要看价值衰减的快慢,它直接决定保留策略。

长期记忆里有一类叫情景记忆(episodic memory),保存具体的交互事件和结果,重视时序和上下文,比如这位用户上一次纠纷是怎么解决的、当时哪一步让用户不满。客户档案、偏好、领域知识这些稳定信息也属于长期记忆,不少资料把它称为知识库,这个叫法的范围偏宽,边界怎么划,下面几段说明。

按时长划分,只能确定记忆在会话内还是会话外;长期这一半里保存什么,还要继续细分。心理学有一套现成的分法,放在这里正合适:情景记忆保存事件;保存步骤和流程的是程序性记忆(procedural memory),保存事实和概念的是语义记忆(semantic memory)。这套分法套进客服场景很顺:用户的沟通偏好属于语义记忆,上次工单的处理经过属于情景记忆,赔付流程怎么走属于程序性记忆。

程序性记忆有一个特殊待遇:可以固化成工具和动作交给智能体。固化成工具之后,每个新会话一开始,它就随工具一起存在,不需要另外再保存一份。

工程上还应该单列一类:工具与接口记忆,专门记录各个系统自己特有的行为。任何用了多年的企业系统,都有一些没有写进文档的规则:某个系统提交采购单后,必须手动刷新一次,状态才会更新;某个接口的日期参数只接受固定的写法,换成别家常用的格式,它不报错,只是默默返回空。这些特殊行为目前只存在于老员工的经验里,智能体把它们逐条积累下来,“怎么与这个系统打交道”就成了它自己的经验。

工具调用失败后,如果查明是系统的特殊行为造成的,就把这个行为连同解决办法写成一条记忆,下次再调用同一个工具,先取出来看一眼。这类记忆见效最快,同一个问题不用再解决第二次。它与 8.3 要讲的失败日志有关联:一个积累接口的特殊行为,一个积累任务的教训。

还有一条界线要划清:知识和记忆不是一回事。知识来自已有的文档;记忆保存的是智能体在与用户、与自己、与其他系统打交道的过程中积累下来的内容。前面说“叫知识库”范围太宽,本书采用更严格的划分。第 6 章的知识库保存制度和规则文档,这一章的记忆库保存用户和工单的沉淀,两个库分开管理,检索和更新的策略也完全不同。

动手写记忆之前,先问自己一句:这条内容,模型的预训练里是不是本来就有?要写的内容分两层:通用技术知识属于公共层,这个团队、这套系统独有的事实属于私有层。“遵循 SOLID 原则”“防 SQL 注入要用参数化查询”这类属于前者,写进记忆只是占用空间,模型本来就会。

记忆真正要管理的是私有层。本仓库的索引命名一律用 idx_表名_列名,这条流水线的部署顺序不能变,这位审批人习惯先看风险摘要,这些内容哪本教材里都查不到,智能体不记录下来就永远不知道。因此公共层不要囤积,记忆的预算全部投给私有层。模型本来就会的内容,保存得再多也不会让它多会一样东西;而私有事实只要漏存一条,下次用到时智能体就真的无从下手。

类别分完,还有一条轴要看:价值衰减有多快。记忆并不都同样有价值。错误修复类衰减最快,记录的是修复了哪个 bug、打了哪个补丁,版本一升级、场景一变化,这条记忆就作废了。教训类和偏好类保持时间长,“升级这个库之前先跑迁移测试”“用户不接受电话回访”,放一年也仍然有用。

保留策略直接由这条轴决定:衰减快的,设一个较短的有效期,定期清理,不要让已经失效的修复记录占着检索位置。衰减慢的,才值得长期维护,做 8.3 讲的聚类压缩。设计的精力该投向哪里,也看这条轴:投给衰减最慢的那一类,回报才能积累下来。

载体怎么选,要先看长期记忆保存的是什么。保存的内容不一定非要放进向量库,检索增强这条路对数据源不挑,文件系统、知识图谱、传统数据库和各类 API 都能当底子。选载体之前,先理清分类,每类记忆的去处才能确定:情景记忆当案例检索用,用户偏好进入档案,程序性记忆固化成工具。前两类需要选择载体,第三类不需要,工具本身就是它的形态。去处不同,载体也随之变化,工程上有三条路。

条目入库之后还有一个常见的召回毛病,存储时的措辞和将来查询的措辞对不上。用户上个月存的是“用户不接受电话回访”,这个月客服的问法是“这位客户能打电话吗”,字面差得远,向量相似度可能就差那么一点。有个办法是在写入时让模型反过来想,将来什么样的问法会命中这条记忆,把想到的问句和条目存在一起,一并建索引。检索时条目和问句任何一边对上就算命中,问法对不上字面的召回失败就少一大截。这个做法在检索领域叫文档扩展,用问题预测扩充文档是其中一路,智能体记忆系统里也有产品做了同样的事。

第一条路是文件。给智能体划出一个专属的工作目录,也就是 agent workspace,读写文件、保存中间产物和维护笔记都在这个目录里。有一个好习惯:每次任务开始前,把工作目录从缓存重置回初始状态,任务才可以复现。安全上还有一条硬性规定:文件工具永远不允许越出工作目录,避免智能体接触项目之外的文件。

文件这条路也有大厂的产品实现:Anthropic 的记忆工具就是文件式的,模型在 /memories 目录下可以执行的操作共六种:查看、新建、替换片段、插入、删除和重命名。存储完全放在客户端,应用把这六种操作映射到真实存储,为每个用户开一个目录,或者落到数据库的若干键上。

文件这条路的好处,与 8.1 给检查点选择文件后端是同一套:明文可读、可审计、能纳入版本库,还不需要额外搭建任何东西。短处也很明确:条目一多,检索就完全取决于目录怎么设计、模型自己会不会查找;并发写入要自己加锁,权限要自己管理,还要防止路径穿越。

第二条路是数据库。会话历史、检查点、结构化的用户档案,本身就是数据库的内容,第 14 章讲运维时还会再遇到。这条路业界早有现成实现:OpenAI Agents SDK 的会话存储,从 SQLite 到 Redis、MongoDB、SQLAlchemy 都提供了实现,分别对应本地开发、跨服务共享和多进程存储几类场景。数据库的长处是多副本共享和事务并发,这两项是生产部署必需的。8.1 里各个容器各记各的问题,只有把会话放进共享存储才算真正解决。

记忆进入共享存储后,接着要做租户隔离。键里要带命名空间,按用户、组织和记忆类型把数据隔离开;拿什么身份来分键,必须来自服务端验证过的会话或令牌,客户端自己报告的 user_id 不能采信,任何人都能修改的字符串,等于把所有人的记忆都暴露了。厂商样本再补 Google 一家:它的 ADK 把短期记忆做成 Session 服务,无状态的模型每轮重放完整历史,靠大上下文窗口支撑当次会话;跨对话需要记住的事实另外提取,存到外部的存储里,需要时检索回来。三家放在一起看,差别不在记不记,而在“会话内靠窗口、跨会话靠提取”这个比例怎么调整。

第三条路才是向量库,什么时候使用它,看两条判据:一是条目多到按 ID 已经翻不过来,二是取用时希望按语义查找,而不是按字段查询。客服智能体的用户偏好也许只有十几条,一个文件就装得下。历史工单的沉淀却每天都在增长,增长到几万条之后,新工单来了要问“以前有没有类似的情况”,按语义检索就成了硬性需求。

向量库的机制第 6 章已经讲透,这里只看它作为记忆载体的用法。整条流水线是这样的:入库前先用模型从执行历史里抽取结构化记忆,抽取出来的是任务摘要、解法和结果三项;接着查一遍相近记忆做去重判断;再把结构化内容连同元数据存入 ChromaDB。取用时把检索包装成 recall_memory 工具交给模型按需调用,或者做成每次模型调用前自动注入。

存储这一侧,记忆要与 6.2 的制度条文分开,所以另建一个 memories 集合来存放。抽取记忆的 client 仍然使用 DeepSeek,嵌入函数 embed 沿用 6.2 的百炼端点,两家各负责一段。完整代码如下。

python
import os
import uuid
import chromadb
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="<DeepSeek 兼容端点,见官方文档>",
)

# 记忆另起一个库,不与 6.2 的制度条文库混在一起
chroma = chromadb.PersistentClient(path="./memories_chroma")
memories = chroma.get_or_create_collection(
    "memories", metadata={"hnsw:space": "cosine"})

def extract_memory(history: str) -> str:
    """从一段执行历史里抽取一条结构化记忆。"""
    resp = client.chat.completions.create(
        model="deepseek-v4-flash",     # 抽取走低价档就够,8.3 算过成本
        messages=[{
            "role": "user",
            "content": "从下面的处理记录里抽一条对以后有用的记忆,输出 JSON,"
                       "字段:summary(干了什么)、solution(怎么解的)、outcome(结果)。"
                       "寒暄与一次性细节不要。\n\n" + history,
        }],
    )
    return resp.choices[0].message.content

DEDUP_DIST = 0.2   # 示意值:距离小于它判为重复,按自己嵌入模型的分布实测调

def store_memory(memory: str) -> str:
    """先查近邻判重,重复的跳过,新的连同元数据入库。"""
    vec = embed([memory])[0]                 # embed 沿用 6.2 的定义,嵌入自己算好传进去
    near = memories.query(query_embeddings=[vec], n_results=1)
    if near["distances"][0] and near["distances"][0][0] < DEDUP_DIST:
        return "已有相近记忆,跳过"
    memories.add(ids=[str(uuid.uuid4())], documents=[memory],
                 embeddings=[vec], metadatas=[{"kind": "episode"}])
    return "已入库"

recall_tool = {
    "type": "function",
    "function": {
        "name": "recall_memory",
        "description": "按语义检索记忆库,处理新任务前先查以前有没有类似情况",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "要找的记忆,如“物流延误的赔付方案”"},
            },
            "required": ["query"],
        },
    },
}

抽取这一步,是把第 3 章的结构化输出换了个用途:输入执行历史,输出一条干净的记忆。判重时把新记忆当作查询去找近邻,距离足够近就说明已经记过了;阈值没有通用的数值,要用自己的嵌入模型实测分布后确定。取用时包装成 recall_memory 工具,新任务来了查不查、查什么,由模型自己决定。

抽取出来的记忆长什么样,也有两头可选。一头是自由文本,抽取出来是什么样就保存成什么样,写入最省事,查询时比较费力,同一位用户的偏好分散在好几条句子里,想拼出全貌要一条条翻。另一头是结构化卡片,把人的属性、关系拆成一个个字段,收货城市、通知偏好各占一格,消歧、更新都容易处理,代价是抽取时要多拆一步,字段也要持续维护。往哪一头靠,看记忆用来做什么:按语义翻查旧记录,自由文本配合向量检索就够了;要按人、按关系精确更新和查询,就值得往结构化这一头走。

抽取和去重这两步,每保存一条记忆都要多花一次模型调用的费用。这笔开销有多大,8.3 专门讲。

载体适合存什么怎么取优势短板
文件(workspace、记忆文件)中间产物、偏好档案、进度笔记模型用文件工具直接读写零设施、明文可审计、可进版本库规模大后靠目录设计,并发与权限自己管
数据库(关系型或文档型)会话历史、检查点、结构化档案按 ID 与字段查询多副本共享、事务与并发成熟语义检索弱,要运维
向量库记忆条目、知识切片语义相似检索条目多时按意思取建库与检索质量是工程活

三条路不是三选一。客服智能体上线时完全可以这样搭配:用户偏好和禁忌就十几条,文件式记忆足够,每个会话开始时读一遍;会话流水和检查点放进数据库,供服务多副本共享;历史工单的沉淀放进向量库,检索的对象是摘要和引用,工单原文二十万字放在对象存储里,库里每条记忆都带一个指回原文的键。每一层按访问方式选择成本最低的载体,这也是第 7 章的那条教训:原文始终在窗口之外,窗口里只放摘要和引用。

窗口小而快、外存大而慢、检索在两者之间搬运,这套分层做法在计算机里有一个老原型:操作系统的虚拟内存。物理内存小而贵,磁盘大而便宜,换页机制实时决定哪一页留在内存、哪一页移出去,程序因此能使用比物理内存大得多的空间。2023 年的 MemGPT 把这层对应关系明确了:上下文窗口当内存,外部存储当磁盘,检索当换页,看起来记忆没有上限,实际是检索在底层搬运数据。第 7 章的存储与呈现分离、这一章按访问方式选择载体,都是在为同一套思路补充工程细节。

8.3 写入、遗忘与压缩 ​

记忆系统的质量,在写入那一刻就已经决定了大半,检索做得再好也突破不了写入划定的上限。一个常见的反面场景:刚上线的客服智能体由工程师定下规则,每句对话都写进长期记忆,三个月后检索“这位用户的偏好”返回两百条,其中一百八十条是“今天天气不错”这类寒暄,十几条是已经过期的活动规则,还有几条带手机号的记录留在库里,成了隐患。写什么、由谁决定,有两条路线。

一条叫应用管理,由规则驱动,开发者写代码决定什么内容进入记忆。这是记忆管理最朴素的做法:应用代码调用记忆库的写入接口,把成功的产出、用户偏好、对话摘要存进去;记忆库再提供注入接口,把记忆拼进提示。智能体拿到这些上下文,但存什么、取什么,不由它决定。

放到客服场景里,规则可以写得很细:工单结算时,把用户明确表达的偏好抽取出来写进档案;纠纷解决后,把处理经过记入情景记忆;其余一律不存。这条路的好处是可控:存什么、何时存、用什么格式存,都可以测试、可以审计,合规审查一页纸就能看完。代价是死板:规则不可能穷举,用户随口说的长尾信息,只要规则没有覆盖,就会永远漏掉。

另一条叫智能体自管理,由模型驱动,做法是给模型配一个记忆工具,让它边工作边自己记录。Anthropic 的记忆工具就是这条路线的标准样例。工具一开启,模型开始工作时先查看记忆目录,工作中把值得保留的内容写进 /memories 下的文件,后续会话再读取出来继续使用。存储的工作全部由客户端应用完成,模型只负责发出请求。

还有一个细节:工具开启后,平台会自动往系统提示里追加一段协议,大意是开始工作时先查看记忆目录,边推进边把状态和想法记录进去,同时假定上下文随时可能重置,没有写进记忆目录的进度都可能丢失。正是这句“假定随时中断”,把记忆从可选项变成了必备项。

官方文档还配套了一组防护措施:记忆文件要设置容量上限,长期没有人访问的要定期删除,路径必须校验以防止目录穿越攻击,敏感信息通常应该拒绝写入,应用那边最好再过滤一遍。模型自管理的代价,从这组防护措施就能看出来:写入不可预测,可能记下寒暄,也可能记下敏感信息,权限和生命周期都要由应用自己承担。

最简实现里,同样的思路有两种接入方式:要么做成模型可以显式调用的工具,要么隐藏起来,只在每次模型调用前自动注入检索结果。前一种节省预算,后一种覆盖更全,怎么选,看预算和覆盖面哪个更紧。

选择自动注入这种方式时,注入这一侧还有几件工程工作。第一件是裁剪,检索回来的条目不整段照搬,每条先压缩成要点,与当前任务无关的段落删掉。第二件是排序和限额,条目按相关度从高到低排序,总量设上限,条数和 token 都封顶。检索命中不等于值得占用位置,勉强相关的记忆也放进窗口,挤占的是当前任务的预算,这个代价第 7 章算过。

第三件是注入的位置,通行做法是把记忆放在系统提示之后、当轮用户消息之前,作为一块格式固定的背景资料。位置和格式一旦确定就不要再改变,模型每一轮都在同一个位置读到记忆,查找的成本最低。还有一条界线不能越过:记忆不要混进对话历史里冒充某一轮说过的话,轮次和角色一混乱,事后排查对话就没有了依据。多用户的产品还有一条硬边界,每个用户的记忆各自独立,检索时按用户隔离,甲的档案不能出现在乙的回答里,这道隔离与第 6 章检索阶段的权限过滤是同一套思路。

注入这一侧处理的是取用,写入那一头还有一道判断:“这段内容值不值得记”由谁来判断,同样不必在全部用规则和全部用模型之间二选一。中间还有几个档位的判断机制,越往上越准确,也越昂贵。下面这张表是工程实践的量级参考,数字只用来看档位之间的差距,不要当作精确基准。

判定机制额外调用精确率量级成本
正则与规则0约 70%免费
嵌入相似度0,但占推理算力约 85%低
仅编码器小分类器0,但占推理算力约 90%低到中
完整 LLM 裁决器每步 1 次约 95%账单翻倍

正则这一档扫描的是命名实体、显式的用户偏好、结构化输出这类固定模式,没有额外调用、没有额外延迟,存了什么一清二楚,代价是在边缘情况上比较脆弱。最高一档让 LLM 逐条裁决,最准确也最昂贵,每一步多一次模型调用,费用直接翻倍。把“该记什么”全部交给 LLM,成本会直接上去,这与其他环节滥用模型调用是同一个问题。分层的做法往往能用几分之一的成本获得大部分价值:结构化数据交给正则,检索相关性交给嵌入相似度,轻量的记忆决策交给 BERT 量级的仅编码器小分类器,只有含糊而且风险高的场景才让 LLM 裁决。

嵌入相似度这一档,拿新条目与库里已有的条目计算距离,高度相似说明库里已经有了,不必重复记录,它能判断新不新,判断不了值不值。小分类器这一档,用历史上人工标注的“记了、没记”数据训练一个二分类器,给新条目打“值得记”的分数。实际动手时从两端开始:先把正则跑起来,LLM 裁决只留给含糊而且风险高的那一小部分,中间两档是数据量大了之后才值得增加的层。

值得永久保留的比例,本来就很小。记忆库 mem0 的生命周期模型就有这样的假设:交互信息里只有大约一成值得保留,其余的在它两段式的流程里当作冗余或低价值内容丢掉。这个数字作为参考口径看,不要当作常数。做法上的方向只有一条:写入要从严。多存的条目占用检索位置,却带不来任何价值。

写入环节还有一道绕不开的问题:重复与冲突。用户上个月说常用收货城市是杭州,这个月说搬去上海了,这第二条该新增进去,还是把第一条改掉?记忆库 mem0 与 graphiti 用四个操作处理这类冲突:ADD 新增、UPDATE 更新、DELETE 删除、SKIP 跳过。

四个操作不必都用。任务解决记录这类不可变的信息,ADD 和 SKIP 就够了:同一个问题的不同解法都值得各留一条,只有完全相同的尝试才跳过;失败的记录尤其要保存,留着才不会再犯同样的错。用户档案类正好相反:偏好会变、手机号会换,UPDATE 和 DELETE 就成了必需。

把判断交给模型时,模板如下。

text
你是客服智能体的记忆管理员。对照现有记忆逐条检查新对话,决定长期记忆该怎么变。
每个变更一条,动作用 ADD、UPDATE、DELETE、SKIP 之一。ADD 与 UPDATE 给出
新内容,DELETE 给出理由。只处理对以后的服务有用的信息,寒暄与一次性的
工单细节一律 SKIP,身份证号、手机号、住址一律 SKIP。

### 示例
[现有记忆]
M1:用户不接受电话回访,偏好短信通知
M2:常用收货城市:杭州
[新对话]
用户:最近搬到上海了,以后有事发短信就行,别打电话。

示例输出
UPDATE M1:用户不接受电话回访,偏好短信通知(本次确认仍有效)
UPDATE M2:常用收货城市:上海
SKIP:其余内容为寒暄与一次性信息

### 输入数据
[现有记忆]
{existing}
[新对话]
{conversation}

一次失败最好记两条:一条是修法,把这次到底怎么解决的写清楚,比如安装 Python 的 JWT 库必须装 pyjwt,那个同名的 jwt 包是完全不同的东西;另一条是启发式,把以后怎么预防写下来,安装任何认证库前先检查包名有没有重名问题。修法只适用于当前这一次,启发式却能覆盖这一类。

只记修法的失败日志,用起来顶多算一张查询表:原样的问题再次出现,查表可以处理。问题换个样子再来呢?还是包名冲突,但这回发生在另一个库上,表里查不到这一行,同样的问题照样会再出现。真正能迁移的是启发式这一层。这个判断有现成的学术支持:让智能体把失败转成文字反思、带着反思重试,或者从一批失败里提炼跨任务规则,两者都已经做成了完整的机制,出处见本章参考资料。

实现层面要遵守两条纪律。第一条,动手之前先查:调用工具、下结论之前,先用当前情境检索一遍相似的失败,命中就把教训读进来。第二条,日志只增不改:旧记录即使过时也保留在那里,由 8.2 的衰减轴定期清理。失败日志是记忆库里回报最高的一类:8.2 讲过,教训类保持时间最长,它记的正是教训。

从经验里学习,能记录的不止失败。记忆增强学习(memory-augmented learning)的做法,是把每段经历固化成三元组:当时的查询、实际执行的行动计划、最终得到的结果。之后遇到相似的查询,先按查询检索旧案例,成功案例的路线作为参考,失败案例用来避开。它与微调的区别是:微调修改权重,全局生效,需要重新训练;这条路在运行时增量积累案例,小数据也能学习。客服场景里最顺手的应用是工单:查询是纠纷描述,方案是赔付路线,结果是用户是否接受,积累一个季度,新纠纷来了先查旧案例。

这条路有三个局限:结果常常不明确,用户不回复、任务结束没有评价,正负样本都无法确定;行动计划会过期,环境和工具一变,同样的查询要重新生成路线,旧案例只能参考,不能照搬;照案例回放还容易过拟合,把个别情况当成通例。至于怎么从积累的案例里总结规律,还有一个对照的办法:把成功和失败两批放在一起比较,系统性的差距才看得出来,哪些假设错了、缺了哪一步、哪种做法导致失败;只看失败那一批,多半只能看到表面症状。有研究把这套做法设计成两个角色的配合,一个专门比较两批案例、产出整体性的改进指令,另一个带着指令执行任务,整体性的指令比逐条改写更能防止过拟合。落到客服场景:上个月七十单一次解决、三十单升级,两批一比,升级那批的共同点如果都是没有先核对物流时效,这一条就值得写进程序性记忆。

从经验里学习还有最高的一档:把一次成功的探索固化成一个带参数的小流程。输入输出定义清楚,前置检查和后置检查写全,再独立回放验证一遍,这个流程就脱离了当初那次任务,成为同类任务直接执行的资产;哪天它应付不了新任务,就退回智能体模式重新探索一遍,重新探索得到的解法固化成下一版。8.2 讲程序性记忆时留过一句话,这类记忆的特殊待遇是能固化成工具和动作,这一档正是那句话的延伸:记忆条目保存的是知道什么,流程保存的是会做什么。这条路有实证依据,Voyager 让智能体把学会的本领写成可执行代码、存入不断增长的技能库,新任务先查旧技能再决定要不要自己摸索,整个库还能迁移到新环境继续使用,出处见本章参考资料;第 21 章解剖编码智能体时还会回到这套做法,那边的技能库就是它的工程版本。

记忆库如果不设遗忘,迟早会退化成噪声库,检索质量随条目数持续下降。遗忘分三层。最外层管过期:长期未访问的记忆定期删除,这是 Anthropic 官方文档给记忆工具用户写明的建议。客服场景还可以加入业务规则,比如活动规则这类记忆,活动一结束就强制失效。

中间层管容量:满了按先进先出驱逐,比如用 max_memories 给记忆条数设上限,超出的淘汰最旧的。最里层管语义过期:信息还没到保质期,内容却已经失真了,用户换了手机号、公司改了流程,这类失真靠本节前面那套判重模板处理,写入时顺便把旧的清掉。

三层之外还有一条不能让步的底线:隐私。客服对话里的身份证号、住址和支付信息,根本不应该进入长期记忆。不能指望模型一定拒绝写入,真正动手拦截的,是应用这一侧的校验和剥离。第 13 章讲安全时会回到这里。

语义过期能处理失真的旧信息,但有一层它处理不了:一件事什么时候是真的,和系统什么时候知道它是真的,是两个时间点,不能混为一谈。

用户在一月已经更换了号码,系统档案却拖到三月的某次更新才修改,二月那次会话里智能体手里还是旧号码。事后排查那场会话,光看当前档案,回答不了“当时为什么这样回答”。要回答这个问题,必须知道二月时系统里实际保存的是什么。

把手机号这个例子放到两条时间轴上看:业务世界里,旧号和新号各自管一段时间;系统这边的记录,到三月才更新。

图 8-1 双时态的两条时间轴:业务世界的有效窗口与系统这边的记录的摄取时刻

图谱记忆系统处理这类问题,用的是双时态(bitemporal)记法。每条记忆带两个时间轴:一条记录业务世界里这条信息从什么时候开始为真、到什么时候为止,字段名惯用 valid_from 与 valid_until;另一条记录系统什么时候把这条信息收进来、什么时候更新,字段名惯用 ingested_at。旧值不删除,只是把有效窗口关闭,“当时系统知道什么”这类审计问题就有了依据(Zep 和它的开源内核 graphiti 是这种做法的生产代表,上文提到的 mem0 也推出了图谱变体)。

简单的库用不着照搬全套设施,但至少不要让更新直接覆盖旧值:改成流水式追加,再配一个当前指针;或者退一步,只保存一份修改时间。代价很小,出了问题复盘时就有依据可查。

把这两条建议并成一段最小代码,得到一条带生效窗口的记忆记录,语义过期关窗、到期清理与容量驱逐全在同一份流水上。

python
"""示意代码:记忆记录的最小实现,python3 可直接运行。"""

from dataclasses import dataclass

@dataclass
class Entry:
    """一条记忆带两个时间轴:业务上何时为真,系统何时知道。"""
    key: str
    value: str
    valid_from: str           # 业务世界里从何时开始为真
    valid_until: str | None   # None 表示窗口仍开着
    ingested_at: str          # 系统摄取这条记录的时刻

LEDGER = [Entry("常用收货城市", "杭州", "2026-01", None, "2026-01"),
          Entry("优惠活动", "春季满减", "2026-03", "2026-04", "2026-03")]

def update(key: str, value: str, now: str) -> None:
    """语义过期走这里:旧条目只关窗口不删除,新条目追加进流水。"""
    for e in LEDGER:
        if e.key == key and e.valid_until is None:
            e.valid_until = now
    LEDGER.append(Entry(key, value, now, None, now))

def current(key: str, now: str) -> str | None:
    """当前指针:按业务有效窗口取当下为真的那一条。"""
    for e in reversed(LEDGER):
        if e.key == key and e.valid_from <= now and (
                e.valid_until is None or now < e.valid_until):
            return e.value
    return None

def purge(now: str, cap: int = 3) -> int:
    """另两层遗忘:到期条目整条清出,超额按先进先出驱逐最旧的。"""
    alive = [e for e in LEDGER if e.valid_until is None or now < e.valid_until]
    evicted = len(LEDGER) - len(alive)          # 过期那层:到期整条失效
    while len(alive) > cap:
        alive.pop(0)                            # 容量那层:先进先出
        evicted += 1
    LEDGER[:] = alive
    return evicted

update("常用收货城市", "上海", "2026-09")    # 用户搬家,杭州那条只关窗
print(current("常用收货城市", "2026-09"))
print(f"清理掉 {purge('2026-05')} 条")       # 春季活动到期失效

用户搬家那一笔只把旧条目的窗口关掉,按 ingested_at 回看,当时系统知道什么仍还原得出来;遗忘的另外两层在 purge 里,活动到期整条清出,超额按先进先出驱逐最旧的。

最后一项是压缩,压缩的对象是记忆库本身,与第 7 章压缩上下文是两回事,但动机一致:冗余和重复正在稀释有效信号。做法分两步:先对记忆做向量聚类,相近的条目归入同一簇;再让模型按簇生成摘要,一簇缩成一条新记忆。

什么时候压缩?经验是:检查存储时发现重复和冗余,就该压缩了,簇特别大或者大小失衡,更是明显的信号。节奏上,记忆库适合周期性压缩,知识库通常首次装载时压缩一次就够。同一批内容还可以压缩出多个层级:一个知识库压缩两次,得到原文、一级摘要和二级摘要。

客服库里有三十条“退款进度查询”记录,聚类后缩成一条:“该用户在三月密集咨询退款进度,已全部结清”。检索时用这一条,代替原来的三十条。

更主动一些的合并,是把多次观察巩固成一条事实。与其分别保存“周一你说截止日期是周五、周二你确认了周五、周三又提了一遍”,不如合成一句“项目截止日期:周五,多次确认”。

巩固选在什么时候执行,有专门的研究给出过答案。Letta 与加州大学伯克利分校 2025 年的一篇论文提出睡眠期计算(sleep-time compute):让模型在空闲时段先对上下文做一轮推演,等真正来问时,要现场干的活就轻了,主动推理的算力开销因此降下来。一份上下文如果后面有很多次查询要用,空闲时段预先算好的那部分成果,后面每一次交互都能直接使用,查询越多,节省的算力就越多。这一节末尾要讲的低峰期批处理,用的就是这条原则。

算力该放在哪一边,还有一条原则:宁可花在入库时,不要花在查询时。图谱记忆系统 Zep 把这条原则贯彻成了体系:入库环节就把优化结构建好,记忆先聚成语义簇,簇再聚成更高一层的超级簇。入库时把结构算好、保存成现成的,查询这一头就保持简单:检索时不调用模型,为的是用户规模涨上去之后,检索耗时依然可预期。落实到自己的系统里,就是宁可写入慢一点、贵一点,也不要让每一次检索都跟着变慢。

另一种更省的做法,是入库前先压缩:只保存抽取后的结构化摘要,原文不进库。抽取加判重,每条记忆要花两次辅助模型调用。这类工作用不着旗舰模型,DeepSeek V4-Flash 或 qwen-flash 这一档的低价模型就够用。

应用管理和智能体自管理,真要二选一吗?通常不用,两边都要。生产系统里常见的分工是:核心档案和合规敏感信息走应用管理,靠规则写入、可审计、可删除;长尾经验和项目笔记交给智能体自管理,让记忆工具去补充规则覆盖不到的角落;载体按 8.2 的选型确定,抽取、压缩这类活不赶时间,放在低峰期批量跑就行;材料大到窗口装不下、任务又要当场使用时,记忆系统处理不了,解法换成隔离,见 8.4。

写入、遗忘、压缩这三套机制是否真正生效,要靠评测来验证,而且要一层比一层验得深。最浅的一层不算数:单轮问答答对了说明不了什么,那一轮的窗口里本来就有全部历史。再进一层验证跨会话:隔几天新开一个会话,专门检验上一段会话里应该记住的内容,用户交代过的偏好、工单的处理结论,能检索得回来,记忆才算真正落了库;这一层顺带要验反方向的一半,过期的信息有没有更新、有没有忘记,新旧混在一起,回答出来照样是错的。最深的一层测的不是问它才答得出,而是不问也用得上:用户提过一次不要电话回访,下次处理工单就直接使用短信,不用再说第二遍。评测集按这个阶梯搭建,先积累成对的跨会话对话,前一段埋入偏好和事实,后一段隔开几轮再检验,既测检索,也测主动调用。方法与第 12 章的评测方法论属于同一体系,落到记忆上多出两道关口,一道跨会话,一道不提示。

8.4 子智能体 ​

把隔离策略做到底的形态就是子智能体(subagent):由主智能体派生出来、在独立上下文里完成专项工作的智能体,主上下文只拿到一份结构化摘要。它服务的是大体量材料。工单 114 升级成纠纷,用户一口咬定发货延迟,商家坚持按时交了货,要判定责任,得通读两年的交易聊天记录,整整二十万字;这段材料直接读进主上下文,就是第 7 章开篇那场越干越乱的事故重演,一个文件占掉窗口的大半,后面每一轮推理都要带着它。第 9 章会继续使用这个概念,称它是多智能体的最简版本,只做隔离、不做协作,任务要不要升级到多智能体,那边的四级阶梯会给出答案。

跨过隔离边界的内容只有两份,一进一出。进去的是一份任务书和材料位置,出来的是一份 Finding 契约,包含结论、证据指针和置信度三项。子智能体内部读了多少万字、经过多少轮推理,主智能体一概不管。

图 8-2 子智能体隔离:独立上下文、专项工具集、单向交接,边界上只过任务书与 Finding

这套机制由三件事组成。独立上下文是第一件:子智能体使用一份全新的干净上下文,主对话里的噪声全都进不来,能进入的只有一份任务书。专项工具集是第二件:判责的子智能体只带读取记录的工具,不带修改订单的、也不带发送消息的,工具越少判断越稳定,这正是第 4 章工具设计原则在架构层面的应用。单向交接是第三件:子智能体完成工作后,只向主上下文返回一份摘要,中间过程都留在子窗口里。

工具清单里还有一条要严格限定:派生工具不进入子智能体的工具箱。只有主智能体能创建子智能体,子智能体不允许再创建自己的子智能体;如果放开,一条失控的派生链会把成本、时长和复杂度一起拖垮。权限管住了它能做什么,这一条再管住它还能不能继续派生,两道限制都设置好,隔离的边界就闭合了。

Anthropic 的工程文章给出过摘要长度的量级:深入调研型的子智能体返回给主智能体时,通常是一千到两千 token 的浓缩摘要。这中间的成本一目了然:二十万字进去,两千 token 出来,全部在这一进一出之间。

到底能节省多少,把第 7 章那张对比表里“隔离”那一行单独拿出来看,最清楚。同一个代码评审任务(44 个文件的代码库)用五种配置各运行一遍:不做管理的基线消耗 915,591 个输入 token;几档压实在 14 万到 40 万之间;隔离配置把文件通读全部交给子智能体,主智能体加子智能体合计 118,824,重复读取率 0%,膨胀根本没在主窗口出现。注意这只是单一实验,看趋势就好,不要当作普适常数。

代价也要看全。隔离配置质量分 4.5,比不压实的 6.0 还低;协调和摘要都要付出成本,值不值得,第 9 章的升级阶梯会展开。这一节只讲隔离本身怎么做。

做得对不对,关键看摘要的契约。摘要不可逆,第 7 章讲过:原文一旦换成摘要,就再也找不回来。因此子智能体返回的不能只是一句结论,而应该是一份结构化的交接物:结论、证据指针和置信度。

证据指针是一个关键设计。主上下文里放的不是证据本身,而是“证据存放在哪里”的引用;真要复核时,按指针去子智能体自己的存储里取。写入和检索这一对策略,在隔离架构里又原样用了一遍。契约确定之后,还有两条细节:一条是,上下文干净,不是说里面什么都不放,任务书要给足,判定标准、输出格式、边界情况怎么处理都要写明;另一条是,输出尽量使用第 3 章的结构化输出,主智能体这边的代码直接解析,不要靠正则从散文里提取字段。

这份契约还值得再增加一个字段:未验证项。排障类的工作里它最有价值,子智能体返回的不只是查到了什么,还有哪些地方根本没有查:路由表没有直接查看,只验证了连通性,没有检查配置。要求模型把没做的事写出来,是对付过度自信的一道硬性防线,置信度是模型自己给出的估计,未验证项是它必须交代的内容,两者合在一起,主智能体才知道这份结论该相信几分、该复核哪些地方。再配一条规则:不健康的信号不许隐藏,工具结果里有报错、有中断,结论里就必须提到。

派活还可以并行进行。客服纠纷需要同时查交易记录、物流轨迹、历史工单三个来源,三个子智能体各读各的,互不污染,主智能体收到三份摘要后合并裁决。摘要彼此矛盾时,由主智能体决定,这正是摘要契约里保留置信度字段的原因:低置信度的证据优先复核。

隔离也不是唯一的解法。后一个子智能体的工作依赖前一个成果时,只传摘要会丢掉“为什么”。拿编程智能体做网站落地页来说:子智能体 A 调研了品牌色,得到用户对红色的批准,做了一个红色按钮;子智能体 B 接手做其余页面元素。A 只给 B 传递一句“我做了一个红色按钮”,B 不知道这种红色从哪里来、用户批准过没有,往下用色就缺少依据。反过来,B 如果能看到 A 的完整轨迹,用户请求、品牌色调研、用户的批准全在里面,它就明白其他重点元素也可以使用同一种红色。

共享上下文换来一致性,隔离换来的是上下文上的节省,两种取向都有产品采用。Claude Code 使用子智能体但不共享上下文,站在彻底隔离这一头。落点还是摘要契约:决策依据写进结构化摘要,这种红色从哪来、用户批准过哪些颜色,一行字交代清楚,整段轨迹就不必共享。

图 8-3 记忆拓扑的三种组织方式:各记各的、全共享、两层混合配治理

把这个选择放大到整个系统,就是记忆拓扑的问题,组织方式有三种。各智能体各记各的,窗口干净、互不干扰,代价是各自会形成不一致的内部表示,同一套事实每个智能体都要存一份;全体共用一份,单一事实来源、推理保持一致,但写入一多就会互相干扰,噪声上升、信号下降,并发读写也要额外小心。生产系统多半采用两层混合:推理和中间状态留在本地,系统级事实和需要协调的产物放在共享层。共享层要配套治理:条目带来源标记、带写入时间以便判断新旧,再指定一个角色定期修剪,否则共享库比本地库更容易变成噪声库。还有一个判断藏在派活的那一刻:子智能体跨调用记不记得,决定它是工具还是专家,什么都不记,每次都是全新的调用,它就是一件工具;下次接着上次记的继续,它就是一个常驻的专家。第 10 章讲编排时的黑板与共享表,正是共享这一端的具体形态。

共享层的读写与治理写成代码,就是一份带来源和时间戳的共享事实表。

python
"""示意代码:记忆拓扑共享层的最小实现,python3 可直接运行。"""

from dataclasses import dataclass

@dataclass
class SharedFact:
    """共享层条目:带来源与写入时间,读的人据此判断新旧与可信度。"""
    key: str
    value: str
    source: str       # 写入方,治理时找得到人
    written_at: int   # 写入时间,判断新旧

SHARED: dict[str, SharedFact] = {}   # 系统级事实放这里,推理留在各智能体本地

def write_shared(key: str, value: str, source: str, tick: int) -> None:
    """写入一律带来源与时间,同键新值顶掉旧值,谁写的查得到。"""
    SHARED[key] = SharedFact(key, value, source, tick)

def read_shared(key: str) -> str | None:
    """任何角色读到的都是同一份,单一事实来源。"""
    fact = SHARED.get(key)
    return fact.value if fact else None

def prune(tick: int, ttl: int = 10) -> list[str]:
    """治理角色定期修剪:过期条目清出共享层,免得长成噪声库。"""
    stale = [k for k, f in SHARED.items() if tick - f.written_at > ttl]
    for k in stale:
        del SHARED[k]
    return stale

if __name__ == "__main__":
    write_shared("用户偏好-回访方式", "仅短信", "客服智能体", tick=1)
    write_shared("纠纷结论-工单114", "责任在承运方", "判责子智能体", tick=2)
    print(read_shared("用户偏好-回访方式"))
    print(f"修剪 {len(prune(tick=20))} 条过期条目")

条目自带来源与写入时间,读的一侧判断新旧与可信度就有了依据;prune 那一步是治理角色的定期活,防的正是拓扑那一段说过的毛病,共享库比本地库更容易积噪声。

隔离拦下了中间过程,结果本身还是要流回主上下文,这里藏着最典型的失败:一次并行派出十个探索不同模块的子智能体,每个返回一份详细报告,十份报告占满主上下文,留给主智能体推理的空间反而比不派出时更紧。缓解办法有两条。一条是让子智能体只返回简明摘要,不要交面面俱到的报告,比如改动文件清单加每处一句话描述就够了。另一条是让子智能体把详细发现写进文件,只返回文件路径,主智能体需要哪一块就读哪一块。设计准则:跨边界的双向数据流里,出境的任务指派通常很小,入境的结果才是风险所在,入境的数据也要保持小。

失败的代价,也随着隔离一起变小了。判责子智能体读完十万字后判断跑偏,或者某个来源的记录格式混乱到读不下去,要重来只需再开一份子窗口,主上下文一个 token 都不受影响。不隔离就是另一种情况:跑偏的推理已经写进消息历史,污染会影响后续每一轮,要么把整段压实重写,要么新开会话从零开始。

安全上也得了同样的好处:材料里埋的注入指令,最多污染子窗口,主智能体能接触到的只有一份结构化摘要,攻击面本来就很小。第 13 章讲提示注入时,会再回到这里。

摘要本身也要设一个上限,比如硬性限制在两千 token 以内。如果写不下,多半说明任务还应该再拆分,而不是把上限放宽。

写成代码,骨架比想象的短得多。这段是示意代码,模型调用先用桩代替,真实实现把桩换掉,接上任意厂商的模型调用就行。

python
"""示意代码:子智能体的最小隔离骨架,python3 可直接运行。"""

from dataclasses import dataclass

@dataclass
class Finding:
    """子智能体交回主上下文的结构化摘要,这是主侧唯一拿到的东西。"""

    conclusion: str     # 结论,一句话
    evidence_ref: str   # 证据指针,指向子侧存储,不把原文带回
    confidence: str     # 置信度,high / medium / low

def run_subagent(task: str, material_ref: str, tools: list[str]) -> Finding:
    """在独立上下文里干专项活。窗口内的一切用完即弃,只留返回值。"""

    context = [f"[系统] {task}",              # 干净上下文,只有任务书
               f"[用户] 材料位置:{material_ref}"]
    # 子智能体在这里用 tools 逐段读材料、推理、写笔记,
    # 中间过程全部只存在于本函数的局部 context 里,用完即弃。
    context.append(f"[助手] 已用 {tools} 读完全部材料")
    return Finding(
        conclusion="责任在承运方,揽收记录晚于承诺发货日两天",
        evidence_ref="notes://ticket-114/证据第3段",
        confidence="high",
    )

def main_agent() -> None:
    """主上下文,派活、收摘要,从头到尾不接触二十万字原文。"""

    history = ["[用户] 工单 114 升级为纠纷,判定责任方"]
    summary = run_subagent(
        "通读全部交易聊天记录,判定发货延迟的责任在哪一方,"
        "按 Finding 结构返回结论、证据位置与置信度",
        "records://ticket-114/全部聊天记录",
        ["read_records"],
    )
    history.append(
        f"[工具结果] 子智能体摘要:{summary.conclusion};"
        f"证据见 {summary.evidence_ref};置信 {summary.confidence}"
    )
    history.append("[助手] 已判定责任方,开始起草赔付方案")
    for line in history:
        print(line)

if __name__ == "__main__":
    main_agent()

主智能体的 history 到结束时只有三条,二十万字材料一行都没有进来。真正称得上子智能体全部上下文的,是 run_subagent 内部那个局部 context,函数一返回就丢弃,下次再派同样的工作,又是一份干净的上下文。真实系统里再叠加 8.1 的会话和 8.2 的存储:子智能体的笔记和证据保存到自己的 workspace,指针稳定可复核,整套骨架就完整了。

厂商这边同样有现成的原语:Claude Agent SDK 把 subagent 能力直接打包好,内置工具和智能体回路都在里面。什么时候派谁、并行怎么汇合、失败怎么重试,这类编排问题留给第 10 章。

材料只有几百 token 时,直接读进主上下文比派出子智能体更便宜,也更稳妥。隔离是为大体量、容易互相污染的材料准备的,不是什么活都要走这套流程。判断标准回到第 7 章:这段材料进入窗口的注意力代价,与开一份新上下文的协调代价相比,看哪个更大。两头的例子都很明确:工单纠纷那二十万字,明显该隔离;改地址前的三行订单摘要,明显不该。中间地带没有通用的阈值,把两类代价都折算成 token 估算一下,再用第 12 章的评测方法验证一遍,答案自然就出来了。

本章小结 ​

  • 8.1 短期记忆:会话管连续性,草稿区(Session 的 state 字段)管任务状态,检查点管断点续跑。恢复执行需要三类信息(已完成步骤、各步产出和共享状态);恢复前比对结构哈希,对不上就拒绝恢复;检查点存放按场景分层,测试放内存、单机用文件、分布式用数据库或对象存储。
  • 8.2 长期记忆:分类先于载体,情景、程序和语义三类,工程上另列工具与接口记忆;公共层知识不囤,记忆预算全给私有层;载体按访问方式选,偏好档案放文件、流水和检查点放数据库、按语义翻查几万条沉淀放向量库,库里只存摘要和引用,原文放对象存储。
  • 8.3 写入、遗忘与压缩:核心档案走应用管理(规则写入、可审计),长尾经验交给智能体自管理的记忆工具。判定机制按成本分四档,正则约 70% 且免费,LLM 裁决器约 95% 但每步多一次调用,从严写入。冲突用 ADD、UPDATE、DELETE、SKIP 四操作管理;失败记两条,修法加可迁移的启发式;遗忘分三层(过期、容量、语义过期),隐私由应用侧校验剥离;压缩用聚类加摘要,算力宁可花在摄取时,不花在查询时。
  • 8.4 子智能体:跨边界只过两份,进的是任务书和材料位置,出的是 Finding 契约(结论、证据指针和置信度,可加未验证项)。摘要硬性上限约两千 token;并行派活摘要矛盾时由主智能体按置信度裁决;材料只有几百 token 时直接读进主上下文更便宜,隔离只给大体量、易污染的材料用。
  • 运行得好不好看四个数:检索命中率(取回的记忆有多少影响了下一步判断)、重复踩坑率(同一错误第二次发生且中途未翻失败日志的频率)、新鲜度(取回条目平均多旧、写入时的环境是否还成立)、巩固率(会话结束进入长期存储的比例,太低没在学习,太高是在囤积)。四个数都从工具调用日志和记忆库查询里统计。
  • 状态的三层去处:窗口里的会话和草稿区,窗口外的文件和库,另一份窗口里的子智能体。窗口是最贵的一层,能移到外面的都移出去,只留当前这一步真正用得上的部分。

参考资料 ​

  • Nogueira、Cho《Document Expansion by Query Prediction》(doc2query,用问题预测扩充文档,arXiv 1904.08375)https://arxiv.org/abs/1904.08375 ;Mem0 记忆写入管线(生成记忆与关联问题同入库)见 https://docs.mem0.ai ,核实于 2026-10-07

  • Anthropic 官方文档《Memory tool》(/memories 目录、六种文件操作、客户端执行存储、自动注入的记忆协议、容量与过期及路径穿越护栏),https://platform.claude.com/docs/en/agents-and-tools/tool-use/memory-tool ,核实于 2026-08。

  • Anthropic《Effective context engineering for AI agents》(子智能体干净上下文与一两千 token 摘要回传),https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents ,核实于 2026-08。

  • OpenAI Agents SDK 官方文档《Sessions》(自动会话记忆、运行前取历史运行后存增量、SQLite/Redis/MongoDB/SQLAlchemy 等存储实现),https://openai.github.io/openai-agents-python/sessions/ ,核实于 2026-08。

  • Claude Agent SDK 官方文档(打包智能体回路、内置工具与 subagent),https://code.claude.com/docs/en/agent-sdk/overview ,核实于 2026-08。

  • Younghee Song, Jungjun Hur《Build an AI Agent (From Scratch)》(Manning MEAP V3,2026)第 2 章(无状态 API 与对话历史)、第 5 章(agent workspace 与重置)、第 6 章(三挑战、Session 与 SessionManager、暂停恢复、长期记忆流水线、mem0 四操作转述、MemoryTool)。见 txt-ref-zh/build-ai-agent-from-scratch/。

  • 《构建 Agentic AI 系统》第 7 章(短期、长期、情景三分类记忆)。见 txt-ref/构建AgenticAI系统/。

  • Micheal Lanham《AI Agents in Action》(Manning,2025)第 8 章(知识与记忆之分、语义/情景/程序性记忆、k-means 聚类压缩与压缩时机)。见 txt-ref-zh/ai-agents-in-action/。

  • Victor Dibia《Designing Multi-Agent Systems》(2025)第 4 章(短期与长期记忆实现、应用管理的记忆接口、ListMemory 容量驱逐)、第 6 章(工作流检查点与结构哈希、检查点存储后端)、第 15 章(五种配置对比实验、工作目录隔离)。见 txt-ref-zh/designing-multi-agent-systems/。

  • 《Agentic GraphRAG》(O'Reilly,2026)第 4 章(双时态记法 valid_from/valid_until/ingested_at、Zep 与 graphiti 的时态知识图谱、mem0 图谱变体、睡眠期计算与巩固转述、摄取时预计算与检索不调 LLM 的算力分配、mem0 约一成信息配永久保留的口径)。译文见 txt-ref-zh/agentic-graphrag/。

  • Kay Lin 等《Sleep-time Compute: Beyond Inference Scaling at Test-time》(Letta 与加州大学伯克利分校,2025;空闲时段对上下文预做推演,压低响应路径上的主动推理开销),https://arxiv.org/abs/2504.13171 ,核实于 2026-08-31。

  • 《Patterns for Building AI Agents》(Sam Bhagwat、Michelle Gienow,O'Reilly 2025)第 6 章(子智能体之间共享上下文的红按钮例、Claude Code 用子智能体但不共享上下文的口径)。译文见 txt-ref-zh/patterns-for-building-ai-agents/。

  • 《Claude Code: The Definitive Guide to Agentic Development》(独立出版,2026)第 10 章(子智能体上下文回传溢出、详细发现写文件只回传路径、按小入境数据设计)。译文见 txt-ref-zh/claude-code-definitive/。

  • Nicole Koenigstein《AI Agents: The Definitive Guide》(O'Reilly,2026)第 10 章(表 10-5 记忆写入判定机制四档:正则、嵌入相似度、仅编码器分类器、LLM 裁决器的精确率与成本量级,源书自标高层量级)。译文见 txt-ref-zh/ai-agents-definitive-guide/。

  • Jia Huang《Designing AI Agents》(Manning,MEAP)第 4 章(公共/私有知识分层:公共技术知识别囤、记忆预算全给私有层;记忆价值衰减曲线:修复类快、教训与偏好类久、投入跟着衰减最慢的那类走;失败日志两层学习:修法之外再记可迁移启发式、只记修法是查询表不是学习系统),见 txt/designing-ai-agents.txt(英文原文,第 4 章,行 4866、5611-5760、5854-5858)

  • Dhivya Nagasubramanian《Agentic AI for Engineers》(Apress/Springer 2026)第 4 章(记忆类型表中的工具与接口记忆:企业系统各自的怪癖——提交采购单后须手动刷新状态才更新,把怎么跟这个系统打交道攒成经验),见 txt/agentic-ai-for-engineers.txt(英文原文,第 4 章,行 5024-5041)

  • Shinn N. 等:Reflexion: Language Agents with Verbal Reinforcement Learning(NeurIPS 2023,arXiv:2303.11366,把失败转写成文字反思、带着反思重试的言语强化学习)https://arxiv.org/abs/2303.11366 核实于 2026-09-01

  • Zhao A. 等:ExpeL: LLM Agents Are Experiential Learners(AAAI 2024,arXiv:2308.10144,从一批任务经验里提炼跨任务规则与洞见的经验学习智能体)https://arxiv.org/abs/2308.10144 核实于 2026-09-01

  • Wang G. 等:Voyager: An Open-Ended Embodied Agent with Large Language Models(arXiv:2305.16291,2023 年 5 月首发;技能库:可执行代码写成的技能经环境反馈与自我验证迭代而成、按需检索复用、整库迁移到新环境解新任务)https://arxiv.org/abs/2305.16291 核实于 2026-09-16

  • Nicole Koenigstein《AI Agents: The Definitive Guide》(O'Reilly,2026)第 10 章(记忆拓扑:本地记忆防噪声但会漂移、全局共享保一致但易吵、生产系统混合两层;子智能体跨调用记不记得决定它是工具还是常驻专家;记忆命名空间按用户与组织分键、身份须来自服务端验证),见 txt-ref-zh/ai-agents-definitive-guide/。

  • 《Agentic Architectural Patterns for Building Multi-Agent Systems》(Dr. Ali Arsanjani、Juan Pablo Bustos,2026,Packt)第 5 章(共享记忆治理:条目带来源与时间戳、治理角色定期修剪、黑板卫生与清理策略),见 txt-ref-zh/agentic-architectural-patterns/。

  • Jia Huang《Designing AI Agents》(Manning,MEAP)第 4 章(MemGPT 的虚拟内存式分层:窗口当内存、外存当磁盘、检索当换页;记忆质量四信号:检索命中率、重复踩坑率、条目新鲜度、巩固率),见 txt/designing-ai-agents.txt(英文原文,第 4 章,行 4655、4866-4878)。

  • Charles Packer 等《MemGPT: Towards LLMs as Operating Systems》(加州大学伯克利分校,2023;虚拟上下文管理,在小窗口与外部存储之间换页),https://arxiv.org/abs/2310.08560 ,核实于 2026-09-16。

  • Vasyl Zvarydchuk《Building Agent-Powered Applications》(Packt,2026)第 9 章(记忆增强学习:查询、行动计划、结果三元组入长期记忆、运行时案例式复用;反馈不清、计划漂移、照抄过拟合三局限),见 txt/building-agent-powered-apps.txt(英文原文)。

  • Shirley (Yingxin) Wu《Collaborative AI Agents》(斯坦福大学博士论文,2026)第 4 章(经验学习对照法:成功与失败两批对照定位系统性差距,比较器与执行器协作把对照洞察转成改进指令),见 txt/collaborative-agents-diss.txt(英文原文)。

  • Heiko Hotz、Sokratis Kartakis《Multimodal, Real-Time AI Agent Systems》(O'Reilly,抢先版 2026)第 2 章(无状态模型每轮重放完整历史模拟记忆、依赖大上下文窗口)、第 4、5 章(ADK 的 Session 服务管会话记忆、长期记忆落数据侧喂给检索),见 txt-ref-zh/multimodal-realtime-agents/。

  • Stefanos Drakos《The Bible of Agentic AI Systems》(自出版,2026)第 20 章(子智能体不得再派生子智能体:派生工具不进子智能体工具清单,防失控派生链),见 txt-ref-zh/agentic-ai-bible/。

  • Sif Baksh《Building AI Agents for Network Operations》(Packt,2026)第 7 章(排障回答五段契约 Finding、Evidence、Likely cause、Next checks、Unknowns:未验证项逼智能体交代哪些没查),见 txt/netops-agents.txt(英文原文)。

  • Vasyl Zvarydchuk《Building Agent-Powered Applications》(Packt,2026)第 10 章(智能体记忆的评测:跨会话记忆测试集——记住前次会话提到的事实与偏好、更新或遗忘过期信息、检索过往知识主动个性化作答,报告格式偏好在两天后的请求中被自动采用为示例),见 txt/building-agent-powered-apps.txt(英文原文)。