Google ADK - 构建生产级、高效的上下文感知多智能体框架

Google ADK - 构建生产级、高效的上下文感知多智能体框架

本文翻译自 Google Developer Blog 的 Architecting efficient context-aware multi-agent framework for production


AI 智能体(Agent)的开发领域正经历着风起云涌的变化。我们早就跨过了还在做单轮聊天机器人原型的阶段。如今,各家机构正在部署的是那种复杂的、自主的智能体,它们能处理长链路任务(long-horizon tasks):比如自动化工作流、开展深度研究,甚至维护庞大的代码库。

但这一愿景很快就撞上了一堵墙:上下文(Context)

随着智能体运行时间的拉长,它们需要“记住”的信息量——聊天记录、工具返回的数据、外部文档、中间的推理过程——会呈爆炸式增长。目前的“通解”通常是依赖基础模型(Foundation Models)越来越大的上下文窗口(Context Window)。但是,单纯指望给智能体更大的空间来粘贴文本,绝不可能是长久的扩展之道。

为了构建可靠、高效且易于调试的生产级智能体,业界正在探索一门新的学科:

上下文工程(Context Engineering) —— 不再把上下文仅仅当作一段文本,而是将其视为系统中的“一等公民”,拥有独立的架构、生命周期和约束条件。

基于我们在扩展复杂的单智能体或多智能体系统方面的经验,我们在 Google Agent Development Kit (ADK) 中设计并迭代了上下文栈(Context Stack)来支持这一学科。ADK 是一个开源的、原生支持多智能体的框架,旨在让主动的上下文工程在实际系统中落地。

扩展性的瓶颈

更大的上下文窗口虽能缓解问题,却治标不治本。在实践中,那种幼稚的模式——把所有东西都追加到一个巨大的提示词(Prompt)里——会在以下三重压力下崩塌:

  1. 成本与延迟的恶性循环: 模型的推理成本和“首字延迟”(Time-to-first-token)会随着上下文长度迅速飙升。把原始的历史记录和冗长的工具返回结果“一股脑塞进”窗口,会让智能体变得既迟钝又昂贵。
  2. 信号衰减(“迷失在中间”): 一个充斥着无关日志、过时工具输出或废弃状态的上下文窗口,会分散模型的注意力。这会导致模型死盯着过去的模式,而忽略了当前的指令。为了确保决策稳健,我们必须最大化相关信息的密度。
  3. 物理极限: 现实世界的工作负载——涉及完整的 RAG 检索结果、中间产物(Artifacts)和漫长的对话痕迹——最终甚至会撑爆最大的固定窗口。

单纯靠“砸 Token”只能争取时间,却改变不了问题的本质。要实现规模化,我们需要改变上下文的表示和管理方式,而不仅仅是纠结于一次调用能塞进多少内容。

设计理念:上下文即“编译视图”

在上一代智能体框架中,上下文被视作一个可变的字符串缓冲区。ADK 则建立在一个截然不同的理念之上:上下文是一个基于更丰富状态系统的“编译视图”(Compiled View)。

在这个视角下:

  • 会话(Sessions)、记忆(Memory)和制品(Artifacts/Files)源(Sources) —— 它们构成了交互及其数据的完整、结构化状态。
  • 流(Flows)和处理器(Processors)编译器流水线(Compiler pipeline) —— 一系列对上述状态进行转换的处理工序。
  • 工作上下文(Working Context)编译视图(Compiled view) —— 你为了这一次调用而专门“编译”出来发给大模型的内容。

一旦你接受了这个心智模型,上下文工程就不再是“提示词微调的奇技淫巧(prompt gymnastics)”,而变成了正经的系统工程。你将被迫思考标准的系统问题:中间表示(IR)是什么?在哪里进行压缩?如何让转换过程可被观测?

ADK 的架构通过三个设计原则回答了这些问题:

  • 存储与展示分离: 我们区分了持久化状态(会话 Session)和单次调用的视图(工作上下文 Working Context)。这意味着你可以独立地升级存储结构和提示词格式,互不干扰。
  • 显式转换: 上下文是通过一系列命名的、有序的处理器(Processors)构建的,而不是临时拼凑的字符串拼接。这使得“编译”步骤变得可观测、可测试。
  • 默认最小权限(Scope by default): 每次模型调用和子智能体只能看到它所需的最小上下文。智能体必须通过工具显式地获取更多信息,而不是默认被信息洪流淹没。

