Skip to content

第 22 章 · Google ADK 实战:从单 Agent 到多 Agent 07-09

本章观点

前面 19-21 章我们讲了规划、多智能体协作、知识图谱三个高级模式。这些模式都用伪代码或 Anthropic SDK 演示,本章换一个视角:用 Google ADK 落地。

吴恩达 Agentic AI 课程(Bilibili BV1DfrdByE2H,p35-p36)选 Google ADK 作为知识图谱模块的实战框架,理由是:

Google ADK 把 Agent 拆成工程化组件:Agent / Runner / Session / Memory / Event / ToolContext,每个组件职责清晰,组合起来既能跑单 Agent,也能组织多 Agent 系统。

这个拆法对应本教程"四权模型":

  • 控制权:Runner 负责执行事件循环,LLM 负责决策,代码负责执行
  • 工具权:工具函数 + docstring 让 LLM 知道怎么用
  • 状态权:Session / Memory / State 保存对话和跨步骤记忆
  • 停止权:事件循环的终止条件(final response / escalation / error)

本章会讲清楚四件事:

  1. Google ADK 的核心组件和它们的关系。
  2. 单 Agent 最小示例:定义 Agent、写工具、跑 Runner。
  3. 多 Agent 系统:根代理 + 子代理委派 + ToolContext 共享状态。
  4. ADK 和其他框架(Claude Agent SDK / LangGraph)的对比。

22.1 Google ADK 是什么

Google ADK(Agent Development Kit)是 Google 官方出的 Agent 开发框架,2025 年开源。它和 Claude Agent SDK、OpenAI Agents SDK 是同类产品,但有几个独特设计:

特性Google ADKClaude Agent SDKLangGraph
主推模型Gemini(也支持其他)Claude任意
多 Agent 委派一等公民(sub_agents)支持通过图实现
状态管理Session + Memory + State内置Graph state
工具上下文ToolContext 显式注入通过 closure通过 state
跨模型LiteLLM 适配器仅 Claude任意

ADK 的核心抽象是"Agent 是控制流操作符":

text
Agent = LLM 决策 + 代码执行 + 状态记忆 + 事件循环

LLM 决定下一步干什么,代码和工具负责执行,Session 保存记忆,Runner 把这些串成事件循环。这个抽象和第 2 章"Agent = LLM + 工具 + 反馈循环 + 状态记忆 + 停止边界"完全一致。

22.2 核心组件

22.2.1 Agent

Agent 是 ADK 的一等公民。一个 Agent 至少需要:

python
from google.adk import Agent

agent = Agent(
    name="hello_agent",
    model="gemini-2.0-flash",  # 或通过 LiteLLM 用其他模型
    description="一个问候代理,负责向用户问好",  # 给其他代理看
    instruction="你是问候代理。当用户说名字时,调用 say_hello 工具生成问候语。",
    tools=[say_hello],  # Python 函数列表
)

关键字段

  • name:代理唯一标识
  • model:LLM 模型(Gemini / 通过 LiteLLM 接 OpenAI 等)
  • description给其他代理看,说明这个代理负责什么、何时调用
  • instruction给当前代理看,说明它如何完成任务、何时用工具
  • tools:Python 函数列表

descriptioninstruction 的区分是 ADK 的关键设计:

  • description 决定"什么时候被委派"(路由)
  • instruction 决定"被委派后怎么做"(执行)

这个区分在第 20 章多 Agent 协作里很重要:经理代理根据 description 决定调谁,子代理根据 instruction 决定怎么做。

22.2.2 Runner

Runner 负责执行 Agent、调度事件循环、调用 LLM、处理工具调用、生成响应。

python
from google.adk import Runner

runner = Runner(agent=agent, session_service=session_service)

Runner 接收:

  • 一个 Agent(或多个 Agent 的根代理)
  • SessionService(管理会话)
  • 可选:MemoryService(跨会话记忆)

Runner 的工作流:

text
1. 接收用户消息
2. 包装成 ADK content / part 结构
3. 把消息送进当前 Agent
4. Agent 调用 LLM 决定下一步
5. 如果 LLM 决定调工具 -> 执行工具 -> 把结果回传给 LLM
6. 如果 LLM 决定委派子代理 -> 切换到子代理 -> 子代理走自己的循环
7. 如果 LLM 决定结束 -> 产生 final response 事件
8. 遍历事件,返回最终响应

