Agent 工程体系 · 第 22/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 短期记忆:消息历史、工具轨迹、Token 窗口和裁剪

Agent 的“短期记忆”不是一个单独的数据结构,而是下一次模型调用时,系统能够重新提供给模型的、与当前任务有关的上下文状态

这个定义包含三个边界:

  1. 短期记忆服务于当前会话、当前线程或当前运行过程;
  2. 它必须能够恢复模型继续推理所需的状态,而不只是保存聊天文本;
  3. 它最终要被编码为一次模型请求可以接受的输入,受到 Token 窗口限制。

因此,消息历史、工具轨迹、持久化检查点、摘要和长期记忆都可能参与短期记忆,但它们不是同一个概念。LangGraph 将单线程的图状态检查点归为短期记忆,将跨线程的用户偏好、事实和共享知识归为长期记忆;两者通常需要同时使用。(docs.langchain.com)


1. 先区分四种“历史”

一个 Agent 运行过程中,至少存在四种容易混淆的历史。

1.1 消息历史

**消息历史(message history)**是按顺序排列的对话和控制消息,例如:

system      你是一个订单助手
user        查询订单 ORD-1001 的物流状态
assistant   我先查询订单信息
user        ...
assistant   ...

在简单的聊天 Agent 中,消息历史可以近似表示为:

messages = [
    {"role": "system", "content": "你是一个订单助手"},
    {"role": "user", "content": "查询订单 ORD-1001 的物流状态"},
    {"role": "assistant", "content": "我先查询订单信息"},
]

但对能够调用工具的 Agent 来说,这个表示还不完整。一次工具调用通常至少包含:

  • 模型决定调用哪个工具;
  • 工具调用的唯一标识;
  • 工具名称;
  • JSON 参数;
  • 工具执行结果;
  • 工具结果与调用之间的对应关系;
  • 必要时还包括错误、重试次数和执行状态。

OpenAI 的 Conversations API 将消息、工具调用、工具输出和其他数据都视为 conversation items,而不是只保存纯文本消息。(developers.openai.com)

1.2 工具轨迹

**工具轨迹(tool trajectory)**是 Agent 在当前任务中采取过的行动及其结果:

模型决定调用 get_order(order_id="ORD-1001")
工具返回:
{
  "status": "shipped",
  "tracking_no": "YT123456",
  "updated_at": "2026-08-31T10:20:00+08:00"
}
模型根据结果决定调用 query_logistics(tracking_no="YT123456")
工具返回:
{
  "location": "杭州转运中心",
  "estimated_delivery": "2026-09-02"
}

工具轨迹与普通消息的关键区别是:它包含行动语义和因果关系

以下两种历史对模型而言并不等价:

用户:订单已经发货,预计后天送达。

和:

assistant 调用 get_order,参数 order_id=ORD-1001
tool 返回 status=shipped
assistant 调用 query_logistics,参数 tracking_no=YT123456
tool 返回 estimated_delivery=2026-09-02

前者是一个未经验证的陈述,后者是可追溯的执行轨迹。若后续任务是“取消订单”,Agent 需要知道订单当前状态是工具查询得到的,还是用户自己声称的;若要重试物流查询,还需要知道上一次使用了哪个 tracking number。

1.3 持久化状态

**持久化状态(persisted state)**是为了跨进程、跨请求或故障恢复而写入存储系统的数据。它可能包含:

{
  "thread_id": "thread-1001",
  "messages": [],
  "pending_tool_call": null,
  "summary": "...",
  "last_checkpoint": "cp-42",
  "version": 42
}

它不一定会全部发送给模型。持久化状态是系统内部的恢复材料,模型上下文是从这些材料构造出的请求输入。

LangGraph 的 checkpointer 保存单个 thread 的图状态快照,用于会话连续性、人机协作、时间旅行和故障容错;store 则保存跨 thread 的应用级键值数据。(docs.langchain.com)

1.4 模型上下文

