Function Calling 深度解析

引言

想象一下,你的 AI 助手不仅能回答问题,还能:

  • 🌤️ 查询实时天气数据
  • 📦 检查库存并下单
  • 📧 自动发送邮件通知
  • 💳 处理支付交易

这就是 Function Calling(函数调用) 的魅力——让 AI 从"只会说话"的聊天机器人,进化为"能做事"的智能助手。

本文将基于 Microsoft.Extensions.AI (MEAI) 框架,深入讲解 Function Calling 的原理、实现方式和最佳实践。

什么是 Function Calling?

核心概念

Function Calling 是指大语言模型在对话过程中,能够识别何时需要外部数据或执行操作,并自动调用预定义的函数来获取结果。

sequenceDiagram
    participant U as 用户
    participant AI as AI模型
    participant F as 业务函数
    
    U->>AI: "北京今天天气怎么样?"
    AI->>AI: 分析:需要天气数据
    AI->>F: 调用 GetWeather("北京")
    F-->>AI: 返回 "晴,25℃"
    AI-->>U: "北京今天晴天,气温25℃"

与传统 API 调用的区别

特性

传统 API 调用

Function Calling

触发方式

开发者硬编码判断

AI 自动识别意图

参数提取

手动解析用户输入

AI 自动提取参数

多函数选择

if-else 分支

AI 自动选择函数

灵活性

固定流程

根据上下文动态调用

传统方式示例

// ❌ 硬编码的判断逻辑
if (userInput.Contains("天气"))
{
    var city = ExtractCity(userInput);  // 手动提取
    var weather = await GetWeather(city);
    return $"{city}天气是{weather}";
}
else if (userInput.Contains("库存"))
{
    var product = ExtractProduct(userInput);
    var stock = await CheckStock(product);
    return $"{product}库存为{stock}";
}

Function Calling 方式

// ✅ AI 自动判断和调用
var tools = new ToolCollection
{
    AIFunctionFactory.Create(GetWeather),
    AIFunctionFactory.Create(CheckStock)
};

var options = new ChatOptions
{
    Tools = tools,
    ToolMode = ChatToolMode.Auto  // AI 自动决定是否调用
};

var response = await chatClient.GetResponseAsync(messages, options);
// AI 会自动识别意图,选择正确的函数,提取参数,并整合结果


MEAI Function Calling 架构

核心组件

graph TB
    A[IChatClient] --> B[ChatOptions]
    B --> C[Tools 工具集合]
    B --> D[ToolMode 调用模式]
    C --> E[AIFunction]
    E --> F[函数元数据]
    E --> G[函数执行器]
    
    style A fill:#4CAF50
    style C fill:#2196F3
    style E fill:#FF9800

1. AIFunction - 工具的抽象表示

public class AIFunction
{
    public AIFunctionMetadata Metadata { get; }  // 函数描述、参数 Schema
    public Delegate? Invoke { get; }             // 实际执行的委托
}

2. AIFunctionFactory - 工具创建工厂

// 从静态方法创建
var tool = AIFunctionFactory.Create(GetWeather);

// 从实例方法创建
var toolset = new WeatherService();
var tool = AIFunctionFactory.Create(toolset.GetWeather);

// 从 Lambda 创建
var tool = AIFunctionFactory.Create(() => DateTime.Now.ToString());

// 批量创建(从类的所有公开方法)
var tools = AIFunctionFactory.Create(typeof(WeatherService));

3. ToolMode - 调用策略

模式

说明

使用场景

Auto

AI 自动判断是否调用

大多数场景(推荐)

Required

强制 AI 调用至少一个工具

确保执行业务逻辑

None

禁用工具调用

纯对话场景

var options = new ChatOptions
{
    ToolMode = ChatToolMode.Auto
};


基础实战:构建天气查询助手

步骤 1:定义业务函数

using System.ComponentModel;

