tc9011

【译】上下文工程:会话与记忆

110 min

原文:Context Engineering: Sessions & Memory 作者:Kimberly Milam, Antonio Gulli 发布日期:2025 年 11 月

引言

有状态的、个性化的 AI,始于上下文工程。

本白皮书探讨了会话(Session)与记忆(Memory)在构建有状态、智能 LLM Agent 中的关键作用,旨在帮助开发者创建更强大、更个性化、更持久的 AI 体验。为了让大型语言模型(LLM)能够记住、学习并个性化交互,开发者必须在上下文窗口(Context Window)内动态组装和管理信息——这一过程称为上下文工程(Context Engineering)。

本白皮书的核心概念概述如下:

  • 上下文工程:在 LLM 上下文窗口内动态组装和管理信息的过程,旨在构建有状态的智能 Agent。
  • 会话(Session):与 Agent 进行完整对话的容器,保存对话的时间顺序历史记录及 Agent 的工作记忆。
  • 记忆(Memory):实现长期持久化的机制,通过跨多个会话捕获和整合关键信息,为 LLM Agent 提供连续且个性化的体验。

上下文工程

LLM 本质上是无状态的。在其训练数据之外,其推理和感知能力仅限于单次 API 调用的”上下文窗口”所提供的信息。这带来了一个根本性问题:AI Agent 必须配备操作指令(识别可执行的动作)、用于推理的证据与事实数据,以及定义当前任务的即时对话信息。为了构建能够记住、学习并个性化交互的有状态智能 Agent,开发者必须为每一轮对话动态构建上下文。这种为 LLM 动态组装和管理信息的过程,就是上下文工程

上下文工程代表着从传统提示词工程(Prompt Engineering)的进化。提示词工程专注于制作最优的、通常是静态的系统指令;而上下文工程则处理整个有效载荷,基于用户、对话历史和外部数据动态构建感知状态的提示词。它涉及战略性地选择、总结和注入不同类型的信息,以最大化相关性,同时最小化噪声。外部系统——例如 RAG 数据库、会话存储和记忆管理器——负责管理大部分上下文。Agent 框架必须协调这些系统,检索并将上下文组装进最终提示词。

可以把上下文工程比作 Agent 的”mise en place”(法式厨房备料)——厨师在烹饪前收集和准备所有食材的关键步骤。如果只给厨师一份食谱(提示词),他们用手边随机的食材可能做出还过得去的餐食。但如果先确保厨师拥有所有正确的、高质量的食材、专业工具,以及对摆盘风格的清晰理解,他们就能可靠地产出优秀的、定制化的结果。上下文工程的目标是确保模型拥有的信息不多也不少,恰好是完成任务最相关的内容。

上下文工程负责组装一个复杂的有效载荷,可以包含多种组件:

引导推理的上下文定义 Agent 的基本推理模式和可用动作,指导其行为:

  • 系统指令(System Instructions):定义 Agent 角色、能力和约束的高层指令。
  • 工具定义(Tool Definitions):Agent 可用于与外部世界交互的 API 或函数的模式(Schema)。
  • 少样本示例(Few-Shot Examples):通过上下文学习引导模型推理过程的精选示例。

证据与事实数据是 Agent 推理的实质性数据,包括预存知识和为特定任务动态检索的信息,作为 Agent 响应的”证据”:

  • 长期记忆(Long-Term Memory):跨多个会话收集的、关于用户或主题的持久化知识。
  • 外部知识(External Knowledge):从数据库或文档中检索的信息,通常使用检索增强生成(RAG)。
  • 工具输出(Tool Outputs):工具返回的数据或结果。
  • 子 Agent 输出(Sub-Agent Outputs):被委派特定子任务的专业 Agent 返回的结论或结果。
  • 制品(Artifacts):与用户或会话关联的非文本数据(如文件、图片)。

即时对话信息将 Agent 锚定在当前交互中,定义即时任务:

  • 对话历史(Conversation History):当前交互的逐轮记录。
  • 状态/草稿本(State/Scratchpad):Agent 用于即时推理过程的临时、进行中的信息或计算。
  • 用户提示词(User’s Prompt):需要回答的即时查询。

上下文的动态构建至关重要。例如,记忆不是静态的;随着用户与 Agent 交互或摄入新数据,必须对其进行选择性检索和更新。此外,有效推理通常依赖于上下文学习(in-context learning)——LLM 从提示词中的示例学习如何执行任务的过程。当 Agent 使用与当前任务相关的少样本示例时,上下文学习会更有效,而不是依赖硬编码的示例。类似地,外部知识由 RAG 工具根据用户的即时查询进行检索。

构建上下文感知 Agent 最关键的挑战之一是管理不断增长的对话历史。理论上,拥有大上下文窗口的模型可以处理大量记录;但实际上,随着上下文增长,成本和延迟也会增加。此外,模型可能遭受”上下文衰减”(context rot)——随着上下文增长,其注意关键信息的能力下降的现象。上下文工程通过采用动态变换历史的策略来直接应对这一问题——例如摘要化(summarization)、选择性剪枝或其他压缩技术——以在管理整体 token 数量的同时保留关键信息,最终带来更健壮、更个性化的 AI 体验。

这一实践在 Agent 每轮对话的操作循环中表现为一个持续的循环:

图 1:Agent 上下文管理流程
图 1:Agent 上下文管理流程
  1. 获取上下文(Fetch Context):Agent 首先检索上下文——例如用户记忆、RAG 文档和近期对话事件。对于动态上下文检索,Agent 将使用用户查询和其他元数据来确定检索哪些信息。
  2. 准备上下文(Prepare Context):Agent 框架为 LLM 调用动态构建完整提示词。尽管各个 API 调用可能是异步的,但准备上下文是一个阻塞式的”热路径”过程。Agent 在上下文准备好之前无法继续。
  3. 调用 LLM 和工具(Invoke LLM and Tools):Agent 迭代调用 LLM 和必要工具,直到为用户生成最终响应。工具和模型输出被追加到上下文中。
  4. 上传上下文(Upload Context):本轮收集的新信息被上传到持久化存储。这通常是一个”后台”过程,允许 Agent 在记忆整合或其他后处理异步进行的同时完成执行。

这一生命周期的核心是两个基本组件:会话记忆。会话管理单次对话的逐轮状态。而记忆则提供长期持久化机制,跨多个会话捕获和整合关键信息。

可以将会话视为正在进行特定项目的工作台或桌子。工作时,桌上摆满了所有必要的工具、笔记和参考材料。所有东西都可以立即获取,但也是临时的,特定于手头的任务。项目完成后,你不会把整个乱糟糟的桌子直接塞进存储空间,而是开始创建记忆的过程——就像整理有序的文件柜。你检查桌上的材料,丢掉草稿和冗余笔记,只将最关键的、最终确定的文件归档到有标签的文件夹中。这确保文件柜始终是清晰、可靠、高效的真实信息来源,不会被工作台上的临时混乱所污染。这个类比直接映射了有效 Agent 的运作方式:会话作为单次对话的临时工作台,而 Agent 的记忆则是精心整理的文件柜,使其能够在未来交互中回忆关键信息。

基于对上下文工程的高层概述,我们现在可以探索两个核心组件:会话和记忆,从会话开始。

会话

上下文工程的基础元素是会话,它封装了单次连续对话的即时对话历史和工作记忆。每个会话都是与特定用户绑定的独立记录。会话允许 Agent 维护上下文并在单次对话范围内提供连贯响应。一个用户可以有多个会话,但每个会话都是特定交互的独立、不相连的日志。每个会话包含两个关键组件:时间顺序历史(事件)和 Agent 的工作记忆(状态)。

事件是对话的构建块。常见事件类型包括:用户输入(用户的消息——文本、音频、图像等)、Agent 响应(Agent 对用户的回复)、工具调用(Agent 决定使用外部工具或 API)或工具输出(工具调用返回的数据,Agent 用其继续推理)。

除了聊天历史之外,会话通常还包含状态——一个结构化的”工作记忆”或草稿本。它保存与当前对话相关的临时结构化数据,例如购物车中的商品。

随着对话进展,Agent 会在会话中追加更多事件,并可能根据 Agent 中的逻辑变更状态。

事件的结构类似于传递给 Gemini API 的 Content 对象列表,其中每个带有 role 和 parts 的条目代表对话中的一轮——或一个事件。

contents = [
    {
        "role": "user",
        "parts": [ {"text": "What is the capital of France?"} ]
    }, {
        "role": "model",
        "parts": [ {"text": "The capital of France is Paris."} ]
    }
]
response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=contents
)

代码片段 1:向 Gemini 发起多轮调用示例

生产 Agent 的执行环境通常是无状态的,意味着请求完成后不会保留任何信息。因此,必须将其对话历史保存到持久化存储中以维持连续的用户体验。虽然内存存储适合开发阶段,但生产应用应利用健壮的数据库来可靠地存储和管理会话。例如,可以在 Agent Engine Sessions 等托管解决方案中存储对话历史。

框架与模型间的差异

虽然核心思想相似,但不同的 Agent 框架以不同方式实现会话、事件和状态。Agent 框架负责为 LLM 维护对话历史和状态,使用此上下文构建 LLM 请求,并解析和存储 LLM 响应。

图 2:Agent 上下文管理流程
图 2:Agent 上下文管理流程

Agent 框架充当代码与 LLM 之间的通用翻译器。作为开发者,你使用框架一致的内部数据结构处理每一轮对话,而框架则负责关键任务:将这些结构转换为 LLM 所需的精确格式。这种抽象非常强大,因为它将 Agent 逻辑与所使用的具体 LLM 解耦,防止了供应商锁定。