22.2.3 Session / SessionService / Memory

Session 保存一次对话或一次任务运行的上下文、状态和记忆。

python
from google.adk.sessions import InMemorySessionService

session_service = InMemorySessionService()
session = await session_service.create_session(
    app_name="my_app",
    user_id="user_001",
    session_id="session_001"
)

Session 的核心字段:

  • id:会话 ID
  • state:会话状态字典(重要!见 22.5 节 ToolContext)
  • events:事件历史
  • user_id / app_name:归属

SessionService 管理多个 Session,支持:

  • InMemorySessionService:内存存储,开发用
  • DatabaseSessionService:数据库持久化
  • VertexAISessionService:Google Cloud 托管

MemoryService 跨会话长期记忆。和 Session 的区别:

  • Session 是短期的(一次对话)
  • Memory 是长期的(跨多次对话)

22.2.4 Event

事件是 Runner 产生的基本单位。一次 Agent 运行会产生一系列事件:

python
async for event in runner.run_async(
    user_id="user_001",
    session_id="session_001",
    new_message=user_message
):
    if event.is_final_response():
        print("最终响应:", event.content)
    elif event.get_function_calls():
        print("工具调用:", event.get_function_calls())
    # ... 其他事件类型

事件类型:

  • message_in:消息进入代理
  • message_out:代理输出消息
  • tool_call:代理决定调工具
  • tool_result:工具返回结果
  • agent_transfer:代理委派给另一个代理
  • final_response:最终响应
  • escalation:升级处理
  • error:错误

事件循环的设计让 Agent 行为可观察、可调试。第 20 章 20.6 节强调"多 Agent 必须看 trace",ADK 的事件就是 trace 的基本单位。

22.2.5 ToolContext

ToolContext 是工具函数的可选第二参数,让工具能读写会话状态。

python
from google.adk.tools import ToolContext

def say_hello(person_name: str, tool_context: ToolContext) -> dict:
    """问候工具。"""
    # 写入状态
    tool_context.state["username"] = person_name
    return {"status": "success", "greeting": f"你好,{person_name}!"}

ToolContext 的核心方法:

  • tool_context.state:会话状态字典
  • tool_context.state["key"] = value:写入状态
  • tool_context.state.get("key"):读取状态
  • tool_context.actions:触发动作(如 escalate / transfer)

ToolContext 让工具调用不只是"输入 -> 输出",还能更新会话记忆。这是多 Agent 共享状态的基础(见 22.5 节)。

22.3 单 Agent 最小示例

把上面的组件合起来,写一个最小的 ADK Agent:问候代理,能记住用户名字并问候。

22.3.1 配置 LLM

ADK 通过 LiteLLM 支持多种模型。课程示例用 OpenAI 的 GPT-4o,本教程改用 Gemini(ADK 原生支持)。

python
import os
from google.adk import Agent
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types

# 方式 1:用 Gemini(需要 GOOGLE_API_KEY)
os.environ["GOOGLE_API_KEY"] = "your-api-key"

# 方式 2:用 LiteLLM 调 OpenAI
# os.environ["OPENAI_API_KEY"] = "your-openai-key"
# model = "litellm/gpt-4o"

22.3.2 写工具函数

python
from google.adk.tools import ToolContext

def say_hello(person_name: str, tool_context: ToolContext) -> dict:
    """生成个性化问候语。

    当用户告知姓名时调用此工具,生成问候语并记住用户姓名。

    Args:
        person_name: 用户的名字
        tool_context: 工具上下文(自动注入)

    Returns:
        dict: 包含 status 和 greeting 字段
            - status: "success" 或 "error"
            - greeting: 问候语
    """
    if not person_name:
        return {"status": "error", "message": "名字不能为空"}
    # 把名字存到会话状态
    tool_context.state["username"] = person_name
    return {"status": "success", "greeting": f"你好,{person_name}!很高兴认识你。"}

工具设计要点

  1. 函数名表达用途say_hello 一看就知道是问候
  2. 类型标注person_name: str 让 LLM 知道参数类型
  3. docstring 详细:说明用途 / 参数 / 返回 / 错误情况
  4. 返回结构化字典{"status": ..., "greeting": ...} 而不是纯文本
  5. 状态写入:通过 tool_context.state 保存可复用信息