ADK 的分层结构、相关性机制及其多智能体交接语义——本质上就是这一“编译器”理念及上述三个原则的应用:

  • 结构 —— 一个分层模型,将“信息的存储方式”与“模型所看到的内容”分离开来。
  • 相关性 —— 由智能体和人类控制逻辑共同决定当下什么最重要。
  • 多智能体上下文 —— 显式的语义定义,用于在智能体之间交接恰当的上下文切片。

接下来的部分将逐一解析这些支柱。

1. 结构:分层模型

大多数早期的智能体系统隐含地假设了一个单一的上下文窗口。ADK 则反其道而行之。它将存储与展示分离,并将上下文组织成职责分明的层级:

  • 工作上下文(Working Context) —— 本次模型调用的即时提示词:包含系统指令、智能体身份、筛选过的历史记录、工具输出、可选的记忆结果以及对制品的引用。
  • 会话(Session) —— 交互的持久化日志:每一条用户消息、智能体回复、工具调用、执行结果、控制信号和错误,都被捕获为结构化的事件(Event)对象。
  • 记忆(Memory) —— 长效的、可搜索的知识,其生命周期超越单个会话:比如用户偏好和过往的对话。
  • 制品(Artifacts) —— 与会话或用户关联的大型二进制或文本数据(文件、日志、图像)。它们通过名称和版本进行寻址,而不是直接粘贴到提示词中。

1.1 工作上下文:一个重算的视图

对于每次调用,ADK 都会从底层状态重新构建工作上下文。它从指令和身份开始,拉取选定的会话事件,并可选地附加记忆结果。这个视图是瞬态的(调用完即焚)、可配置的(改格式不需要动存储数据),并且是模型无关的

这种灵活性是编译器视图带来的第一个红利:你不再硬编码“那个提示词”,而是开始将其视为一种可以不断迭代的衍生表示。

1.2 流与处理器:流水线式的上下文处理

一旦分离了存储与展示,你就需要一套机制将前者“编译”为后者。在 ADK 中,每个基于 LLM 的智能体背后都有一个 LLM 流(Flow),这个流维护着一份有序的处理器列表。

一个(简化的)SingleFlow 可能长这样:

self.request_processors += [
    basic.request_processor,
    auth_preprocessor.request_processor,
    request_confirmation.request_processor,
    instructions.request_processor,
    identity.request_processor,
    contents.request_processor,
    context_cache_processor.request_processor,
    planning.request_processor,
    code_execution.request_processor,
    output_schema_processor.request_processor,
]

self.response_processors += [
    planning.response_processor,
    code_execution.response_processor,
]

这些流就是 ADK 编译上下文的机制。顺序至关重要:每个处理器都在前一步骤的输出之上进行构建。这为你提供了天然的切入点,用于插入自定义过滤、压缩策略、缓存逻辑和多智能体路由。你不再需要重写巨大的“提示词模板”;你只需添加或重新排序处理器。

1.3 会话与事件:结构化、语言无关的历史

一个 ADK 会话(Session)代表了对话或工作流实例的确定性状态。具体来说,它是一个容器,装着会话元数据(ID、应用名称)、用于存放结构化变量的状态暂存器,以及——最重要的——按时间顺序排列的**事件(Events)**列表。

ADK 不存储原始的提示词字符串,而是将每一次交互——用户发的消息、智能体回的话、工具的调用与结果、控制信号和报错——都记录为强类型的 Event 对象。这种结构化选择带来了三个明显的优势:

  • 模型无关性: 你可以更换底层模型而无需重写历史记录,因为存储格式与提示词格式是完全解耦的。
  • 丰富的操作空间: 下游组件(如压缩、时间旅行调试和记忆摄取)可以直接处理丰富的数据流,而不需要去解析那团不透明的文本。
  • 可观测性: 它为数据分析提供了一个天然的切面,让你能检查精确的状态转换和动作。

连接“会话”和“工作上下文”的桥梁是 contents 处理器。它负责将会话转换为工作上下文中的历史记录部分,主要干了三件大事:

  • 选择(Selection): 过滤事件流,剔除掉那些不该让模型看到的无关事件、残缺事件和框架噪音。
  • 转换(Transformation): 将筛选后的事件“拍平”为 Content 对象,并根据当前使用的特定模型 API,打上正确的角色标签(用户/助手/工具)和注解。
  • 注入(Injection): 将格式化后的历史记录写入 llm_request.contents,确保下游处理器——以及模型本身——接收到的是一条干净、连贯的对话线索。

在这个架构中,会话是你的事实基准(Ground Truth);工作上下文仅仅是一个计算出来的投影,你可以随着时间推移对其进行优化和打磨。