最终目标是生成 LLM 能理解的”请求”。对于 Google 的 Gemini 模型,这是一个 List[Content]。每个 Content 对象是一个简单的类字典结构,包含两个键:role(定义发言者,“user”或”model”)和 parts(定义消息的实际内容——文本、图像、工具调用等)。

框架自动处理从其内部对象(例如 ADK 事件)到 Content 对象中对应 role 和 parts 的数据映射,然后再进行 API 调用。本质上,框架为开发者提供了一个稳定的内部 API,同时在幕后管理不同 LLM 的复杂且多样的外部 API。

ADK 使用显式的 Session 对象,其中包含 Event 对象列表和单独的状态对象。Session 就像一个文件柜,一个文件夹用于对话历史(事件),另一个用于工作记忆(状态)。

LangGraph 没有正式的”session”对象。相反,状态就是会话。这个包罗万象的状态对象保存对话历史(作为 Message 对象列表)和所有其他工作数据。与传统会话的只追加日志不同,LangGraph 的状态是可变的。它可以被转换,而历史压缩等策略可以改变记录。这对于管理长对话和 token 限制非常有用。

多 Agent 系统中的会话

在多 Agent 系统中,多个 Agent 协作工作,每个 Agent 专注于更小的、专业化的任务。为了使这些 Agent 有效协作,它们必须共享信息。如下图所示,系统架构定义了它们用于共享信息的通信模式。这一架构的核心组件是系统如何处理会话历史——所有交互的持久化日志。

图 3:不同的多 Agent 架构模式
图 3:不同的多 Agent 架构模式

在探索管理这一历史的架构模式之前,区分它与发送给 LLM 的上下文至关重要。将会话历史视为整个对话的永久、未删减的记录。上下文则是发送给 LLM 进行单轮推理的精心构建的信息载荷。Agent 可能通过仅选择历史中相关的摘录,或添加特殊格式(如引导性前言)来构建此上下文以引导模型响应。本节关注的是在 Agent 之间传递哪些信息,而不一定是发送给 LLM 的上下文。

Agent 框架处理多 Agent 系统的会话历史时,使用两种主要方法之一:共享统一历史(所有 Agent 贡献到单一日志)或独立个人历史(每个 Agent 维护自己的视角)。两种模式的选择取决于任务的性质和 Agent 之间期望的协作风格。

共享统一历史模型中,系统中所有 Agent 读取并向同一个单一对话历史写入所有事件。每个 Agent 的消息、工具调用和观察结果都按时间顺序追加到一个中央日志中。这种方法最适合需要单一真实信息来源的紧密耦合、协作性任务,例如一个 Agent 的输出是下一个 Agent 直接输入的多步骤问题解决过程。即使在共享历史的情况下,子 Agent 也可能在将日志传递给 LLM 之前对其进行处理——例如,过滤相关事件子集或添加标签以识别哪个 Agent 生成了每个事件。

如果使用 ADK 的 LLM 驱动委派将任务转交给子 Agent,子 Agent 的所有中间事件都会写入与根 Agent 相同的会话中:

from google.adk.agents import LlmAgent
# 子 Agent 可以访问 Session 并向其写入事件
sub_agent_1 = LlmAgent(...)
# 可选地,子 Agent 可以将最终响应文本(或结构化输出)保存到指定的状态键
sub_agent_2 = LlmAgent(
    ...,
    output_key="..."
)
# 父 Agent
root_agent = LlmAgent(
    ...,
    sub_agents=[sub_agent_1, sub_agent_2]
)

代码片段 2:跨多个 Agent 框架的 A2A 通信

独立个人历史模型中,每个 Agent 维护自己的私有对话历史,对其他 Agent 来说像一个黑盒。所有内部过程——如中间思考、工具使用和推理步骤——保存在 Agent 的私有日志中,对其他 Agent 不可见。通信仅通过显式消息进行,Agent 共享其最终输出,而非其过程。

这种交互通常通过Agent 作为工具(Agent-as-a-Tool)或使用 Agent 到 Agent(A2A)协议来实现。Agent 作为工具时,一个 Agent 像调用标准工具一样调用另一个 Agent,传递输入并接收最终的独立输出。使用 A2A 协议时,Agent 使用结构化协议进行直接消息传递。

我们将在下一节更详细地探讨 A2A 协议。

跨多个 Agent 框架的互操作性

图 4:跨不同框架的多 Agent A2A 通信
图 4:跨不同框架的多 Agent A2A 通信

框架使用内部数据表示引入了多 Agent 系统的关键架构权衡:将 Agent 与 LLM 解耦的抽象同时也将其与使用其他 Agent 框架的 Agent 隔离开来。这种隔离在持久化层面固化。Session 的存储模型通常将数据库模式直接与框架的内部对象耦合,创建了相对不可移植的对话记录。因此,使用 LangGraph 构建的 Agent 无法原生解释 ADK Agent 持久化的不同 Session 和 Event 对象,使无缝任务交接变得不可能。

协调这些隔离 Agent 之间协作的一种新兴架构模式是 Agent 到 Agent(A2A)通信。虽然这种模式使 Agent 能够交换消息,但它未能解决共享丰富上下文状态的核心问题。每个 Agent 的对话历史以其框架的内部模式编码。因此,任何包含会话事件的 A2A 消息都需要翻译层才能有用。

更健壮的互操作性架构模式是将共享知识抽象到与框架无关的数据层——例如记忆。与保存原始、框架特定对象(如事件和消息)的会话存储不同,记忆层被设计为保存经过处理的、规范化的信息。关键信息——如摘要、提取的实体和事实——从对话中提取,通常以字符串或字典形式存储。记忆层的数据结构不与任何单一框架的内部数据表示耦合,这允许它作为通用的、公共的数据层。这种模式允许异构 Agent 通过共享一个公共认知资源来实现真正的协作智能,而无需自定义翻译器。

会话的生产注意事项

将 Agent 迁移到生产环境时,其会话管理系统必须从简单日志进化为健壮的企业级服务。关键考虑因素分为三个关键领域:安全与隐私、数据完整性和性能。像 Agent Engine Sessions 这样的托管会话存储专门设计用于满足这些生产需求。

安全与隐私

保护会话中包含的敏感信息是不可妥协的要求。严格隔离是最关键的安全原则。会话归属于单个用户,系统必须强制执行严格隔离,确保一个用户永远无法访问另一个用户的会话数据(例如通过访问控制列表 ACL)。对会话存储的每个请求都必须针对会话的所有者进行认证和授权。

处理个人身份信息(PII)的最佳实践是在会话数据写入存储之前进行脱敏处理。这是一项基本安全措施,可以大幅降低潜在数据泄露的风险和”爆炸半径”。通过使用 Model Armor 等工具确保敏感数据永远不被持久化,可以简化对 GDPR 和 CCPA 等隐私法规的合规性,并建立用户信任。

数据完整性与生命周期管理

生产系统需要明确的规则来规定会话数据如何随时间存储和维护。会话不应永久存活。可以实施存活时间(TTL)策略,自动删除不活跃的会话以管理存储成本并减少数据管理开销。这需要一个明确的数据保留策略,定义会话在归档或永久删除之前应保留多长时间。

此外,系统必须保证操作以确定性顺序追加到会话历史中。维护事件的正确时间顺序是对话日志完整性的基础。

性能与可扩展性

会话数据处于每次用户交互的”热路径”上,使其性能成为首要关注点。读写会话历史必须极快,以确保响应式用户体验。Agent 运行时通常是无状态的,因此整个会话历史在每轮开始时从中央数据库检索,会产生网络传输延迟。

为了减轻延迟,关键是减少传输数据的大小。一个关键优化是在将会话历史发送给 Agent 之前对其进行过滤或压缩。例如,可以删除当前对话状态不再需要的旧的、不相关的函数调用输出。下一节详细介绍了几种压缩历史以有效管理长上下文对话的策略。

管理长上下文对话:权衡与优化

在简单架构中,会话是用户与 Agent 之间对话的不可变日志。但随着对话规模扩大,对话的 token 使用量也会增加。现代 LLM 可以处理长上下文,但存在局限性,尤其是对于延迟敏感的应用:

  1. 上下文窗口限制:每个 LLM 都有一次能处理的最大文本量(上下文窗口)。如果对话历史超过此限制,API 调用将失败。
  2. API 成本:大多数 LLM 提供商按发送和接收的 token 数量收费。更短的历史意味着更少的 token 和更低的每轮成本。
  3. 延迟(速度):向模型发送更多文本需要更长时间处理,导致用户响应时间更慢。压缩使 Agent 保持快速响应。
  4. 质量:随着 token 数量增加,由于上下文中的额外噪声和自回归错误,性能可能会下降。

管理与 Agent 的长对话可以比作精明旅行者为长途旅行打包行李。行李箱代表 Agent 有限的上下文窗口,衣物和物品是对话中的信息片段。如果简单地把所有东西塞进去,行李箱会变得太重太乱,难以快速找到需要的东西——就像过载的上下文窗口会增加处理成本并降低响应速度一样。另一方面,如果带得太少,你可能会落下必要的物品(如护照或厚外套),危及整个旅程——就像 Agent 可能丢失关键上下文,导致不相关或不正确的答案一样。旅行者和 Agent 都在类似的约束下运作:成功不在于能携带多少,而在于只携带必需的。

