ADK 函数工具实战:把普通 Python 函数变成 Agent 可调用的工具(function_tools 示例详解)
2026/9/13 10:02:53 网站建设 项目流程

ADK 函数工具实战:把普通 Python 函数变成 Agent 可调用的工具(function_tools 示例详解)

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

本文以 ADK(Agent Development Kit)官方示例contributing/samples/tools/function_tools为主体,完整还原"两个纯 Python 函数如何被注册为 Agent 工具"的全过程:从函数定义、类型注解与 docstring 规范,到tools=[...]注册方式、测试事件文件中的完整调用链,再到 ADK 源码层面FunctionTool如何从函数签名自动推导工具声明(FunctionDeclaration)、如何注入框架上下文。读完本文,你可以照此模式将任意本地 Python 方法转化为 Agent 能力,并理解 ADK 底层参数解析与声明缓存的实现原理。

一、示例定位:最小可用的 Function Tools Agent

该示例(README)的目标非常聚焦:演示如何用ADK框架构造一个配备了内置 Python 函数工具的 Agent。它定义了一个Agent,包裹了两个工具函数generate_random_numberis_even——LLM 会根据用户提示词自动调用这两个底层 Python 函数,展示"把原始 Python 方法变成 Agent 可执行能力"有多简单。

示例目录结构:

contributing/samples/tools/function_tools/ ├── README.md # 本文主体文档 ├── __init__.py # 将 agent 模块暴露为 ADK agent 包入口 ├── agent.py # 工具函数 + root_agent 定义 └── tests/ ├── a_random_number.json # 场景1:单次工具调用 ├── a_random_number_and_check_even.json # 场景2:两步串行调用 └── random_number_and_is_5_even.json # 场景3:单步并行调用

init.py 仅一行from . import agent,这正是 ADK 标准 agent 包的约定:目录名即 agent 目录,agent.py中必须导出名为root_agent的 Agent 实例。

README 中给出的调用关系图如下:

二、完整实现:agent.py 逐行解析

示例的全部业务代码就是 agent.py,去掉 License 头后如下:

import os import random from google.adk.agents import Agent _counter = 0 def generate_random_number(max_value: int = 100) -> int: """Generates a random integer between 0 and max_value (inclusive). Args: max_value: The upper limit for the random number. Returns: A random integer between 0 and max_value. """ # Return a growing value in tests to ensure determinism while allowing # multiple calls. if "PYTEST_CURRENT_TEST" in os.environ: global _counter _counter += 1 return _counter return random.randint(0, max_value) def is_even(number: int) -> bool: """Checks if a given number is even. Args: number: The number to check. Returns: True if the number is even, False otherwise. """ return number % 2 == 0 root_agent = Agent( name="function_tools", tools=[generate_random_number, is_even], )

对照 README 的 "How To",这里有三个关键写法要点:

  1. 标准 Python 函数 + 类型注解 + 精确 docstring。README 明确要求"Define standard Python functions with type hints and precise docstrings"。这不是风格建议,而是框架硬需求——ADK 依赖inspect.signature提取参数类型与默认值来生成工具声明(见第四节源码分析)。Google 风格 docstring 的Args/Returns段会成为 LLM 理解该工具何时该调用、参数语义为何的主要依据。
  2. 函数默认值决定"可选参数"max_value: int = 100有默认值,因此在生成的声明中它是可选参数;模型完全可以像测试文件里那样以空args: {}发起调用。
  3. 注册即传入:直接把函数对象放进Agenttools列表。注意这里传的是未调用的函数引用,而不是FunctionTool(...)实例——ADK 会在内部自动包装(源码佐证见下文)。

值得注意的细节:generate_random_number在 pytest 环境下(检测到PYTEST_CURRENT_TEST环境变量)改为返回自增计数器(1、2、3……)。源码注释写明这是"ensure determinism while allowing multiple calls"——让集成测试可重复断言,同时支持同一函数被多次调用。这也解释了为什么三个测试 JSON 中该函数第一次调用的结果恒为1

