Agent Basic / 06

Context、State 与 Memory

“模型记得我们刚才说过什么”是一种方便的产品感受,却不是准确的工程描述。

大多数大模型 API 都是无状态的:一次请求结束后,下一次请求不会自动继承上一次请求。聊天应用之所以能连续对话,是因为应用保存了历史,并在下一轮重新组织成模型输入。

因此,学习 Agent 的 Memory 之前,需要先把几件常被混用的东西分开:

  • messages:一次 API 请求采用的协议化消息
  • Context:本次推理时模型实际可见的工作集
  • State:应用维护的结构化执行状态
  • Memory:系统选择保留、更新并在以后取回的信息
  • RAG:从外部知识源检索当前任务所需资料的方法

它们会相互传递信息,但职责并不相同。

messages 不是模型的记忆

假设用户先问天气,模型决定调用工具。一个典型的交互不是“一次请求完成全部工作”,而是两次请求:

请求 1
  system: 你是天气助手
  user: 上海今天需要带伞吗?

响应 1
  assistant: tool_call(name=get_weather, arguments={city: "上海"}, id=call_123)

程序执行 get_weather

请求 2
  system: 你是天气助手
  user: 上海今天需要带伞吗?
  assistant: tool_call(..., id=call_123)
  tool: tool_result(call_id=call_123, content="有阵雨")

响应 2
  assistant: 建议带伞。

第二次请求之所以能继续,不是因为模型在服务端“想起了”第一次请求,而是应用把必要的消息轨迹再次提交了。

不同厂商会使用不同字段名、角色名和内容块结构。有的接口还允许引用服务端保存的会话或响应 ID,但这只是 API 提供的状态管理能力,不能据此假设模型自身拥有跨请求记忆。实现时应以当前 SDK 和接口文档为准。

工具调用必须保持因果配对

一条工具结果不是独立事实,它是某次工具调用的结果。Harness 至少要保留:

  • 模型发出的工具调用
  • 协议要求的关联信息(常见实现是唯一 call ID)
  • 程序返回的工具结果
  • 调用与结果的先后顺序

工具结果应按当前协议与发起它的调用对应;带 call ID 的协议应原样引用该 ID,没有显式 ID 的协议则遵守其消息结构和顺序约束。不要只保留 tool_result 而删掉前面的 tool_call,也不要把 A 调用的结果错配给 B 调用。部分 API 会直接拒绝这种消息序列;即使接口接受,模型看到的因果关系也已经被破坏。

本课把一次 assistant 响应发出的整组工具调用,以及后续请求中按接口要求提交的对应结果,视为一个完整的工具 API 轮次。如果模型并行发出多个调用,就要保留整组调用、每个结果及其协议关联关系。

因此,裁剪或压缩历史时,一个尚未完成的工具调用及其结果不能被切开。对已完成的轮次,可以整体保留,也可以在不再需要协议细节后把整个轮次压缩为可验证的状态或摘要,但不能留下“孤儿结果”。

Context 是本次推理的工作集

Context 不只是聊天记录。一次 Agent 调用中,模型可见的内容通常包括:

  • 系统或开发者指令
  • 工具名称、描述和参数 Schema
  • 当前用户输入
  • 选中的历史消息
  • 工具调用与工具结果
  • 从 State、Memory 或 RAG 注入的资料
  • 输出格式和其他运行约束

可以把它理解为:

Context = 静态前缀 + 动态执行轨迹 + 本轮补充信息

这里的“静态”和“动态”是工程上的相对划分。

静态前缀

在多次请求间通常较稳定,例如:

  • 系统指令
  • 工具定义
  • 固定输出规范
  • 长期不变的背景说明

动态执行轨迹

随着任务推进不断变化,例如:

  • 用户的新消息
  • 模型发起的工具调用
  • 工具返回结果
  • 可观察的阶段结论
  • 当前任务状态

执行轨迹应记录系统真正需要复现和审计的事件,不应要求模型输出或应用保存私有思维链。工具调用、工具结果、最终答案、显式状态变更以及必要的简短理由,通常已经足以支持调试。

messages 最终还要经过 Chat Template

在应用代码里,输入看起来像一个 messages 数组;模型实际接收的却是一串 token。中间通常会经过模型或服务端定义的 Chat Template,把角色、内容块、工具调用和边界标记序列化。

这带来三个直接结论:

  1. systemuserassistanttool 不是普通文本标签,而是协议的一部分。
  2. 不能假设不同模型、不同供应商的消息格式可以原样互换。
  3. 手工把所有消息拼成一段字符串,可能丢失角色边界、工具调用 ID 或内容块类型。

如果使用托管 API,通常让官方 SDK 完成序列化;如果使用开源模型,则应使用与该模型训练方式匹配的 Chat Template。切换模型时,消息适配层也需要纳入测试。

分清 Context、State、Memory 与 RAG