压缩策略缩短长对话历史,将对话压缩以适应模型的上下文窗口,降低 API 成本和延迟。随着对话变长,每轮发送给模型的历史可能变得太大。压缩策略通过智能修剪历史同时尽量保留最重要的上下文来解决这个问题。

如何知道可以从会话中丢弃哪些内容而不损失有价值的信息?策略从简单截断到复杂压缩:

  • 保留最后 N 轮:最简单的策略。Agent 只保留最近 N 轮对话(“滑动窗口”),丢弃更早的内容。
  • 基于 Token 的截断:在将历史发送给模型之前,Agent 计算消息中的 token,从最近的开始向后统计。包含尽可能多的消息,但不超过预定义的 token 限制(例如 4000 个 token)。更早的内容被直接截掉。
  • 递归摘要化:对话的较早部分被 AI 生成的摘要替换。随着对话增长,Agent 周期性地使用另一个 LLM 调用来摘要最早的消息。这个摘要然后作为历史的压缩形式使用,通常作为前缀放在更近的、逐字的消息前面。

例如,可以通过使用 ADK 的内置插件来保留最后 N 轮,以限制发送给模型的上下文。这不会修改存储在会话存储中的历史事件:

from google.adk.apps import App
from google.adk.plugins.context_filter_plugin import ContextFilterPlugin
app = App(
    name='hello_world_app',
    root_agent=agent,
    plugins=[
        # 保留最后 10 轮和最近的用户查询
        ContextFilterPlugin(num_invocations_to_keep=10),
    ],
)

代码片段 3:使用 ADK 仅使用最后 N 轮进行会话截断

鉴于复杂压缩策略旨在降低成本和延迟,在后台异步执行昂贵的操作(如递归摘要化)并持久化结果至关重要。“后台”确保客户端不会等待,“持久化”确保昂贵的计算不会被过度重复。通常,Agent 的记忆管理器负责生成和持久化这些递归摘要。Agent 还必须保留哪些事件包含在压缩摘要中的记录;这可以防止原始的、更详细的事件不必要地发送给 LLM。

此外,Agent 必须决定何时需要压缩。触发机制通常分为几个不同类别:

  • 基于计数的触发器(即 token 大小或轮次数量阈值):一旦对话超过某个预定义阈值,就会进行压缩。这种方法通常对管理上下文长度”足够好”。
  • 基于时间的触发器:压缩不是由对话大小触发,而是由活动缺失触发。如果用户在设定时间段内停止交互(例如 15 或 30 分钟),系统可以在后台运行压缩任务。
  • 基于事件的触发器(即语义/任务完成):当 Agent 检测到特定任务、子目标或对话主题已结束时,Agent 决定触发压缩。

例如,可以使用 ADK 的 EventsCompactionConfig 在配置的轮次数量后触发基于 LLM 的摘要化:

from google.adk.apps import App
from google.adk.apps.app import EventsCompactionConfig
app = App(
    name='hello_world_app',
    root_agent=agent,
    events_compaction_config=EventsCompactionConfig(
        compaction_interval=5,
        overlap_size=1,
    ),
)

代码片段 4:使用 ADK 通过摘要化进行会话压缩

记忆生成是从冗长嘈杂的数据源中提取持久知识的广泛能力。在本节中,我们涵盖了从对话历史提取信息的主要示例:会话压缩。压缩将整个对话的逐字记录提炼,提取关键事实和摘要,同时丢弃对话填充词。

在压缩的基础上,下一节将更广泛地探讨记忆生成和管理。我们将讨论创建、存储和检索记忆以构建 Agent 长期知识的各种方式。

记忆

记忆与会话具有深度共生关系:会话是生成记忆的主要数据源,而记忆是管理会话大小的关键策略。记忆是从对话或数据源中提取的、有意义信息的快照。它是保留重要上下文的浓缩表示,使其对未来交互有用。通常,记忆在多个会话中持久化,以提供连续且个性化的体验。

作为专业化的、解耦的服务,“记忆管理器”为多 Agent 互操作性提供了基础。记忆管理器频繁使用与框架无关的数据结构,如简单的字符串和字典。这允许基于不同框架构建的 Agent 连接到单一记忆存储,从而创建任何连接的 Agent 都可以利用的共享知识库。

注:某些框架也可能将会话或逐字对话称为”短期记忆”。在本白皮书中,记忆被定义为提取的信息,而非逐轮对话的原始对话。

存储和检索记忆对于构建复杂智能 Agent 至关重要。健壮的记忆系统通过解锁几个关键能力,将基本聊天机器人转变为真正智能的 Agent:

  • 个性化:最常见的用例是记住用户偏好、事实和过去的交互以定制未来响应。例如,记住用户最喜欢的球队或他们在飞机上的首选座位,能创造更有帮助的个性化体验。
  • 上下文窗口管理:随着对话变长,完整历史可能超过 LLM 的上下文窗口。记忆系统可以通过创建摘要或提取关键事实来压缩此历史,在每轮不发送数千个 token 的情况下保留上下文。这降低了成本和延迟。
  • 数据挖掘与洞察:通过跨多个用户分析存储的记忆(以聚合、保护隐私的方式),可以从噪声中提取洞察。例如,零售聊天机器人可能发现许多用户在询问特定产品的退货政策,标记潜在问题。
  • Agent 自我改进与适应:Agent 通过创建关于自身表现的程序记忆来从以前的运行中学习——记录哪些策略、工具或推理路径导致了成功的结果。这使 Agent 能够建立有效解决方案的”剧本”,允许其随时间适应并改进问题解决。

在 AI 系统中创建、存储和利用记忆是一个协作过程。栈中的每个组件——从最终用户到开发者的代码——都有其独特的角色:

  1. 用户:提供记忆的原始数据来源。在某些系统中,用户可以直接提供记忆(例如通过表单)。
  2. Agent(开发者逻辑):配置如何决定记住什么以及何时记住,协调对记忆管理器的调用。在简单架构中,开发者可以实现始终检索记忆并始终触发生成的逻辑。在更高级的架构中,开发者可以实现”记忆作为工具”,Agent(通过 LLM)决定何时应该检索或生成记忆。
  3. Agent 框架(如 ADK、LangGraph):提供记忆交互的结构和工具。框架充当管道,定义开发者的逻辑如何访问对话历史并与记忆管理器交互,但它本身不管理长期存储。它还定义如何将检索到的记忆填充到上下文窗口中。
  4. 会话存储(如 Agent Engine Sessions、Spanner、Redis):存储会话的逐轮对话。原始对话将被摄入记忆管理器以生成记忆。
  5. 记忆管理器(如 Agent Engine Memory Bank、Mem0、Zep):处理记忆的存储、检索和压缩。存储和检索记忆的机制取决于使用的提供商。这是专门的服务或组件,接收 Agent 识别的潜在记忆并处理其整个生命周期:
    • 提取(Extraction):从源数据中提炼关键信息
    • 整合(Consolidation):管理记忆以合并重复实体
    • 存储(Storage):将记忆持久化到持久化数据库
    • 检索(Retrieval):获取相关记忆以为新交互提供上下文
图 5:会话、记忆与外部知识之间的信息流
图 5:会话、记忆与外部知识之间的信息流

职责的划分确保开发者可以专注于 Agent 的独特逻辑,而无需构建复杂的底层记忆持久化和管理基础设施。重要的是要认识到,记忆管理器是一个主动系统,而不仅仅是一个被动的向量数据库。虽然它使用相似性搜索进行检索,但其核心价值在于随时间智能地提取、整合和管理记忆的能力。Agent Engine Memory Bank 等托管记忆服务处理记忆生成和存储的整个生命周期,让你专注于 Agent 的核心逻辑。

这种检索能力也是记忆经常与另一个关键架构模式——检索增强生成(RAG)——进行比较的原因。然而,它们建立在不同的架构原则之上:RAG 处理静态的外部数据,而记忆则管理动态的、用户特定的上下文。它们履行两种不同且互补的角色:RAG 使 Agent 成为事实专家,而记忆使其成为用户专家。下表列出了它们的高层次差异:

维度RAG 引擎记忆管理器
主要目标将外部事实知识注入上下文创建个性化和有状态的体验。Agent 记住事实,随时间适应用户,并维护长期运行的上下文
数据来源静态、预索引的外部知识库(如 PDF、维基、文档、API)用户与 Agent 之间的对话
隔离级别通常共享。知识库通常是全局的只读资源,所有用户均可访问,以确保一致的事实答案高度隔离:记忆几乎总是按用户范围划分,以防止数据泄露
信息类型静态、事实性和权威性。通常包含领域特定数据、产品详情或技术文档动态且通常用户特定。记忆来自对话,因此具有固有的不确定性
写入模式批处理。由离线管理操作触发基于事件的处理。在某个节奏(即每轮或会话结束时)或”记忆作为工具”(Agent 决定生成记忆)时触发
读取模式RAG 数据几乎总是”作为工具”检索。当 Agent 判断用户查询需要外部信息时检索两种常见读取模式:记忆作为工具(当用户查询需要关于用户的额外信息时检索);静态检索(每轮开始时始终检索记忆)
数据格式自然语言”块”自然语言片段或结构化配置文件
数据准备分块和索引:源文档被分解为更小的块,转换为嵌入向量并存储以供快速查找提取和整合:从对话中提取关键细节,确保内容不重复或相互矛盾

表 1:RAG 引擎与记忆管理器的比较

理解两者差异的一个有益方式是将 RAG 视为 Agent 的研究馆员,将记忆管理器视为其私人助理。

