Files
Agent-Docs/ADKLearning/google-adk-tutorial/04-custom-tools.md
Nelson 514723dbe7 init
2026-04-06 12:36:04 +08:00

19 KiB
Raw Blame History

第04章自定义工具开发

📌 本章目标

  • 理解 ADK 工具系统的工作原理
  • 掌握函数工具Function Tool的开发方法
  • 学习 MCP 工具的集成方式
  • 了解 OpenAPI 工具的生成方法
  • 掌握工具间数据传递的技巧

4.1 工具系统概述

4.1.1 工具是什么?

在 ADK 中,工具Tool 是 Agent 与外部世界交互的桥梁。Agent 通过调用工具来执行特定任务,如查询数据库、调用 API、执行计算等。

┌─────────────────────────────────────────────┐
│                  Agent                       │
│                                              │
│  用户: "帮我查一下北京的天气"                  │
│    ↓                                         │
│  LLM 分析: 需要调用天气查询工具               │
│    ↓                                         │
│  调用工具: get_weather(city="北京")           │
│    ↓                                         │
│  工具返回: {"status": "success",              │
│             "report": "晴天25°C"}           │
│    ↓                                         │
│  LLM 生成回复: "北京今天天气晴朗气温25°C"   │
└─────────────────────────────────────────────┘

4.1.2 工具类型

类型 说明 适用场景
Function Tool Python 函数自动转换 自定义逻辑、API 调用
MCP Tool 连接 MCP 服务器 标准化工具集成
OpenAPI Tool 从 OpenAPI 规范生成 REST API 集成
Agent Tool 将 Agent 作为工具使用 Agent 复用
内置工具 Google 提供的预置工具 搜索、代码执行等

4.2 函数工具Function Tool

4.2.1 基本原理