三、三条示例输入与对应的真实事件流

README 的 "Sample Inputs" 给出三条典型提示词,与tests/下三个事件快照文件一一对应,是理解"函数工具调用协议"的最佳材料:

示例输入事件文件行为特征
Give me a random number.a_random_number.json单工具单步调用
Give me a random number up to 50, and tell me if it's even.a_random_number_and_check_even.json两次工具调用串行跨步(结果依赖)
Give me a random number and is 44 even?random_number_and_is_5_even.json同一模型事件内并行发起两个工具调用

场景 1:单工具调用

a_random_number.json 中一次交互产生 4 个事件,完整呈现了 ADK 的 function-call 协议闭环:

  1. 用户事件author: user):"a random number"
  2. 模型 functionCallauthor: function_tools):{"functionCall": {"args": {}, "id": "fc-1", "name": "generate_random_number"}}——注意args为空,即模型省略了有默认值的max_valueid: fc-1是后续响应配对用的调用标识;
  3. 框架 functionResponse{"functionResponse": {"id": "fc-1", "name": "generate_random_number", "response": {"result": 1}}}——通过id与第 2 步的调用配对,结果包裹在result键下;
  4. 模型文本事件"Here is a random number: 1\n"finishReason: STOP,交互结束。

每个事件还携带nodeInfo.path(如function_tools@1@1表示第 1 次调用)与invocationId,多 Agent 场景下用于区分事件归属于哪个节点的第几轮执行。

场景 2:串行依赖调用

a_random_number_and_check_even.json 有 6 个事件:模型先调用generate_random_number拿到1在下一个模型轮次中才基于该结果调用is_even(number=1)得到false,最终回复"The random number is 1. It is not an even number."。这展示了工具间存在数据依赖时,ADK 会逐轮回传结果,由 LLM 决定下一步调用哪个工具、传什么参数。

场景 3:单步并行工具调用

random_number_and_is_5_even.json 是 README 特别标注的场景("This will cause parallel tools being called in a single step"):同一个模型事件parts里同时携带两个 functionCall(generate_random_numberis_even(number=5)),随后框架在一个 functionResponse 事件中并行返回两个结果,模型再一次性汇总作答。这证明 ADK 对模型单轮发起的多个工具调用做了并发执行与批量回传,无需模型往返两轮。

四、源码深挖:一个函数引用如何变成工具

示例里tools=[generate_random_number, is_even]传的是裸函数,但 ADK 的工具体系里真正干活的是FunctionTool。整条链路可以拆成"包装 → 声明生成 → 参数注入 → 执行"四步。

4.1 自动包装:LlmAgent 解析 tools 列表

在 llm_agent.py 的工具解析逻辑中,对tools列表中的每一项做了类型分派:

if isinstance(tool_union, BaseTool): return [tool_union] if callable(tool_union): return [FunctionTool(func=tool_union)]

也就是说:BaseTool子类直接采用;普通可调用对象会被自动包一层FunctionToolBaseToolset(如 MCP 工具集)则展开为其下属工具;BaseAgent/BaseNode走另一分支(子 Agent 不能作为工具)。这就是示例中"裸函数直接进 tools 列表"能工作的根本原因。

4.2 名称与描述:来自函数名和 docstring

FunctionTool 构造函数 中:

  • 工具name取自函数名(经_function_tool_declarations.get_callable_name解析,保证以注册名声明);
  • 工具description直接取self._spec.doc——即函数 docstring 全文。

这解释了为何示例 docstring 写得如此完整:它一字不差地出现在发给模型的 FunctionDeclaration 里,是模型"决定何时调用该工具"的唯一文本依据。

4.3 声明生成:从签名到 FunctionDeclaration