// 使用 [Description] 特性帮助 AI 理解函数用途
public class WeatherService
{
    [Description("查询指定城市的实时天气信息")]
    public WeatherReport GetWeather(
        [Description("要查询的城市名称,如'北京'、'上海'")] string city
    )
    {
        // 模拟天气查询(实际应调用天气 API)
        var temperature = Random.Shared.Next(-5, 36);
        var condition = Random.Shared.NextDouble() > 0.5 ? "晴" : "雨";
        
        return new WeatherReport(city, temperature, condition);
    }
    
    [Description("根据天气情况提供穿搭建议")]
    public string SuggestOutfit(
        [Description("城市名称")] string city
    )
    {
        var weather = GetWeather(city);
        
        return weather switch
        {
            { Condition: "雨" } => $"{city}可能下雨,建议带伞穿防水外套",
            { Temperature: < 5 } => $"{city}温度{weather.Temperature}℃,请穿冬装保暖",
            { Temperature: > 28 } => $"{city}天气炎热({weather.Temperature}℃),建议穿短袖",
            _ => $"{city}气温{weather.Temperature}℃,穿日常装束即可"
        };
    }
}

public record WeatherReport(string City, int Temperature, string Condition);

关键点

  • ✅ 使用 [Description] 让 AI 理解函数和参数的含义
  • ✅ 参数类型要明确(string、int、bool 等基础类型)
  • ✅ 返回值可以是复杂对象,AI 会自动序列化

步骤 2:注册工具集合

using Microsoft.Extensions.AI;

// 创建工具集合
var weatherService = new WeatherService();
var tools = AIFunctionFactory.Create(typeof(WeatherService), weatherService);

// 或者手动注册
var tools = new ToolCollection
{
    AIFunctionFactory.Create(weatherService.GetWeather),
    AIFunctionFactory.Create(weatherService.SuggestOutfit)
};

Console.WriteLine($"已注册 {tools.Count} 个工具");


步骤 3:配置 ChatOptions

var options = new ChatOptions
{
    Tools = tools,
    ToolMode = ChatToolMode.Auto,  // AI 自动判断是否调用
    
    // 可选:允许单次响应中调用多个工具
    AllowMultipleToolCalls = true,
    
    // 可选:设置最大调用次数(防止无限循环)
    MaxToolCalls = 5
};


步骤 4:执行对话

using Microsoft.Extensions.AI;

IChatClient chatClient = ...; // 创建客户端(见第一篇文章)

var messages = new List<ChatMessage>
{
    new(ChatRole.System, "你是一个天气助手,可以查询天气和提供穿搭建议"),
    new(ChatRole.User, "北京今天天气怎么样?穿什么合适?")
};

var response = await chatClient.GetResponseAsync(messages, options);

Console.WriteLine(response.Text);
// 输出示例: "北京今天晴天,气温25℃。气温适中,穿日常装束即可。"

完整执行流程

sequenceDiagram
    participant U as 用户
    participant C as ChatClient
    participant AI as AI模型
    participant F1 as GetWeather()
    participant F2 as SuggestOutfit()
    
    U->>C: "北京天气怎么样?穿什么?"
    C->>AI: 发送消息 + 工具定义
    AI->>AI: 分析:需要天气信息
    AI-->>C: 请求调用 GetWeather("北京")
    C->>F1: 执行函数
    F1-->>C: 返回 {City:"北京", Temperature:25, Condition:"晴"}
    C->>AI: 发送函数结果
    AI->>AI: 分析:需要穿搭建议
    AI-->>C: 请求调用 SuggestOutfit("北京")
    C->>F2: 执行函数
    F2-->>C: 返回 "气温25℃,穿日常装束即可"
    C->>AI: 发送函数结果
    AI-->>C: 生成最终回答
    C-->>U: "北京今天晴天,气温25℃。穿日常装束即可。"


高级技巧

技巧 1:自动函数调用 vs 手动处理

MEAI 提供了两种处理 Function Calling 的方式:

方式 A:自动调用(推荐)