docstring 在 ADK 里会被转成工具描述传给 LLM,写得越清楚 LLM 越会用。这是第 4 章 Tool Use 讲过的"工具描述要写'何时使用'"原则的体现。

22.3.3 创建 Agent

python
hello_agent = Agent(
    name="hello_agent",
    model="gemini-2.0-flash",
    description="一个问候代理,当用户告知姓名或问好时使用。",
    instruction="""你是问候代理。

你的职责:
1. 当用户告知姓名时,调用 say_hello 工具生成问候语
2. 当用户问好但没说名字时,先问名字
3. 不要处理非问候类请求

调用工具后,把工具返回的 greeting 字段作为回复。""",
    tools=[say_hello],
)

注意 descriptioninstruction 的分工:

  • description:"当用户告知姓名或问好时使用" -- 让其他代理知道何时委派给它
  • instruction:"你的职责..." -- 让这个代理知道自己该怎么做

22.3.4 配置运行环境

python
session_service = InMemorySessionService()
runner = Runner(agent=hello_agent, session_service=session_service)

# 创建 session
session = await session_service.create_session(
    app_name="hello_app",
    user_id="user_001",
)

22.3.5 打包用户消息

ADK 用 types.Content 包装用户消息:

python
user_message = types.Content(
    role="user",
    parts=[types.Part.from_text("你好,我是 ABK")]
)

role 是消息角色(user / model),parts 是消息内容列表(文本 / 图片 / 工具调用等)。

22.3.6 运行事件循环

python
async for event in runner.run_async(
    user_id="user_001",
    session_id=session.id,
    new_message=user_message
):
    # 工具调用事件
    if event.get_function_calls():
        for fc in event.get_function_calls():
            print(f"[工具调用] {fc.name}({fc.args})")

    # 工具结果事件
    if event.get_function_responses():
        for resp in event.get_function_responses():
            print(f"[工具结果] {resp.response}")

    # 最终响应
    if event.is_final_response():
        final_text = event.content.parts[0].text
        print(f"[最终响应] {final_text}")

22.3.7 封装 AgentCaller

把上述样板封装成可复用工具,方便后续测试:

python
class AgentCaller:
    def __init__(self, agent, app_name="default_app"):
        self.session_service = InMemorySessionService()
        self.runner = Runner(agent=agent, session_service=self.session_service)
        self.app_name = app_name
        self.session = None

    async def init(self, user_id="user_001"):
        self.session = await self.session_service.create_session(
            app_name=self.app_name,
            user_id=user_id,
        )
        return self

    async def call(self, user_text: str) -> str:
        message = types.Content(
            role="user",
            parts=[types.Part.from_text(user_text)]
        )
        final = ""
        async for event in self.runner.run_async(
            user_id=self.session.user_id,
            session_id=self.session.id,
            new_message=message
        ):
            if event.is_final_response():
                final = event.content.parts[0].text
        return final

    async def get_state(self) -> dict:
        return self.session.state if self.session else {}

# 用法
caller = await AgentCaller(hello_agent).init()
print(await caller.call("你好,我是 ABK"))
print("状态:", await caller.get_state())
# 状态: {"username": "ABK"}

AgentCaller 的价值:把 Runner / Session / 事件循环的样板收起来,后续测试多 Agent 时只需 caller.call(...)

22.3.8 多轮测试

python
# 第一轮
print(await caller.call("你好,我是 ABK"))
# 输出: 你好,ABK!很高兴认识你。

# 第二轮(验证状态保持)
print(await caller.call("你还记得我叫什么吗?"))
# 输出: 你叫 ABK,对吧?

# 查看状态
print(await caller.get_state())
# {"username": "ABK"}

第二轮能记得名字,是因为第一轮 say_hello 把名字写进了 tool_context.state["username"],Session 持久化了这个状态。

22.4 多 Agent 系统:根代理 + 子代理

p36 的核心内容是用 ADK 实现多 Agent 委派。我们用根代理协调问候代理和告别代理。

22.4.1 设计通信模式

这是第 20 章 20.3.2 单层层级模式的最小实现:

text
         根代理(不直接干活)
        /              \
   问候代理          告别代理
   工具:say_hello   工具:say_goodbye