**模型上下文(model context)**是某一次模型调用实际收到的输入以及该调用允许生成的输出空间。它是短期记忆的“物理边界”。

可以把一次请求抽象为:

Ct=S+Mt+Tt+Rt+UtC_t = S + M_t + T_t + R_t + U_t

其中:

  • SS:系统指令、开发者指令和固定策略;
  • MtM_t:保留的消息历史;
  • TtT_t:保留的工具定义和工具轨迹;
  • RtR_t:检索结果、文件片段或外部上下文;
  • UtU_t:本轮用户输入。

这些内容并不等于持久化数据库中的全部历史。短期记忆系统的工作,就是从完整状态中构造一个满足约束的 CtC_t


2. 消息历史为什么不是简单的字符串数组

2.1 消息具有角色、顺序和结构

最基本的消息结构包含:

{
  "role": "user",
  "content": "查询订单 ORD-1001"
}

但真实 Agent 还会遇到:

  • 多段文本或多模态内容;
  • assistant 的工具调用;
  • tool 的工具输出;
  • 并行工具调用;
  • 流式响应中的中间事件;
  • 结构化输出;
  • reasoning model 产生的内部推理相关状态;
  • 人工审批或中断事件。

因此,消息历史应被理解为有类型的事件序列,而不是 list[str]

一种更稳妥的内部模型是:

from dataclasses import dataclass
from typing import Any, Literal

Role = Literal["system", "developer", "user", "assistant", "tool"]

@dataclass
class Event:
    id: str
    role: Role
    content: Any
    tool_call_id: str | None = None
    name: str | None = None
    parent_id: str | None = None
    status: str = "completed"

这里的 parent_idtool_call_id 用来表达因果关系。例如:

assistant tool_call id=call_7
tool result   tool_call_id=call_7

如果裁剪或序列化时丢失这个关联,模型可能收到一个“没有对应调用的工具结果”,或者看到工具调用却没有结果。两种情况都可能导致请求失败、重复调用或错误推理。

2.2 工具调用必须成组保存

假设当前历史是:

H0 system
H1 user: 查询订单 ORD-1001
H2 assistant: tool_call get_order(...)
H3 tool: get_order result
H4 assistant: tool_call query_logistics(...)
H5 tool: query_logistics result
H6 assistant: 订单预计……

不能只保留:

H0 system
H1 user
H5 tool

因为 H5 依赖 H4 的调用标识和参数。如果只保留工具结果,模型无法判断该结果对应哪个调用。

也不能只保留:

H0 system
H1 user
H4 assistant: tool_call query_logistics(...)

因为模型会认为工具调用尚未完成,下一步可能再次等待或重新执行。

工程上通常将一个工具交互视为不可拆分的原子块:

assistant(tool_call) + tool(result)

若存在并行工具调用,则原子块应扩展为:

assistant(tool_call_1, tool_call_2)
+ tool(result_1)
+ tool(result_2)

裁剪算法不得从原子块中间切断。


3. 工具轨迹保存什么,丢弃什么

工具轨迹不是越完整越好。它需要同时满足三个目标:

  1. 让模型知道已经采取过什么行动;
  2. 让模型能够解释当前事实的来源;
  3. 让系统能够在失败后恢复或审计。

因此应区分三类数据。

3.1 对下一步推理必需的数据

例如:

{
  "tool": "query_weather",
  "arguments": {"city": "Hangzhou"},
  "result": {
    "temperature": 28,
    "condition": "rain"
  }
}

如果下一步要根据天气安排行程,工具结果中的 temperaturecondition 可能必须保留。

3.2 对恢复必需但不一定发送给模型的数据

例如:

{
  "tool_call_id": "call_7",
  "attempt": 2,
  "started_at": "2026-09-01T09:00:01+08:00",
  "finished_at": "2026-09-01T09:00:03+08:00",
  "idempotency_key": "weather:hangzhou:2026-09-01",
  "status": "succeeded"
}

这些信息对模型未必有用,但对系统重启、重试和排查重复副作用很重要。它们应进入持久化状态或审计日志,而不是无条件塞进上下文。