// ✅ 使用 UseFunctionInvocation 中间件
IChatClient chatClient = new OpenAIClient(...)
    .AsChatClient("gpt-4")
    .UseFunctionInvocation();  // 自动执行函数调用

// 完全透明,无需手动处理
var response = await chatClient.GetResponseAsync(messages, options);
Console.WriteLine(response.Text);  // 直接得到最终答案

优点

  • ✅ 代码简洁,无需手动循环
  • ✅ 自动处理多轮函数调用
  • ✅ 自动将结果注入对话历史

方式 B:手动处理(更灵活)

// ✅ 需要审批或日志时使用
var response = await chatClient.GetResponseAsync(messages, options);

// 检查是否有函数调用请求
while (response.Finish Reason == ChatFinishReason.ToolCalls)
{
    foreach (var call in response.Message.Contents.OfType<FunctionCallContent>())
    {
        Console.WriteLine($"AI 请求调用: {call.Name}({call.Arguments})");
        
        // 可选:请求用户审批
        if (!await RequestApprovalAsync(call))
        {
            continue;
        }
        
        // 手动执行函数
        var result = await call.InvokeAsync();
        
        // 将结果添加到对话历史
        messages.Add(new ChatMessage(ChatRole.Tool, result));
    }
    
    // 继续对话
    response = await chatClient.GetResponseAsync(messages, options);
}

Console.WriteLine(response.Text);

适用场景

  • 需要用户审批敏感操作(如支付)
  • 需要详细日志记录
  • 需要自定义错误处理

技巧 2:参数验证与错误处理

public class OrderService
{
    [Description("提交订单")]
    public async Task<OrderResult> SubmitOrder(
        [Description("商品ID")] string productId,
        [Description("数量(必须大于0)")] int quantity
    )
    {
        // 参数验证
        if (quantity <= 0)
        {
            return new OrderResult(false, "数量必须大于0");
        }
        
        if (string.IsNullOrWhiteSpace(productId))
        {
            return new OrderResult(false, "商品ID不能为空");
        }
        
        try
        {
            // 执行业务逻辑
            await _orderRepository.CreateOrderAsync(productId, quantity);
            return new OrderResult(true, "订单创建成功");
        }
        catch (Exception ex)
        {
            // 返回错误信息给 AI
            return new OrderResult(false, $"创建失败: {ex.Message}");
        }
    }
}

public record OrderResult(bool Success, string Message);

关键点

  • ✅ 在函数内部进行参数验证
  • ✅ 返回结构化的结果(成功/失败 + 消息)
  • ✅ 捕获异常并转换为用户友好的消息

技巧 3:上下文传递(ToolContext)

有时工具需要访问当前用户信息、会话状态等上下文:

using Microsoft.Extensions.AI;

public class UserService
{
    [Description("获取当前用户的订单列表")]
    public List<Order> GetMyOrders(ToolContext context)
    {
        // 从上下文中获取用户信息
        var userId = context.Properties["UserId"] as string;
        
        if (string.IsNullOrEmpty(userId))
        {
            throw new InvalidOperationException("用户未登录");
        }
        
        return _orderRepository.GetOrdersByUser(userId);
    }
}

// 使用时传递上下文
var options = new ChatOptions
{
    Tools = tools,
    ToolMode = ChatToolMode.Auto,
    
    // 传递上下文信息
    Properties = new Dictionary<string, object>
    {
        ["UserId"] = "user-123"
    }
};


技巧 4:复杂参数类型

MEAI 支持复杂的参数类型,包括枚举、嵌套对象等:

public enum OrderStatus
{
    Pending,
    Confirmed,
    Shipped,
    Delivered
}