FunctionTool._get_declaration 调用build_function_declaration(定义于 _automatic_function_calling_util.py)。核心机制:

  • inspect.signature遍历形参,仅接受位置/关键字参数*args/**kwargs不支持),按类型注解映射出 JSON Schema 属性,str→STRINGint→INTEGERfloat→NUMBERbool→BOOLEANlist/tuple→ARRAYdict→OBJECT等(见模块内_py_type_2_schema_type映射表);
  • 无默认值且不可空的参数进入required,有默认值的(如max_value)为可选——与场景 1 中args: {}的行为严格对应;
  • 使用 Pydantic 临时建模(create_model+model_json_schema)再清洗为平台 Schema;对 Vertex AI 变体,还会依据返回类型注解附加responseSchema(_get_return_type,L210-L217),因此写全-> int这类返回注解对 Vertex AI 通道有价值;
  • 结果经_build_declaration_cachedlru_cache(1024)缓存:因为"pydantic 建模 + JSON Schema 生成开销较大,而结果只取决于静态输入,否则每次 LLM 调用每个工具都要重算"(源码注释原话)。

4.4 框架上下文注入:模型看不见的参数

FunctionTool初始化时(L121-L123)会把tool_context(以及 live 流式模式的input_stream)列入_ignore_params——这些参数从声明中剔除,模型永远看不到。实际调用时,_prepare_invocation_args会做两件事:把 Pydantic 模型字典参数反序列化回实例(_preprocess_args),再把tool_context实例注入到函数签名中声明了该参数的位置,并过滤掉签名之外的多余键。

由此得到一个实战技巧:若你的工具函数需要读写会话状态、追加事件,只需在签名里加一个tool_context: ToolContext参数(例如def is_even(number: int, tool_context: ToolContext) -> bool),框架自动完成注入,LLM 侧的声明不受影响。此外FunctionTool(func, require_confirmation=...)还支持布尔或可调用的确认条件,用于高危操作的 Human-in-the-Loop 拦截(L100-L114)——本示例的随机数/奇偶判断属于纯计算、无副作用,故未启用。

五、运行与验证方式

本仓库为只读参考,以下均为"查看/运行"说明。前提:已安装google-adkpip install google-adk),并配置好模型凭据(如环境变量GOOGLE_API_KEYAgent未显式指定model时使用框架默认值,具体默认模型以你安装的 ADK 版本为准)。

命令行交互运行(agent 目录须包含__init__.pyagent.py,本示例已满足):

adk run contributing/samples/tools/function_tools

Web 界面运行(传入示例所在父目录,即可在浏览器中挑选该 agent):

adk web contributing/samples/tools

随后输入 README 中的三条提示词,观察事件流即可复现tests/目录下三个 JSON 快照所示的行为:单次调用、串行依赖调用与单步并行调用。若要在自动化测试中验证,可复用PYTEST_CURRENT_TEST分支带来的确定性返回(1、2、3……),对工具结果做精确断言——这正是示例自带三份事件快照的用途。

六、要点回顾

  • 写法契约:普通 Python 函数 + 完整类型注解 + Google 风格 docstring + 有意义的默认值,是"零装饰器"注册工具的全部前提;
  • 注册方式:函数引用直接放入Agent(tools=[...]),ADK 在 llm_agent.py 中自动包装为FunctionTool
  • 协议闭环:模型发出带idfunctionCall→ 框架执行后以functionResponseid配对回传 → 模型继续推理;多个调用可在同一轮并行执行;
  • 框架级注入tool_context/input_stream参数对模型不可见、由框架注入,是工具访问会话状态与流式数据的标准通道;
  • 声明性能:FunctionDeclaration 生成昂贵但静态,ADK 用 LRU 缓存避免每次 LLM 调用重复计算。

按这一模式,任何确定性的本地 Python 方法(查询、计算、格式化等)都可以按同样步骤接入 ADK Agent;需要访问外部服务或复杂权限时,再考虑BaseTool子类或BaseToolset(如 MCP 工具集)等更重的形式。

【免费下载链接】adk-pythonAn open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents with flexibility and control.项目地址: https://gitcode.com/GitHub_Trending/ad/adk-python

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询