将函数工具与代理配合使用

本教程步骤介绍如何将函数工具与代理配合使用,其中代理是在 Azure OpenAI 聊天完成服务上构建的。

重要

并非所有代理类型都支持函数工具。 有些可能仅支持自定义内置工具,而不允许调用方提供自己的函数。 此步骤使用 ChatClientAgent,它确实支持函数工具。

先决条件

有关先决条件和安装 NuGet 包,请参阅本教程中的 “创建并运行简单代理 ”步骤。

使用函数工具创建代理

函数工具只是希望代理在需要时能够调用的自定义代码。 可以通过使用AIFunctionFactory.Create方法从该方法创建AIFunction实例,将任何 C# 方法转换为函数工具。

如果需要向代理提供有关函数或其参数的其他说明,以便它可以更准确地在不同函数之间进行选择,则可以对方法及其参数使用 System.ComponentModel.DescriptionAttribute 属性。

下面是一个简单函数工具示例,该工具可模拟获取给定位置的天气。 它使用说明属性进行修饰,以向代理提供有关自身及其位置参数的其他说明。

using System.ComponentModel;

[Description("Get the weather for a given location.")]
static string GetWeather([Description("The location to get the weather for.")] string location)
    => $"The weather in {location} is cloudy with a high of 15°C.";

创建代理时,现在可以通过将工具列表传递给 AsAIAgent 方法来向代理提供函数工具。

using System;
using Azure.AI.Projects;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

AIAgent agent = new AIProjectClient(
    new Uri("<your-foundry-project-endpoint>"),
    new DefaultAzureCredential())
     .AsAIAgent(
        model: "gpt-4o-mini",
        instructions: "You are a helpful assistant",
        tools: [AIFunctionFactory.Create(GetWeather)]);

警告

DefaultAzureCredential 对于开发来说很方便,但在生产中需要仔细考虑。 在生产环境中,请考虑使用特定凭据(例如), ManagedIdentityCredential以避免延迟问题、意外凭据探测以及回退机制的潜在安全风险。

现在,你可以像正常方式运行代理,并且代理在需要时能够调用 GetWeather 函数工具。

Console.WriteLine(await agent.RunAsync("What is the weather like in Amsterdam?"));

小窍门

有关完整的可运行示例,请参阅 .NET 示例

重要

并非所有代理类型都支持函数工具。 有些可能仅支持自定义内置工具,而不允许调用方提供自己的函数。 此步骤使用通过聊天客户端创建的代理,这些代理支持函数工具。

先决条件

有关先决条件和安装 Python 包,请参阅本教程中的 “创建并运行简单代理 ”步骤。

使用函数工具创建代理

函数工具只是希望代理在需要时能够调用的自定义代码。 可以通过在创建代理时将其传递给代理 tools 的参数,将任何 Python 函数转换为函数工具。

如果你需要向智能体提供有关函数或其参数的其他描述,以便它能在不同函数之间更准确地选择,你可以使用 Python 的类型注释与 Annotated 和 Pydantic 的 Field 来提供描述。

下面是一个简单函数工具示例,该工具可模拟获取给定位置的天气。 它使用类型注释向代理提供有关函数及其位置参数的其他说明。

from typing import Annotated
from pydantic import Field

