ag2-add-custom-tool

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Add a custom Python tool

添加自定义Python工具

When to use

适用场景

The user wants their
Agent
to take a real-world action: hit an API, query a database, compute something, return an image. If they want shipped tools (web search, code exec, shell), see
ag2-use-builtin-tools
and
ag2-shell-tool
instead.
用户希望其
Agent
执行实际操作:调用API、查询数据库、进行计算、返回图片。如果需要使用内置工具(网页搜索、代码执行、Shell),请查看
ag2-use-builtin-tools
ag2-shell-tool

60-second recipe

60秒快速入门

python
from ag2 import Agent, tool
from ag2.config import OpenAIConfig

@tool
def calculate_shipping_cost(destination: str, weight_kg: float) -> str:
    """Calculates shipping cost for a package to a destination."""
    return "$15.00"

agent = Agent(
    "shipping",
    prompt="Use tools when helpful.",
    config=OpenAIConfig(model="gpt-4o-mini"),
    tools=[calculate_shipping_cost],
)
The
@tool
decorator generates the LLM-facing schema from the function signature, type hints, and docstring. The docstring is the description the LLM sees — write it for an LLM reader, not just a human.
You can also pass plain undecorated functions in
tools=[...]
and AG2 wraps them automatically:
python
def get_weather(location: str) -> str:
    """Returns the current weather for a given location."""
    return "Sunny, 22°C"

agent = Agent("weather", tools=[get_weather])
Or attach a tool to an existing agent with
@agent.tool
:
python
agent = Agent("calc")

@agent.tool
def multiply(a: int, b: int) -> int:
    """Multiplies two integers and returns the result."""
    return a * b
python
from ag2 import Agent, tool
from ag2.config import OpenAIConfig

@tool
def calculate_shipping_cost(destination: str, weight_kg: float) -> str:
    """Calculates shipping cost for a package to a destination."""
    return "$15.00"

agent = Agent(
    "shipping",
    prompt="Use tools when helpful.",
    config=OpenAIConfig(model="gpt-4o-mini"),
    tools=[calculate_shipping_cost],
)
@tool
装饰器会根据函数签名、类型提示和文档字符串生成面向LLM的Schema。文档字符串是LLM可见的描述——请为LLM读者撰写,而非仅面向人类。
你也可以在
tools=[...]
中传入未装饰的普通函数,AG2会自动为其包装:
python
def get_weather(location: str) -> str:
    """Returns the current weather for a given location."""
    return "Sunny, 22°C"

agent = Agent("weather", tools=[get_weather])
或者使用
@agent.tool
将工具附加到已有的Agent上:
python
agent = Agent("calc")

@agent.tool
def multiply(a: int, b: int) -> int:
    """Multiplies two integers and returns the result."""
    return a * b

Sync vs async

同步与异步

Both
def
and
async def
are supported. Synchronous tools run in a thread by default so blocking I/O does not freeze the event loop. For ultra-fast pure-Python tools, opt out:
python
@tool(sync_to_thread=False)
def format_name(first: str, last: str) -> str:
    """Formats a full name."""
    return f"{last.upper()}, {first.capitalize()}"
Native async tools run in the main event loop directly:
python
import aiohttp

@tool
async def fetch(url: str) -> str:
    """Fetches a URL with aiohttp."""
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as r:
            return await r.text()
def
async def
均受支持。同步工具默认在线程中运行,因此阻塞式I/O不会冻结事件循环。对于超快速的纯Python工具,可以选择关闭该特性:
python
@tool(sync_to_thread=False)
def format_name(first: str, last: str) -> str:
    """Formats a full name."""
    return f"{last.upper()}, {first.capitalize()}"
原生异步工具直接在主事件循环中运行:
python
import aiohttp

@tool
async def fetch(url: str) -> str:
    """Fetches a URL with aiohttp."""
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as r:
            return await r.text()

Validating inputs with Pydantic
Field

使用Pydantic
Field
验证输入

Use
Annotated[T, Field(...)]
to give the LLM strict bounds. The framework forwards these into the JSON Schema:
python
from typing import Annotated
from pydantic import Field
from ag2 import tool

@tool
def set_temperature(
    temp: Annotated[int, Field(description="Target temperature.", ge=10, le=30)],
    mode: Annotated[str, Field(description="Mode.", pattern="^(heat|cool|auto)$")],
) -> str:
    """Sets the thermostat."""
    return f"Set to {temp}°C in {mode} mode."
You can also override the tool name and description on the decorator:
python
@tool(name="custom_math_tool", description="Performs advanced math.")
def math_op(a: int, b: int) -> int:
    return a + b
