深入Function Calling

深入Function Calling

上文详细说了一下让大模型既能聊天又能干活的原理,其实很简单,不过如此嘛,没有看的小伙伴可以返回去看一下,一眼懂。这一节,详细谈谈Function Calling的用法。其实就是这么一个图。

1
2
3
用户消息 → 模型(发现需要调用函数) → 执行函数 → 模型(基于结果) → 最终答案
↑________________________________________________|
(自动迭代,最多 MaximumIterationsPerRequest 次)

上一章节就直接这么用了,其实可以UseFunctionInvocation()配置好多东西

1
2
3
4
IChatClient funcClient = chatService
.AsBuilder()
.UseFunctionInvocation()
.Build();

可以加详细的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//...省略   
.UseFunctionInvocation(configure: options =>
{
options.AdditionalTools = [datetimeTool]; // 注册一些额外的工具,比如时间工具等
options.AllowConcurrentInvocation = true; // 允许模型并发调用多个函数,默认 false
options.IncludeDetailedErrors = true; // 包含详细错误信息,默认 false
options.MaximumConsecutiveErrorsPerRequest = 3; // 每个请求允许的最大连续错误数,防止无限循环,默认 3次
options.MaximumIterationsPerRequest = 5; // 每个请求允许的最大迭代次数,防止无限循环,默认 40次
options.TerminateOnUnknownCalls = false; // 当模型调用了未知的函数时,是否终止对话
options.FunctionInvoker = (context, cancellationToken) =>
{
var functionCall = context.Function;

Console.WriteLine($"Invoking function: {functionCall.Name} with arguments: {functionCall.AdditionalProperties}");

return context.Function.InvokeAsync(context.Arguments, cancellationToken);
};
})

详细解释一下

1. AdditionalTools(全局工具集)

1
options.AdditionalTools = [datetimeTool, weatherTool];
  • 用途:注册跨所有请求共享的工具,无需每次在 ChatOptions.Tools 中重复指定
  • 适用场景:系统级工具(如时间、日期、日志)
  • 与 ChatOptions.Tools 的关系:两者会合并,AdditionalTools 优先级更低

2. AllowConcurrentInvocation(并发调用)

1
options.AllowConcurrentInvocation = true; // 默认 false
  • 用途:允许模型在单次响应中并发调用多个函数
  • 性能优势:多个独立函数可并行执行,显著减少等待时间
  • 注意事项:确保工具函数是线程安全的
  • 示例场景:同时查询多个城市的天气

3. MaximumIterationsPerRequest(最大迭代次数)

1
options.MaximumIterationsPerRequest = 5; // 默认 40
  • 用途:限制单次请求中函数调用的迭代轮数,防止无限循环
  • 计数规则:每次”用户消息 → 模型响应 → 函数调用”算一次迭代
  • 触发场景:模型反复调用函数但无法得出结论
  • 建议值:简单任务 3-5 次,复杂任务 10-20 次

4. MaximumConsecutiveErrorsPerRequest(最大连续错误)

1
options.MaximumConsecutiveErrorsPerRequest = 3; // 默认 3
  • 用途:当函数调用连续失败时终止迭代
  • 错误重置:一次成功的函数调用会重置错误计数器
  • 错误处理:配合 IncludeDetailedErrors 使用

5. IncludeDetailedErrors(详细错误信息)

1
options.IncludeDetailedErrors = true; // 默认 false
  • 用途:将函数执行异常的详细堆栈信息传递给模型
  • 安全考量:生产环境中应谨慎使用,可能泄露敏感信息
  • 调试价值:帮助模型理解错误原因,尝试不同的调用方式

6. TerminateOnUnknownCalls(未知函数终止)

1
options.TerminateOnUnknownCalls = false; // 默认 false
  • 用途:当模型请求调用未注册的函数时是否终止对话
  • false:忽略未知函数,继续对话
  • true:立即抛出异常

7. FunctionInvoker(自定义函数执行器)

1
2
3
4
5
6
7
8
9
10
options.FunctionInvoker = async (context, cancellationToken) =>
{
Console.WriteLine($"[LOG] 调用函数: {context.Function.Name}");
Console.WriteLine($"[LOG] 参数: {JsonSerializer.Serialize(context.Arguments)}");

var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);