1.4 会话层的上下文压缩与过滤

如果无休止地追加原始事件,延迟和 Token 消耗量迟早会失控。ADK 的上下文压缩功能旨在从会话层解决这个问题。

当达到设定的阈值(比如调用次数)时,ADK 会触发一个异步进程。它利用 LLM 在一个滑动窗口上对旧事件进行总结,并将摘要作为一个带有“compaction(压缩)”动作的新事件写回会话。关键在于,这让系统可以删减或降低那些已被总结的原始事件的优先级。

由于压缩直接作用于事件流本身,其收益会向下游传导:

  • 可扩展性: 即使是超长对话,会话文件在物理体积上依然可控。
  • 视图清晰: contents 处理器会自动处理已经被压缩过的历史记录,查询时无需编写复杂的逻辑。
  • 解耦: 你可以在不触碰任何智能体代码或模板逻辑的情况下,调整压缩的提示词和策略。

这为长上下文建立了一个可扩展的生命周期。对于那些严格基于规则的缩减,ADK 提供了类似的功能——过滤(Filtering)——通过预置插件,在上下文到达模型之前,根据既定规则全局丢弃或修剪内容。

1.5 上下文缓存

现代模型支持上下文缓存(Context Caching,即前缀缓存),允许推理引擎在多次调用间复用注意力机制的计算结果。ADK 的“会话”(存储)与“工作上下文”(视图)分离的设计,为这种优化提供了天然的土壤。

该架构有效地将上下文窗口划分为两个区域:

  • 稳定前缀(Stable prefixes): 系统指令、智能体身份和长效摘要。
  • 可变后缀(Variable suffixes): 最新的用户轮次、新工具输出和小的增量更新。

因为 ADK 的流和处理器是显式的,你可以将“对缓存友好”作为一个硬性的设计指标。你可以通过对流水线排序,确保存放在上下文窗口前端的片段是经常复用的,同时将高度动态的内容推向末端。为了强制执行这种严谨性,我们引入了 static instruction(静态指令),这是一种保证系统提示词不可变的原语,确保缓存前缀在多次调用中始终有效。

这是上下文工程作为全栈系统工作的一个典型例子:你不仅在决定模型看什么,还在优化硬件底层重新计算张量的频率。

2. 相关性:智能体主导的“按需获取”

结构一旦搭好,核心挑战就变成了相关性:既然有了分层的上下文架构,那么哪些具体信息应该放入模型当前活跃的窗口里?

ADK 通过人类领域知识与智能体决策之间的协作来回答这个问题。光靠硬编码规则既省钱但太死板;光靠智能体去浏览所有内容则灵活但太贵且不稳定。

最佳的工作上下文是两者协商的结果。人类工程师定义架构——数据存在哪里、如何总结以及应用什么过滤器。然后,智能体提供“智力”,动态决定何时主动“伸手”去获取特定的记忆块或制品,以满足当前用户的请求。

2.1 制品(Artifacts):大型状态的外部化

早期的智能体实现经常掉进“上下文倾倒(context dumping)”的坑里:把巨大的数据载荷——一个 5MB 的 CSV、一个超大的 JSON 响应或完整的 PDF 实录——直接塞进聊天记录。这就像给会话背上了一笔永久的高利贷;随后的每一轮对话都要拖着这个包袱,既掩埋了关键指令,又拉高了成本。

ADK 通过将大型数据视为制品(Artifacts) 来解决这个问题:由 ArtifactService 管理的命名、版本化的二进制或文本对象。

概念上,ADK 对大数据应用了句柄模式(Handle Pattern)。大数据存在制品库里,而不是提示词里。默认情况下,智能体通过请求处理器只看到一个轻量级的引用(名字和摘要)。当——且仅当——智能体需要原始数据来回答问题时,它会使用 LoadArtifactsTool。这个动作才会将内容临时加载到工作上下文中。

关键点在于,ADK 支持临时扩展(Ephemeral Expansion)。一旦模型调用或任务完成,默认情况下制品会从工作上下文中卸载。这一招通过按需加载,将“每个提示词里 5MB 的噪音”变成了一种精准调用的资源。数据可以很大,但上下文窗口始终保持清爽。

2.2 记忆:按需检索的长效知识

制品处理的是离散的大文件,而 ADK 的**记忆(Memory)**层则管理超越单个会话的长效语义知识——用户偏好、过去的决策和领域事实。

我们围绕两个原则设计了 MemoryService:记忆必须是可搜索的(而不是永久钉在上下文中),且检索应由智能体主导