使用
Annotated[T, Field(...)]
为LLM设置严格的边界。框架会将这些约束转发到JSON Schema中:
python
from typing import Annotated
from pydantic import Field
from ag2 import tool

@tool
def set_temperature(
    temp: Annotated[int, Field(description="Target temperature.", ge=10, le=30)],
    mode: Annotated[str, Field(description="Mode.", pattern="^(heat|cool|auto)$")],
) -> str:
    """Sets the thermostat."""
    return f"Set to {temp}°C in {mode} mode."
你也可以在装饰器上覆盖工具名称和描述:
python
@tool(name="custom_math_tool", description="Performs advanced math.")
def math_op(a: int, b: int) -> int:
    return a + b

Returning typed
Input
/
ToolResult

返回带类型的
Input
/
ToolResult

A plain
str
return is wrapped in
TextInput
automatically. For richer payloads, return an
Input
subtype or compose with
ToolResult
:
python
from ag2 import DataInput, ImageInput, TextInput, ToolResult, tool

@tool
def get_status(task_id: str) -> TextInput:
    return TextInput(f"Task {task_id} is in progress.")

@tool
def get_user_profile(user_id: str) -> DataInput:
    return DataInput({"id": user_id, "name": "Alice", "role": "admin"})

@tool
def fetch_chart(chart_id: str) -> ImageInput:
    return ImageInput(f"https://charts.example.com/{chart_id}.png")

@tool
def analyze_product(product_id: str) -> ToolResult:
    """Returns image + structured metadata in one tool call."""
    return ToolResult(
        ImageInput(f"https://cdn.example.com/products/{product_id}.jpg"),
        {"id": product_id, "name": "Widget Pro", "stock": 42},
    )
For raw bytes of arbitrary format, use
BinaryInput(data=..., media_type="application/pdf")
.
普通
str
返回值会自动包装为
TextInput
。对于更丰富的负载,可以返回
Input
子类型或与
ToolResult
组合:
python
from ag2 import DataInput, ImageInput, TextInput, ToolResult, tool

@tool
def get_status(task_id: str) -> TextInput:
    return TextInput(f"Task {task_id} is in progress.")

@tool
def get_user_profile(user_id: str) -> DataInput:
    return DataInput({"id": user_id, "name": "Alice", "role": "admin"})

@tool
def fetch_chart(chart_id: str) -> ImageInput:
    return ImageInput(f"https://charts.example.com/{chart_id}.png")

@tool
def analyze_product(product_id: str) -> ToolResult:
    """Returns image + structured metadata in one tool call."""
    return ToolResult(
        ImageInput(f"https://cdn.example.com/products/{product_id}.jpg"),
        {"id": product_id, "name": "Widget Pro", "stock": 42},
    )
对于任意格式的原始字节,使用
BinaryInput(data=..., media_type="application/pdf")

End the turn early with
final=True

使用
final=True
提前结束对话轮次

When the tool already knows the exact final answer, skip the extra LLM round-trip:
python
from ag2 import ToolResult, tool

@tool
def handoff_to_human(ticket_id: str) -> ToolResult:
    """Escalates and returns the final user-facing message verbatim."""
    return ToolResult(f"Ticket {ticket_id} was escalated.", final=True)
A
final=True
ToolResult
must contain exactly one part (
TextInput
or
DataInput
).
当工具已经知道确切的最终答案时,可以跳过额外的LLM往返:
python
from ag2 import ToolResult, tool

@tool
def handoff_to_human(ticket_id: str) -> ToolResult:
    """Escalates and returns the final user-facing message verbatim."""
    return ToolResult(f"Ticket {ticket_id} was escalated.", final=True)
带有
final=True
ToolResult
必须恰好包含一个部分(
TextInput
DataInput
)。

Dependency injection (Context / Inject / Variable / Depends)

依赖注入(Context / Inject / Variable / Depends)

Tools can pull execution-time values without exposing them to the LLM. See
references/dependency_injection.md
for the full table; the basics:
python
from typing import Annotated
from ag2 import Context, Inject, Variable, tool

@tool
def query_db(query: str, ctx: Context) -> str:
    """Runs a SQL query."""
    db = ctx.dependencies["db"]
    return db.execute(query)

@tool
def fetch(url: str, http: Annotated[object, Inject("http_session")]) -> str:
    """Fetches with a shared HTTP session."""
    return http.get(url).text

@tool
def send(text: str, api_key: Annotated[str, Variable()]) -> str:
    """Sends a message via the configured channel."""
    ...