Console.WriteLine($"[LOG] 结果: {result}");
return result;
};
  • 用途:拦截并自定义所有函数调用的执行逻辑
  • 常见应用
    • 日志记录
    • 性能监控
    • 权限验证
    • 结果缓存
    • 错误重试

玩几个案例

场景 1:监控函数调用过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
using System.Diagnostics;
using System.Text.Json;
using System.Threading;

// 创建模拟工具集
var monitoringTools = new[]
{
AIFunctionFactory.Create((string city) =>
{
Thread.Sleep(500); // 模拟API延迟
return new { Temperature = Random.Shared.Next(15, 35), Humidity = Random.Shared.Next(40, 80) };
}, "get_weather", "获取指定城市的天气信息"),

AIFunctionFactory.Create((string city) =>
{
Thread.Sleep(300);
return new { Hotels = Random.Shared.Next(50, 200), AvgPrice = Random.Shared.Next(300, 1500) };
}, "get_hotels", "查询指定城市的酒店数量和平均价格"),

AIFunctionFactory.Create((int temperature) =>
{
return temperature switch
{
< 15 => "建议穿冬装,携带保暖衣物",
< 25 => "建议穿春秋装,温度适宜",
_ => "建议穿夏装,注意防晒"
};
}, "suggest_clothing", "根据温度推荐穿搭")
};

// 构建带监控的客户端
int iterationCount = 0;
int functionCallCount = 0;

var monitoredClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.AllowConcurrentInvocation = true;
options.MaximumIterationsPerRequest = 10;
options.FunctionInvoker = async (context, cancellationToken) =>
{
functionCallCount++;
var sw = Stopwatch.StartNew();

Console.WriteLine($"\n🔵 [函数调用 #{functionCallCount}]");
Console.WriteLine($" 函数名: {context.Function.Name}");
Console.WriteLine($" 参数: {JsonSerializer.Serialize(context.Arguments)}");

var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
sw.Stop();

Console.WriteLine($" 结果: {result}");
Console.WriteLine($" 耗时: {sw.ElapsedMilliseconds}ms");

return result;
};
})
.Build();

// 执行复杂查询
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("📋 用户查询:帮我查询北京和上海的天气,并根据北京的温度推荐穿搭\n");

var monitoringOptions = new ChatOptions
{
ToolMode = ChatToolMode.Auto,
AllowMultipleToolCalls = true,
Tools = monitoringTools
};

var monitoringResult = await monitoredClient.GetResponseAsync(
"帮我查询北京和上海的天气,并根据北京的温度推荐穿搭",
monitoringOptions
);

Console.WriteLine("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("✅ 最终响应:");
Console.WriteLine(monitoringResult.Text);
Console.WriteLine($"\n📊 统计: 共 {iterationCount} 次迭代,{functionCallCount} 次函数调用");

image-20251114171658066

场景 2:错误处理与重试机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 创建一个可能失败的工具
int callAttempt = 0;

var unreliableTool = AIFunctionFactory.Create((string orderId) =>
{
callAttempt++;
Console.WriteLine($" [尝试 #{callAttempt}] 查询订单 {orderId}");

// 前两次调用失败,第三次成功
if (callAttempt < 3)
{
throw new InvalidOperationException($"数据库连接超时 (尝试 {callAttempt}/3)");
}

return new { OrderId = orderId, Status = "已发货", EstimatedDelivery = "2024-10-15" };
}, "query_order", "查询订单状态");

// 配置错误处理客户端
var errorHandlingClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.MaximumConsecutiveErrorsPerRequest = 5; // 允许更多错误重试
options.IncludeDetailedErrors = true; // 让模型看到错误详情
options.FunctionInvoker = async (context, cancellationToken) =>
{
try
{
return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
}
catch (Exception ex)
{
Console.WriteLine($" ❌ 函数执行失败: {ex.Message}");
throw; // 重新抛出,让 FunctionInvokingChatClient 处理
}
};
})
.Build();

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🧪 测试:模拟函数调用失败与重试\n");

callAttempt = 0; // 重置计数器