MemoryService 将数据(通常来自已结束的会话)摄取到向量或关键词语料库中。然后,智能体通过两种模式访问这些知识:

  • 反应式召回(Reactive recall): 智能体意识到自己知识有缺口(“用户的饮食限制是啥来着?”),并显式调用 load_memory_tool 来搜索语料库。
  • 主动召回(Proactive recall): 系统使用预处理器根据最新的用户输入运行相似性搜索,在模型被调用之前通过 preload_memory_tool 预先注入可能相关的片段。

这种方法用“基于记忆”的工作流取代了“上下文填充(Context Stuffing)”的反模式。智能体只回忆当前步骤所需的那一点片段,而不是背负着它们这辈子所有对话的沉重包袱。

3. 多智能体上下文:谁在何时看什么

单智能体系统面临上下文膨胀的困扰;多智能体系统则会加剧这一问题。如果根智能体(Root Agent)将其完整的历史记录传给子智能体,而子智能体也照做,就会引发上下文爆炸。Token 数量激增,子智能体也会被无关的“祖传”对话历史搞得晕头转向。

每当一个智能体调用另一个智能体时,ADK 允许你显式限定(Explicitly Scope) 被调用者能看到的内容——也许只是最新的那条用户查询和一个制品——同时屏蔽掉绝大部分祖先历史。

3.1 两种多智能体交互模式

宏观上看,ADK 将多智能体交互归纳为两种架构模式。

第一种是 智能体即工具(Agents as Tools)。在这里,根智能体将专用智能体严格视为一个函数:给它一个专有的提示词,拿回结果,然后继续。被调用者只看到特定的指令和必要的制品——没有历史包袱。

第二种是 智能体转移(Agent Transfer,或层级结构)。在这里,控制权完全移交给子智能体以继续对话。子智能体继承会话的一个视图,并可以主导工作流,调用自己的工具或进一步向下转移控制权。

3.2 智能体转移中的作用域交接

交接(Handoff)行为由诸如 include_contents(包含内容)之类的开关控制,这些开关决定了有多少上下文能从根智能体流向子智能体。在默认模式下,ADK 传递调用者工作上下文的全部内容——当子智能体确实需要完整历史来理解语境时,这很有用。在 none 模式下,子智能体看不到之前的历史;它只接收你为它构建的新提示词(例如,最新的用户轮次加上几个工具调用和响应)。让专用智能体只拿它们所需的最小上下文,而不是默认继承一份巨大的聊天记录。

因为子智能体的上下文也是通过处理器构建的,这些交接规则可以无缝插入到与单智能体调用相同的流管道中。你不需要单独搞一套多智能体机制;你只是调整了现有的上下文编译器被允许看到的上游状态的范围。

3.3 为智能体转移“翻译”对话

基础模型通常在固定的角色模式下运行:system(系统)、user(用户)和 assistant(助手)。它们天生不理解“助手 A”与“助手 B”的区别。

当 ADK 转移控制权时,它通常必须重构(Reframe) 现有的对话,以便新智能体看到连贯的工作上下文。如果新智能体只是看到来自前一个智能体的一连串“Assistant”消息,它会产生幻觉,以为那些动作是它自己 做的。

为了防止这种情况,ADK 在交接期间执行主动翻译:

  • 叙事转换(Narrative casting): 之前的“Assistant”消息可能会被重铸为叙事背景(例如,修改角色或注入标签如 [背景信息]: 智能体 B 说...),而不是作为新智能体自己的输出出现。
  • 动作归因(Action attribution): 来自其他智能体的工具调用被标记或总结,以便新智能体基于结果 行动,而不会将 执行过程与自身的能力混淆。

实际上,ADK 从子智能体的视角构建了一个全新的工作上下文,同时保留了会话中的事实历史。这确保了正确性,允许每个智能体都能扮演“Assistant”角色,而不会将整个系统的历史错误地归揽到自己头上。

结语

随着我们推动智能体去解决更长周期的问题,“上下文管理”不能再仅仅意味着“字符串操作”。它必须被视为与存储和计算同等重要的架构级关注点。

ADK 的上下文架构——分层存储、编译视图、流水线处理和严格的作用域划分——正是我们对这一挑战的回答。它将构建智能体所需的严谨系统工程封装起来,助力开发者从有趣的 Demo 原型,迈向可扩展、高可靠的生产级系统。

(转载本站文章请注明作者和出处乱世浮生,请勿用于任何商业用途)

comments powered by Disqus