def get_weather(
    location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
    """Get the weather for a given location."""
    return f"The weather in {location} is cloudy with a high of 15°C."

还可以使用 @tool 修饰器显式指定函数的名称和说明:

from typing import Annotated
from pydantic import Field
from agent_framework import tool

@tool(name="weather_tool", description="Retrieves weather information for any location")
def get_weather(
    location: Annotated[str, Field(description="The location to get the weather for.")],
) -> str:
    return f"The weather in {location} is cloudy with a high of 15°C."

如果你没有在name修饰器中指定description@tool参数,框架将会自动使用函数的名称和文档字符串作为回退。

使用显式架构与 @tool

当你需要对暴露给模型的架构进行完全控制时,将 schema 参数传递给 @tool。 可以提供 Pydantic 模型或原始 JSON 架构字典。

# Approach 1: Pydantic model as explicit schema
class WeatherInput(BaseModel):
    """Input schema for the weather tool."""

    location: Annotated[str, Field(description="The city name to get weather for")]
    unit: Annotated[str, Field(description="Temperature unit: celsius or fahrenheit")] = "celsius"


@tool(
    name="get_weather",
    description="Get the current weather for a given location.",
    schema=WeatherInput,
    approval_mode="never_require",
)
def get_weather(location: str, unit: str = "celsius") -> str:
    """Get the current weather for a location."""
    return f"The weather in {location} is 22 degrees {unit}."
# Approach 2: JSON schema dictionary as explicit schema
get_current_time_schema = {
    "type": "object",
    "properties": {
        "timezone": {"type": "string", "description": "The timezone to get the current time for", "default": "UTC"},
    },
}


@tool(
    name="get_current_time",
    description="Get the current time in a given timezone.",
    schema=get_current_time_schema,
    approval_mode="never_require",
)
def get_current_time(timezone: str = "UTC") -> str:
    """Get the current time."""

将仅限运行时的上下文传递给工具

对模型应提供的值使用普通函数参数。 使用 FunctionInvocationContext 处理仅限运行时的值,例如 function_invocation_kwargs 或当前会话。 注入的上下文参数在向模型展示的架构中隐藏。

import asyncio
from typing import Annotated

from agent_framework import Agent, FunctionInvocationContext, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
from pydantic import Field
# Define the function tool with explicit invocation context.
# The context parameter can also be declared as an untyped ``ctx`` parameter.
@tool(approval_mode="never_require")
def get_weather(
    location: Annotated[str, Field(description="The location to get the weather for.")],
    ctx: FunctionInvocationContext,
) -> str:
    """Get the weather for a given location."""
    # Extract the injected argument from the explicit context
    user_id = ctx.kwargs.get("user_id", "unknown")

    # Simulate using the user_id for logging or personalization
    print(f"Getting weather for user: {user_id}")

    return f"The weather in {location} is cloudy with a high of 15°C."


async def main() -> None:
    agent = Agent(
        client=OpenAIChatClient(),
        name="WeatherAgent",
        instructions="You are a helpful weather assistant.",
        tools=[get_weather],
    )

    # Pass the runtime context explicitly when running the agent.
    response = await agent.run(
        "What is the weather like in Amsterdam?",
        function_invocation_kwargs={"user_id": "user_123"},
    )

    print(f"Agent: {response.text}")

有关ctx.kwargsctx.session和函数中间件的更多详细信息,请参阅运行时上下文

创建声明专用工具

如果工具是在框架外部实现的(例如在 UI 中的客户端),可以在没有具体实现的情况下使用 FunctionTool(..., func=None) 声明该工具。 模型仍可以推理和调用该工具,应用程序以后可以提供结果。

# A declaration-only tool: the schema is sent to the LLM, but the framework
# has no implementation to execute. The caller must supply the result.
get_user_location = FunctionTool(
    name="get_user_location",
    func=None,
    description="Get the user's current city. Only the client application can resolve this.",
    input_model={
        "type": "object",
        "properties": {
            "reason": {"type": "string", "description": "Why the location is needed"},
        },
        "required": ["reason"],
    },
)

现在在创建代理时,可以通过将函数工具传递到 tools 参数来为代理提供功能。

import asyncio
import os
from agent_framework.openai import OpenAIChatCompletionClient
from azure.identity import AzureCliCredential

agent = OpenAIChatCompletionClient(
    model=os.environ["AZURE_OPENAI_CHAT_COMPLETION_MODEL"],
    azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
    api_version=os.getenv("AZURE_OPENAI_API_VERSION"),
    credential=AzureCliCredential(),
).as_agent(
    instructions="You are a helpful assistant",
    tools=get_weather
)

现在,你可以像正常方式运行代理,并且代理在需要时能够调用 get_weather 函数工具。

async def main():
    result = await agent.run("What is the weather like in Amsterdam?")
    print(result.text)

asyncio.run(main())

使用多个函数工具创建类

当多个工具共享依赖项或可变状态时,将它们包装在类中,并将绑定方法传递给代理。 对模型不应提供的值使用类属性,例如服务客户端、功能标志或缓存状态。

import asyncio
from typing import Annotated

from agent_framework import Agent, tool
from agent_framework.openai import OpenAIChatClient
from dotenv import load_dotenv
class MyFunctionClass:
    def __init__(self, safe: bool = False) -> None:
        """Simple class with two tools: divide and add.

        The safe parameter controls whether divide raises on division by zero or returns `infinity` for divide by zero.
        """
        self.safe = safe

    def divide(
        self,
        a: Annotated[int, "Numerator"],
        b: Annotated[int, "Denominator"],
    ) -> str:
        """Divide two numbers, safe to use also with 0 as denominator."""
        result = "∞" if b == 0 and self.safe else a / b
        return f"{a} / {b} = {result}"

    def add(
        self,
        x: Annotated[int, "First number"],
        y: Annotated[int, "Second number"],
    ) -> str:
        return f"{x} + {y} = {x + y}"


async def main():
    # Creating my function class with safe division enabled
    tools = MyFunctionClass(safe=True)
    # Applying the tool decorator to one of the methods of the class
    add_function = tool(description="Add two numbers.")(tools.add)

    agent = Agent(
        client=OpenAIChatClient(),
        name="ToolAgent",
        instructions="Use the provided tools.",
    )
    print("=" * 60)
    print("Step 1: Call divide(10, 0) - tool returns infinity")
    query = "Divide 10 by 0"
    response = await agent.run(
        query,
        tools=[add_function, tools.divide],
    )
    print(f"Response: {response.text}")
    print("=" * 60)
    print("Step 2: Call set safe to False and call again")
    # Disabling safe mode to allow exceptions
    tools.safe = False

此模式非常适合长期存在的工具状态。 如果值在每次调用时变更,请改用FunctionInvocationContext

函数工具

函数工具允许代理调用自定义 Go 函数。 该 functool 包提供了一种使用自动架构生成定义类型安全工具的简单方法。

定义函数工具

import (
    "context"

    "github.com/microsoft/agent-framework-go/tool"
    "github.com/microsoft/agent-framework-go/tool/functool"
)

var weatherTool = functool.MustNew(functool.Config{
    Name:        "weather",
    Description: "Get the current weather for a given location",
}, func(_ context.Context, location string) (string, error) {
    return fmt.Sprintf("The weather in %s is cloudy with a high of 15°C.", location), nil
})

函数签名确定工具的输入架构。 参数 context.Context 由框架注入,不会向模型公开。

结构化输入类型

对于具有多个参数的工具,请定义结构:

type WeatherInput struct {
    Location string `json:"location" jsonschema:"description=The city to check weather for"`
    Unit     string `json:"unit" jsonschema:"description=Temperature unit (celsius or fahrenheit),enum=celsius,enum=fahrenheit"`
}

var weatherTool = functool.MustNew(functool.Config{
    Name:        "weather",
    Description: "Get weather for a location",
}, func(_ context.Context, input WeatherInput) (string, error) {
    return fmt.Sprintf("Weather in %s: 15°%s", input.Location, input.Unit), nil
})

使用工具创建代理

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You are a helpful assistant.",
    Config: agent.Config{
        Tools: []tool.Tool{weatherTool},
    },
})