public class OrderFilter
{
    public OrderStatus? Status { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
    public decimal? MinAmount { get; set; }
}

public class OrderService
{
    [Description("查询订单,支持多种过滤条件")]
    public List<Order> SearchOrders(
        [Description("过滤条件")] OrderFilter filter
    )
    {
        // AI 会自动构建 OrderFilter 对象
        return _orderRepository.Search(filter);
    }
}

// AI 可以理解复杂的查询
// 用户输入: "查询今年金额超过1000元的已发货订单"
// AI 自动构建: new OrderFilter 
// { 
//     Status = OrderStatus.Shipped,
//     StartDate = new DateTime(2025, 1, 1),
//     MinAmount = 1000 
// }


技巧 5:异步工具

MEAI 完全支持异步工具:

public class EmailService
{
    [Description("发送邮件通知")]
    public async Task<EmailResult> SendEmailAsync(
        [Description("收件人邮箱")] string to,
        [Description("邮件主题")] string subject,
        [Description("邮件内容")] string body
    )
    {
        await _emailClient.SendAsync(new EmailMessage
        {
            To = to,
            Subject = subject,
            Body = body
        });
        
        return new EmailResult(true, "邮件已发送");
    }
}

public record EmailResult(bool Success, string Message);


生产环境最佳实践

1. 安全性:权限控制

public class SecureOrderService
{
    private readonly IAuthorizationService _authService;
    
    [Description("删除订单(需要管理员权限)")]
    public async Task<OperationResult> DeleteOrder(
        [Description("订单ID")] string orderId,
        ToolContext context
    )
{
        var userId = context.Properties["UserId"] as string;
        
        // 权限检查
        if (!await _authService.HasPermissionAsync(userId, "order:delete"))
        {
            return new OperationResult(false, "权限不足");
        }
        
        // 执行删除
        await _orderRepository.DeleteAsync(orderId);
        return new OperationResult(true, "订单已删除");
    }
}

2. 可观测性:日志与遥测

public class LoggingOrderService
{
    private readonly ILogger<LoggingOrderService> _logger;
    
    [Description("创建订单")]
    public async Task<OrderResult> CreateOrder(
        string productId, 
        int quantity,
        ToolContext context)
    {
        var userId = context.Properties["UserId"];
        
        _logger.LogInformation(
            "用户 {UserId} 请求创建订单: Product={ProductId}, Quantity={Quantity}",
            userId, productId, quantity
        );
        
        try
        {
            var order = await _orderRepository.CreateAsync(productId, quantity);
            
            _logger.LogInformation(
                "订单创建成功: OrderId={OrderId}",
                order.Id
            );
            
            return new OrderResult(true, order.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "创建订单失败");
            return new OrderResult(false, "系统错误,请稍后重试");
        }
    }
}

3. 性能优化:缓存

public class CachedWeatherService
{
    private readonly IMemoryCache _cache;
    private readonly IWeatherApiClient _apiClient;
    
    [Description("查询天气")]
    public async Task<WeatherReport> GetWeatherAsync(string city)
    {
        var cacheKey = $"weather:{city}";
        
        // 检查缓存
        if (_cache.TryGetValue(cacheKey, out WeatherReport? cached))
        {
            return cached!;
        }
        
        // 调用 API
        var weather = await _apiClient.GetWeatherAsync(city);
        
        // 缓存 30 分钟
        _cache.Set(cacheKey, weather, TimeSpan.FromMinutes(30));
        
        return weather;
    }
}

4. 成本控制:限制调用次数

var options = new ChatOptions
{
    Tools = tools,
    ToolMode = ChatToolMode.Auto,
    
    // 限制单次对话中的工具调用次数
    MaxToolCalls = 10,  // 防止无限循环
    
    // 限制并发调用
    AllowMultipleToolCalls = false  // 一次只能调用一个工具
};

5. 降级策略

public class ResilientWeatherService
{
    [Description("查询天气")]
    public WeatherReport GetWeather(string city)
    {
        try
        {
            // 尝试调用主要 API
            return _primaryApiClient.GetWeather(city);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "主要天气API失败,使用备用方案");
            
            try
            {
                // 降级到备用 API
                return _fallbackApiClient.GetWeather(city);
            }
            catch
            {
                // 返回默认值
                return new WeatherReport(city, 20, "数据暂时不可用");
            }
        }
    }
}


