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

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

重要

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

Prerequisites

有关先决条件和安装 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)]);

Warning

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

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

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

Tip

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

重要

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

Prerequisites

有关先决条件和安装 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 架构字典。

from pydantic import BaseModel, Field

# Load environment variables from .env file
load_dotenv()


# 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.",
)
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 for a trusted, non-sensitive tool.
# This receives lightweight top-level checks only; use Pydantic for runtime enforcement.
get_current_time_schema = {
    "type": "object",
    "properties": {
        "timezone": {"type": "string", "description": "The timezone to get the current time for", "default": "UTC"},
    },
}


@tool(

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

对模型应提供的值使用普通函数参数。 使用 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.kwargs、ctx.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"],
    },
)

分析自定义工具结果

当您需要将原始返回值转换为字符串或 FunctionTool 项列表时,请在 @tool 或 Content 上设置 result_parser。 如果自定义解析器抛出异常,则直接调用会将异常继续向外传播,而自动函数调用会返回常规的工具错误结果,并遵从 include_detailed_errors。 原始返回值不会用作后备值,因此请在自定义解析器中处理可恢复的转换错误。

现在在创建代理时,可以通过将函数工具传递到 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())

限制自动工具调用

为聊天客户端设置限制,以通过模型往返次数、函数调用总数和实际经过时间来控制自动工具调用:

max_function_calls 和 max_duration_seconds 默认为 None,即无限制。 将其设置为正值。 当客户端达到限制时,它会停止调用工具并请求模型进行最终文本响应。

这些限制仅按尽力而为的原则执行,并会在每一批由模型请求的工具调用之后进行检查,因此单个批次可能会超过调用次数限制或时长限制。 等待工具批准所花费的时间计入 max_duration_seconds。

from agent_framework.openai import OpenAIChatCompletionClient

client = OpenAIChatCompletionClient()
client.function_invocation_configuration.update(
    {
        "max_iterations": 5,
        "max_function_calls": 20,
        "max_duration_seconds": 30.0,
    }
)

控制并发工具调用

默认情况下,在一条助理消息中返回的函数调用同时运行。 当工具共享可变状态或必须按模型顺序运行时,将 allow_concurrent_invocation 设置为 False:

client.function_invocation_configuration["allow_concurrent_invocation"] = False

此设置控制客户端侧的工具执行,不受提供程序侧选项(例如 allow_multiple_tool_calls)的影响。 对于基于会话的审批批次,决定可以分别返回,但在原始批次中的每个审批项都有结果之前,不会运行任何工具,模型也不会恢复执行。 然后,已批准的工具按模型的原始顺序运行。

控制工具错误详细信息

默认情况下,include_detailed_errors 为 False。 函数执行和参数验证失败将泛型结果返回到模型和其他序列化通道。 原始诊断信息在 Content.exception 中仍可供受信任的主机代码访问。 Content.to_dict() 将该字段替换为固定的非敏感故障标记,并且协议转换不会公开诊断。

仅当结果始终通过可信通道时,才设置 client.function_invocation_configuration["include_detailed_errors"] = True。 异常文本可以包含敏感数据,此设置会将该文本添加到通道可见的结果。

使用多个函数工具创建类

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

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 。

Tip

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

将函数工具与Harness 代理结合使用

普通代理使用在构造代理时传递的工具,任何其他提供程序或中间件都需要自行组合。 Harness 代理使用相同的函数工具,但会预先配置函数调用管道、每次服务调用的历史记录持久化、工具审批支持和其他工具套件功能。

使用AsHarnessAgent创建HarnessAgent时,通过HarnessAgentOptions.ChatOptions.Tools传递函数工具:

using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;

AIAgent agent = chatClient.AsHarnessAgent(new HarnessAgentOptions
{
    ChatOptions = new ChatOptions
    {
        Instructions = "You are a helpful assistant.",
        Tools = [AIFunctionFactory.Create(GetWeather)],
    },
});

AgentSession session = await agent.CreateSessionAsync();
AgentResponse response = await agent.RunAsync(
    "What is the weather like in Amsterdam?",
    session);

HarnessAgent自动配置FunctionInvokingChatClient。 将 HarnessAgentOptions.MaximumIterationsPerRequest 设为覆盖其函数调用上限;默认情况下,null 采用 FunctionInvokingChatClient 的默认值。 Harness默认还会添加HostedWebSearchTool,因此,如果代理应仅公开ChatOptions.Tools中的工具,请设置DisableWebSearch = true。

将单个工具或工具序列传递给create_harness_agent的tools参数:

from agent_framework import create_harness_agent

agent = create_harness_agent(
    client=client,
    agent_instructions="You are a helpful assistant.",
    tools=get_weather,
)

session = agent.create_session()
response = await agent.run(
    "What is the weather like in Amsterdam?",
    session=session,
)
print(response.text)

工厂配置了自动函数调用以及针对每次服务调用的历史记录持久化。 使用 @tool 修饰的函数默认使用 approval_mode="never_require"。 disable_web_search=False 还会在客户端支持时添加客户端的 Web 搜索工具;设置 disable_web_search=True 以省略该工具。

Harness默认安装 ToolApprovalMiddleware(disable_tool_auto_approval=False),该中间件要求每次运行都提供一个AgentSession。 按照所示方式传递session=agent.create_session();如果不需要Harness审批中间件,也可以显式设置disable_tool_auto_approval=True。

目前还没有可用的打包版 Go harness。 将函数工具添加到 agent.Config.Tools,并直接组合所需的中间件和上下文提供程序。

后续步骤

在运行时控制工具可用性

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