概念 主要职责 典型内容 是否一定进入当前 Context
Context 为本次推理提供可见信息 指令、消息、工具结果、检索片段 是,它本身就是当前输入工作集
State 记录任务现在执行到哪里 步骤、预算、中间结果、完成条件 否,只注入当前决策需要的部分
短期记忆 保持当前会话或任务连续性 最近对话、临时约束、未完成事项 不一定,可裁剪、摘要或结构化
长期记忆 跨会话保留与主体相关的信息 用户偏好、长期约束、历史决策 不一定,通常按需召回
RAG 从外部知识源补充事实 文档、报告、知识库片段 只有被检索并选中的内容会进入

1. State:执行现场

State 是程序可以读取、校验和更新的结构化数据,例如:

  • 当前分析到哪一步
  • 已经查过哪些数据
  • 剩余工具调用预算
  • 中间结果及其来源
  • 是否满足结束条件

它应该尽量是任务的事实来源,而不是散落在自然语言历史里。很多所谓的 Memory 问题,本质上是 State 没有设计好。

2. Short-term Memory:会话内连续性

短期记忆服务于当前会话或当前任务,例如:

  • 用户刚才补充的约束
  • 尚未解决的问题
  • 最近几轮对话中的指代关系
  • 当前任务暂时需要的事实

短期记忆不等于“把全部消息永远带上”。它可以由原始消息、滚动摘要和结构化 State 共同构成。

3. Long-term Memory:跨任务沉淀

长期记忆是在会话结束后仍有价值,并且未来可能需要取回的信息,例如:

  • 用户稳定的输出偏好
  • 经确认的长期约束
  • 历史决策及其适用条件
  • 某类任务的持续背景

长期记忆需要明确写入条件、来源、更新时间、过期规则和删除能力。未经确认的模型猜测不应直接升级为长期事实。

为什么很多系统不需要先做长期记忆

许多 Agent 场景只要具备:

  • 清晰的当前任务 State
  • 受控的会话 Context

就已经足够。

过早加入长期记忆,反而容易造成:

  • 召回不相关信息
  • 旧事实污染当前任务
  • 多条记忆相互冲突
  • 无法判断何时更新或遗忘
  • 敏感信息被过度保存

更实用的设计顺序通常是:

  1. 先设计结构化 State
  2. 再管理会话内 Context
  3. 最后验证是否真的需要长期 Memory

常见的 Memory 设计模式

模式 1:对话窗口保留

只保留最近若干轮消息。

优点:

  • 实现简单
  • 原始细节保真度高

问题:

  • 长任务容易超过上下文预算
  • 早期关键约束可能被窗口挤出
  • 裁剪时必须按完整 API 轮次处理

模式 2:摘要式记忆

把较早的内容压缩成滚动摘要,只保留关键事实、约束、决定和未完成事项。

优点:

  • 节省输入 token
  • 适合长会话

问题:

  • 摘要可能遗漏或改写事实
  • 早期错误会在后续轮次中持续传播
  • 需要保留来源,才能在关键时回查

模式 3:结构化状态存储

把任务进度和中间结果保存在明确字段中,例如:

{
  "goal": "比较两个方案",
  "constraints": ["预算不超过 1 万元"],
  "completed_steps": ["读取报价"],
  "open_questions": ["维护成本是否含税"],
  "tool_budget_remaining": 3
}

优点:

  • 可校验、可调试、可恢复
  • 不依赖模型从长对话中重新寻找状态
  • 适合工作流和 Agent 系统

问题:

  • 需要设计 Schema 和更新规则
  • 自然语言信息进入字段时仍需处理冲突与来源

模式 4:检索式长期记忆

把长期信息保存到数据库、文档索引或向量库,在满足条件时召回。

优点:

  • 不必把全部历史一直放进 Context
  • 适合跨会话积累

问题:

  • 召回不保证永远相关
  • 需要处理权限、过期、冲突和删除
  • 很容易把普通知识库检索误称为“记忆”

Memory 和 RAG 不是一回事

RAG 解决的是:

如何从外部知识源取回当前问题需要的资料。

Memory 更关注:

系统应该为某个用户、任务或主体保留什么,以及以后在什么条件下取回。

两者可以共用数据库或检索技术,但语义不同:

  • “用户偏好使用表格输出”更像长期记忆
  • “某篇市场研究报告的结论”更像外部知识

判断标准不是“是否用了向量数据库”,而是信息为什么被保存、归属于谁、何时应被召回。

KV Cache 与 Prompt Cache 不是一回事

“缓存”也经常被混成一个概念。

KV Cache

KV Cache 是模型生成过程中的推理优化。模型在处理同一条序列并逐 token 生成时,保存已经计算过的注意力 Key/Value,避免每生成一个 token 都从头计算整个前缀。

它主要发生在一次推理过程内部,是模型运行时层面的机制。

Provider Prompt/Prefix Cache

部分模型服务会在不同请求之间复用满足其匹配规则的前缀计算,常见名称包括 Prompt Cache 或 Prefix Cache。具体支持条件、生命周期、计费方式、匹配粒度和可观测指标都由供应商决定。