try
{
var errorResult = await errorHandlingClient.GetResponseAsync(
"帮我查询订单号 ORD123456 的状态",
new ChatOptions
{
ToolMode = ChatToolMode.Auto,
Tools = [unreliableTool]
}
);

Console.WriteLine("\n✅ 最终成功!");
Console.WriteLine($"响应: {errorResult.Text}");
}
catch (Exception ex)
{
Console.WriteLine($"\n❌ 达到最大错误次数,请求终止: {ex.Message}");
}

image-20251114172347687

场景 3:并发测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 创建一组模拟耗时的工具
var performanceTools = new[]
{
AIFunctionFactory.Create((string city) => { Thread.Sleep(1000); return $"{city}: 晴天 25℃"; }, "weather", "查询天气"),
AIFunctionFactory.Create((string city) => { Thread.Sleep(1000); return $"{city}: 98% 运行正常"; }, "traffic", "查询交通"),
AIFunctionFactory.Create((string city) => { Thread.Sleep(1000); return $"{city}: 空气质量良好"; }, "air_quality", "查询空气质量")
};

// 测试 1: 串行执行(默认)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🐢 测试 1: 串行函数调用 (AllowConcurrentInvocation = false)\n");

var sequentialClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.AllowConcurrentInvocation = false; // 串行执行
})
.Build();

var sw1 = Stopwatch.StartNew();
var sequentialResult = await sequentialClient.GetResponseAsync(
"请同时查询北京的天气、交通和空气质量",
new ChatOptions { ToolMode = ChatToolMode.Auto, AllowMultipleToolCalls = true, Tools = performanceTools }
);
sw1.Stop();

Console.WriteLine($"⏱️ 总耗时: {sw1.ElapsedMilliseconds}ms");
Console.WriteLine($"📝 响应: {sequentialResult.Text?[..Math.Min(100, sequentialResult.Text.Length)]}...\n");

// 测试 2: 并发执行
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🚀 测试 2: 并发函数调用 (AllowConcurrentInvocation = true)\n");

var concurrentClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.AllowConcurrentInvocation = true; // 并发执行
})
.Build();

var sw2 = Stopwatch.StartNew();
var concurrentResult = await concurrentClient.GetResponseAsync(
"请同时查询北京的天气、交通和空气质量",
new ChatOptions { ToolMode = ChatToolMode.Auto, AllowMultipleToolCalls = true, Tools = performanceTools }
);
sw2.Stop();