根代理根据用户意图决定委派给谁,子代理拿到控制权后独立处理。

22.4.2 定义工具

python
def say_hello(person_name: str, tool_context: ToolContext) -> dict:
    """问候工具。当用户告知姓名或问好时使用。"""
    tool_context.state["username"] = person_name
    return {"status": "success", "greeting": f"你好,{person_name}!"}

def say_goodbye(tool_context: ToolContext) -> dict:
    """告别工具。当用户说再见、谢谢再见、下次见时使用。"""
    username = tool_context.state.get("username", "朋友")
    return {"status": "success", "goodbye": f"再见,{username}!期待下次见。"}

注意 say_goodbyetool_context.stateusername。这就是 ToolContext 跨代理共享状态的机制。

22.4.3 创建子代理

python
greeting_agent = Agent(
    name="greeting_agent",
    model="gemini-2.0-flash",
    description="处理问候请求。当用户说你好、嗨、问好时委派给此代理。",
    instruction="""你是问候代理。

当用户告知姓名或问好时:
1. 调用 say_hello 工具
2. 把工具返回的 greeting 作为回复

不要处理告别或其他请求。""",
    tools=[say_hello],
)

farewell_agent = Agent(
    name="farewell_agent",
    model="gemini-2.0-flash",
    description="处理告别请求。当用户说再见、谢谢再见、下次见时委派给此代理。",
    instruction="""你是告别代理。

当用户告别时:
1. 调用 say_goodbye 工具
2. 把工具返回的 goodbye 作为回复

不要处理问候或其他请求。""",
    tools=[say_goodbye],
)

注意 description 的写法:"当用户说 X、Y、Z 时委派给此代理" -- 这是给根代理看的路由信号。

22.4.4 创建根代理

python
root_agent = Agent(
    name="coordinator_agent",
    model="gemini-2.0-flash",
    description="协调代理,负责识别用户意图并委派给合适的子代理。",
    instruction="""你是协调代理。

你的职责:
1. 识别用户意图
2. 把问候请求委派给 greeting_agent
3. 把告别请求委派给 farewell_agent
4. 不要自己执行问候或告别,只做委派

委派时把用户的完整消息传给子代理,让子代理处理。""",
    sub_agents=[greeting_agent, farewell_agent],
    # 注意:根代理没有 tools,只有 sub_agents
)

关键设计

  • 根代理 tools=[](空),只有 sub_agents
  • 这强制根代理只做协调,不执行具体任务
  • 这是第 20 章 20.7 反模式 3 "经理代理配一堆工具"的工程规避

22.4.5 跑多 Agent 系统

python
caller = await AgentCaller(root_agent, app_name="multi_agent_app").init()

# 第一轮:问候
print(await caller.call("你好,我是 ABK"))
# [根代理] 识别意图 -> 委派给 greeting_agent
# [问候代理] 调用 say_hello("ABK")
# [问候代理] 工具返回 {"greeting": "你好,ABK!"}
# [问候代理] 最终响应: 你好,ABK!
# 状态: {"username": "ABK"}

# 第二轮:告别
print(await caller.call("谢谢再见"))
# [根代理] 识别意图 -> 委派给 farewell_agent
# [告别代理] 调用 say_goodbye()
# [告别代理] 工具读取 state.username = "ABK"
# [告别代理] 工具返回 {"goodbye": "再见,ABK!"}
# [告别代理] 最终响应: 再见,ABK!期待下次见。

第二轮告别代理能叫出名字"ABK",是因为第一轮问候工具把名字写进了会话状态,第二轮告别工具从状态里读出来。这就是 ToolContext 跨代理共享状态的工作机制。

22.4.6 打开 verbose 调试

学习多 Agent 时,强烈建议打开 verbose 看事件流:

python
async def call_verbose(caller, user_text: str):
    message = types.Content(
        role="user",
        parts=[types.Part.from_text(user_text)]
    )
    async for event in caller.runner.run_async(
        user_id=caller.session.user_id,
        session_id=caller.session.id,
        new_message=message
    ):
        print(f"[Event] type={event.type}")
        if event.author:
            print(f"  author: {event.author}")
        if event.get_function_calls():
            print(f"  function_calls: {event.get_function_calls()}")
        if event.get_function_responses():
            print(f"  function_responses: {event.get_function_responses()}")
        if event.is_final_response():
            print(f"  final_response: {event.content.parts[0].text}")