完整实战案例:智能客服系统

让我们构建一个完整的企业级客服系统:

using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.ComponentModel;

// 1. 定义工具服务
public class CustomerService
{
    private readonly IOrderRepository _orders;
    private readonly ILogger<CustomerService> _logger;
    
    public CustomerService(
        IOrderRepository orders, 
        ILogger<CustomerService> logger)
    {
        _orders = orders;
        _logger = logger;
    }
    
    [Description("查询订单状态")]
    public async Task<OrderInfo> GetOrderStatus(
        [Description("订单号")] string orderId,
        ToolContext context)
    {
        _logger.LogInformation("查询订单: {OrderId}", orderId);
        
        var order = await _orders.GetByIdAsync(orderId);
        
        if (order == null)
        {
            return new OrderInfo(null, "未找到该订单");
        }
        
        return new OrderInfo(order, null);
    }
    
    [Description("申请退款")]
    public async Task<RefundResult> RequestRefund(
        [Description("订单号")] string orderId,
        [Description("退款原因")] string reason,
        ToolContext context)
    {
        _logger.LogInformation("退款申请: OrderId={OrderId}, Reason={Reason}", 
            orderId, reason);
        
        var order = await _orders.GetByIdAsync(orderId);
        
        if (order == null)
        {
            return new RefundResult(false, "订单不存在");
        }
        
        if (order.Status == OrderStatus.Delivered 
            && (DateTime.Now - order.DeliveredAt).TotalDays > 7)
        {
            return new RefundResult(false, "超过7天无理由退货期限");
        }
        
        // 创建退款申请
        var refundId = await _orders.CreateRefundAsync(orderId, reason);
        
        return new RefundResult(true, $"退款申请已提交,申请号: {refundId}");
    }
    
    [Description("修改收货地址")]
    public async Task<AddressChangeResult> ChangeAddress(
        [Description("订单号")] string orderId,
        [Description("新地址")] string newAddress,
        ToolContext context)
    {
        var order = await _orders.GetByIdAsync(orderId);
        
        if (order == null)
        {
            return new AddressChangeResult(false, "订单不存在");
        }
        
        if (order.Status != OrderStatus.Pending)
        {
            return new AddressChangeResult(false, "订单已发货,无法修改地址");
        }
        
        await _orders.UpdateAddressAsync(orderId, newAddress);
        
        return new AddressChangeResult(true, "地址已更新");
    }
}

// 2. 定义数据模型
public record OrderInfo(Order? Order, string? ErrorMessage);
public record RefundResult(bool Success, string Message);
public record AddressChangeResult(bool Success, string Message);

public class Order
{
    public string Id { get; set; } = "";
    public string ProductName { get; set; } = "";
    public decimal Amount { get; set; }
    public OrderStatus Status { get; set; }
    public DateTime OrderedAt { get; set; }
    public DateTime? DeliveredAt { get; set; }
    public string ShippingAddress { get; set; } = "";
}

public enum OrderStatus
{
    Pending,
    Shipped,
    Delivered,
    Cancelled
}

// 3. 配置依赖注入
var services = new ServiceCollection();

services.AddLogging(builder => builder.AddConsole());
services.AddMemoryCache();

// 注册业务服务
services.AddSingleton<IOrderRepository, OrderRepository>();
services.AddSingleton<CustomerService>();

// 注册 AI 客户端
services.AddSingleton<IChatClient>(sp =>
{
    var logger = sp.GetRequiredService<ILoggerFactory>();
    var cache = sp.GetRequiredService<IDistributedCache>();
    
    return new OpenAIClient(new ApiKeyCredential("your-key"))
        .AsChatClient("gpt-4")
        .UseLogging(logger)
        .UseDistributedCache(cache)
        .UseFunctionInvocation();  // 自动执行函数调用
});

var provider = services.BuildServiceProvider();

// 4. 创建客服助手
var chatClient = provider.GetRequiredService<IChatClient>();
var customerService = provider.GetRequiredService<CustomerService>();