3.3 可以压缩的数据

例如一个返回 20 万字的网页抓取结果。后续模型可能只需要:

{
  "source": "供应商 API",
  "query": "ORD-1001",
  "facts": [
    "订单状态:已发货",
    "物流单号:YT123456",
    "预计送达:2026-09-02"
  ],
  "retrieved_at": "2026-09-01T09:00:03+08:00"
}

但压缩不能把“事实”和“推测”混在一起。建议让工具结果明确区分:

{
  "verified_facts": [
    {"key": "order.status", "value": "shipped"},
    {"key": "order.tracking_no", "value": "YT123456"}
  ],
  "uncertain_observations": [],
  "source": "order-service",
  "retrieved_at": "2026-09-01T09:00:03+08:00"
}

这样摘要或裁剪后,Agent 仍然知道哪些信息来自工具确认,哪些只是模型推断。


4. Token、Token 窗口和预算关系

4.1 Token 是模型处理文本的计量单位

Token不是字符,也不是单词。不同语言、符号、代码和 JSON 的切分方式不同,因此不能用“字符数除以四”作为可靠生产算法。

模型请求通常包含输入 Token 和输出 Token。对支持推理的模型,还可能存在推理 Token。OpenAI 文档将上下文窗口定义为单次请求可使用的最大 Token 数,并说明输入、输出以及部分模型的推理 Token 都可能计入总量。(developers.openai.com)

4.2 Token 窗口不是“可放入的历史长度”

设模型上下文窗口为 WW,固定指令为 SS,历史为 HH,本轮输入为 UU,预留输出和推理空间为 OO,则必须满足:

tokens(S)+tokens(H)+tokens(U)+OWtokens(S) + tokens(H) + tokens(U) + O \leq W

这里的 OO 不能简单设为零。若输入占满窗口,模型即使能够开始生成,也可能无法生成完整输出,或者出现输出截断。OpenAI 文档也指出,超出上下文窗口的 Token 可能导致响应被截断。(developers.openai.com)

实际系统还应留出安全余量 ϵ\epsilon

tokens(S)+tokens(H)+tokens(U)+O+ϵWtokens(S) + tokens(H) + tokens(U) + O + \epsilon \leq W

其中 ϵ\epsilon 用来吸收:

  • 消息包装结构;
  • 工具定义;
  • JSON 转义;
  • 多模态内容的额外计量;
  • 估算器与服务端实际计数之间的差异。

因此历史预算是:

BH=Wtokens(S)tokens(U)OϵB_H = W - tokens(S) - tokens(U) - O - \epsilon

只有当 tokens(H)>BHtokens(H) > B_H 时,才需要裁剪或压缩。

4.3 一个完整算例

假设:

上下文窗口 W = 16,000
系统与开发者指令 S = 1,200
工具定义 T = 1,000
本轮用户输入 U = 800
预留输出 O = 2,000
安全余量 ε = 500

历史可用预算为:

BH=16000120010008002000500=10500B_H = 16000 - 1200 - 1000 - 800 - 2000 - 500 = 10500

当前历史包含:

最近 3 个完整回合:2,100 Token
较早的 8 个回合:8,900 Token
工具结果:3,200 Token
历史总计:14,200 Token

超出预算:

1420010500=370014200 - 10500 = 3700

这时不能直接按字符串截断 3,700 Token。正确顺序通常是:

  1. 保留系统指令;
  2. 保留当前用户输入;
  3. 保留最近的未完成工具交互;
  4. 保留最近若干个完整回合;
  5. 将较早回合压缩为摘要;
  6. 对大工具结果做字段级压缩;
  7. 重新计数并验证。

例如压缩结果变成:

固定部分:3,000
最近回合:2,100
历史摘要:2,000
工具事实摘要:1,200
安全余量:500
预留输出:2,000
总计:10,800

它仍然低于 16,000,并且比“删除最早 3,700 Token”保留了更多任务状态。