Console.WriteLine($"⏱️ 总耗时: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"📝 响应: {concurrentResult.Text?[..Math.Min(100, concurrentResult.Text.Length)]}...\n");

// 性能对比
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("📊 性能对比:");
Console.WriteLine($" 串行耗时: {sw1.ElapsedMilliseconds}ms");
Console.WriteLine($" 并发耗时: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($" ✨ 性能提升: {(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds:F2}x");
Console.WriteLine($" 💡 并发执行节省了约 {sw1.ElapsedMilliseconds - sw2.ElapsedMilliseconds}ms");

image-20251114172652325

场景4:审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 定义审计日志结构
public record FunctionCallAuditLog(
DateTime Timestamp,
string FunctionName,
object Arguments,
object? Result,
TimeSpan Duration,
bool Success,
string? ErrorMessage
);

var auditLogs = new List<FunctionCallAuditLog>();

// 构建带审计的客户端
var auditedClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.FunctionInvoker = async (context, cancellationToken) =>
{
var timestamp = DateTime.UtcNow;
var sw = Stopwatch.StartNew();
object? result = null;
Exception? error = null;

try
{
result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
return result;
}
catch (Exception ex)
{
error = ex;
throw;
}
finally
{
sw.Stop();

// 记录审计日志
auditLogs.Add(new FunctionCallAuditLog(
Timestamp: timestamp,
FunctionName: context.Function.Name,
Arguments: context.Arguments,
Result: result,
Duration: sw.Elapsed,
Success: error == null,
ErrorMessage: error?.Message
));
}
};
})
.Build();

// 执行一些函数调用
var auditResult = await auditedClient.GetResponseAsync(
"现在几点了?",
new ChatOptions
{
ToolMode = ChatToolMode.Auto,
Tools = [AIFunctionFactory.Create(() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "get_time", "获取当前时间")]
}
);

Console.WriteLine("📋 审计日志:");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
foreach (var log in auditLogs)
{
Console.WriteLine($"[{log.Timestamp:HH:mm:ss}] {log.FunctionName}");
Console.WriteLine($" ✅ 状态: {(log.Success ? "成功" : "失败")}");
Console.WriteLine($" ⏱️ 耗时: {log.Duration.TotalMilliseconds:F2}ms");
Console.WriteLine($" 📊 结果: {log.Result}");
if (!log.Success)
Console.WriteLine($" ❌ 错误: {log.ErrorMessage}");
Console.WriteLine();
}

image-20251114173022742

场景5:权限控制与安全检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 定义权限系统
public class UserPermissions
{
public string UserId { get; init; }
public HashSet<string> AllowedFunctions { get; init; } = new();
}

var currentUser = new UserPermissions
{
UserId = "user_001",
AllowedFunctions = new HashSet<string> { "query_order", "get_weather" }
// 注意: "delete_order" 不在允许列表中
};

// 创建敏感操作工具
var sensitiveTools = new[]
{
AIFunctionFactory.Create((string orderId) => $"查询到订单 {orderId} 的详情", "query_order", "查询订单"),
AIFunctionFactory.Create((string orderId) => $"订单 {orderId} 已删除", "delete_order", "删除订单(敏感操作)")
};

// 构建带权限控制的客户端
var secureClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.FunctionInvoker = async (context, cancellationToken) =>
{
var functionName = context.Function.Name;

// 权限检查
if (!currentUser.AllowedFunctions.Contains(functionName))
{
var errorMsg = $"🚫 用户 {currentUser.UserId} 无权调用函数 '{functionName}'";
Console.WriteLine(errorMsg);
throw new UnauthorizedAccessException(errorMsg);
}

Console.WriteLine($"✅ 权限验证通过: {currentUser.UserId} -> {functionName}");
return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
};
})
.Build();

// 测试:允许的操作
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🧪 测试 1: 查询订单(允许)\n");

try
{
var allowedResult = await secureClient.GetResponseAsync(
"帮我查询订单 ORD-12345",
new ChatOptions { ToolMode = ChatToolMode.Auto, Tools = sensitiveTools }
);
Console.WriteLine($"✅ 成功: {allowedResult.Text}\n");
}
catch (Exception ex)
{
Console.WriteLine($"❌ 失败: {ex.Message}\n");
}

// 测试:禁止的操作
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🧪 测试 2: 删除订单(禁止)\n");

try
{
var deniedResult = await secureClient.GetResponseAsync(
"帮我删除订单 ORD-12345",
new ChatOptions { ToolMode = ChatToolMode.Auto, Tools = sensitiveTools }
);
Console.WriteLine($"✅ 成功: {deniedResult.Text}\n");
}
catch (Exception ex)
{
Console.WriteLine($"❌ 权限拒绝已生效: {ex.GetType().Name}\n");
}

image-20251114173300311

场景6:使用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 简单的内存缓存
var functionCache = new Dictionary<string, (object Result, DateTime Timestamp)>();
var cacheDuration = TimeSpan.FromSeconds(30);

// 创建耗时的 API 工具
var expensiveTool = AIFunctionFactory.Create((string stockSymbol) =>
{
Console.WriteLine($" 🌐 调用外部 API 查询股票 {stockSymbol}...");
Thread.Sleep(2000); // 模拟 API 延迟
return new { Symbol = stockSymbol, Price = Random.Shared.Next(100, 500), Change = Random.Shared.NextDouble() * 10 - 5 };
}, "get_stock_price", "查询股票实时价格");

// 构建带缓存的客户端
var cachedFunctionClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.FunctionInvoker = async (context, cancellationToken) =>
{
var cacheKey = $"{context.Function.Name}:{JsonSerializer.Serialize(context.Arguments)}";

// 检查缓存
if (functionCache.TryGetValue(cacheKey, out var cached))
{
if (DateTime.UtcNow - cached.Timestamp < cacheDuration)
{
Console.WriteLine($" ⚡ 从缓存返回: {context.Function.Name}");
return cached.Result;
}
else
{
Console.WriteLine($" ⏰ 缓存已过期,重新调用");
functionCache.Remove(cacheKey);
}
}

// 执行函数并缓存结果
var result = await context.Function.InvokeAsync(context.Arguments, cancellationToken);
functionCache[cacheKey] = (result, DateTime.UtcNow);
return result;
};
})
.Build();