研究馆员(RAG)在一个装满百科全书、教科书和官方文件的庞大公共图书馆中工作。当 Agent 需要已知事实——如产品的技术规格或历史日期——时,它咨询馆员。馆员从这个静态、共享和权威的知识库中检索信息,提供一致的事实答案。馆员是世界事实的专家,但对提问的用户一无所知。

相比之下,私人助理(记忆)跟随 Agent,携带一个私人笔记本,记录与特定用户每次交互的细节。这个笔记本是动态的、高度隔离的,包含个人偏好、过去的对话和不断演变的目标。当 Agent 需要回忆用户最喜欢的球队或上周项目讨论的上下文时,它向助理询问。助理的专业知识不在于全球事实,而在于用户本身。

最终,真正智能的 Agent 两者都需要。RAG 为其提供对世界的专业知识,而记忆为其提供对所服务用户的深入理解。

下一节通过检查记忆的核心组件来解构记忆的概念:它存储的信息类型、其组织模式、其存储和创建的机制、其范围的战略定义,以及对多模态与文本数据的处理。

记忆的类型

Agent 的记忆可以按信息的存储方式和捕获方式进行分类。这些不同类型的记忆协同工作,创建对用户及其需求的丰富、上下文性理解。在所有类型的记忆中,有一条规则成立:记忆是描述性的,而非预测性的

“记忆”是记忆管理器返回并被 Agent 用作上下文的原子上下文片段。虽然确切的模式可能有所不同,但单条记忆通常由两个主要组件组成:内容和元数据。

内容是从源数据(即会话的原始对话)中提取的记忆实质。关键是,内容被设计为与框架无关,使用任何 Agent 都可以轻松摄入的简单数据结构。内容可以是结构化或非结构化数据:

  • 结构化记忆:通常以字典或 JSON 等通用格式存储信息,其模式通常由开发者而非特定框架定义。例如:{"seat_preference": "Window"}
  • 非结构化记忆:捕捉更长交互、事件或主题本质的自然语言描述。例如:“用户偏好靠窗座位。”

元数据提供关于记忆的上下文,通常以简单字符串形式存储。这可以包括记忆的唯一标识符、记忆”所有者”的标识符,以及描述记忆内容或数据来源的标签。

信息类型

超越基本结构,记忆可以按其代表的基本知识类型进行分类。这种区分,对于理解 Agent 如何使用记忆至关重要,将记忆分为来自认知科学的两个主要功能类别:陈述性记忆(“知道什么”)和程序性记忆(“知道如何”)。

陈述性记忆是 Agent 对事实、数据和事件的知识。这是所有 Agent 可以明确陈述或”声明”的信息。如果记忆是对”什么”问题的回答,它就是陈述性的。这个类别包括一般世界知识(语义)和特定用户事实(实体/情节性)。

程序性记忆是 Agent 对技能和工作流程的知识。它通过隐式展示如何正确执行任务来引导 Agent 的行动。如果记忆帮助回答”如何”的问题——例如预订旅行的正确工具调用序列——它就是程序性的。

组织模式

一旦创建了记忆,下一个问题是如何组织它。记忆管理器通常使用以下一种或多种模式来组织记忆:集合(Collections)、结构化用户配置文件或”滚动摘要”。这些模式定义了各个记忆之间以及与用户之间的关系。

集合模式将内容组织为单个用户的多个独立的自然语言记忆。每条记忆是一个独特的事件、摘要或观察,尽管可能有多条记忆涉及单个高层主题。集合允许存储和搜索与特定目标或主题相关的更大、结构较少的信息池。

结构化用户配置文件模式将记忆组织为关于用户的一组核心事实,就像一张不断用新的、稳定信息更新的联系卡。它被设计用于快速查找基本的、事实性信息,如姓名、偏好和账户详细信息。

与结构化用户配置文件不同,“滚动”摘要模式将所有信息整合到一个不断演化的记忆中,代表整个用户-Agent 关系的自然语言摘要。与创建新的个别记忆不同,管理器不断更新这个主文档。这种模式经常用于压缩长会话,在管理整体 token 数量的同时保留关键信息。

存储架构

此外,存储架构是决定 Agent 检索记忆速度和智能程度的关键决策。架构的选择定义了 Agent 是否擅长找到概念相似的想法、理解结构化关系,或两者兼顾。

记忆通常存储在向量数据库和/或知识图谱中。向量数据库帮助找到与查询概念相似的记忆;知识图谱将记忆存储为实体及其关系的网络。

向量数据库是最常见的方法,支持基于语义相似性而非精确关键词的检索。记忆被转换为嵌入向量,数据库找到与用户查询在概念上最接近的匹配项。这在检索上下文和含义是关键的非结构化自然语言记忆(即”原子事实”)方面表现出色。

知识图谱用于将记忆存储为实体(节点)及其关系(边)的网络。检索涉及遍历此图以找到直接和间接连接,允许 Agent 推理不同事实之间的联系。它非常适合结构化的关系查询和理解数据内的复杂连接(即”知识三元组”)。

还可以将两种方法组合成混合方法,通过向量嵌入丰富知识图谱的结构化实体。这使系统能够同时执行关系搜索和语义搜索,提供图谱的结构化推理和向量数据库的细微概念搜索,兼得两者之长。

创建机制

我们还可以根据记忆的创建方式(包括信息的推导方式)对记忆进行分类。显式记忆是当用户给 Agent 直接命令记住某事时创建的(例如,“记住我的周年纪念日是 10 月 26 日”)。另一方面,隐式记忆是当 Agent 在没有直接命令的情况下从对话中推断和提取信息时创建的(例如,“下周是我的周年纪念日,你能帮我给伴侣找一份礼物吗?”)。

记忆还可以根据记忆提取逻辑是位于 Agent 框架内部还是外部来区分:

  • 内部记忆是指直接内置于 Agent 框架的记忆管理。它便于入门,但通常缺乏高级功能。内部记忆可以使用外部存储,但生成记忆的机制在 Agent 内部。
  • 外部记忆涉及使用专门的、独立的记忆管理服务(如 Agent Engine Memory Bank、Mem0、Zep)。Agent 框架向这个外部服务发起 API 调用来存储、检索和处理记忆。这种方法提供更复杂的功能,如语义搜索、实体提取和自动摘要化,将复杂的记忆管理任务转移给专门构建的工具。

记忆范围

还需要考虑记忆描述的是谁或什么。这对你用来聚合和检索记忆的实体(即用户、会话或应用)有影响。

用户级范围是最常见的实现,旨在为每个人创建连续的、个性化的体验;例如,“用户偏好中间座位。“记忆与特定用户 ID 绑定,在其所有会话中持久化,允许 Agent 建立对其偏好和历史的长期理解。

会话级范围是为压缩长对话而设计的;例如,“用户正在购买 2025 年 11 月 7 日至 14 日之间纽约和巴黎之间的机票。他们偏好直飞航班和中间座位。“它创建从单个会话提取的洞察的持久记录,允许 Agent 用一组简洁的关键事实替换冗长的、占用大量 token 的记录。关键是,这种记忆不同于原始会话日志;它只包含来自对话的处理洞察,而非对话本身,其上下文被隔离到该特定会话。

应用级范围(或全局上下文)是所有用户均可访问的记忆;例如,“代号 XYZ 指的是该项目……”这种范围用于提供共享上下文、广播全系统信息,或建立公共知识基线。应用级记忆的常见用例是程序记忆,为 Agent 提供”操作指南”指令;这些记忆通常旨在帮助所有用户的 Agent 推理。关键是这些记忆必须清除所有敏感内容,以防止用户之间的数据泄露。

多模态记忆

“多模态记忆”是一个关键概念,描述 Agent 如何处理非文本信息,如图像、视频和音频。关键是区分记忆的来源(数据来自哪里)和记忆的内容(数据存储为什么)。

来自多模态来源的记忆是最常见的实现。Agent 可以处理各种数据类型——文本、图像、音频——但其创建的记忆是从该来源派生的文本洞察。例如,Agent 可以处理用户的语音备忘录来创建记忆。它不存储音频文件本身;而是转录音频并创建文本记忆,如”用户对最近的配送延迟表达了不满。”

包含多模态内容的记忆是更高级的方法,记忆本身包含非文本媒体。Agent 不仅仅描述内容;它直接存储内容。例如,用户可以上传图像并说”记住这是我们的 Logo 设计。” Agent 创建一个直接包含图像文件的记忆,与用户的请求相关联。

大多数当代记忆管理器专注于处理多模态来源同时生成文本内容。这是因为生成和检索非结构化二进制数据(如图像或音频)需要专门的模型、算法和基础设施。将所有输入转换为通用的、可搜索的格式(文本)要简单得多。

例如,可以使用 Agent Engine Memory Bank 从多模态输入生成记忆。输出的记忆将是从内容中提取的文本洞察:

from google.genai import types
client = vertexai.Client(project=..., location=...)
response = client.agent_engines.memories.generate(
    name=agent_engine_name,
    direct_contents_source={
        "events": [
            {
                "content": types.Content(
                    role="user",
                    parts=[
                        types.Part.from_text(
                            "This is context about the multimodal input."
                        ),
                        types.Part.from_bytes(
                            data=CONTENT_AS_BYTES,
                            mime_type=MIME_TYPE
                        ),
                        types.Part.from_uri(
                            file_uri="file/path/to/content",
                            mime_type=MIME_TYPE
                        )
                    ])}]},
    scope={"user_id": user_id}
)