5. 短期记忆的状态模型

一个可恢复的 Agent 状态可以表示为:

Xt=(I,Ht,Pt,Qt,Vt)X_t = (I, H_t, P_t, Q_t, V_t)

其中:

  • II:会话身份和隔离范围;
  • HtH_t:逻辑消息历史;
  • PtP_t:当前任务摘要或压缩状态;
  • QtQ_t:未完成工具调用、审批和重试状态;
  • VtV_t:状态版本或检查点版本。

模型输入不是直接等于 XtX_t,而是一个投影函数:

Ct=Π(Xt,budgett)C_t = \Pi(X_t, budget_t)

budget_t 由模型窗口、系统提示、工具定义、本轮输入和输出预留共同决定。

一次请求的典型数据流如下:

flowchart LR
    A[用户事件] --> B[加载 thread 状态]
    B --> C[追加用户消息]
    C --> D[计算 Token 预算]
    D --> E{是否超预算}
    E -- 否 --> F[构造模型上下文]
    E -- 是 --> G[摘要/工具结果压缩/裁剪]
    G --> H[校验消息结构]
    H --> F
    F --> I[模型调用]
    I --> J{是否工具调用}
    J -- 是 --> K[执行工具]
    K --> L[追加工具结果]
    L --> M[写入检查点]
    M --> D
    J -- 否 --> N[追加最终回答]
    N --> M

关键点是:裁剪发生在“构造模型上下文”之前,而不是随意删除数据库中的原始历史。原始历史、压缩后的上下文和检查点应分开管理,否则一次错误裁剪可能永久破坏会话。


6. 裁剪算法:保留什么,删除什么

6.1 裁剪的基本不变量

一个合格的裁剪算法至少应保持以下不变量:

不变量一:身份和策略不能被裁掉

系统指令、租户策略、权限约束和安全规则不能按普通消息参与淘汰。

不变量二:当前任务不能被拆断

当前用户问题、尚未完成的工具调用、人机审批状态和重试上下文必须完整保留。

不变量三:工具调用关系必须闭合

对每个保留的工具调用:

assistant tool_call -> tool result

要么两者都保留,要么使用明确的“已压缩结果”替代完整轨迹。不能保留悬空的 tool result。

不变量四:消息顺序必须可解释

模型看到的事件顺序应符合实际因果关系:

user -> assistant tool_call -> tool result -> assistant

不能把工具结果移动到对应调用之前。

不变量五:裁剪结果仍然可序列化

裁剪后的状态必须能重新写入检查点,并能够在下一次请求中恢复。否则“请求成功、状态损坏”会形成更难排查的故障。

6.2 一个可执行的简化实现

下面的代码演示“按原子块从旧到新删除”的最小实现。它不负责真正的 Token 编码,而是要求调用方提供计数函数。

from dataclasses import dataclass
from typing import Callable, Literal

Role = Literal["system", "developer", "user", "assistant", "tool"]

@dataclass
class Msg:
    role: Role
    content: str
    tool_call_id: str | None = None
    kind: str = "message"  # message / tool_call / tool_result