Inject
annotations are stripped from the LLM-facing schema — they're an internal injection mechanism.
工具可以在不向LLM暴露的情况下获取运行时的值。完整说明请参考
references/dependency_injection.md
;基础用法如下:
python
from typing import Annotated
from ag2 import Context, Inject, Variable, tool

@tool
def query_db(query: str, ctx: Context) -> str:
    """Runs a SQL query."""
    db = ctx.dependencies["db"]
    return db.execute(query)

@tool
def fetch(url: str, http: Annotated[object, Inject("http_session")]) -> str:
    """Fetches with a shared HTTP session."""
    return http.get(url).text

@tool
def send(text: str, api_key: Annotated[str, Variable()]) -> str:
    """Sends a message via the configured channel."""
    ...
Inject
注解会从面向LLM的Schema中移除——它们是内部注入机制。

Going deeper

深入学习

  • references/dependency_injection.md
    Context
    vs
    Inject
    vs
    Variable
    vs
    Depends
    , defaults, factories, mutability, overrides.
  • website/docs/user-guide/tools/tools.mdx
    — full
    @tool
    reference, including the synthesized JSON Schema.
  • website/docs/user-guide/depends.mdx
    Depends
    lifecycle, yield-based teardown, caching, test overrides.
  • website/docs/user-guide/multimodal/inputs.mdx
    — the
    Input
    factory hierarchy and provider support matrix.
  • website/docs/user-guide/tools/toolkits.mdx
    — bundle related tools into a reusable
    Toolkit
    .
  • website/docs/user-guide/tools/tool_middleware.mdx
    — async hooks around a single tool (validation, redaction, approval — see also
    ag2-hitl
    ).
  • references/dependency_injection.md
    ——
    Context
    Inject
    Variable
    Depends
    的对比、默认值、工厂函数、可变性、重写。
  • website/docs/user-guide/tools/tools.mdx
    ——完整的
    @tool
    参考,包括生成的JSON Schema。
  • website/docs/user-guide/depends.mdx
    ——
    Depends
    生命周期、基于yield的清理、缓存、测试重写。
  • website/docs/user-guide/multimodal/inputs.mdx
    ——
    Input
    工厂层级结构和提供者支持矩阵。
  • website/docs/user-guide/tools/toolkits.mdx
    ——将相关工具打包为可复用的
    Toolkit
  • website/docs/user-guide/tools/tool_middleware.mdx
    ——单个工具的异步钩子(验证、编辑、审批——另见
    ag2-hitl
    )。

Common pitfalls

常见陷阱

  • Vague docstring — the LLM uses it to decide when to call the tool. "Calculates shipping cost based on destination and weight" is much better than "Shipping calc".
  • No type hints — without them the framework can't generate a useful JSON Schema; the LLM may not call your tool at all.
  • Blocking the event loop — if you write
    def
    (sync) tool with heavy CPU or network and pass
    sync_to_thread=False
    , the loop blocks. Default behaviour (run in a thread) is safe; only opt out for cheap pure-Python work.
  • Function-level imports inside tools — repo convention disallows them. Hoist
    import
    to module top.
  • Nested function definitions inside the tool body — also disallowed (recreates the function on every call).
  • Returning
    dict
    directly when you wanted structured data
    — wrap it in
    DataInput(...)
    so the framework treats it as structured rather than coercing to text.
  • Forgetting
    final=True
    requires exactly one part
    — combining multiple
    Input
    s with
    final=True
    will raise.
  • 模糊的文档字符串——LLM会根据它决定何时调用工具。“根据目的地和重量计算运费”比“运费计算”要好得多。
  • 无类型提示——没有类型提示,框架无法生成有用的JSON Schema;LLM可能根本不会调用你的工具。
  • 阻塞事件循环——如果你编写的
    def
    (同步)工具包含大量CPU或网络操作,且设置了
    sync_to_thread=False
    ,会阻塞事件循环。默认行为(在线程中运行)是安全的;仅在处理低成本纯Python任务时选择关闭该特性。
  • 工具内部的函数级导入——仓库规范禁止这种写法。请将
    import
    提升到模块顶部。
  • 工具体内部的嵌套函数定义——同样被禁止(每次调用都会重新创建函数)。
  • 直接返回
    dict
    而你需要的是结构化数据
    ——请将其包装在
    DataInput(...)
    中,这样框架会将其视为结构化数据,而非强制转换为文本。
  • 忘记
    final=True
    要求恰好包含一个部分
    ——将多个
    Input
    final=True
    组合会引发错误。