resp, err := a.RunText(ctx, "What is the weather like in Amsterdam?").Collect()

使用代理作为函数工具

任何代理都可以包装为供另一个代理使用的函数工具:

import "github.com/microsoft/agent-framework-go/tool/agenttool"

weatherAgent := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You answer questions about the weather.",
    Config: agent.Config{
        Name:        "WeatherAgent",
        Description: "An agent that answers weather questions.",
        Tools:       []tool.Tool{weatherTool},
    },
})

mainAgent := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "You are a helpful assistant who responds in French.",
    Config: agent.Config{
        Tools: []tool.Tool{agenttool.New(weatherAgent, agenttool.Config{})},
    },
})

使用本地 shell 工具

Go SDK 包括 tool/shelltool 用于本地 shell 执行。 该工具默认需要审批,并且可以与环境上下文提供程序配对,以便模型知道当前的 shell 系列、工作目录和常见工具版本。

import "github.com/microsoft/agent-framework-go/tool/shelltool"

shell, err := shelltool.NewLocal(shelltool.LocalConfig{
    Mode: shelltool.ModeStateless,
})
if err != nil {
    return err
}
defer shell.Close()

envProvider := shelltool.NewEnvironmentProvider(shell, shelltool.EnvironmentProviderConfig{})

a := foundryprovider.NewAgent(endpoint, token, foundryprovider.ModelDeployment(model), foundryprovider.AgentConfig{
    Instructions: "Run shell commands only when needed and summarize the result.",
    Config: agent.Config{
        Tools:            []tool.Tool{shell},
        ContextProviders: []agent.ContextProvider{envProvider},
    },
})

当每个调用应在新的 shell 中运行时使用 shelltool.ModeStateless 。 仅在单个代理会话需要让 shell 状态(例如已更改的目录或已导出的环境变量)在多次调用之间保持不变时,才使用 shelltool.ModePersistent。 仅在提供独立隔离边界且不需要内置审批门时设置 AcknowledgeUnsafe: true

小窍门

有关完整示例,请参阅 函数工具示例代理作为工具示例带有环境示例的 shell

后续步骤

在运行时控制工具可用性

您可以在代理运行期间使用 FunctionInvocationContext.add_tools() / remove_tools() 添加或移除工具,通过函数中间件控制调用,或使用 tool_choice 强制首次调用为特定调用。 有关完整模式,请参阅 控制工具可用性