ADK 会自动检查 Python 函数的以下信息来生成工具 Schema

  • 函数名:工具的名称
  • docstring:工具的描述(发送给 LLM
  • 参数类型:参数的类型提示
  • 默认值:区分必选和可选参数
  • 返回值:工具的输出

4.2.2 基础示例

"""
函数工具基础示例
演示如何创建简单的自定义工具
"""

from google.adk.agents import Agent         # 导入 Agent 类


# ========================================
# 示例一:简单的天气查询工具
# ========================================

def get_weather(city: str) -> dict:
    """
    获取指定城市的天气信息

    Args:
        city (str): 城市名称,例如"北京"、"上海"

    Returns:
        dict: 包含天气状态的字典
    """
    # 模拟天气数据(实际应用中应调用真实天气 API
    weather_data = {                        # 模拟天气数据库
        "北京": {"temp": "25°C", "condition": "晴天"},  # 北京天气
        "上海": {"temp": "28°C", "condition": "多云"},  # 上海天气
        "广州": {"temp": "32°C", "condition": "雷阵雨"}, # 广州天气
    }

    # 查找城市天气数据
    data = weather_data.get(city)           # 从字典中获取天气数据

    # 如果找不到该城市的天气
    if not data:                            # 检查数据是否存在
        return {                            # 返回错误信息
            "status": "error",              # 状态:错误
            "error_message": f"未找到'{city}'的天气信息"  # 错误描述
        }

    # 返回成功结果
    return {                                # 返回天气信息
        "status": "success",                # 状态:成功
        "city": city,                       # 城市名称
        "temperature": data["temp"],        # 温度
        "condition": data["condition"],     # 天气状况
    }


# ========================================
# 示例二:带可选参数的工具
# ========================================

def search_restaurant(
    city: str,                              # 必选参数:城市
    cuisine: str = "中餐",                   # 可选参数:菜系,默认中餐
    price_range: str = "中等",               # 可选参数:价格范围,默认中等
) -> dict:
    """
    搜索指定城市的餐厅

    Args:
        city (str): 城市名称
        cuisine (str, optional): 菜系类型,默认为"中餐"
        price_range (str, optional): 价格范围,默认为"中等"

    Returns:
        dict: 包含搜索结果的字典
    """
    # 模拟餐厅搜索结果
    return {                                # 返回搜索结果
        "status": "success",                # 状态:成功
        "city": city,                       # 城市
        "cuisine": cuisine,                 # 菜系
        "price_range": price_range,         # 价格范围
        "results": [                        # 餐厅列表
            {"name": "美味餐厅", "rating": 4.5},  # 餐厅1
            {"name": "佳肴轩", "rating": 4.2},    # 餐厅2
        ],
    }


# ========================================
# 创建使用工具的 Agent
# ========================================

agent = Agent(
    name="tool_agent",                      # Agent 名称
    model="gemini-2.0-flash",               # 使用 Gemini 模型
    instruction=(                           # 系统指令
        "你是一个生活助手。\n"
        "你可以查询天气和搜索餐厅。\n"
        "根据用户需求使用合适的工具。"
    ),
    tools=[                                 # 注册工具列表
        get_weather,                        # 天气查询工具
        search_restaurant,                  # 餐厅搜索工具
    ],
)

4.2.3 参数类型详解

"""
工具参数类型详解
展示不同类型的参数定义方式
"""

from typing import Optional, List           # 导入类型提示
from google.adk.agents import Agent         # 导入 Agent 类


# ========================================
# 必选参数 vs 可选参数
# ========================================

def calculate_loan(
    principal: float,                       # 必选:贷款本金
    annual_rate: float,                     # 必选:年利率
    years: int,                             # 必选:贷款年限
    extra_payment: float = 0.0,             # 可选额外月供默认0
) -> dict:
    """
    计算贷款月供和总利息

    Args:
        principal (float): 贷款本金(元)
        annual_rate (float): 年利率(百分比,如 4.5 表示 4.5%
        years (int): 贷款年限
        extra_payment (float, optional): 每月额外还款金额,默认为 0

    Returns:
        dict: 包含月供、总利息等信息的字典
    """
    monthly_rate = annual_rate / 100 / 12   # 计算月利率
    total_months = years * 12               # 计算总月数

    # 计算等额本息月供
    monthly_payment = principal * monthly_rate * (1 + monthly_rate) ** total_months / ((1 + monthly_rate) ** total_months - 1)

    total_amount = monthly_payment * total_months + extra_payment * total_months  # 总还款额
    total_interest = total_amount - principal  # 总利息

    return {                                # 返回计算结果
        "status": "success",                # 状态
        "monthly_payment": round(monthly_payment, 2),  # 月供
        "total_interest": round(total_interest, 2),     # 总利息
        "total_amount": round(total_amount, 2),         # 总还款额
    }


# ========================================
# 使用 Optional 标记可选参数
# ========================================

def create_user_profile(
    username: str,                          # 必选:用户名
    bio: Optional[str] = None,              # 可选:个人简介
    avatar_url: Optional[str] = None,       # 可选:头像 URL
) -> dict:
    """
    创建用户档案

    Args:
        username (str): 用户名
        bio (str, optional): 个人简介
        avatar_url (str, optional): 头像图片 URL

    Returns:
        dict: 创建结果
    """
    profile = {                             # 创建用户档案字典
        "username": username,               # 用户名
        "bio": bio or "这个人很懒,什么都没写。",  # 简介(默认值)
        "avatar_url": avatar_url or "",     # 头像 URL
    }

    return {                                # 返回结果
        "status": "success",                # 状态
        "profile": profile,                 # 用户档案
    }


# ========================================
# 使用 List 类型参数
# ========================================

def compare_cities(cities: List[str]) -> dict:
    """
    比较多个城市的信息

    Args:
        cities (List[str]): 城市名称列表

    Returns:
        dict: 城市比较结果
    """
    city_info = {                           # 城市信息数据库
        "北京": {"population": "2189万", "area": "16410km²"},  # 北京
        "上海": {"population": "2487万", "area": "6341km²"},   # 上海
        "广州": {"population": "1881万", "area": "7434km²"},   # 广州
    }

    results = []                            # 初始化结果列表
    for city in cities:                     # 遍历城市列表
        info = city_info.get(city)          # 获取城市信息
        if info:                            # 如果信息存在
            results.append({                # 添加到结果列表
                "city": city,               # 城市名
                **info,                     # 展开城市信息
            })

    return {                                # 返回比较结果
        "status": "success" if results else "error",  # 状态
        "results": results,                 # 比较结果
    }


# 创建 Agent
agent = Agent(
    name="param_agent",                     # Agent 名称
    model="gemini-2.0-flash",               # 模型
    instruction="你是一个多功能助手。",       # 指令
    tools=[                                 # 工具列表
        calculate_loan,                     # 贷款计算器
        create_user_profile,                # 用户档案创建
        compare_cities,                     # 城市比较
    ],
)

4.2.4 工具返回值最佳实践

"""
工具返回值最佳实践
返回值会被 LLM 读取,因此需要清晰、结构化
"""

# ❌ 不好的返回值:信息不清晰
def bad_tool(query: str) -> str:
    """不好的返回值示例"""
    return "0"  # LLM 不知道 0 代表什么


# ✅ 好的返回值:结构化、有状态码
def good_tool(query: str) -> dict:
    """
    好的返回值示例
    包含状态码和详细描述
    """
    return {
        "status": "success",                # 状态码success / error / pending
        "query": query,                     # 原始查询
        "result": "查询结果详情",            # 具体结果
        "metadata": {                       # 可选的元数据
            "source": "database",           # 数据来源
            "timestamp": "2025-04-05T10:00:00Z",  # 时间戳
        },
    }


# ✅ 错误处理:返回有意义的错误信息
def robust_tool(query: str) -> dict:
    """
    健壮的工具:包含完善的错误处理
    """
    if not query or not query.strip():      # 检查空输入
        return {                            # 返回参数错误
            "status": "error",              # 状态:错误
            "error_code": "INVALID_INPUT",  # 错误码
            "error_message": "查询内容不能为空,请提供有效的查询参数。"  # 人类可读的错误信息
        }

    try:                                    # 尝试执行操作
        result = do_something(query)        # 执行实际操作
        return {                            # 返回成功结果
            "status": "success",            # 状态:成功
            "result": result,               # 操作结果
        }
    except Exception as e:                  # 捕获异常
        return {                            # 返回异常信息
            "status": "error",              # 状态:错误
            "error_code": "INTERNAL_ERROR", # 错误码
            "error_message": f"处理请求时发生错误:{str(e)}"  # 错误描述
        }


def do_something(query: str) -> str:
    """模拟操作函数"""
    return f"处理结果: {query}"              # 返回处理结果

4.3 工具间数据传递

"""
工具间数据传递
使用 session.state 的 temp: 前缀在工具间传递数据
"""

from google.adk.agents import Agent         # 导入 Agent 类
from google.adk.tools import FunctionTool   # 导入 FunctionTool
from google.adk.tools import ToolContext    # 导入工具上下文


# ========================================
# 使用 ToolContext 访问会话状态
# ========================================

def step1_fetch_data(
    query: str,                             # 搜索关键词
    ctx: ToolContext,                       # 工具上下文(自动注入)
) -> dict:
    """
    第一步:获取原始数据

    Args:
        query (str): 搜索关键词
        ctx (ToolContext): 工具上下文,可用于访问会话状态

    Returns:
        dict: 原始数据
    """
    # 模拟获取数据
    raw_data = f"关于'{query}'的原始数据..."  # 模拟数据

    # 将数据保存到临时状态temp: 前缀)
    # temp: 状态只在当前调用中有效,调用结束后自动清除
    ctx.state["temp:raw_data"] = raw_data   # 保存到临时状态

    return {                                # 返回结果
        "status": "success",                # 状态
        "message": f"已获取关于'{query}'的数据",  # 消息
        "data_length": len(raw_data),       # 数据长度
    }


def step2_analyze_data(ctx: ToolContext) -> dict:
    """
    第二步:分析第一步获取的数据

    Args:
        ctx (ToolContext): 工具上下文

    Returns:
        dict: 分析结果
    """
    # 从临时状态中读取第一步保存的数据
    raw_data = ctx.state.get("temp:raw_data")  # 读取临时状态

    if not raw_data:                        # 如果没有数据
        return {                            # 返回错误
            "status": "error",
            "error_message": "没有找到原始数据,请先执行数据获取步骤。"
        }

    # 模拟数据分析
    analysis = f"对数据进行了深度分析:{raw_data[:50]}..."  # 分析结果

    return {                                # 返回分析结果
        "status": "success",                # 状态
        "analysis": analysis,               # 分析结果
    }


# 创建 Agent
agent = Agent(
    name="pipeline_agent",                  # Agent 名称
    model="gemini-2.0-flash",               # 模型
    instruction=(                           # 指令
        "你是一个数据分析助手。\n"
        "当用户需要分析数据时,先使用 step1_fetch_data 获取数据,\n"
        "然后使用 step2_analyze_data 进行分析。"
    ),
    tools=[                                 # 工具列表
        step1_fetch_data,                   # 数据获取工具
        step2_analyze_data,                 # 数据分析工具
    ],
)

4.4 Agent-as-a-Tool智能体作为工具

"""
Agent-as-a-Tool
将一个 Agent 包装为工具,供其他 Agent 调用
"""

from google.adk.agents import Agent         # 导入 Agent 类
from google.adk.tools import AgentTool      # 导入 AgentTool 类


# ========================================
# 定义一个专门的翻译 Agent
# ========================================

translator_agent = Agent(
    name="translator",                      # 翻译 Agent 名称
    model="gemini-2.0-flash",               # 使用 Gemini 模型
    instruction="你是一个专业翻译,将用户输入的文本翻译成目标语言。只输出翻译结果。",  # 指令
)

# ========================================
# 将翻译 Agent 包装为工具
# ========================================

translation_tool = AgentTool(               # 创建 AgentTool
    agent=translator_agent,                 # 传入要包装的 Agent
)

# ========================================
# 在主 Agent 中使用翻译工具
# ========================================

main_agent = Agent(
    name="main_agent",                      # 主 Agent 名称
    model="gemini-2.0-flash",               # 使用 Gemini 模型
    instruction=(                           # 系统指令
        "你是一个多语言助手。\n"
        "当用户需要翻译时,使用 translation_tool 工具。\n"
        "其他情况直接回答用户问题。"
    ),
    tools=[translation_tool],               # 注册翻译工具
)

# AgentTool 与 sub_agents 的区别:
# - sub_agents通过 LLM Transfer 机制委派任务Agent 切换执行
# - AgentTool作为工具被调用在当前 Agent 上下文中执行

4.5 MCP 工具集成

"""
MCPModel Context Protocol工具集成
连接外部 MCP 服务器作为 Agent 的工具
"""

from google.adk.agents import Agent         # 导入 Agent 类
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset  # 导入 MCP 工具集


# ========================================
# 连接本地 MCP 服务器
# ========================================

async def create_mcp_agent():
    """创建使用 MCP 工具的 Agent"""

    # 创建 MCP 工具集
    # 需要先启动 MCP 服务器(如 filesystem、database 等)
    mcp_tools = McpToolset(
        connection_params={                  # MCP 服务器连接参数
            "url": "http://localhost:3000",  # MCP 服务器地址
        },
        tool_names=[                        # 要导入的工具名称列表
            "read_file",                    # 读取文件工具
            "write_file",                   # 写入文件工具
            "list_directory",               # 列出目录工具
        ],
    )

    # 获取 MCP 工具列表
    tools = await mcp_tools.get_tools()     # 异步获取工具

    # 创建使用 MCP 工具的 Agent
    agent = Agent(
        name="mcp_agent",                   # Agent 名称
        model="gemini-2.0-flash",           # 使用 Gemini 模型
        instruction="你是一个文件管理助手,可以读写文件和浏览目录。",  # 指令
        tools=tools,                        # 注册 MCP 工具
    )

    return agent                            # 返回 Agent

4.6 完整代码示例

详见 code/custom_tools_demo.py — 包含完整的自定义工具开发示例。


📌 本章小结

  • ADK 自动将 Python 函数转换为工具,通过函数签名生成 Schema
  • 好的 docstring 是 LLM 理解工具的关键
  • 返回值推荐使用字典,包含 status 字段
  • ToolContext 允许工具访问会话状态,实现工具间数据传递
  • AgentTool 可以将 Agent 包装为工具供其他 Agent 使用
  • MCP 工具集支持连接外部标准化工具服务器

下一章第05章 - 多智能体系统 → 学习如何构建多 Agent 协作系统。