代码片段 5:Agent Engine Memory Bank 的记忆生成 API 调用示例

记忆生成:提取与整合

记忆生成自主地将原始对话数据转化为结构化的、有意义的洞察。可以将其视为 LLM 驱动的 ETL(提取、转换、加载)管道,专门用于提取和浓缩记忆。记忆生成的 ETL 管道将记忆管理器与 RAG 引擎和传统数据库区分开来。与其要求开发者手动指定数据库操作,记忆管理器使用 LLM 来智能决定何时添加、更新或合并记忆。这种自动化是记忆管理器的核心优势;它抽象掉了管理数据库内容、链接 LLM 调用以及为数据处理部署后台服务的复杂性。

图 6:记忆生成的高层算法,从新数据源提取记忆并与现有记忆整合
图 6:记忆生成的高层算法,从新数据源提取记忆并与现有记忆整合

虽然不同平台(如 Agent Engine Memory Bank、Mem0、Zep)的具体算法有所不同,但记忆生成的高层过程通常遵循以下四个阶段:

  1. 摄入(Ingestion):当客户端向记忆管理器提供原始数据来源(通常是对话历史)时,过程开始。
  2. 提取与过滤(Extraction & Filtering):记忆管理器使用 LLM 从源数据中提取有意义的内容。关键是,这个 LLM 并不提取所有内容;它只捕获符合预定义主题定义的信息。如果摄入的数据不包含与这些主题匹配的信息,则不创建记忆。
  3. 整合(Consolidation):这是最复杂的阶段,记忆管理器处理冲突解决和去重。它执行”自我编辑”过程,使用 LLM 将新提取的信息与现有记忆进行比较。为了确保用户的知识库保持连贯、准确,并随时间基于新信息演变,管理器可以决定:
    • 将新洞察合并到现有记忆中
    • 如果现有记忆已被作废则删除
    • 如果主题是全新的则创建全新记忆
  4. 存储(Storage):最后,新的或更新的记忆被持久化到耐久存储层(如向量数据库或知识图谱),以便在未来交互中检索。

像 Agent Engine Memory Bank 这样的托管记忆管理器完全自动化这个管道。它们提供一个单一的、连贯的系统,将对话噪声转化为结构化知识,让开发者专注于 Agent 逻辑,而不是自己构建和维护底层数据基础设施。例如,使用 Memory Bank 触发记忆生成只需一个简单的 API 调用:

from google.cloud import vertexai
client = vertexai.Client(project=..., location=...)
client.agent_engines.memories.generate(
    name="projects/.../locations/...reasoningEngines/...",
    scope={"user_id": "123"},
    direct_contents_source={
        "events": [...]
    },
    config={
        # 在后台运行记忆生成
        "wait_for_completion": False
    }
)

代码片段 6:使用 Agent Engine Memory Bank 生成记忆

记忆生成过程可以比作勤劳园丁照料花园的工作。提取就像接收新的种子和幼苗(来自对话的新信息)。园丁不是随意将它们扔在地里,而是通过整合来拔除杂草(删除冗余或相互矛盾的数据),修剪过度生长的枝条以改善现有植物的健康状况(精炼和摘要化现有记忆),然后小心地将新幼苗种植在最佳位置。这种持续的、深思熟虑的策划确保花园保持健康、有序,并随时间继续繁荣,而不是变成杂乱不堪、无法使用的状态。这个异步过程在后台发生,确保花园始终为下次访问做好准备。

现在,让我们深入了解记忆生成的两个关键步骤:提取和整合。

深入解析:记忆提取

记忆提取的目标是回答一个根本性问题:“这次对话中哪些信息有意义到足以成为记忆?” 这不是简单的摘要化;它是一个有针对性的、智能的过滤过程,旨在从噪声(礼貌用语、填充文字)中分离信号(重要事实、偏好、目标)。

“有意义”不是一个通用概念;它完全由 Agent 的目的和用例定义。客户支持 Agent 需要记住的内容(例如订单号、技术问题)与个人健康教练需要记住的内容(例如长期目标、情感状态)根本不同。因此,自定义保留哪些信息是创建真正有效 Agent 的关键。

记忆管理器的 LLM 通过遵循一套仔细构建的程序化护栏和指令来决定提取什么,通常嵌入在复杂的系统提示词中。这个提示词通过为 LLM 提供一组主题定义来定义”有意义”的含义:

  • 基于模式和模板的提取:LLM 被提供预定义的 JSON 模式或使用 LLM 特性(如结构化输出)的模板;指示 LLM 使用对话中的相应信息构建 JSON。
  • 自然语言主题定义:LLM 受到简单自然语言描述主题的引导。
  • 少样本提示:使用示例”展示”LLM 提取哪些信息。提示词包含几个输入文本示例和应该提取的理想、高保真记忆。LLM 从示例中学习所需的提取模式,使其对难以用模式或简单定义描述的自定义或细微主题非常有效。

大多数记忆管理器开箱即用地寻找常见主题,如用户偏好、关键事实或目标。许多平台还允许开发者定义自己的自定义主题,根据其特定领域定制提取过程。例如,可以通过提供自定义主题定义和少样本示例来自定义 Agent Engine Memory Bank 认为有意义的信息:

from google.genai.types import Content, Part
# 更多信息请见 https://cloud.google.com/agent-builder/agent-engine/memory-bank/set-up
memory_bank_config = {
    "customization_configs": [{
        "memory_topics": [
            { "managed_memory_topic": {"managed_topic_enum": "USER_PERSONAL_INFO" }},
            {
                "custom_memory_topic": {
                    "label": "business_feedback",
                    "description": """用户在咖啡店的体验反馈。
                    包括对饮品、食物、甜点、氛围、员工友好度、
                    服务速度、清洁度以及改进建议的意见。"""
                }
            }
        ],
        "generate_memories_examples": {
            "conversationSource": {
                "events": [
                    {
                        "content": Content(
                            role="model",
                            parts=[Part(text="欢迎再次光临 The Daily Grind!我们很想听听您的访问反馈。")])
                    },{
                        "content": Content(
                            role="user",
                            parts=[Part(text="嘿,今天的滴漏咖啡有点温,有点遗憾。还有,音乐太响了,我几乎听不到朋友说话。")])
                    }]
            },
            "generatedMemories": [
                {"fact": "用户反映滴漏咖啡是温的。"},
                {"fact": "用户觉得店内音乐太响了。"}
            ]
        }
    }]
}
agent_engine = client.agent_engines.create(
    config={
        "context_spec": {"memory_bank_config": memory_bank_config }
    }
)

代码片段 7:自定义 Agent Engine Memory Bank 认为有意义的待持久化信息

虽然记忆提取本身不是”摘要化”,但算法可能结合摘要化来提炼信息。为提高效率,许多记忆管理器将对话的滚动摘要直接纳入记忆提取提示词中。这个压缩的历史提供了从最近交互中提取关键信息的必要上下文,无需在每轮重复处理完整的、详细的对话来维护上下文。

从数据来源提取信息后,必须通过整合来更新现有记忆语料库以反映新信息。

深入解析:记忆整合

从详细对话中提取记忆后,整合应将新信息集成到连贯的、准确的、不断演变的知识库中。这可以说是记忆生命周期中最复杂的阶段,将简单的事实集合转化为对用户的精心理解。没有整合,Agent 的记忆很快就会成为嘈杂的、相互矛盾的、不可靠的曾经捕获的所有信息日志。这种”自我策划”通常由 LLM 管理,是将记忆管理器提升到简单数据库之上的原因。

整合解决了对话数据产生的基本问题,包括:

  • 信息重复:用户可能在多次对话中以不同方式提及相同的事实(例如,“我需要飞往纽约的航班”,后来又说”我在计划去纽约的旅行”)。简单的提取过程会创建两条冗余记忆。
  • 信息冲突:用户的状态随时间变化。没有整合,Agent 的记忆会包含相互矛盾的事实。
  • 信息演变:一个简单的事实可能变得更加细化。关于”用户对营销感兴趣”的初始记忆可能演变为”用户正在领导一个专注于第四季度客户获取的营销项目”。
  • 记忆相关性衰减:并非所有记忆都永远有用。Agent 必须参与”遗忘”——主动剪枝旧的、陈旧的或低置信度的记忆,以保持知识库的相关性和效率。遗忘可以通过在整合期间指示 LLM 优先使用较新信息,或通过存活时间(TTL)自动删除来实现。

整合过程是一个 LLM 驱动的工作流,将新提取的洞察与用户现有记忆进行比较。首先,工作流尝试检索与新提取记忆相似的现有记忆,这些现有记忆是整合的候选。如果现有记忆被新信息所矛盾,它可能被删除;如果被扩充,它可能被更新。

其次,LLM 同时接收现有记忆和新信息。其核心任务是分析它们并确定应执行什么操作。主要操作包括:

  • 更新(UPDATE):用新的或更正的信息修改现有记忆。
  • 创建(CREATE):如果新洞察完全新颖且与现有记忆无关,创建新记忆。
  • 删除/作废(DELETE/INVALIDATE):如果新信息使旧记忆完全不相关或不正确,删除或作废它。

最后,记忆管理器将 LLM 的决定转换为更新记忆存储的事务。

记忆溯源

机器学习的经典公理”垃圾进,垃圾出”对 LLM 来说更为关键,因为结果往往是”垃圾进,自信的垃圾出”。为了使 Agent 做出可靠决策,并使记忆管理器有效整合记忆,它们必须能够批判性地评估自身记忆的质量。这种可信度直接来自记忆的溯源——对其起源和历史的详细记录。

