深入Function Calling
上文详细说了一下让大模型既能聊天又能干活的原理,其实很简单,不过如此嘛,没有看的小伙伴可以返回去看一下,一眼懂。这一节,详细谈谈Function Calling的用法。其实就是这么一个图。
1 | 用户消息 → 模型(发现需要调用函数) → 执行函数 → 模型(基于结果) → 最终答案 |
上一章节就直接这么用了,其实可以UseFunctionInvocation()配置好多东西
1 | IChatClient funcClient = chatService |
可以加详细的配置
1 | //...省略 |
详细解释一下
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 | options.FunctionInvoker = async (context, cancellationToken) => |
- 用途:拦截并自定义所有函数调用的执行逻辑
- 常见应用:
- 日志记录
- 性能监控
- 权限验证
- 结果缓存
- 错误重试
玩几个案例
场景 1:监控函数调用过程
1 | using System.Diagnostics; |

场景 2:错误处理与重试机制
1 | // 创建一个可能失败的工具 |

场景 3:并发测试
1 | // 创建一组模拟耗时的工具 |

场景4:审计
1 | // 定义审计日志结构 |

场景5:权限控制与安全检查
1 | // 定义权限系统 |

场景6:使用缓存
1 | // 简单的内存缓存 |

AdditionalTools 与 ChatOptions.Tools 的区别
理解这两个工具集合的区别和使用场景非常重要:
| 特性 | AdditionalTools | ChatOptions.Tools |
|---|---|---|
| 配置位置 | UseFunctionInvocation(configure: options => ...) | 每次请求的 ChatOptions 对象 |
| 作用域 | 全局,对所有使用该客户端的请求生效 | 单次请求,仅对当前对话生效 |
| 典型用途 | 系统级工具(时间、日志、通用查询) | 业务特定工具(订单查询、用户管理) |
| 优先级 | 较低,会被 ChatOptions.Tools 覆盖 | 较高,可以临时覆盖全局工具 |
| 修改成本 | 需要重新构建 ChatClient | 可以动态调整,灵活性高 |
组合使用示例
1 | // 定义系统级工具(所有请求都需要) |
最佳实践与注意事项
✅ 推荐做法
合理设置迭代次数
1
options.MaximumIterationsPerRequest = 10; // 根据任务复杂度调整
- 简单任务:3-5 次
- 中等任务:10-15 次
- 复杂任务:20-30 次
- 避免使用过大的值(如 100),可能导致成本失控
启用并发调用提升性能
1
options.AllowConcurrentInvocation = true; // 适用于独立函数
- 确保函数是线程安全的
- 适用于查询类操作
- 避免在有状态依赖的函数上使用
生产环境关闭详细错误
1
options.IncludeDetailedErrors = false; // 生产环境默认值
- 开发环境:
true(便于调试) - 生产环境:
false(防止信息泄露)
- 开发环境:
使用 AdditionalTools 注册通用工具
1
options.AdditionalTools = [timeTool, logTool, idGeneratorTool];
- 减少每次请求的配置负担
- 确保系统级工具始终可用
在 FunctionInvoker 中添加可观测性
- 日志记录
- 性能监控
- 错误追踪
- 审计合规
⚠️ 常见陷阱
无限循环风险
- 问题:模型反复调用同一个函数,无法收敛
- 解决:设置合理的
MaximumIterationsPerRequest和MaximumConsecutiveErrorsPerRequest
并发安全问题
- 问题:启用
AllowConcurrentInvocation但函数不是线程安全的 - 解决:使用锁或确保函数无状态
- 问题:启用
工具描述不清晰
- 问题:模型不知道何时调用工具,或传入错误参数
- 解决:提供清晰的函数名、描述和参数说明
忽略错误处理
- 问题:函数抛出异常后流程中断
- 解决:在函数内部或
FunctionInvoker中妥善处理异常
过度依赖函数调用
- 问题:简单问题也触发复杂的函数调用链
- 解决:合理使用
ToolMode,考虑在 system message 中引导模型判断
📊 性能优化建议
减少函数调用延迟
- 使用缓存避免重复调用
- 启用并发执行
- 优化函数内部逻辑
控制上下文大小
- 函数返回结果应简洁明了
- 避免返回大量无关数据
- 配合 Chat Reducer 使用
智能工具选择
- 根据用户意图动态调整
ChatOptions.Tools - 不要一次性注册过多工具(建议 < 20 个)
- 将相似功能合并为单个工具
- 根据用户意图动态调整
🔒 安全考虑
输入验证
1
2
3
4
5
6
7
8
9
10
11options.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);
};权限检查(参考场景 5 示例)
敏感数据脱敏
- 在返回结果前脱敏处理
- 避免在日志中记录敏感信息
与其他中间件的协同
FunctionInvokingChatClient 可以与其他 MEAI 中间件组合使用,构建强大的处理管道。
推荐的中间件顺序
1 | var comprehensiveClient = baseChatClient.AsBuilder() |
顺序说明
- 日志在最外层:捕获完整的请求/响应周期
- 缓存在函数调用前:避免重复执行相同的函数调用链
- Reducer 在中间:压缩历史消息但保留函数调用上下文
- 函数调用在核心:处理工具调用逻辑
- 重试在最内层:处理底层 API 的暂时性故障
⚠️ 注意:某些中间件的顺序会显著影响行为。例如,将缓存放在函数调用后会缓存整个函数调用流程,但无法为单个函数结果提供缓存。
深入Function Calling