因此不要假设:

  • 前缀相同就一定命中缓存
  • 所有模型都支持跨请求缓存
  • 本地看到的字符串相同就代表服务端 token 前缀完全相同
  • 命中缓存一定带来同样的延迟或费用收益

只有当服务端响应明确给出 cached input tokens、cache read/write 或类似字段时,才能把它作为本次请求的观测数据。

稳定前缀原则

在不影响语义正确性的前提下,可以按“稳定内容在前、动态内容在后”组织输入:

较稳定:系统指令 -> 工具 Schema -> 固定背景
较动态:历史轨迹 -> 本轮检索结果 -> 当前用户输入

避免在稳定前缀中插入时间戳、随机 ID、无关排序变化或每轮都会改写的文本。这种组织方式有机会提高前缀复用,但是否命中仍取决于具体服务。

正确性始终优先于缓存。不能为了保持前缀不变而保留错误信息、过期权限或不适用于当前请求的指令。

上下文压缩:压什么,保什么

压缩的目标不是单纯减少 token,而是在预算内保住后续决策所需的信息。

一个务实的优先顺序是:

  1. 删除重复日志、冗余格式和已确认无用的原始输出
  2. 把大型文件或工具结果移到外部存储,只在 Context 中保留引用和关键字段
  3. 将任务进度、约束和未完成事项提取到结构化 State
  4. 对较早且已经结束的对话轮次生成滚动摘要
  5. 需要时通过检索重新取回原始材料
  6. 超长任务用检查点开启新 Context

无论使用滑动窗口、摘要还是检查点,都要遵守以下边界:

  • 不切断未完成的 tool_call / tool_result 配对
  • 不改变调用与结果的协议关联关系
  • 不把仍待确认的推测摘要成既定事实
  • 保留用户硬约束、关键决定、未解决问题及其来源
  • 摘要无法覆盖的原始证据应可回查

可以把每次压缩看成一次有损变换,因此压缩器也需要测试,而不是只看 token 是否下降。

上下文消融实验

本仓库的 agent-api-lab 用可观察的请求、响应和工具事件检验不同上下文策略。实验不读取或展示模型私有思维链。

实验问题

选择一组必须记住早期约束、并且至少需要一次工具调用的固定任务,比较以下策略:

组别 Context 策略 目的
A 保留完整且合法的消息轨迹 建立基线
B 只保留最近 N 个完整 API 轮次 观察窗口裁剪的影响
C 早期轮次压缩为摘要,同时保留结构化 State 观察压缩后的质量与成本
D 从 Context 中有意删除一个关键事实 测量事实依赖程度
E 构造孤立工具结果或错配调用 ID,由本地校验器拦截 验证协议完整性检查

E 组是协议负例,不应作为正常请求发送。不同 API 对非法轨迹的处理可能不同,实验重点是确保 Harness 在调用模型前发现问题。

记录指标

每种策略在同一任务集上重复运行,至少记录:

  • 任务成功率:最终结果是否满足可自动检查的完成条件
  • 重复调用数:同一语义的工具操作被重复执行多少次
  • Token 用量:输入、输出和总 token;仅在 API 返回 usage 时记录
  • 事实保留率:最终答案正确保留的关键事实数 / 应保留事实总数

缓存相关指标只在服务端明确提供时记录,例如 cached input tokens、cache read/write 或首 token 延迟。缺少服务端数据时标记为“不可观测”,不要把推测写成缓存命中。

为了让结果可比较,还应固定模型版本、工具实现、任务集和采样参数,并保存脱敏后的可观察轨迹。实验得到的不是“最佳上下文策略”的普遍结论,而是当前任务和模型下的证据。

设计 Memory 时最应该问的问题

  1. 这条信息是执行 State、会话 Context,还是跨会话 Memory?
  2. 它需要完整保留、摘要保留,还是按需检索?
  3. 它的来源是什么,能否验证?
  4. 它何时过期,发生冲突时以谁为准?
  5. 它是否包含不应长期保存的敏感信息?
  6. 写入、读取、更正和删除分别由谁控制?

如果这些问题没有答案,Memory 往往只会让系统更难预测。

小结

Memory 不是一个让模型突然“有记性”的魔法模块,而是一套由应用负责的信息保留与召回策略。

建立系统时,先记住四条边界:

  • API 通常无状态,连续性来自 Harness 重新提交 Context
  • State 是结构化执行现场,messages 只是当前请求的协议载体
  • 工具调用与结果是不能随意切开的因果单元
  • 长期 Memory 和 RAG 可以共享技术,但解决的问题不同

在这些边界之上,再选择窗口、摘要、结构化状态、检索式记忆和缓存优化,系统才会既可解释又可测试。

下一篇建议继续看:

参考资料

  • 李博杰《深入理解 AI Agent:设计原理与工程实践》,第二章:上下文工程,固定提交 e3883f8c。本文基于相关概念自行组织和表述。