输出会让你看到:

  • 哪个代理创建了初始事件
  • 根代理是否正确委派
  • 子代理是否接收了完整上下文
  • 工具调用名和参数
  • 工具返回结构
  • 哪个事件被标记为 final response
  • session state 的变化

这是第 20 章 20.6 节"多 Agent 必须看 trace"的具体实践。

22.5 ToolContext 与会话状态

ToolContext 是 ADK 多 Agent 系统的关键设计。理解它能让你写好可复用工具。

22.5.1 工具的两种模式

无状态工具:纯输入 -> 输出,不读不写状态。

python
def calculate(expression: str) -> str:
    """数学计算。"""
    return str(eval(expression))  # 仅演示

有状态工具:读或写会话状态。

python
def save_user_goal(goal: str, tool_context: ToolContext) -> dict:
    """保存用户目标到会话状态。"""
    tool_context.state["user_goal"] = goal
    return {"status": "success"}

def recall_user_goal(tool_context: ToolContext) -> dict:
    """从会话状态读取用户目标。"""
    goal = tool_context.state.get("user_goal")
    if goal:
        return {"status": "success", "goal": goal}
    return {"status": "error", "message": "尚未设置目标"}

22.5.2 状态的常见用法

第 21 章知识图谱多 Agent 系统里,状态可以这样用:

python
# 用户意图代理写入
def set_user_intent(intent: str, tool_context: ToolContext) -> dict:
    tool_context.state["user_intent"] = intent
    return {"status": "success"}

# 文件建议代理读取 + 写入
def suggest_files(intent: str, tool_context: ToolContext) -> dict:
    saved_intent = tool_context.state.get("user_intent", "")
    files = pick_files(saved_intent)
    tool_context.state["confirmed_files"] = files
    return {"status": "success", "files": files}

# Schema 提案代理读取
def propose_schema(tool_context: ToolContext) -> dict:
    files = tool_context.state.get("confirmed_files", [])
    intent = tool_context.state.get("user_intent", "")
    schema = generate_schema(files, intent)
    tool_context.state["schema_proposal"] = schema
    return {"status": "success", "schema": schema}

这样每个代理的输出都进入会话状态,下游代理可以直接读,不用重新传参。

22.5.3 output_key:代理输出写入状态

除了 ToolContext,ADK 还支持 output_key 参数:代理的最终输出会写入指定状态键。

python
research_agent = Agent(
    name="research_agent",
    model="gemini-2.0-flash",
    description="研究代理",
    instruction="...",
    output_key="research_result",  # 最终输出写入 state["research_result"]
)

这样下游代理可以直接读 state["research_result"],不用解析消息。

22.6 ADK 多 Agent 调试检查清单

把第 20 章 20.6.3 节的检查清单具体到 ADK:

text
观察 ADK 事件流时重点看:

1. 初始事件
   - 哪个代理创建了第一条事件?
   - 是根代理直接处理,还是委派给子代理?

2. 委派是否正确
   - 根代理收到用户消息后,是否调用了 sub_agent?
   - 调用的是哪个子代理?是不是该调的那个?
   - 子代理是否接收了完整用户消息?

3. 工具调用
   - 子代理调用了哪个工具?
   - 工具参数是否从用户消息中正确提取?
   - 工具返回是否符合 docstring 描述的结构?

4. 状态变化
   - 每次工具调用后 session.state 改了什么?
   - 下一个代理是否能读到上一步写入的状态?

5. 终止条件
   - 哪个事件被标记为 final_response?
   - 是子代理产生的,还是根代理?
   - 是否触发了 escalation 或 error?

22.7 ADK vs 其他框架

22.7.1 ADK vs Claude Agent SDK

维度Google ADKClaude Agent SDK
主推模型Gemini(也支持其他)Claude
多 Agent 委派sub_agents 一等公民通过 query / subagent
状态管理Session + Memory + State内置
工具上下文ToolContext 显式closure 隐式
跨模型LiteLLM仅 Claude
学习曲线中等简单
生态Gemini 生态Claude 生态