图 7:数据来源与记忆之间的信息流。单条记忆可以来自多个数据来源,单个数据来源可以贡献多条记忆。
图 7:数据来源与记忆之间的信息流。单条记忆可以来自多个数据来源,单个数据来源可以贡献多条记忆。

记忆整合过程——将来自多个来源的信息合并到单个不断演变的记忆中——创造了跟踪其谱系的需要。如上图所示,单条记忆可能是多个数据来源的混合,而单个来源可能被分割为多条记忆。

为了评估可信度,Agent 必须跟踪每个来源的关键细节,例如其起源(来源类型)和年龄(“新鲜度”)。这些细节因两个原因至关重要:它们决定了每个来源在记忆整合期间的权重,以及通知 Agent 在推理期间应多大程度依赖该记忆。

来源类型是确定信任度最重要的因素之一。数据来源分为三个主要类别:

  • 引导数据(Bootstrapped Data):从内部系统预加载的信息,例如 CRM。这种高信任数据可用于初始化用户记忆,解决冷启动问题——即为 Agent 从未交互过的用户提供个性化体验的挑战。
  • 用户输入(User Input):包括通过表单显式提供的数据(高信任)或从对话中隐式提取的信息(通常可信度较低)。
  • 工具输出(Tool Output):外部工具调用返回的数据。通常不鼓励从工具输出生成记忆,因为这些记忆往往脆弱且陈旧,使这种来源类型更适合短期缓存。

记忆管理期间的谱系考虑

这种动态的、多来源的记忆方法在管理记忆时创造了两个主要操作挑战:冲突解决和删除派生数据。

记忆整合不可避免地会导致一个数据来源与另一个冲突。记忆的溯源允许记忆管理器为其信息来源建立信任层次结构。当来自不同来源的记忆相互矛盾时,Agent 必须在冲突解决策略中使用这个层次结构。常见策略包括优先使用最受信任的来源、支持最新信息,或在多个数据点之间寻找相互印证。

管理记忆的另一个挑战发生在删除记忆时。记忆可以来自多个数据来源。当用户撤销对某个数据来源的访问时,从该来源派生的数据也应被删除。删除每条被该来源”接触”过的记忆可能过于激进。更精确(尽管计算成本更高)的方法是仅使用剩余的有效来源从头重新生成受影响的记忆。

超越静态溯源细节,对记忆的信心必须演变。信心通过印证增加,例如当多个受信任的来源提供一致信息时。然而,高效的记忆系统还必须通过记忆剪枝主动策划其现有知识——识别并”遗忘”不再有用的记忆的过程。这种剪枝可以由几个因素触发:

  • 基于时间的衰减:记忆的重要性可能随时间降低。关于两年前会议的记忆可能比上周的记忆相关性低。
  • 低置信度:从弱推断创建且从未被其他来源印证的记忆可能被剪枝。
  • 不相关性:随着 Agent 对用户建立更复杂的理解,它可能确定某些旧的、琐碎的记忆与用户当前目标不再相关。

通过将反应性整合管道与主动剪枝相结合,记忆管理器确保 Agent 的知识库不只是不断增长的所有曾说过的话的日志,而是对用户精心策划的、准确的、相关的理解。

推理期间的谱系考虑

除了在策划语料库内容时考虑记忆的谱系外,记忆的可信度也应在推理时加以考虑。Agent 对记忆的信心不应是静态的;它必须根据新信息和时间推移而演变。信心通过印证增加,例如当多个受信任的来源提供一致信息时。相反,信心随着旧记忆变陈旧而随时间降低(衰减),当引入相互矛盾的信息时也会下降。最终,系统可以通过归档或删除低置信度记忆来”遗忘”。这个动态置信度分数在推理时至关重要。

记忆和其置信度分数(如果可用)被注入提示词,使 LLM 能够评估信息可靠性并做出更细致的决策,而不是直接显示给用户。

整个信任框架服务于 Agent 的内部推理过程。记忆及其置信度分数通常不直接显示给用户。相反,它们被注入系统提示词,允许 LLM 权衡证据、考虑信息的可靠性,并最终做出更细致和可信的决策。

触发记忆生成

虽然记忆管理器在触发生成后自动化记忆提取和整合,但 Agent 仍必须决定何时应尝试记忆生成。这是一个关键的架构选择,需要在数据新鲜度与计算成本和延迟之间取得平衡。这个决定通常由 Agent 的逻辑管理,可以采用几种触发策略。记忆生成可以基于各种事件发起:

  • 会话完成:在多轮会话结束时触发生成。
  • 轮次节奏:在特定数量的轮次后运行过程(例如每 5 轮)。
  • 实时:在每轮之后生成记忆。
  • 显式命令:在用户直接命令时激活过程(例如,“记住这个”)。

触发器的选择涉及成本与保真度之间的直接权衡。频繁生成(例如实时)确保记忆高度详细和新鲜,捕获对话的每个细节。然而,这会产生最高的 LLM 和数据库成本,如果处理不当可能引入延迟。不频繁的生成(例如在会话完成时)成本效率要高得多,但有创建低保真度记忆的风险,因为 LLM 必须一次性摘要更大的对话块。还需要注意确保记忆管理器不会多次处理相同的事件,因为这会引入不必要的成本。

记忆作为工具

更复杂的方法是允许 Agent 自行决定何时创建记忆。在这种模式中,记忆生成作为工具暴露(即 create_memory);工具定义应定义哪些类型的信息应被视为有意义。Agent 然后可以分析对话并在识别到有意义的值得持久化的信息时自主决定调用此工具。这将”识别有意义信息”的责任从外部记忆管理器转移到 Agent(以及作为开发者的你)本身。

例如,可以使用 ADK 通过将记忆生成代码打包成工具来实现这一点,Agent 在认为对话值得持久化时自主调用。可以将会话发送给 Memory Bank,Memory Bank 将从对话历史中提取和整合记忆:

from google.adk.agents import LlmAgent
from google.adk.memory import VertexAiMemoryBankService
from google.adk.runners import Runner
from google.adk.tools import ToolContext
def generate_memories(tool_context: ToolContext):
    """触发记忆生成以记住会话。"""
    # 选项 1:使用 ADK 记忆服务从完整对话历史中提取记忆
    tool_context._invocation_context.memory_service.add_session_to_memory(
        session)
    # 选项 2:从最后一轮对话中提取记忆
    client.agent_engines.memories.generate(
        name="projects/.../locations/...reasoningEngines/...",
        direct_contents_source={
            "events": [
                {"content": tool_context._invocation_context.user_content}
            ]
        },
        scope={
            "user_id": tool_context._invocation_context.user_id,
            "app_name": tool_context._invocation_context.app_name
        },
        # 在后台生成记忆
        config={"wait_for_completion": False}
    )
    return {"status": "success"}
agent = LlmAgent(
    ...,
    tools=[generate_memories]
)
runner = Runner(
    agent=agent,
    app_name=APP_NAME,
    session_service=session_service,
    memory_service=VertexAiMemoryBankService(
        agent_engine_id=AGENT_ENGINE_ID,
        project=PROJECT,
        location=LOCATION
    )
)

代码片段 8:使用自定义工具触发记忆生成的 ADK Agent。Memory Bank 将提取和整合记忆。

另一种方法是利用内部记忆,Agent 主动决定从对话中记住什么。在这个工作流中,Agent 负责提取关键信息。可选地,这些提取的记忆然后被发送到 Agent Engine Memory Bank 以与用户现有记忆进行整合:

def extract_memories(query: str, tool_context: ToolContext):
    """触发记忆生成以记住信息。
    参数:
        query: 应该关于用户持久化的有意义信息。
    """
    client.agent_engines.memories.generate(
        name="projects/.../locations/...reasoningEngines/...",
        # 有意义信息已从对话中提取,所以只需与同一用户的现有记忆整合
        direct_memories_source={
            "direct_memories": [{"fact": query}]
        },
        scope={
            "user_id": tool_context._invocation_context.user_id,
            "app_name": tool_context._invocation_context.app_name
        },
        config={"wait_for_completion": False}
    )
    return {"status": "success"}
agent = LlmAgent(
    ...,
    tools=[extract_memories]
)

代码片段 9:使用自定义工具从对话中提取记忆并触发与 Agent Engine Memory Bank 整合的 ADK Agent。与代码片段 8 不同,Agent 负责提取记忆,而非 Memory Bank。

后台与阻塞操作

记忆生成是一个需要 LLM 调用和数据库写入的昂贵操作。对于生产中的 Agent,记忆生成几乎总是应该作为后台进程异步处理。

Agent 向用户发送响应后,记忆生成管道可以在不阻塞用户体验的情况下并行运行。这种解耦对于保持 Agent 快速响应至关重要。阻塞式(或同步)方法——用户必须等待记忆写入才能收到响应——会造成令人无法接受的缓慢且令人沮丧的用户体验。这要求记忆生成发生在与 Agent 核心运行时在架构上分离的服务中。

记忆检索

有了记忆生成机制,就可以将注意力转向检索的关键任务。智能检索策略对于 Agent 的性能至关重要,涵盖关于应检索哪些记忆以及何时检索的决策。

检索记忆的策略很大程度上取决于记忆的组织方式。对于结构化用户配置文件,检索通常是完整配置文件或特定属性的简单查找。然而,对于记忆集合,检索是一个复杂得多的搜索问题。目标是从大量非结构化或半结构化数据中发现最相关的、概念上相关的信息。本节讨论的策略旨在解决记忆集合的这个复杂检索挑战。