// 注册工具
var tools = AIFunctionFactory.Create(
    typeof(CustomerService), 
    customerService
);

// 配置系统提示词
var systemPrompt = @"你是一个专业的客服助手,可以帮助用户:
1. 查询订单状态
2. 申请退款(需要验证退货政策)
3. 修改收货地址(仅限未发货订单)

请友好、专业地回答用户问题,并根据需要调用工具获取信息。
如果用户请求不在服务范围内,礼貌地说明并引导用户联系人工客服。";

// 5. 处理对话
var messages = new List<ChatMessage>
{
    new(ChatRole.System, systemPrompt)
};

var options = new ChatOptions
{
    Tools = tools,
    ToolMode = ChatToolMode.Auto,
    MaxToolCalls = 5,
    Temperature = 0.3f  // 客服场景需要准确性
};

// 模拟多轮对话
var conversation = new[]
{
    "我的订单 ORDER-123 到哪了?",
    "我想申请退款,不太满意",
    "收货地址能改吗?我想改成朝阳区XX路XX号"
};

foreach (var userInput in conversation)
{
    Console.WriteLine($"\n用户: {userInput}");
    
    messages.Add(new ChatMessage(ChatRole.User, userInput));
    
    var response = await chatClient.GetResponseAsync(messages, options);
    
    Console.WriteLine($"客服: {response.Text}");
    
    messages.Add(response.Message);
}

// 输出示例:
// 用户: 我的订单 ORDER-123 到哪了?
// [AI 调用 GetOrderStatus("ORDER-123")]
// 客服: 您的订单已发货,预计明天送达。商品是 iPhone 15,金额 5999元。

// 用户: 我想申请退款,不太满意
// [AI 调用 RequestRefund("ORDER-123", "不太满意")]
// 客服: 很抱歉给您带来不好的体验。您的退款申请已提交,申请号为 REFUND-456。

// 用户: 收货地址能改吗?
// [AI 调用 ChangeAddress(...)]
// 客服: 很抱歉,您的订单已发货,无法修改地址。建议您收货后联系快递公司处理。


常见陷阱与解决方案

陷阱 1:函数描述不清晰

// ❌ 描述模糊
[Description("获取数据")]
public Data GetData(string id) { ... }

// ✅ 描述清晰
[Description("根据订单ID获取订单详细信息,包括商品、金额、状态等")]
public OrderDetails GetOrderDetails(
    [Description("订单ID,格式为 ORDER-XXXXX")] string orderId
) { ... }

陷阱 2:忘记处理异常

// ❌ 异常直接抛出
public OrderInfo GetOrder(string id)
{
    return _repository.GetById(id);  // 可能抛出 NotFoundException
}

// ✅ 返回友好的错误信息
public OrderResult GetOrder(string id)
{
    try
    {
        var order = _repository.GetById(id);
        return new OrderResult(order, null);
    }
    catch (NotFoundException)
    {
        return new OrderResult(null, "未找到该订单");
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "查询订单失败");
        return new OrderResult(null, "系统错误,请稍后重试");
    }
}

陷阱 3:过多的工具导致混乱

// ❌ 注册所有方法
var tools = AIFunctionFactory.Create(typeof(HugeService));  // 50+ 方法

// ✅ 只注册相关工具
var tools = new ToolCollection
{
    AIFunctionFactory.Create(service.GetOrder),
    AIFunctionFactory.Create(service.RefundOrder),
    AIFunctionFactory.Create(service.ChangeAddress)
};


总结

Function Calling 的核心要点:

让 AI 做决策:由 AI 判断何时调用、调用哪个、如何传参
清晰的描述:使用 [Description] 帮助 AI 理解工具用途
结构化返回:返回强类型对象而非字符串
错误处理:捕获异常并转换为用户友好的消息
安全第一:权限验证、参数校验、审计日志
性能优化:缓存、限流、降级策略

记住:好的 Function Calling = 明确的工具定义 + 智能的调用策略 + 完善的错误处理