选型

  • 用 Claude 模型 -> Claude Agent SDK
  • 用 Gemini 或多模型 -> Google ADK
  • 需要严格多 Agent 委派 -> ADK(sub_agents 更直观)
  • 快速原型 -> Claude Agent SDK(更轻量)

22.7.2 ADK vs LangGraph

维度Google ADKLangGraph
抽象方式Agent + RunnerStateGraph + Node + Edge
多 Agentsub_agents 委派图节点
通信模式委派 / 工具调用边 / 条件边
状态Session stateGraph state
调试事件流图可视化
灵活度中等(约定大于配置)高(图任意拓扑)
学习曲线中等较陡

选型

  • 喜欢"声明角色 + 自动委派" -> ADK
  • 喜欢"显式画图 + 完全控制" -> LangGraph
  • 简单多 Agent -> ADK
  • 复杂状态机 + 多条件分支 -> LangGraph

22.7.3 何时不用 ADK

ADK 不是银弹。以下场景其他选择更合适:

  • 极简单 Agent:直接用 Anthropic SDK 写 50 行 loop(第 3 章示例),更透明
  • 超复杂状态机:LangGraph 的图抽象更灵活
  • 只用 Claude:Claude Agent SDK 集成更深
  • TypeScript 项目:考虑 Mastra 或 OpenAI Agents SDK
  • 类型安全优先:Pydantic AI 类型检查更强

22.8 把第 21 章的多 Agent 架构落地

p35-p36 的最终目的是为第 21 章知识图谱模块做实战铺垫。把第 21 章的五代理架构用 ADK 实现:

python
# 1. 顶层对话代理
top_agent = Agent(
    name="top_dialogue_agent",
    model="gemini-2.0-flash",
    description="引导用户完成知识图谱构建流程",
    instruction="你是知识图谱构建助手。引导用户:1) 澄清目标 2) 选择文件 3) 设计 schema 4) 抽取实体 5) 构建图谱 6) GraphRAG 查询",
    sub_agents=[intent_agent, file_agent, schema_agent, extraction_agent, graphrag_agent],
)

# 2. 用户意图代理
intent_agent = Agent(
    name="intent_agent",
    description="当用户陈述业务目标时委派。澄清为什么要构建图谱、要回答什么问题。",
    instruction="...",
    tools=[set_user_intent],
)

# 3. 文件建议代理
file_agent = Agent(
    name="file_agent",
    description="当需要选择数据文件时委派。基于用户目标推荐相关文件。",
    instruction="...",
    tools=[list_files, suggest_files, confirm_files],
)

# 4. Schema 提案代理(含提出者+批评者)
schema_proposer = Agent(
    name="schema_proposer",
    description="基于结构化数据文件生成图构建计划。",
    instruction="...",
    tools=[analyze_csv, propose_schema],
)

schema_critic = Agent(
    name="schema_critic",
    description="审查并改进 schema 提案。",
    instruction="...",
    tools=[review_schema, suggest_improvements],
)

schema_agent = Agent(
    name="schema_agent",
    description="协调 schema 提案和审查。",
    instruction="先让 schema_proposer 提案,再让 schema_critic 审查,循环直到收敛。",
    sub_agents=[schema_proposer, schema_critic],
    output_key="schema_plan",
)

# 5. 知识提取计划代理
extraction_agent = Agent(
    name="extraction_agent",
    description="基于非结构化文档生成知识提取计划。",
    instruction="...",
    tools=[plan_extraction],
    output_key="extraction_plan",
)

# 6. 执行工具(不是 Agent,是代码)
def build_graph_tool(schema_plan: dict, extraction_plan: dict, files: list) -> dict:
    """图构建执行工具。基于计划和文件构建三层图。"""
    # 调用第 21 章 21.4.3 节的 build_graph 函数
    return {"status": "success", "graph_stats": {...}}

# 7. GraphRAG 代理
graphrag_agent = Agent(
    name="graphrag_agent",
    description="基于已构建图谱回答用户问题。",
    instruction="...",
    tools=[build_graph_tool, query_graph],
)

这个结构把第 21 章的设计图直接映射成 ADK 代码。关键观察:

  • 每个代理职责单一
  • sub_agents 组织层级
  • output_key 让输出进入会话状态
  • 用 ToolContext 在代理间共享数据
  • 执行工具(build_graph_tool)是代码,不是 LLM 调用