记忆检索搜索当前对话最相关的记忆。有效的检索策略至关重要;提供不相关的记忆可能混淆模型并降低其响应质量,而找到完美的上下文片段可以带来非常智能的交互。核心挑战是在严格的延迟预算内平衡记忆的”有用性”。

高级记忆系统超越简单搜索,跨多个维度对候选记忆进行评分以找到最佳匹配:

  • 相关性(语义相似性):这条记忆与当前对话在概念上的相关程度?
  • 时效性(基于时间):这条记忆最近创建的时间?
  • 重要性(显著性):这条记忆整体上有多关键?与相关性不同,记忆的”重要性”可以在生成时定义。

仅依赖基于向量的相关性是一个常见陷阱。相似度分数可能会浮现出概念相似但陈旧或琐碎的记忆。最有效的策略是结合所有三个维度分数的混合方法。

对于准确性至关重要的应用,检索可以使用查询重写、重排序或专门检索器等方法进行细化。然而,这些技术计算成本高且增加显著延迟,使其不适合大多数实时应用。对于这些复杂算法必要且记忆不会很快变陈旧的场景,缓存层可以是有效的缓解措施。缓存允许临时存储检索查询的昂贵结果,绕过后续相同请求的高延迟成本。

通过查询重写,可以使用 LLM 改进搜索查询本身。这可能涉及将用户模糊的输入重写为更精确的查询,或将单个查询扩展为多个相关查询以捕获主题的不同方面。虽然这显著提高了初始搜索结果的质量,但在过程开始时增加了额外 LLM 调用的延迟。

通过重排序,初始检索使用相似性搜索获取一组广泛的候选记忆(例如前 50 个结果),然后 LLM 可以重新评估和重新排列这个较小的集合以产生更准确的最终列表。

最后,可以通过微调训练专门检索器。然而,这需要访问标注数据并可能显著增加成本。

最终,最佳检索方法始于更好的记忆生成。确保记忆语料库高质量且没有不相关信息,是保证任何检索到的记忆集都有帮助的最有效方法。

检索时机

检索的最终架构决策是何时检索记忆。一种方法是主动检索,记忆在每轮开始时自动加载。这确保上下文始终可用,但为不需要记忆访问的轮次引入了不必要的延迟。由于记忆在单轮内保持静态,可以有效地缓存它们以减轻此性能成本。

例如,可以使用内置的 PreloadMemoryTool 或自定义回调在 ADK 中实现主动检索:

# 选项 1:使用内置 PreloadMemoryTool,每轮使用相似性搜索检索记忆
agent = LlmAgent(
    ...,
    tools=[adk.tools.preload_memory_tool.PreloadMemoryTool()]
)
# 选项 2:使用自定义回调以更好地控制记忆检索方式
def retrieve_memories_callback(callback_context, llm_request):
    user_id = callback_context._invocation_context.user_id
    app_name = callback_context._invocation_context.app_name
    response = client.agent_engines.memories.retrieve(
        name="projects/.../locations/...reasoningEngines/...",
        scope={
            "user_id": user_id,
            "app_name": app_name
        }
    )
    memories = [f"* {memory.memory.fact}" for memory in list(response)]
    if not memories:
        # 没有记忆添加到系统指令
        return
    # 将格式化的记忆追加到系统指令
    llm_request.config.system_instruction += "\nHere is information that you have about the user:\n"
    llm_request.config.system_instruction += "\n".join(memories)
agent = LlmAgent(
    ...,
    before_model_callback=retrieve_memories_callback,
)

代码片段 10:使用内置工具或自定义回调在每轮开始时用 ADK 检索记忆

或者,可以使用反应式检索(“记忆作为工具”),Agent 被提供一个查询其记忆的工具,自行决定何时检索上下文。这更高效和健壮,但需要额外的 LLM 调用,增加延迟和成本;但记忆只在必要时检索,所以延迟成本发生的频率更低。此外,Agent 可能不知道是否存在相关信息可以检索。然而,可以通过让 Agent 了解可用的记忆类型(例如,在工具描述中,如果使用自定义工具)来缓解这个问题,从而对何时查询做出更明智的决定。

# 选项 1:使用内置 LoadMemory
agent = LlmAgent(
    ...,
    tools=[adk.tools.load_memory_tool.LoadMemoryTool()],
)
# 选项 2:使用可以描述可能可用信息类型的自定义工具
def load_memory(query: str, tool_context: ToolContext):
    """为用户检索记忆。
    以下类型的信息可能为用户存储:
    * 用户偏好,例如用户最喜欢的食物
    ...
    """
    # 使用相似性搜索检索记忆
    response = tool_context.search_memory(query)
    return response.memories
agent = LlmAgent(
    ...,
    tools=[load_memory],
)

代码片段 11:使用内置或自定义工具配置 ADK Agent 以决定何时检索记忆

使用记忆进行推理

一旦检索到相关记忆,最后一步是将它们战略性地放入模型的上下文窗口。这是一个关键过程;记忆的放置可以显著影响 LLM 的推理,影响操作成本,并最终决定最终答案的质量。

记忆主要通过追加到系统指令或注入对话历史来呈现。实践中,混合策略通常最有效:对稳定的、全局性记忆(如用户配置文件)使用系统提示词,这些记忆应始终存在;对于仅与对话即时上下文相关的短暂、情节性记忆,使用对话注入或”记忆作为工具”。这在持久上下文的需求与即时信息检索的灵活性之间取得了平衡。

系统指令中的记忆

在推理中使用记忆的一个简单选项是将记忆追加到系统指令中。这种方法通过将检索到的记忆直接追加到系统提示词旁边的前言中,保持对话历史清晰,将记忆作为整个交互的基础上下文。例如,可以使用 Jinja 动态添加记忆到系统指令中:

from jinja2 import Template
template = Template("""
{{ system_instructions }}
<MEMORIES>
Here is some information about the user:
{% for retrieved_memory in data %}* {{ retrieved_memory.memory.fact }}
{% endfor %}</MEMORIES>
""")
prompt = template.render(
    system_instructions=system_instructions,
    data=retrieved_memories
)

代码片段 12:使用检索到的记忆构建系统指令

将记忆包含在系统指令中给予记忆高权威性,清晰地将上下文与对话分离,非常适合稳定的、“全局”信息(如用户配置文件)。然而,存在过度影响的风险,即 Agent 可能尝试将每个主题都与其核心指令中的记忆关联,即使不合适。

这种架构模式引入了几个约束。首先,它要求 Agent 框架在每次 LLM 调用之前支持系统提示词的动态构建;这种功能并不总是现成支持的。此外,该模式与”记忆作为工具”不兼容,因为系统提示词必须在 LLM 决定调用记忆检索工具之前完成。最后,它对非文本记忆处理不佳。大多数 LLM 只接受系统指令的文本,使得直接在提示词中嵌入图像或音频等多模态内容变得困难。

对话历史中的记忆

在这种方法中,检索到的记忆直接注入逐轮对话中。记忆可以放置在完整对话历史之前,或紧接在最新用户查询之前。

然而,这种方法可能会嘈杂,增加 token 成本,并且如果检索到的记忆不相关,可能会混淆模型。其主要风险是对话注入,模型可能错误地将记忆视为在对话中实际说过的内容。还需要更加注意注入到对话中的记忆的视角;例如,如果使用”user”角色和用户级记忆,记忆应以第一人称视角撰写。

将记忆注入对话历史的一个特殊情况是通过工具调用检索记忆。记忆将作为工具输出的一部分直接包含在对话中。

def load_memory(query: str, tool_context: ToolContext):
    """将记忆加载到对话历史中..."""
    response = tool_context.search_memory(query)
    return response.memories
agent = LlmAgent(
    ...,
    tools=[load_memory],
)

代码片段 13:通过工具检索记忆,将记忆直接插入对话

程序性记忆

本白皮书主要关注陈述性记忆,这一集中反映了当前商业记忆领域的现状。大多数记忆管理平台也是为这种陈述性方法而设计的,擅长提取、存储和检索”什么”——事实、历史和用户数据。

然而,这些系统并非为管理程序性记忆而设计,程序性记忆是改进 Agent 工作流程和推理的机制。存储”如何”不是一个信息检索问题;它是一个推理增强问题。管理”知道如何”需要一个完全独立且专业化的算法生命周期,尽管具有相似的高层结构:

  1. 提取:程序性提取需要专门的提示词,旨在从成功的交互中提炼可重用的策略或”剧本”,而不仅仅是捕获事实或有意义的信息。
  2. 整合:陈述性整合合并相关事实(“什么”),而程序性整合则策划工作流本身(“如何”)。这是一个主动的逻辑管理过程,专注于将新的成功方法与现有”最佳实践”集成,修补已知计划中的缺陷步骤,以及剪枝过时或无效的程序。
  3. 检索:目标不是检索数据来回答问题,而是检索一个引导 Agent 如何执行复杂任务的计划。因此,程序性记忆可能具有与陈述性记忆不同的数据模式。

Agent”自我进化”其逻辑的这种能力自然会引发与常见适应方法的比较:微调——通常通过来自人类反馈的强化学习(RLHF)。虽然两个过程都旨在改进 Agent 行为,但其机制和应用从根本上不同。微调是一个相对缓慢的离线训练过程,改变模型权重。程序性记忆通过动态注入正确的”剧本”到提示词中,通过上下文学习引导 Agent,无需任何微调,提供快速的在线适应。