var stockOptions = new ChatOptions { ToolMode = ChatToolMode.Auto, Tools = [expensiveTool] };

// 第一次查询 - 调用 API
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🔵 第一次查询 AAPL 股票价格\n");
var sw1 = Stopwatch.StartNew();
var firstCall = await cachedFunctionClient.GetResponseAsync("查询 AAPL 股票价格", stockOptions);
sw1.Stop();
Console.WriteLine($"⏱️ 耗时: {sw1.ElapsedMilliseconds}ms\n");

// 第二次查询 - 从缓存返回
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("🟢 第二次查询 AAPL 股票价格(应该命中缓存)\n");
var sw2 = Stopwatch.StartNew();
var secondCall = await cachedFunctionClient.GetResponseAsync("查询 AAPL 股票价格", stockOptions);
sw2.Stop();
Console.WriteLine($"⏱️ 耗时: {sw2.ElapsedMilliseconds}ms\n");

Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"📊 缓存效果: 第二次查询快了 {sw1.ElapsedMilliseconds - sw2.ElapsedMilliseconds}ms ({(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds:F1}x 加速)");

image-20251114173656312

AdditionalTools 与 ChatOptions.Tools 的区别

理解这两个工具集合的区别和使用场景非常重要:

特性AdditionalToolsChatOptions.Tools
配置位置UseFunctionInvocation(configure: options => ...)每次请求的 ChatOptions 对象
作用域全局,对所有使用该客户端的请求生效单次请求,仅对当前对话生效
典型用途系统级工具(时间、日志、通用查询)业务特定工具(订单查询、用户管理)
优先级较低,会被 ChatOptions.Tools 覆盖较高,可以临时覆盖全局工具
修改成本需要重新构建 ChatClient可以动态调整,灵活性高

组合使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 定义系统级工具(所有请求都需要)
var systemTools = new[]
{
AIFunctionFactory.Create(() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), "get_current_time", "获取当前时间"),
AIFunctionFactory.Create(() => Guid.NewGuid().ToString(), "generate_id", "生成唯一标识符")
};

// 定义业务工具(特定场景使用)
var orderTools = new[]
{
AIFunctionFactory.Create((string orderId) => $"订单 {orderId}: 已发货", "query_order", "查询订单状态")
};

var userTools = new[]
{
AIFunctionFactory.Create((string userId) => $"用户 {userId}: VIP会员", "query_user", "查询用户信息")
};

// 构建客户端,注入系统级工具
var hybridClient = chatClient.AsBuilder()
.UseFunctionInvocation(configure: options =>
{
options.AdditionalTools = systemTools; // 全局工具
})
.Build();

// 场景 1:订单查询(系统工具 + 订单工具)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("📦 场景 1: 订单查询\n");

var orderResponse = await hybridClient.GetResponseAsync(
"帮我查询订单 ORD-123,并记录查询时间",
new ChatOptions
{
ToolMode = ChatToolMode.Auto,
Tools = orderTools // 只传业务工具,系统工具自动可用
}
);

Console.WriteLine($"响应: {orderResponse.Text}\n");

// 场景 2:用户查询(系统工具 + 用户工具)
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine("👤 场景 2: 用户查询\n");

var userResponse = await hybridClient.GetResponseAsync(
"查询用户 USR-456 的信息,并生成一个新的会话ID",
new ChatOptions
{
ToolMode = ChatToolMode.Auto,
Tools = userTools // 切换到用户工具,系统工具仍然可用
}
);

Console.WriteLine($"响应: {userResponse.Text}");

Console.WriteLine("\n💡 说明: 系统工具(时间、ID生成)在所有场景中都可用,无需重复配置");

最佳实践与注意事项