这是第 21 章 21.4.4 节"计划与执行分离"的工程落地。

前人智慧 / Prior Art

本章把 Google ADK 的核心组件和单 / 多 Agent 用法讲清楚。ADK 的设计思想来自多个源头。

Google ADK 的设计哲学 07-09

Google ADK(2025)的设计哲学:

  1. Agent 是控制流操作符:Agent = LLM 决策 + 代码执行 + 状态记忆
  2. LLM 负责决策,代码负责执行:智能行为来自 LLM 推理,真正操作由程序和工具完成
  3. 多 Agent 是一等公民:sub_agents 让层级委派成为原生能力
  4. 状态显式管理:Session / State / Memory 不是黑箱,可观察可调试

本章 22.1-22.5 节的组件拆解直接对应 ADK 的设计文档。ADK 的工程贡献是把"Agent"从神秘对象变成"模型 + 提示词 + 工具 + 记忆 + 运行器"的组合。

LiteLLM 的跨模型抽象

LiteLLM 是 ADK 跨模型能力的底层。它把 100+ 个 LLM API 统一成 OpenAI 格式:

python
from litellm import completion

# 同一个接口,调不同模型
resp = completion(model="gpt-4o", messages=[...])
resp = completion(model="claude-sonnet-4-5-20250929", messages=[...])
resp = completion(model="gemini/gemini-2.0-flash", messages=[...])

ADK 通过 LiteLLM 适配器让一个 Agent 能调任意模型。这种"框架 + LiteLLM"的组合是 2025 年 Agent 框架的主流模式--OpenAI Agents SDK、CrewAI、LangGraph 都支持类似抽象。

Anthropic Computer Use 的 Agent 抽象

Anthropic Computer Use(2024)的 Agent 抽象和 ADK 类似:

  • Agent = LLM + 工具 + 循环
  • 工具是 Python 函数 + docstring
  • 状态显式管理

这种抽象已经成为 Agent 框架的共识。ADK / Claude Agent SDK / OpenAI Agents SDK 的核心组件高度相似,差别在具体实现和生态绑定。

Andrew Ng Agentic AI 课程的 ADK 模块 07-09

本章内容主要整理自吴恩达 Agentic AI 课程 Module 6(Bilibili BV1DfrdByE2H,p35-p36)。课程的两节递进:

  1. p35 Google ADK 简介 Part 1:单 Agent 的最小组成
  2. p36 Google ADK 简介 Part 2:多 Agent 委派 + ToolContext 共享状态

课程的关键判断:"Agent 不是神秘对象,而是模型、提示词、工具、记忆、会话和运行器的组合"。这正是本章 22.2 节组件拆解的来源。

课程还强调:"description 让其他代理知道这个代理该不该被调用,instruction 让代理知道自己该怎么做"。这是本章 22.2.1 节 description vs instruction 区分的来源,也是第 20 章多 Agent 协作的关键设计。

OpenAI Agents SDK 的对比

OpenAI Agents SDK(2025)和 ADK 在多 Agent 委派上设计相似:

  • ADK: sub_agents=[...]
  • OpenAI: handoffs=[...]

两者都让一个代理能把控制权交给另一个代理。差别在:

  • ADK 用 description 路由
  • OpenAI 用 handoff_description 路由
  • ADK 的 ToolContext 更显式
  • OpenAI 的 lifecycle hooks 更丰富

这两个框架代表 2025 年 Agent 框架的两个主流方向:Google 的"状态显式 + 跨模型"vs OpenAI 的"生命周期 + 单模型深度"。

工具 docstring 的工程价值

ADK 把工具函数的 docstring 当成给 LLM 的工具描述。这个设计来自 OpenAI Function Calling:

python
# docstring 会被自动转成工具描述
def say_hello(person_name: str) -> dict:
    """问候工具。当用户告知姓名时使用。

    Args:
        person_name: 用户名字

    Returns:
        dict: {"status": ..., "greeting": ...}
    """

这种"docstring 即工具描述"的设计有几个好处:

  1. 单一信息源:开发者写一次 docstring,LLM 和人类都看
  2. 类型自动推断:Python 类型标注自动转 JSON schema
  3. 降低 token:比单独写 description 字段更紧凑