测试与评估

现在有了支持记忆的 Agent,应通过全面的质量和评估测试来验证其行为。评估 Agent 记忆是一个多层次的过程,需要验证 Agent 记住了正确的事物(质量)、在需要时能找到这些记忆(检索),以及使用这些记忆实际上有助于实现其目标(任务成功)。虽然学术界关注可重复的基准,但行业评估集中在记忆如何直接影响生产 Agent 的性能和可用性。

记忆生成质量指标评估记忆本身的内容,回答问题:“Agent 记住了正确的事物吗?“这通常通过将 Agent 生成的记忆与手动创建的”黄金集”理想记忆进行比较来衡量:

  • 精确率(Precision):Agent 创建的所有记忆中,有多少比例是准确且相关的?高精确率防范”过于积极”的记忆系统,该系统会用不相关的噪声污染知识库。
  • 召回率(Recall):应该记住的所有相关事实中,Agent 捕获了多少比例?高召回率确保 Agent 不会遗漏关键信息。
  • F1 分数:精确率和召回率的调和平均值,提供单一的质量均衡指标。

记忆检索性能指标评估 Agent 在正确时机找到正确记忆的能力:

  • Recall@K:当需要记忆时,正确的记忆是否在前 K 个检索结果中找到?这是检索系统准确性的主要指标。
  • 延迟:检索处于 Agent 响应的”热路径”上。整个检索过程必须在严格的延迟预算内执行(例如,200ms 以下),以避免降低用户体验。

端到端任务成功指标是最终测试,回答问题:“记忆真的帮助 Agent 更好地执行其工作吗?“这通过评估 Agent 使用其记忆在下游任务上的性能来衡量,通常使用 LLM”法官”将 Agent 的最终输出与黄金答案进行比较。法官确定 Agent 的答案是否准确,有效地衡量记忆系统对最终结果的贡献程度。

评估不是一次性事件;它是持续改进的引擎。上述指标提供了识别弱点和系统性增强记忆系统所需的数据。这个迭代过程涉及建立基线、分析失败、调整系统(例如细化提示词、调整检索算法),以及重新评估以衡量变更的影响。

虽然上述指标关注质量,但生产就绪性还取决于性能。对于每个评估领域,衡量底层算法的延迟及其在负载下的扩展能力至关重要。“热路径”上的记忆检索可能有严格的、亚秒级延迟预算。生成和整合虽然通常是异步的,但必须有足够的吞吐量来跟上用户需求。最终,成功的记忆系统必须对真实世界使用来说是智能的、高效的和健壮的。

记忆的生产注意事项

除了性能之外,将支持记忆的 Agent 从原型过渡到生产需要关注企业级架构关注点。这一转变引入了可扩展性、弹性和安全性的关键要求。生产级系统必须不仅为智能而设计,而且要为企业级健壮性而设计。

为了确保用户体验不会被计算昂贵的记忆生成过程所阻塞,健壮的架构必须将记忆处理与主应用逻辑解耦。虽然这是一种事件驱动模式,但通常通过直接的、非阻塞的 API 调用到专用记忆服务而非自管理消息队列来实现。流程如下:

  1. Agent 推送数据:在相关事件发生后(例如会话结束),Agent 应用向记忆管理器发起非阻塞 API 调用,“推送”要处理的原始源数据(如对话记录)。
  2. 记忆管理器在后台处理:记忆管理器服务立即确认请求并将生成任务放入其自己的内部托管队列。然后它独自负责异步的繁重工作:进行必要的 LLM 调用以提取、整合和格式化记忆。管理器可能延迟处理事件,直到经过一定的非活跃期。
  3. 记忆持久化:服务将最终记忆——可能是新条目或现有条目的更新——写入专用的、耐久的数据库。对于托管记忆管理器,存储是内置的。
  4. Agent 检索记忆:主 Agent 应用程序可以在需要为新用户交互检索上下文时直接查询此记忆存储。

这种基于服务的、非阻塞方法确保记忆管道中的失败或延迟不会直接影响面向用户的应用程序,使系统更具弹性。它还告知了在线(实时)生成和离线(批处理)处理之间的选择,前者对于对话新鲜度是理想的,后者对于从历史数据填充系统很有用。

随着应用程序增长,记忆系统必须在不失败的情况下处理高频事件。考虑到并发请求,系统必须防止多个事件尝试修改同一记忆时的死锁或竞争条件。可以使用事务数据库操作或乐观锁来缓解竞争条件;然而,当多个请求尝试修改相同记忆时,这可能引入排队或限流。健壮的消息队列对于缓冲大量事件并防止记忆生成服务被压垮至关重要。

记忆服务还必须对瞬时错误(故障处理)具有弹性。如果 LLM 调用失败,系统应使用带指数退避的重试机制,并将持久失败路由到死信队列进行分析。

对于全球应用程序,记忆管理器必须使用具有内置多区域复制的数据库,以确保低延迟和高可用性。客户端侧复制不可行,因为整合需要对数据的单一事务一致性视图以防止冲突。因此,记忆系统必须在内部处理复制,向开发者呈现单一的逻辑数据存储,同时确保底层知识库在全球范围内一致。

像 Agent Engine Memory Bank 这样的托管记忆系统应帮助你解决这些生产注意事项,让你专注于核心 Agent 逻辑。

隐私与安全风险

记忆来自用户数据并包含用户数据,因此需要严格的隐私和安全控制。一个有用的类比是将系统的记忆视为由专业档案员管理的安全企业档案,其职责是在保护公司的同时保存有价值的知识。

这个档案的基本规则是数据隔离。就像档案员永远不会混合不同部门的机密文件一样,记忆必须在用户或租户级别严格隔离。服务于一个用户的 Agent 绝不能访问另一用户的记忆,使用限制性访问控制列表(ACL)强制执行。此外,用户必须对其数据拥有程序控制,具有明确的选项来选择退出记忆生成或请求从档案中删除其所有文件。

在归档任何文件之前,档案员执行关键安全步骤。首先,他们仔细审查每页以脱敏个人身份信息(PII),确保在不创建责任的情况下保存知识。其次,档案员经过培训,能识别并丢弃伪造或故意误导性文件——这是防范记忆中毒的保护措施。同样,系统必须在将信息提交到长期记忆之前验证和清洗信息,以防止恶意用户通过提示注入破坏 Agent 的持久知识。系统必须包含如 Model Armor 等保护措施,以在提交信息到长期记忆之前验证和清洗信息。

此外,如果多个用户共享同一组记忆(如程序性记忆,教 Agent 如何做某事),则存在数据泄露风险。例如,如果一个用户的程序性记忆被用作另一个用户的示例——就像全公司共享备忘录一样——档案员必须首先执行严格的匿名化处理,以防止敏感信息跨用户边界泄露。

结论

本白皮书探索了上下文工程的学科,专注于其两个核心组件:会话记忆。从简单的对话轮次到持久的、可操作的智能的旅程,由这一实践所主导,涉及将所有必要信息——包括对话历史、记忆和外部知识——动态组装到 LLM 的上下文窗口中。这整个过程依赖于两个不同但相互关联的系统之间的相互作用:即时的会话和长期的记忆。

会话管理”当下”,作为单次对话的低延迟、时间顺序容器。其主要挑战是性能和安全性,需要低延迟访问和严格隔离。为了防止上下文窗口溢出和延迟,必须使用提取技术(如基于 token 的截断或递归摘要化)来压缩会话历史或单个请求有效载荷中的内容。此外,安全性至关重要,要求在持久化会话数据之前进行 PII 脱敏。

记忆是长期个性化的引擎和跨多个会话持久化的核心机制。它超越了 RAG(使 Agent 成为事实专家)来使 Agent 成为用户专家。记忆是一个主动的、LLM 驱动的 ETL 管道——负责提取、整合和检索——从对话历史中提炼最重要的信息。通过提取,系统将最关键的信息提炼为关键记忆点。随后,整合策划并将这些新信息与现有语料库集成,解决冲突并删除冗余数据,以确保连贯的知识库。为了维持响应式用户体验,记忆生成必须在 Agent 响应后作为异步后台进程运行。通过跟踪溯源并采用防范如记忆中毒等风险的保护措施,开发者可以构建真正与用户一起学习和成长的可信的、适应性强的助手。


尾注

  1. 检索增强生成(RAG)
  2. 上下文学习论文
  3. Agent Engine Sessions 概述
  4. LangGraph 多 Agent 消息传递
  5. ADK 多 Agent 文档
  6. ADK Agent 作为工具
  7. A2A 协议消息概念
  8. A2A:Agent 互操作性新时代
  9. Model Armor 概述
  10. 长上下文限制
  11. HuggingFace 记忆类型博客
  12. LangGraph 语义记忆(集合)
  13. LangGraph 语义记忆
  14. 原子事实论文
  15. 知识三元组论文
  16. Vertex AI 推理 API 多模态
  17. Agent Engine Memory Bank 生成记忆
  18. 结构化输出控制
  19. Memory Bank 配置
  20. 记忆提取与滚动摘要论文
  21. ADK 工具使用
  22. 整合预提取记忆
  23. 后台记忆生成
  24. 重排序论文
  25. ADK 回调
  26. 程序性记忆论文
  27. Google Cloud RLHF 博客
  28. 记忆中毒论文
  29. Model Armor 安全控制
  30. 选择 Agentic AI 系统设计模式
  • 本文作者: tc9011
  • 本文链接: https://tc9011.com/posts/2026/译上下文工程-会话与记忆/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!