✅ 推荐做法

  1. 合理设置迭代次数

    1
    options.MaximumIterationsPerRequest = 10; // 根据任务复杂度调整
    • 简单任务:3-5 次
    • 中等任务:10-15 次
    • 复杂任务:20-30 次
    • 避免使用过大的值(如 100),可能导致成本失控
  2. 启用并发调用提升性能

    1
    options.AllowConcurrentInvocation = true; // 适用于独立函数
    • 确保函数是线程安全的
    • 适用于查询类操作
    • 避免在有状态依赖的函数上使用
  3. 生产环境关闭详细错误

    1
    options.IncludeDetailedErrors = false; // 生产环境默认值
    • 开发环境:true(便于调试)
    • 生产环境:false(防止信息泄露)
  4. 使用 AdditionalTools 注册通用工具

    1
    options.AdditionalTools = [timeTool, logTool, idGeneratorTool];
    • 减少每次请求的配置负担
    • 确保系统级工具始终可用
  5. 在 FunctionInvoker 中添加可观测性

    • 日志记录
    • 性能监控
    • 错误追踪
    • 审计合规

⚠️ 常见陷阱

  1. 无限循环风险

    • 问题:模型反复调用同一个函数,无法收敛
    • 解决:设置合理的 MaximumIterationsPerRequestMaximumConsecutiveErrorsPerRequest
  2. 并发安全问题

    • 问题:启用 AllowConcurrentInvocation 但函数不是线程安全的
    • 解决:使用锁或确保函数无状态
  3. 工具描述不清晰

    • 问题:模型不知道何时调用工具,或传入错误参数
    • 解决:提供清晰的函数名、描述和参数说明
  4. 忽略错误处理

    • 问题:函数抛出异常后流程中断
    • 解决:在函数内部或 FunctionInvoker 中妥善处理异常
  5. 过度依赖函数调用

    • 问题:简单问题也触发复杂的函数调用链
    • 解决:合理使用 ToolMode,考虑在 system message 中引导模型判断

📊 性能优化建议

  1. 减少函数调用延迟

    • 使用缓存避免重复调用
    • 启用并发执行
    • 优化函数内部逻辑
  2. 控制上下文大小

    • 函数返回结果应简洁明了
    • 避免返回大量无关数据
    • 配合 Chat Reducer 使用
  3. 智能工具选择

    • 根据用户意图动态调整 ChatOptions.Tools
    • 不要一次性注册过多工具(建议 < 20 个)
    • 将相似功能合并为单个工具

🔒 安全考虑

  1. 输入验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    options.FunctionInvoker = async (context, cancellationToken) =>
    {
    // 验证参数
    if (context.Arguments.TryGetValue("userId", out var userId))
    {
    if (!IsValidUserId(userId?.ToString()))
    throw new ArgumentException("Invalid user ID");
    }

    return await context.Function.InvokeAsync(context.Arguments, cancellationToken);
    };
  2. 权限检查(参考场景 5 示例)

  3. 敏感数据脱敏

    • 在返回结果前脱敏处理
    • 避免在日志中记录敏感信息

与其他中间件的协同

FunctionInvokingChatClient 可以与其他 MEAI 中间件组合使用,构建强大的处理管道。

推荐的中间件顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var comprehensiveClient = baseChatClient.AsBuilder()
// 1. 日志记录(最外层,记录所有活动)
.UseLogging()

// 2. 缓存(在函数调用前检查缓存)
.UseDistributedCache(cache)

// 3. 消息压缩(减少 Token 消耗)
.UseChatReducer(reducer)

// 4. 函数调用(核心业务逻辑)
.UseFunctionInvocation(configure: options =>
{
options.AdditionalTools = systemTools;
options.AllowConcurrentInvocation = true;
})

// 5. 重试机制(最内层,处理底层 API 失败)
.UseRetry()

.Build();

顺序说明

  1. 日志在最外层:捕获完整的请求/响应周期
  2. 缓存在函数调用前:避免重复执行相同的函数调用链
  3. Reducer 在中间:压缩历史消息但保留函数调用上下文
  4. 函数调用在核心:处理工具调用逻辑
  5. 重试在最内层:处理底层 API 的暂时性故障

⚠️ 注意:某些中间件的顺序会显著影响行为。例如,将缓存放在函数调用后会缓存整个函数调用流程,但无法为单个函数结果提供缓存。

作者

步步为营

发布于

2025-11-14

更新于

2025-11-14

许可协议