本教程步骤介绍如何将函数工具与代理配合使用,其中代理是在 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.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"],
},
)
现在在创建代理时,可以通过将函数工具传递到 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 强制首次调用为特定调用。 有关完整模式,请参阅 控制工具可用性 。