def count_tokens(messages: list[Msg]) -> int:
    # 可替换为与目标模型匹配的 tokenizer。
    # 这里只为演示接口,不把字符数当作生产 Token 数。
    return sum(max(1, len(m.content) // 4) for m in messages)

def is_atomic_tool_pair(a: Msg, b: Msg) -> bool:
    return (
        a.kind == "tool_call"
        and b.kind == "tool_result"
        and a.tool_call_id is not None
        and a.tool_call_id == b.tool_call_id
    )

def trim_history(
    messages: list[Msg],
    budget: int,
    pinned_prefix: int = 1,
    count: Callable[[list[Msg]], int] = count_tokens,
) -> list[Msg]:
    if count(messages) <= budget:
        return messages[:]

    kept = messages[:]
    i = pinned_prefix

    while count(kept) > budget and i < len(kept):
        # 不删除固定前缀,例如 system/developer 消息。
        if i + 1 < len(kept) and is_atomic_tool_pair(kept[i], kept[i + 1]):
            del kept[i:i + 2]
        else:
            # 生产实现还应跳过未完成事件和当前回合。
            del kept[i]

    if count(kept) > budget:
        raise ValueError(
            "固定指令或当前不可裁剪状态本身已超过预算,"
            "不能通过普通历史裁剪解决"
        )

    return kept

这个实现有三个重要限制:

  1. len(content) // 4 只是演示计数接口,不能用于精确生产预算;
  2. 它没有识别“当前回合”“未完成工具调用”和“人工审批”等业务状态;
  3. 它删除的是传入列表,不代表应该物理删除原始数据库历史。

生产实现应先把历史划分为逻辑块:

blocks = [
    SystemBlock(...),
    ConversationTurn(...),
    ToolTrajectory(...),
    SummaryBlock(...),
    CurrentTurn(...),
]

然后以 block 为单位计算和淘汰。因为一个用户回合可能包含多次工具调用,按单条消息删除仍然可能切断语义。


7. “最近消息优先”并不总是正确

最常见的策略是保留最近 NN 条消息:

messages = messages[-N:]

它简单,但有三个反例。

7.1 反例:早期约束被删除

用户:所有金额都用人民币,默认含税。
……
用户:这个价格是多少?

如果裁掉第一条,Agent 可能使用美元或输出未含税价格。这个信息不是长期记忆意义上的用户偏好,而是当前线程的任务约束,应该进入固定状态或摘要。

7.2 反例:工具调用被拆开

assistant: 调用 refund_order(order_id="ORD-1")
tool: 退款接口超时

如果只保留 assistant 调用,不保留超时结果,模型可能以为退款尚未执行;如果系统随后自动重试,又可能造成重复副作用。

7.3 反例:最近内容是大噪声

最近一次搜索可能返回大量网页正文,但真正有用的只有三条事实。按时间保留会浪费窗口,按信息价值压缩才合理。

因此裁剪优先级通常应是:

固定策略
> 当前用户请求
> 未完成动作与审批
> 最近的完整工具轨迹
> 最近完整对话回合
> 历史摘要
> 可重新获取的外部结果
> 已失效或重复内容

这里的“优先级”不是通用规范,而是常见实现策略。具体顺序取决于 Agent 是否允许重新调用工具、工具是否有副作用,以及历史事实是否会发生变化。


8. 裁剪、摘要和压缩不是一回事

8.1 裁剪

**裁剪(trimming)**是删除部分历史,不产生替代内容:

原始历史:A B C D E F
裁剪后:  D E F

优点是不会引入摘要错误;缺点是被删除的信息不可见。

8.2 摘要

**摘要(summarization)**是用较短内容替代较长历史:

原始历史:A B C
摘要后:   S(A,B,C)

它保留语义,但可能丢失精确参数、否定条件、时间和证据来源。

8.3 结构化压缩

结构化压缩不是让模型自由概括,而是把历史投影为有限字段:

{
  "task": "查询订单物流",
  "constraints": {
    "currency": "CNY",
    "need_source": true
  },
  "verified_facts": {
    "order.status": "shipped",
    "tracking_no": "YT123456"
  },
  "pending_actions": [],
  "open_questions": []
}

对工具轨迹而言,结构化压缩通常比自然语言摘要更容易验证,因为字段可以做类型检查、来源检查和时间检查。

8.4 何时只裁剪,何时先摘要

可以用以下条件判断:

  • 如果旧消息只是寒暄或已完成的确认,直接裁剪;
  • 如果旧消息包含当前任务约束,摘要或提升为结构化状态;
  • 如果旧消息包含工具事实,保留事实、来源和时间,删除原始大响应;
  • 如果旧消息对应未完成副作用操作,不能只摘要,必须保留恢复所需的调用状态;
  • 如果内容可以通过幂等工具重新取得,可以保存引用而不是全文,但要接受工具数据已经变化的风险。

9. 持久化与会话恢复

单次 HTTP 请求通常是无状态的。多轮对话必须由应用显式传入历史,或者使用平台提供的会话状态机制。OpenAI 文档同时提供了手动传入历史、通过 previous_response_id 串联响应,以及使用具有持久标识的 Conversations API 等方式。(developers.openai.com)

需要特别注意:previous_response_id 表示响应链的延续关系,不等于应用已经解决了所有短期记忆问题。链上的历史仍然会影响输入 Token 计费;OpenAI 文档明确说明,即使使用该参数,链中之前的输入 Token 仍按输入 Token 计费。(developers.openai.com)

在自建 Agent 中,恢复流程应类似:

1. 根据 user_id、tenant_id、thread_id 定位线程
2. 读取最新完整检查点
3. 检查状态版本和校验和
4. 恢复消息、摘要、未完成工具调用
5. 根据当前模型预算重新构造上下文
6. 继续执行,而不是盲目重放所有工具

LangGraph 中,thread_id 用来访问单线程的检查点状态;checkpointer 负责短期、线程范围的状态,store 负责跨线程的长期数据。(docs.langchain.com)

一个最小的 LangGraph 形式如下:

from langgraph.checkpoint.memory import InMemorySaver

checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)

config = {
    "configurable": {
        "thread_id": "tenant-a:user-42:thread-1001"
    }
}

result = graph.invoke(
    {
        "messages": [
            {"role": "user", "content": "我叫小王,请查询我的订单"}
        ]
    },
    config,
)

这里的 thread_id 不是随意的日志字段,而是状态隔离键。相同的 thread_id 会继续使用同一线程状态,不同的 thread_id 才会产生不同的短期记忆。LangGraph 文档还提醒,内存型 InMemorySaverMemorySaver 在进程重启后会丢失检查点,生产环境应使用持久化 checkpointer;过长的 thread_id 也可能导致数据库错误。(docs.langchain.com)


10. 并发、分叉和重复执行

短期记忆不是单线程代码就能天然保证正确。实际系统中可能同时发生:

请求 A:用户发送“查物流”
请求 B:用户发送“顺便告诉我能否取消”
请求 C:工具回调返回物流结果

如果三个请求同时读取版本 10 的状态,然后分别写回版本 11,最后写入者可能覆盖前一个请求的消息。结果包括:

  • 用户消息丢失;
  • 工具结果附着到错误的回合;
  • 同一个工具被重复执行;
  • 摘要基于旧历史生成,覆盖新历史。

因此状态写入应采用乐观并发控制:

write(Xnew) succeeds only if versiondb=versionreadwrite(X_{new}) \text{ succeeds only if } version_{db}=version_{read}

失败时重新加载最新状态,合并事件,再重新构造上下文。

对于有副作用的工具,必须把以下三者区分开:

模型是否发出了调用
工具服务是否收到调用
副作用是否已经成功提交

推荐保存:

{
  "tool_call_id": "call-7",
  "idempotency_key": "refund:ORD-1001",
  "status": "submitted",
  "attempt": 1
}

恢复时,如果状态是 submitted,不能仅因为模型上下文中没有工具结果就再次退款。系统应先查询幂等键或业务订单状态,再决定是否重试。

分叉状态

某些系统支持从历史检查点创建分支,例如重新执行某个决策。分支必须拥有新的逻辑线程或版本标识:

原线程:thread-1001
分支线程:thread-1001/replay-20260901-01

否则分支写入会污染原会话。检查点的“时间旅行”适合调试和恢复,但不应被误认为普通消息裁剪。LangGraph 将时间旅行和故障容错列为 checkpointer 的短期记忆用途。(docs.langchain.com)


11. 常见失败表现与诊断方法

11.1 模型突然“忘记”早期约束

检查:

原始历史是否还在数据库中?
构造上下文时是否裁掉了约束?
摘要是否保留了否定条件和数值?
当前 thread_id 是否正确?

如果原始历史仍在,但请求上下文没有它,这是裁剪策略问题;如果上下文有它但模型仍忽略,才需要继续检查指令层级、冲突消息和模型行为。

11.2 模型重复调用工具

常见原因不是模型“变笨”,而是状态不闭合:

assistant tool_call 已保存
tool result 未保存

下一次恢复时,模型看见调用但没有结果,就可能再次调用。也可能是工具已成功执行,但应用在工具返回前崩溃,重试时没有使用幂等键。

11.3 请求偶发超出上下文窗口

不要只记录“消息数量”。诊断日志至少应包含:

{
  "model": "target-model",
  "context_window": 16000,
  "system_tokens": 1200,
  "tool_definition_tokens": 1000,
  "history_tokens": 14200,
  "user_tokens": 800,
  "reserved_output_tokens": 2000,
  "estimated_total": 19200,
  "trimmed_blocks": ["turn-01", "tool-trajectory-02"]
}

OpenAI 提供 Token 估算工具,并建议使用与模型匹配的 tokenizer 估计消息消耗。(developers.openai.com)

11.4 重启后会话变成空白

如果使用的是内存型 checkpointer,这是预期行为而不是随机故障。LangGraph 文档明确指出,内存保存器的数据位于 RAM,进程重启后检查点会丢失。(docs.langchain.com)

恢复方案不是“再把最近几条消息放回去”,而是:

  1. 使用持久化 checkpointer;
  2. 对写入进行原子提交;
  3. 保存状态版本;
  4. 对工具副作用使用幂等键;
  5. 在恢复时区分已完成、执行中和未知状态。

12. 生产系统中的最小状态分层

一个可维护的 Agent 通常至少分成四层:

原始事件层
  保存完整用户消息、assistant 输出、工具调用和工具结果

线程状态层
  保存当前摘要、未完成动作、当前版本和检查点

模型上下文层
  根据 Token 预算选择要发送的消息、摘要和工具事实

跨线程记忆层
  保存明确授权的用户偏好、长期事实和共享知识

这四层不能混为一谈:

  • 原始事件层用于审计和重新构造;
  • 线程状态层用于恢复当前流程;
  • 模型上下文层用于本次推理;
  • 跨线程记忆层用于长期个性化或共享知识。

例如,“用户在本线程中说过银行卡后四位”不应自动变成跨线程长期记忆;“用户偏好使用中文回答”只有经过明确的数据治理和作用域设计后,才适合进入跨线程存储。


13. 一套可验证的裁剪流程

每次发送模型前,可以按以下顺序执行:

读取完整线程状态
    ↓
确认 thread、user、tenant 作用域
    ↓
标记固定消息、当前回合、未完成动作
    ↓
估算系统指令、工具定义、本轮输入和输出预算
    ↓
按原子块计算历史大小
    ↓
优先压缩大工具结果和重复内容
    ↓
对旧回合做摘要或裁剪
    ↓
验证工具调用与工具结果闭合
    ↓
验证消息顺序和状态版本
    ↓
重新计数
    ↓
发送模型
    ↓
原子写入新事件和检查点

它的核心不是“尽量多塞历史”,而是保持如下条件:

可恢复性因果闭合作用域隔离Token 总量不超预算\text{可恢复性} \land \text{因果闭合} \land \text{作用域隔离} \land \text{Token 总量不超预算}

其中任一条件不成立,短期记忆就不再可靠:

  • 没有可恢复性,进程重启后会话断裂;
  • 没有因果闭合,工具 Agent 会重复或错误行动;
  • 没有作用域隔离,会发生用户、租户或线程串线;
  • 没有 Token 预算,历史越长越容易在最关键的一轮失败。

短期记忆的工程本质,是在完整性、可恢复性、成本和上下文容量之间维护一个可验证的状态投影。消息历史负责表达对话,工具轨迹负责表达行动,持久化状态负责恢复,而裁剪负责把无限增长的逻辑历史转换成当前模型真正能够处理的有限上下文。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。