第 4 章 Tool Use 讲过"工具描述要写'何时使用'",ADK 的 docstring 设计是这个原则的工程化。

会话状态的演化:从无状态到有状态

Agent 框架的会话状态管理演化:

阶段框架状态管理
1.0(2023)早期 LangChainmessages 列表,无独立状态
2.0(2024)LangGraphgraph state 显式
3.0(2025)ADK / Claude SDKSession + State + Memory 三层

ADK 的 Session / State / Memory 三层是当前最完整的状态管理设计:

  • Session:一次对话的短期记忆
  • State:会话内的键值状态(ToolContext 读写)
  • Memory:跨会话的长期记忆

这种分层让状态管理从"塞 messages 列表"演化到"显式分层的记忆系统"。第 5 章 Context Engineering 讲过 context 管理的重要性,ADK 的状态分层是这条思路的工程化。

Neo4j Python Driver 与 ADK 的集成

p35 课程示例中,Neo4j 通过 Python driver 集成到 ADK 工具:

python
from neo4j import GraphDatabase

class Neo4jWrapper:
    def __init__(self, uri, user, password):
        self.driver = GraphDatabase.driver(uri, auth=(user, password))

    def query(self, cypher: str, params: dict = None) -> dict:
        with self.driver.session() as session:
            result = session.run(cypher, params or {})
            return {"status": "success", "records": [r.data() for r in result]}

neo4j = Neo4jWrapper("bolt://localhost:7687", "neo4j", "password")

def query_graph(cypher: str, tool_context: ToolContext) -> dict:
    """执行 Cypher 查询。当需要从知识图谱检索信息时使用。

    Args:
        cypher: Cypher 查询语句
        tool_context: 工具上下文

    Returns:
        dict: {"status": "success"|"error", "records": [...]}
    """
    try:
        return neo4j.query(cypher)
    except Exception as e:
        return {"status": "error", "message": str(e)}

这个集成的关键设计:

  1. Neo4j 查询结果转字典:让 ADK 容易持久化和消费
  2. 结构化返回status: success/error 让 LLM 知道查询状态
  3. 参数化查询:避免 Cypher 注入
  4. 错误处理:返回结构化错误,让 LLM 决定怎么办(第 3 章 3.4 节原则)

多 Agent 调试的工程实践

p36 强调用 verbose 模式调试多 Agent。这个实践和第 20 章 20.6 节"必须看 trace"完全一致。

ADK 的 verbose 输出包含:

  • 事件类型
  • 事件作者(哪个代理)
  • 工具调用名和参数
  • 工具返回结构
  • 状态变化
  • 最终响应

这种"事件流即 trace"的设计让调试不需要额外工具,框架自带可观察性。LangSmith / Langfuse 等可观测平台进一步把这种事件流持久化、可视化、可分析。

22.9 小结

  • Google ADK 把 Agent 拆成工程化组件:Agent(决策)+ Runner(执行)+ Session(短期记忆)+ Memory(长期记忆)+ Event(事件流)+ ToolContext(工具状态访问)+ State(键值状态)。
  • 单 Agent 最小示例:定义 Agent(name/model/description/instruction/tools)-> 写工具(带 docstring 和类型标注)-> 配 Runner + Session -> 运行事件循环。封装 AgentCaller 把样板收起来。
  • 多 Agent 系统:根代理配置 sub_agents=[...] 做委派,子代理专注单一职责。根代理 tools=[](空)强制只做协调。子代理通过 description 让根代理知道何时委派。
  • ToolContext 让工具读写会话状态,实现跨代理共享数据。output_key 让代理最终输出直接写入 state。
  • 调试多 Agent 必须看事件流:哪个代理创建事件、委派是否正确、工具调用是否对、状态是否更新、终止条件是否合理。
  • ADK vs Claude SDK vs LangGraph:用 Gemini 或多模型选 ADK,用 Claude 选 Claude SDK,需要复杂状态机选 LangGraph。

到此,第 19-22 章形成了一个完整模块:规划(19)-> 多 Agent 模式(20)-> 知识图谱(21)-> ADK 实战(22)。这是 Agentic AI 从单 Agent 走向复杂系统的工程路径。

happy ending, love you.

基于 CC BY-SA 4.0 发布