今天我们将深入挖掘如何通过精巧的工具开发,为您的 AI 代理插上强大的“翅膀”,赋予它们超越核心语言模型限制的“手臂”与“感官”。一个真正强大的代理,不仅要能理解并生成文本,更关键的是能娴熟运用工具与外部世界交互、获取信息、执行复杂操作。本文将为您全方位解析函数工具 (Function Tools) 与代理工具 (Agent Tools) 的开发精髓,探讨工具开发中的权限管理与安全壁垒,并无私分享工具测试流程与调试秘籍,助您构建出功能更强大、运行更可靠的顶尖 AI 代理。
1. 函数工具 (Function Tools):精雕细琢,构筑定制化超能力
函数工具是 ADK 中最为基础且极富灵活性的工具类型。它允许开发者将自定义的 Python 函数或方法巧妙封装,供 AI 代理按需调用。这意味着您的代理将能够执行高度定制化的任务,例如:无缝连接到专有数据库、精准调用内部 API,或是实现独特的业务逻辑算法。
1.1. 函数工具的开发方法与实战精粹
高效函数工具的开发,远不止于编写 Python 代码,其核心在于如何引导大型语言模型 (LLM) 正确理解并高效使用这些工具。
- 定义清晰无歧义的函数签名
- 函数命名 (Function Name): 名称应具备高度描述性,力求“动词-名词”结构,清晰昭示其核心功能 (例如
get_weather
,search_documents
,schedule_meeting
)。务必避免使用如run
,process
,handle_data
或do_stuff
这种模糊不清的命名。一个像do_stuff
这样的名称,即便配有详尽描述,也可能让模型在抉择时感到困惑,远不如cancel_flight
来得直观。LLM 会将函数名作为选择工具时的关键标识之一。 - 参数 (Parameters/Arguments): 参数名需清晰易懂 (例如,使用
city
而非c
,search_query
而非q
)。 - 为所有参数提供明确的类型提示 (例如
city: str
,user_id: int
,items: list[str]
)。这对于 ADK 生成供 LLM 使用的正确模式至关重要。 - 确保所有参数类型均为 JSON 可序列化。标准的 Python 类型如
str
,int
,float
,bool
,list
,dict
及其组合通常是安全的。避免使用复杂的自定义类实例作为直接参数,除非它们拥有清晰的 JSON 表示形式。 - 切勿为参数设置默认值 (例如
def my_func(param1: str = "default")
)。底层模型在生成函数调用时,并不能可靠地支持或利用默认值。所有必要信息应由 LLM 从上下文中推断,或在缺失时主动向用户请求。参数数量宜少不宜多,以降低 LLM 的理解复杂度。
- 返回值 (Return Type): 函数工具的返回值必须是字典 (dict)。
- 若函数返回非字典类型 (如字符串、数字、列表),ADK 框架会自动将其包装成类似
{'result': your_original_return_value}
的字典,然后再将结果传递给模型。
- 精心设计字典的键和值,使其具有描述性且易于 LLM 理解。模型会读取此输出来决定下一步行动。例如,与其仅返回错误代码如
500
,不如返回{'status': 'error', 'error_message': 'Database connection failed'}
。
- 强烈建议包含一个状态键 (status key) (例如
'success'
,'error'
,'pending'
,'ambiguous'
),以清晰地向模型指示工具执行的结果。
- 编写高质量的文档字符串 (Docstrings) 文档字符串对函数工具而言,其重要性不言而喻。它们不仅是给其他开发者阅读的注释,更是 LLM 理解工具功能、参数及预期返回值的核心依据。文档字符串应清晰、全面地描述:
- 函数的功能与用途:它究竟是做什么的?
- 何时应该使用该函数:提供具体的上下文或示例场景。
- 每个参数的含义与预期格式。
- 返回字典的结构与含义,特别是不同
status
值及其关联的数据键。
- 注意:不要在文档字符串中描述由 ADK 注入的
tool_context: ToolContext
参数。LLM 无需了解此参数,它是在 LLM 决定调用工具后由 ADK 自动注入的。
一份精心雕琢的文档字符串,是 LLM 正确选择和使用工具的先决条件。若 LLM 无法理解工具的用途或如何提供参数,工具将形同虚设。这凸显了文档字符串在人机(LLM)协作中的桥梁作用,其质量直接影响代理的智能水平和任务完成的准确性。
- 工具注册与代理集成 将定义好的 Python 函数添加到代理的
tools
列表中即可完成注册。ADK 框架会自动将 Python 函数包装成一个函数工具。
from google.adk.agents import Agent
from typing import Dict, Any # 建议补充 typing
# 假设已定义函数 get_exchange_rate
def get_exchange_rate(currency_from: str, currency_to: str, currency_date: str) -> Dict[str, Any]:
"""
获取指定日期从一种货币到另一种货币的汇率。
Args:
currency_from: 基础货币代码 (例如 "USD")。
currency_to: 目标货币代码 (例如 "EUR")。
currency_date: 查询汇率的日期 (格式 "YYYY-MM-DD")。
Returns:
一个包含汇率信息的字典。
例如: {"status": "success", "rate": 0.92, "date": "2024-05-10"}
{"status": "error", "message": "API request failed"}
"""
# ... 函数实现逻辑 ...
# 示例:
if currency_from == "USD" and currency_to == "EUR":
return {"status": "success", "rate": 0.925, "date": currency_date}
else:
return {"status": "error", "message": f"Exchange rate not found for {currency_from} to {currency_to} on {currency_date}"}
agent = Agent(
model="gemini-2.0-flash", # 或其他 Gemini 模型
name='currency_exchange_agent',
tools=[get_exchange_rate], # 注册函数工具
instruction="你是一个货币汇率查询助手,请使用 get_exchange_rate 工具来回答用户关于汇率的问题。" # 示例指令
# ... 其他参数 ...
)
代理的指令 (instruction) 中可以明确引导 LLM 何时以及如何使用这些工具,甚至可以指定工具调用的顺序或如何处理工具返回的不同结果。
- 利用
ToolContext
访问高级功能 若函数工具需要在执行期间访问或修改会话状态、管理文件 (Artifacts)、与长期记忆 (Memory) 交互或控制代理的后续行为 (例如,请求身份验证、跳过摘要),可在其函数签名中包含一个特殊的tool_context: ToolContext
参数。ToolContext
提供了对以下核心功能的访问:
state: State
: 读取和修改当前会话的状态。
actions: EventActions
: 影响代理的后续动作 (例如,跳过摘要skip_summarization
,转移到另一个代理transfer_to_agent
)。
function_call_id: str
: 当前工具调用的唯一标识符。
function_call_event_id: str
: 触发工具调用的事件的唯一标识符。
auth_response: Any
: 若已完成身份验证流程,则包含身份验证凭据。
- 与已配置服务交互的方法,如 Artifacts (
list_artifacts
,load_artifact
,save_artifact
) 和 Memory (search_memory
)。
对 tool_context.state
的更改会自动记录在结果事件的 state_delta
中,并由 SessionService
持久化。
表1: 函数工具开发最佳实践清单
实践点 | 描述 | 重要性 (对LLM理解/工具可靠性) |
描述性函数名 | 使用动词-名词结构,清晰指示动作 (例如 | 高 |
清晰的参数名 | 使用有意义的参数名 (例如 | 高 |
类型提示 | 为所有参数和返回值提供明确的类型提示。 | 高 |
JSON可序列化参数 | 确保所有参数类型都是JSON可序列化的 ( | 高 |
无默认参数值 | 不要在函数定义中为参数设置默认值。 | 高 |
返回字典类型 | 函数必须返回一个字典。 | 极高 |
包含状态键 | 返回的字典中强烈建议包含一个 | 极高 |
详尽的文档字符串 | 清晰描述工具功能、参数、返回值结构和使用场景。这是LLM理解工具的关键。 | 极高 |
本地测试 | 在集成到代理之前,对函数进行本地测试以确认其按预期工作。 | 高 |
错误处理 | 在工具函数内部实现健壮的错误处理,并通过返回字典的 | 高 |
遵循这些实践,可以显著提升函数工具的可用性和可靠性。特别是返回字典类型并包含明确的 status
键,这一约定为LLM提供了一个标准化的方式来理解工具执行的直接结果,从而能够更智能地决定后续步骤。这形成了一种工具与LLM之间的“契约”,简化了复杂的交互逻辑。
1.2. 长时间运行函数工具 (Long Running Function Tools):优雅处理耗时任务
对于需要较长时间处理而不能阻塞代理执行的任务 (例如,复杂的计算、等待外部批准),ADK 提供了对长时间运行函数工具的优雅支持。
- 工作机制:
- 启动与初始响应: 当 LLM 调用一个长时间运行的函数工具时,该工具启动其耗时操作。它可以选择性地立即返回一个初始结果(例如,操作ID、进度指示),ADK 会将其包装在
FunctionResponse
中发送给 LLM,LLM 可以此通知用户。随后,代理的当前运行会暂停。
- 客户端决策与轮询: 代理客户端(调用 ADK Runner 的应用)可以决定是等待操作完成还是继续其他交互。客户端可以轮询长时间运行操作的进度。
- 中间/最终结果更新: 客户端可以将操作的中间进度或最终结果作为新的
FunctionResponse
发送回代理进行下一次运行。ADK 框架会将这些响应传递给 LLM,以便生成用户友好的消息。
- 创建与使用:通过将 Python 函数用 LongRunningFunctionTool 包装来创建。工具函数可以利用 tool_context 来管理状态或与外部系统交互以获取进度。 Python
import time # 补充导入
from typing import Dict, Any # 建议补充 typing
from google.adk.tools import LongRunningFunctionTool
from google.adk.agents.tool_context import ToolContext # 假设的导入路径,实际应为 from google.adk.agents import ToolContext
def ask_for_approval(purpose: str, amount: float, tool_context: ToolContext) -> Dict[str, Any]:
"""向相关方请求报销批准。这是一个长时间运行的任务。"""
# 实际应用中,这里会创建一个审批工单,并可能返回工单ID
ticket_id = f"APPROVAL_{int(time.time())}" # 示例ID
# 可以将 ticket_id 存入 tool_context.state 以便后续查询
tool_context.state[f"approval_ticket_{ticket_id}"] = {"status": "pending", "purpose": purpose, "amount": amount}
return {"status": "pending", "message": f"Approval request submitted with ID: {ticket_id}. Waiting for response.", "ticket_id": ticket_id}
approval_tool = LongRunningFunctionTool(func=ask_for_approval)
2. 代理工具 (Agent Tools):构建智能协作的多智能体网络
ADK 的一个激动人心的特性是支持构建多智能体系统 (Multi-Agent System, MAS)。在此架构中,一个代理可以将另一个(通常是更专业的)代理作为工具来使用——这便是代理工具 (Agent Tool)。这种模式使得开发者能够构建模块化的、可无限扩展的 AI 应用,其中不同的代理各司其职,负责不同的子任务,并通过协作达成更宏伟的目标。
2.1. 代理工具的设计与实现奥秘
将一个代理用作另一个代理的工具,为构建复杂的、分层级的智能系统提供了前所未有的强大机制。
- 定义与注册 AgentTool要将一个已定义的代理(我们称之为“子代理”)用作另一个代理(“父代理”)的工具,您需要将子代理实例包装在 AgentTool 类中,然后将这个 AgentTool 实例添加到父代理的 tools 列表中。AgentTool 会为 LLM 生成一个相应的函数声明,使得父代理的 LLM 可以像调用普通函数工具一样“调用”这个子代理。 Python
from google.adk.agents import Agent, LlmAgent # LlmAgent 通常用于作为父代理
from google.adk.tools import AgentTool # 确保这是正确的导入路径 for AgentTool
# 定义一个专门处理特定任务的子代理
specialized_agent = Agent(
model="gemini-2.0-flash",
name="special_task_agent",
description="该代理专门处理任务 X。", # Description 对父LLM选择工具有帮助
instruction="你是一位任务 X 的专家。请按要求执行任务。"
# tools= [...] # 子代理也可以有自己的工具
)
# 定义父代理,并将 specialized_agent 作为 AgentTool 添加到其工具列表
coordinator_agent = LlmAgent( # LlmAgent 更适合做协调者
model="gemini-2.0-flash", # 或更强大的模型如 gemini-1.5-pro
name="coordinator_agent",
instruction="你是一个任务协调员。如果需要,请将任务委派给专业代理。对于任务 X,请使用 'special_task_agent' 工具。",
tools=[AgentTool(agent=specialized_agent)] # 注册 AgentTool
)
- 代理间数据流:输入与输出的艺术
- 输入到
AgentTool
: 当父代理的 LLM 决定调用某个AgentTool
时,它会生成一个函数调用。这个函数调用的“参数”(源自父代理的推理)实际上构成了传递给子代理的输入查询或任务描述。
- 执行: ADK 框架会执行
AgentTool.run_async()
方法。此方法使用提供的输入来运行目标子代理。
AgentTool
的输出:AgentTool
会捕获子代理的最终响应。这个响应随后作为AgentTool
调用的“结果”返回给父代理。父代理的 LLM 接着会将此结果整合到其后续步骤或最终响应的推理中。
AgentTool
的上下文与状态管理
- 共享会话状态 (
session.state
): 父代理和通过AgentTool
调用的子代理之间进行复杂数据传递或上下文共享的主要机制是共享的session.state
。
- 父代理可以在调用
AgentTool
之前向context.state
写入数据。
- 子代理(运行时)可以从其自身的
context.state
中读取这些数据(因为它们共享相同的会话上下文)。
- 同样,子代理可以将其结果或中间数据写入
context.state
,父代理在AgentTool
完成后可以访问这些数据。
- 重要提示: 在
state
中使用具有区分性的键名以避免冲突。
- 状态/工件转发:
AgentTool.run_async
还会将子代理所做的任何状态或工件 (artifact) 更改转发回父代理的上下文,确保数据一致性。
将子代理封装为 AgentTool
提供了一种显式的、类似工具调用的交互模式。这与直接在父代理的 sub_agents
列表中定义子代理,然后由父 LLM 隐式决定何时“委托”任务给它们形成对比。AgentTool
机制使得子代理的调用更加可控和同步,父代理明确地“调用一个工具”并期待一个直接的结果。这种方式更适合于那些需要将子任务作为一种定义良好的能力来同步调用的场景。 尽管 AgentTool
处理子代理最终响应的直接输入/输出,但任何更丰富的、多轮的或持久性的数据共享都严重依赖于 session.state
。LlmAgent 上的 output_key
属性为此提供了一种便利。
表2: AgentTool
与 LLM 驱动的子代理委托对比
特性 | AgentTool (显式工具调用) | LLM 驱动的委托 (通过 sub_agents 列表) |
调用机制 | 父 LLM 将子代理视为一个普通工具进行调用。 | 父 LLM 根据子代理描述和当前任务,决定是否将控制权“转移”或“委托”给某个子代理。 |
控制流 | 同步,父代理等待 | 通常也是同步的,但 LLM 的决策过程可能更灵活,更像是一种内部路由。 |
数据传递 (主要) | 直接的函数调用式参数输入和结果输出;更复杂数据依赖共享的 | 输入通常是父 LLM 传递的任务描述;输出是子代理的最终响应;更复杂数据依赖共享的 |
LLM 角色 | 父 LLM 决定何时调用此“工具”,并处理其返回结果。 | 父 LLM 扮演更强的编排角色,决定将任务的哪一部分交给哪个子代理。 |
显式性 | 非常显式,如同调用任何其他函数工具。 | 相对隐式,由 LLM 的内部推理驱动。 |
典型用例 | 将子代理的特定功能封装为父代理的一个明确能力;需要同步获取子代理处理结果的场景。 | 需要根据对话上下文动态地将任务分配给不同专业子代理的场景;更灵活的协作流程。 |
状态/工件转发 |
| 状态和工件变更也通过共享会话上下文反映。 |
ADK 实现 |
|
|
2.2. 运用代理工具的编排与通信模式
借助 AgentTool
和共享会话状态,开发者可以实现多种复杂的多智能体编排模式。
AgentTool
促进的常见多智能体模式:
- 分层任务分解 (Hierarchical Task Decomposition): 父代理使用多个专业的
AgentTool
(子代理)来分解复杂问题并汇总结果。例如,一个旅行规划代理可以使用分别负责航班预订、酒店预订和活动规划的AgentTool
。
- 顺序处理流水线 (Sequential Processing Pipelines): 尽管 ADK 提供了专门的
SequentialAgent
工作流代理,但如果父LlmAgent
被指示按特定顺序调用一系列AgentTool
,并使用session.state
在它们之间传递数据,也可以实现类似的模式。
- 评审/批判模式 (Generator-Critic): 父代理可以使用一个
AgentTool
(生成器)创建内容,并使用另一个AgentTool
(批判器)进行评审。父代理负责协调反馈循环,可能会使用session.state
来传递内容和评论。
- 控制 AgentTool 响应的摘要行为AgentTool 类有一个 skip_summarization: bool 属性。
- 如果设置为
True
,ADK 框架将绕过对子代理(工具代理)响应的基于 LLM 的摘要处理。
- 这在子代理的响应已经是良好格式化的(例如,结构化的 JSON 或预格式化的字符串),并且不需要在被父代理使用或返回给用户之前进行进一步处理或摘要时非常有用。
- 这对于处理来自
AgentTool
的结构化输出尤为重要。开发者可以设计子代理以产生特定的 JSON 输出,并将skip_summarization=True
。
skip_summarization
选项揭示了在智能体系统中,LLM 原生交互与结构化数据处理之间的一种设计平衡。默认情况下,ADK 可能会尝试使用 LLM 总结工具的输出。然而,如果作为 AgentTool
的子代理旨在产生特定的结构化输出(如 JSON),skip_summarization=True
允许开发者绕过这个 LLM 步骤,确保原始结构化输出被精确传递。
- 通过共享会话状态进行通信(再次强调)再次强调,对于不属于 AgentTool 直接输入/输出的数据传递,或在同一父代理多次调用 AgentTool 之间维持上下文,session.state 是核心关键。当多个 AgentTool 或其他组件与相同的会话状态交互时,为状态键使用清晰的命名约定以避免冲突至关重要。 要实现涉及多个
AgentTool
的复杂编排模式,开发者必须为父代理精心设计复杂的指令 (instruction)。这些指令需要概述总体计划、每个AgentTool
的角色以及如何管理中间数据。这将提示工程的重要性从单个代理任务提升到编排整个代理团队的层面。
3. 守护您的工具:权限管理与安全策略
在开发和部署 ADK 工具时,管理权限和确保安全是绝对的重中之重。本节将聚焦 ADK 的身份验证框架以及凭证管理的最佳实践。
3.1. ADK 的工具身份验证框架:坚实后盾
ADK 内置了对工具身份验证的强大支持,使工具能够安全地访问受保护的资源。
- 核心概念:
AuthScheme
:定义了向 API 提供身份验证凭据的预期方法。ADK 支持多种符合 OpenAPI 3.0 规范的方案,包括APIKey
、HTTPBearer
、OAuth2
和OpenIdConnectWithConfig
。
AuthCredential
:包含启动身份验证过程所需的初始信息(例如 API 密钥、OAuth 客户端 ID/密钥、服务帐户详细信息)以及auth_type
(例如API_KEY
,OAUTH2
,SERVICE_ACCOUNT
)。
- 自定义函数工具发起的身份验证 (通过 ToolContext):自定义 FunctionTool 必须在其签名中包含 tool_context: ToolContext 才能访问身份验证功能。
- 检查缓存凭据: 工具应首先检查
tool_context.state
中是否存在先前运行中缓存的有效凭据。
- 检查客户端 Auth 响应: 若无缓存凭据,工具通过调用
exchanged_credential = tool_context.get_auth_response()
检查客户端是否刚完成交互式流程。
- 发起身份验证请求: 若两者皆无,工具通过调用
tool_context.request_credential(AuthConfig(...))
来启动身份验证请求。工具此时通常返回一个表示挂起的状态。
- 预构建工具的身份验证处理:对于像 RestApiTool、OpenAPIToolset、GoogleApiToolSet 这类工具,身份验证在工具初始化时通过传递 AuthScheme 和 AuthCredential 进行配置。当凭据缺失或无效时,ADK 会自动检测并生成 adk_request_credential 事件通知代理客户端。
- 处理 OAuth 2.0 / OpenID Connect 流程:
- 当 ADK 为交互式流程发出
adk_request_credential
事件时,事件中的AuthConfig
对象包含auth_uri
(授权 URL)。 - 代理客户端的责任: 客户端应用(例如运行 ADK 代理的 Web 应用)必须将用户重定向到此
auth_uri
,并附加其自身的预注册redirect_uri
。 - 用户授权后,身份验证提供商会将用户重定向回客户端的
redirect_uri
,并附带一个authorization_code
。 - 客户端必须捕获这个完整的回调 URL,并创建一个
FunctionResponse
(名称为adk_request_credential
,ID 与原始请求匹配),其response
字段包含序列化后的、更新的AuthConfig
对象。此更新对象应将auth_response_uri
字段设置为收到的完整回调 URL,并将redirect_uri
字段设置为所使用的重定向 URI。 - 客户端随后使用此
FunctionResponse
内容作为new_message
再次调用runner.run_async
。 - ADK 接收此响应,执行 OAuth 令牌交换,获取访问令牌,并自动重试最初因缺少身份验证而失败的工具调用。
- 当 ADK 为交互式流程发出
ADK 的身份验证框架极大地简化了 OAuth 的复杂性,但代理客户端在用户重定向和回调处理方面的正确实现至关重要。adk_request_credential
事件在此过程中扮演了关键的信号传递角色,实现了非阻塞的、事件驱动的身份验证流程。
3.2. 凭证管理最佳实践:安全第一
安全地管理 API 密钥和 OAuth 令牌等敏感凭证至关重要。
- 管理函数工具自有的 API 密钥(工具层面凭证):
- 严禁硬编码: 绝不在源代码中硬编码 API 密钥,也不要将它们提交到代码仓库。
- 环境变量: 本地开发和某些部署场景下的常见做法 (例如使用
.env
文件)。
- 安全密钥存储(生产环境推荐): 使用专门的密钥管理服务,如 Google Cloud Secret Manager 或 HashiCorp Vault。
- API 密钥限制: 限制 API 密钥的使用范围(如特定服务、IP 地址或 API 方法)。
- 定期轮换与按应用/环境分离密钥。
- 处理用户委托的凭证 (OAuth 令牌):
- 在会话状态中缓存: OAuth 流程成功后,将访问令牌和刷新令牌缓存在
tool_context.state
中。
- 工具内令牌刷新: 检查访问令牌有效性,必要时使用刷新令牌获取新令牌,并更新
tool_context.state
。
- 持久化
SessionService
中令牌的安全存储: 若使用持久化的SessionService
,切勿以明文形式存储令牌。在存入数据库前应进行加密处理。
- 最小权限原则: 仅请求工具执行其预期操作所必需的 OAuth 范围 (scopes)。
- 服务账户用于代理身份验证(非用户委托):当代理本身需要访问 Google Cloud 资源时,应为其配置服务账户。ADK 与应用默认凭据 (Application Default Credentials, ADC) 集成。 对 API 密钥的安全管理需要多层防护。同样,缓存在
tool_context.state
中的凭证的安全性直接取决于底层SessionService
实现的安全性。
表3: ADK 工具中安全凭证管理策略
凭证类型 | ADK 上下文中的推荐存储/访问方法 | 关键安全考量与 ADK 实现说明 |
工具自有的 API 密钥 | 开发: 环境变量 ( | - 严禁硬编码。 - 限制密钥权限范围。 - 定期轮换密钥。 |
用户 OAuth 访问令牌 | 通过 OAuth 流程获取后,缓存在 | - 检查有效期,必要时使用刷新令牌刷新。 - 若 |
用户 OAuth 刷新令牌 | 同访问令牌,缓存在 | - 安全性要求极高。与访问令牌一样,若持久化存储,必须加密。 - 严格控制其暴露。 |
Google Cloud 服务账户密钥 | 通常通过应用默认凭据 (ADC) 自动管理,ADK 应用在 Google Cloud 环境中运行时可利用此机制。无需手动管理。 | - 遵循服务账户的最小权限原则。 - ADC 简化了 Google Cloud 服务身份验证。 |
3.3. 实现工具内防护与访问策略:纵深防御
除了管理凭证外,工具本身也应包含安全防护机制。
- 利用
ToolContext
实现动态策略执行: 工具可以使用tool_context.state
(其中可能包含用户角色或许可权)来验证当前用户是否被允许执行特定操作。
- 工具内输入验证: 工具应严格验证 LLM 提供的参数,以防止注入攻击或意外操作。可以使用
before_tool_callback
添加对调用的预验证。这种“工具内防护”要求工具不能盲目信任 LLM 生成的输入。
- 与更广泛的安全措施集成:
- 模型安全设置: 为底层 LLM(例如 Gemini)配置安全设置以过滤有害内容。
- Vertex AI IAM & VPC 服务控制: 部署在 Vertex AI 上时,利用 Google Cloud IAM 和网络控制进行访问控制和数据防泄露。
- 第三方权限系统: 对于复杂的权限模型,可考虑与外部权限系统集成(概念性,ADK无直接内置)。
有效的工具安全性是 ADK 框架特性、工具内部逻辑以及部署环境能力之间协作努力的结果。
4. 确保工具可靠性:严谨测试与高效调试
为了确保 ADK 工具在代理系统中正确、可靠地运行,本节将重点介绍测试方法和调试技术。
4.1. 迭代开发与本地测试:快速反馈循环
ADK 提供了便利的本地开发和测试工具,支持快速迭代。
- ADK 开发者界面 (
adk web
): 使用adk web
命令启动一个本地开发者 Web UI。这个界面允许开发者选择代理、与之交互、检查执行步骤、调试交互过程,并可视化代理定义。
- ADK API 服务器 (
adk api_server
):adk api_server
命令会启动一个本地 FastAPI 服务器。开发者可使用 cURL 或 API 客户端(如 Postman)向代理发送请求,测试工具调用和响应。
- 本地测试的会话管理: 会话可以通过 API 创建,并可在创建时初始化状态。注意会话 ID 对于给定用户必须唯一。
4.2. 工具的单元测试:隔离验证
单元测试的重点是独立地测试各个函数工具。
- 模拟
ToolContext
: 这是关键技术。使用 Python 的模拟库(例如unittest.mock.MagicMock
)创建ToolContext
的模拟对象,以模拟不同场景(状态、认证、操作等)。
import unittest
from unittest.mock import MagicMock, ANY # ANY can be useful
# 假设 ToolContext 和你的工具函数定义
# from your_module import your_function_tool, ToolContext # 实际导入
# 简化的 ToolContext 定义,实际应从 ADK 导入
class ToolContext:
def __init__(self):
self.state = {}
self.actions = MagicMock() # 使用 MagicMock 更灵活
def get_auth_response(self): pass
def request_credential(self, auth_config): pass
# ... 其他 ToolContext 方法和属性
def my_weather_tool(city: str, tool_context: ToolContext) -> dict:
"""示例工具,从状态中获取城市天气,若无则返回提示。"""
if f"weather_{city}" in tool_context.state:
return {"status": "success", "report": tool_context.state[f"weather_{city}"]}
else:
tool_context.actions.request_clarification = f"Weather data not found for {city}."
return {"status": "not_found", "message": f"City weather not in state for {city}."}
class TestMyWeatherTool(unittest.TestCase):
def test_weather_found_in_state(self):
mock_context = MagicMock(spec=ToolContext) # spec=ToolContext 确保模拟对象行为符合ToolContext接口
mock_context.state = {f"weather_London": "Sunny, 25C"}
mock_context.actions = MagicMock() # 确保 actions 也是一个 mock
result = my_weather_tool(city="London", tool_context=mock_context)
self.assertEqual(result["status"], "success")
self.assertEqual(result["report"], "Sunny, 25C")
# self.assertIsNone(mock_context.actions.request_clarification) # 确保未被设置 (如果 actions.request_clarification 默认为 None)
# 或者,如果不想它被调用/设置:
self.assertFalse(hasattr(mock_context.actions, 'request_clarification') or mock_context.actions.request_clarification is not None)
def test_weather_not_found_requests_clarification(self):
mock_context = MagicMock(spec=ToolContext)
mock_context.state = {} # 空状态
mock_context.actions = MagicMock()
result = my_weather_tool(city="Paris", tool_context=mock_context)
self.assertEqual(result["status"], "not_found")
# 验证 actions.request_clarification 是否被正确设置
self.assertEqual(mock_context.actions.request_clarification, "Weather data not found for Paris.")
if __name__ == '__main__':
unittest.main()
- 模拟外部服务依赖: 若工具调用外部 API,应模拟这些调用 (例如使用
requests-mock
或unittest.mock.patch
)。
- ADK 评估框架 (
adk eval
): 可用于针对简单的代理-模型交互(单一会话)进行类单元测试。
4.3. 多智能体场景下的集成测试:协同验证
集成测试验证多个组件之间的交互,例如父代理与 AgentTool
,工具序列,以及通过 session.state
的数据流。
- 使用
pytest
与 ADK: ADK 文档提到可以使用pytest
以编程方式运行测试。测试通常涉及设置代理、Runner 和SessionService
(如InMemorySessionService
),然后发送查询并断言结果或状态。
- 测试代理-工具通信: 设计测试用例,验证
AgentTool
是否接收正确输入,父代理是否正确处理其输出,以及session.state
数据是否正确传递。
- 测试 Google Cloud 工具集成: 验证代理是否能正确调用云工具并处理其响应。
4.4. 调试技巧与常见问题故障排除:洞察秋毫
- 有效的日志记录:
- 工具内日志记录: 使用
print
或 Python 的logging
模块。
- ADK 回调日志: 使用
before_tool_callback
、after_tool_callback
等记录工具调用详情。
- 结构化日志记录: 使用一致格式,包含上下文、时间戳和请求 ID。
- 利用可观察性工具: ADK 可与 Comet Opik 等第三方工具集成,捕获详细的代理交互跟踪。
- 常见错误与故障排除: 许多 ADK 工具错误源于 LLM 与工具代码之间的接口不匹配(文档字符串、模式、调用约定)。调试不仅涉及 Python 代码,还需检查代理指令、工具文档字符串和 LLM 交互跟踪。
表4: 常见 ADK 工具开发错误与故障排除指南
错误症状/消息 (或场景) | 可能原因 | ADK 中的关键故障排除步骤与解决方案 |
LLM 未选择正确的工具,或为工具提供了错误的参数。 | 工具的文档字符串 (docstring) 不清晰或不准确;工具名称模糊;代理的指令 (instruction) 未能有效引导 LLM。 | - 优化文档字符串:清晰描述功能、参数、返回值、适用场景。 - 改进工具命名:使用描述性的动词-名词结构。 - 强化代理指令:明确指示 LLM 何时如何使用工具。 - 使用开发者 UI:检查 LLM 推理步骤。 |
| 外部模式 (如 MCP) 与 Vertex AI/ADK 期望不兼容,尤其枚举 (enum) 类型。 | - 模式规范化:转换外部模式以符合 ADK/Vertex AI 要求,确保枚举类型基于原始类型 (如 string)。 |
| 子代理在同一轮交互中尝试使用某些内置工具 (如 | - 结构调整:考虑由父代理使用该工具,或将子代理作为 - 注意工具限制:检查是否违反了内置工具的使用限制。 |
身份验证失败 (例如,HTTP 401/403 错误) | 凭据错误/过期;OAuth 配置错误;权限/范围不足;工具未正确处理 | - 验证凭据。 - 检查令牌刷新逻辑。 - 审查 OAuth 流程:确认客户端重定向 URI 和回调处理。 - 检查范围。 - 使用 ADK 日志/跟踪。 |
工具参数类型错误或参数非 JSON 可序列化 | 工具期望类型与 LLM 提供不匹配;工具定义了 LLM 不支持的复杂对象作参数。 | - 确保参数 JSON 可序列化: - 检查类型提示。 - 简化参数。 |
工具返回格式不正确 | 工具未返回字典;返回字典缺 | - 确保返回字典。 - 包含 - 描述性内容。 |
| 端口冲突;代理路径不正确;网络配置问题;依赖项未正确安装。 | - 检查端口。 - 验证路径。 - 查看控制台输出。 - 检查依赖。 |
5. 总结与展望:开启智能代理新纪元
函数工具 (Function Tools) 和代理工具 (Agent Tools) 无疑是 Google Agent Development Kit (ADK) 中扩展 AI 代理能力的双引擎。通过精心设计的函数工具,开发者能赋予代理执行特定任务、与外部系统交互及访问专有数据的超能力。而代理工具则通过支持多智能体架构,使得构建模块化、可协作的复杂 AI 系统从理念走向现实,其中每个代理都能专注于特定领域,并通过工具化的方式被其他代理高效调用。
本文深入探讨了这两种工具的开发方法、设计原则与最佳实践。我们强调了清晰的函数签名、高质量的文档字符串对于函数工具的基石作用——它们是 LLM 理解和正确使用工具的先决条件。对于代理工具,我们剖析了其在多智能体编排中的核心地位,以及如何通过共享会话状态 (session.state
) 和 AgentTool
的 skip_summarization
等特性来精细管理代理间的通信与数据流。
此外,安全性是工具开发中永恒的主题。ADK 提供了相对完善的身份验证框架来处理工具对受保护资源的访问,特别是对 OAuth 2.0 等复杂流程的内置支持。然而,开发者仍需警钟长鸣,严格遵循凭证管理的最佳实践,例如采用安全密钥存储、在会话状态中安全地缓存和刷新令牌,并实施工具内建的防护策略。
最后,确保工具的可靠性离不开严格的测试与高效的调试。ADK 提供的开发者 UI (adk web
)、API 服务器 (adk api_server
) 以及对单元测试(尤其是模拟 ToolContext
)和集成测试的良好支持,为开发者提供了迭代开发和验证工具功能的强大武器。结合有效的日志记录和对常见错误的深刻理解,可以显著提高开发效率和最终工具的质量。
虽然 ADK 提供了强大的功能集,但构建出真正可靠、可维护且安全的 AI 代理,最终依赖于开发者对这些最佳实践的深刻理解、不懈坚持和灵活应用。随着智能体技术的浪潮奔涌向前,持续学习和探索新的工具开发模式与安全策略,将是每位 AI 应用开发者不断精进的必修课。
希望本文能为您的 ADK 工具开发之旅提供坚实的基础和有力的支持,助您在构建下一代智能应用的道路上乘风破浪!如果您有任何疑问或心得,欢迎在评论区分享交流。