Semantic Kernel 中的 OpenAI Function Calling 支持:从 ADR 决策到源码实现的完整指南
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
导读
本篇文章围绕 docs/decisions/0017-openai-function-calling.md 这份架构决策记录(ADR),深入剖析 Semantic Kernel 项目如何支持 OpenAI Chat Completions API 的 Function Calling 能力。你将理解该能力的 API 背景、当时的三大候选方案与最终决策理由,以及这一决策在当前仓库代码中的具体落地形态——包括请求设置对象、FunctionChoiceBehavior与ToolCallBehavior的用法、自动调用机制与安全护栏。读完本文,你既能掌握在 SK 应用中启用函数调用的完整实操方法,也能从源码层面理解其设计边界。
背景:OpenAI 的 Function Calling 能力是什么
2023 年 OpenAI 为 Chat Completions 的/v1/chat/completions端点引入了 Function Calling 能力,允许开发者在请求中描述函数,由模型自行判断是否需要调用某个函数,并在回答中输出一个包含函数名与参数的 JSON 对象。这项能力由两个新的 API 参数启用:
| 参数 | 取值 | 作用 |
|---|---|---|
function_call | auto(默认)、none、或指定某个函数名 | 控制模型是否调用函数、调用哪个函数 |
functions | 一组 JSON 描述 | 描述模型可用的函数(函数名、参数 schema 等) |
文档中还明确了一个重要的成本特征:提供给模型的函数描述会被注入到 system message 中,并按输入 token 计费。这意味着函数列表越长,单次请求的输入成本越高——这一事实直接影响了后文 ADR 的决策驱动力。
从 Semantic Kernel 的角度看,社区多次提出希望在使用 SK 调用支持该能力的 OpenAI 聊天模型时,能够利用这一特性。这正是本 ADR 的起源。
架构决策:三个候选方案与最终选择
决策驱动力
ADR 记录了三个关键决策驱动力(Decision Drivers):
- 最小化对核心 Kernel 的改动:OpenAI 专属的功能不应污染核心抽象;
- 成本顾虑:请求中携带一长串函数描述会产生显著的 token 开销;
- 安全与成本顾虑:自动执行模型返回的函数存在风险,需要谨慎对待。
三个候选方案
方案一:修改接口以支持函数收发
修改IChatCompletion与IChatResult接口,显式暴露函数信息相关的参数与方法。
- 优点:为使用函数调用提供了清晰路径;应用可控制暴露给模型的函数(包括非 SK 函数)。
- 缺点:对核心 Kernel 抽象产生破坏性变更(breaking change);OpenAI 专属功能会进入核心抽象,其他模型提供商必须忽略这些成员。
方案二:不修改接口,通过现有请求设置对象收发函数(最终选择)
复用现有的 request settings 对象,将函数描述随请求发给模型。应用开发者自行控制包含哪些函数,并负责校验与执行模型返回的函数结果。
- 优点:避免了对核心 Kernel 的破坏性变更;OpenAI 专属逻辑被限制在 OpenAI connector 包内;应用可完全控制暴露给模型的函数(包括非 SK 函数);为未来与 Planner 集成保留了可能性。
- 中性:要求应用开发者自行校验并执行返回的函数。
- 缺点:使用方式不如接口方案直观,函数结果的访问路径不够明显。
方案三:围绕函数调用实现一个 Planner
把"编排外部函数调用"纳入 SK 的规划(Planning)概念,实现一个类似 ActionPlanner 的 Planner,将函数调用结果转换为可由开发者执行的计划。
- 优点:以计划(plan)形式返回结果,便于开发者执行所选函数。
- 缺点:函数必须先注册到 Kernel 才能被执行;会造成"该用哪个 Planner"的混淆。
决策结果
最终选择方案二:不修改接口,利用现有请求设置对象发送函数到模型。ADR 同时记录了后续演进的重要注记:关于"模型返回的函数若已注册到 Kernel 是否自动执行"存在大量讨论,由于仍有诸多悬而未决的问题,初版实现不包含自动调用能力,留待未来版本探索。
决策在源码中的落地:从 ToolCallBehavior 到 FunctionChoiceBehavior
随着项目演进,这一 ADR 的决策在仓库中形成了两代实现,正好对应"先不自动调用、后支持自动调用"的演进脉络。
初代实现:ToolCallBehavior(OpenAI Connector 内)
在 ToolCallBehavior.cs 中,抽象类ToolCallBehavior定义了 OpenAI connector 内的工具调用行为,提供四个静态工厂:
| 工厂方法 | 行为 |
|---|---|
EnableKernelFunctions | 向模型提供 Kernel 中所有插件函数的信息,但不自动执行,函数调用请求回传给调用方 |
AutoInvokeKernelFunctions | 向模型提供所有插件函数信息,并自动执行模型请求的函数,把结果回传给模型 |
EnableFunctions(functions, autoInvoke) | 向模型提供指定的函数列表,可选择性开启自动执行 |
RequireFunction(function, autoInvoke) | 强制模型使用指定的某个函数 |
关键安全护栏在类内常量中体现:
private const int DefaultMaximumAutoInvokeAttempts = 128;即单次用户请求内的自动调用轮次上限为 128 次。注释中说明这是防止模型反复请求同一函数导致"失控执行(runaway execution)"的保险机制,达到上限后AutoInvokeKernelFunctions会退化为EnableKernelFunctions的行为(只提供函数信息,不再自动执行)。
该行为对象被挂载到 OpenAIPromptExecutionSettings.cs 的ToolCallBehavior属性上。其文档注释清晰地给出了四种配置方式的语义:
- 置
null(默认)=完全禁用工具调用; ToolCallBehavior.RequireFunction(...)= 要求模型使用指定函数;ToolCallBehavior.EnableFunctions(...)= 允许模型从给定列表中选择函数;ToolCallBehavior.EnableKernelFunctions/AutoInvokeKernelFunctions= 允许模型请求 Kernel 中的任意函数,后者附带自动执行能力。
演进后的实现:FunctionChoiceBehavior(核心抽象层)
后续版本将函数选择能力提升到了核心抽象层,位于 dotnet/src/SemanticKernel.Abstractions/AI/FunctionChoiceBehaviors/。FunctionChoiceBehavior是抽象基类,通过 JSON 多态反序列化支持三种派生类型(type discriminator 分别为auto/required/none),并提供了三个静态工厂方法:
| 工厂方法 | Choice 语义 | 说明 |
|---|---|---|
FunctionChoiceBehavior.Auto() | FunctionChoice.Auto | 由模型自行决定是否调用、调用哪些函数(对应 OpenAI 的function_call: auto) |
FunctionChoiceBehavior.Required() | FunctionChoice.Required | 强制模型调用一个或多个函数 |
FunctionChoiceBehavior.None() | FunctionChoice.None | 向模型提供函数描述,但指示其不要调用(可用于让模型先"描述"将要执行的函数供人工校验) |
三个工厂方法均接受三个参数:functions(为null时提供 Kernel 中全部插件函数,为空集合时等价于禁用函数调用)、autoInvoke(是否由 AI connector 自动执行函数)、options(行为选项)。
从源码看,函数解析的核心逻辑在基类的 GetFunctions 方法 中,值得注意的推断结论:
- 自动调用必须依赖 Kernel:当
autoInvoke = true而 Kernel 为null时,直接抛出KernelException("Auto-invocation is not supported when no kernel is provided.")。其意图在注释中写明:绝不能向模型承诺"我能处理这些函数"却在执行时失败,因此宁可提前失败。 - 显式指定的函数必须能被解析:指定了函数全名(FQN,如
PluginName.FunctionName)却找不到时,自动调用模式下会提前抛错;非自动调用模式下才会回退到传入的KernelFunction实例列表。 functions为 null 时展开全部插件:遍历kernel.Plugins将所有函数加入候选列表。
Required行为还有一个值得注意的实现细节:在 RequiredFunctionChoiceBehavior.GetConfiguration 中,当RequestSequenceIndex >= 1(即多轮自动调用中的后续请求)时,会停止向模型继续广告函数,防止模型反复调用同一个函数——代码注释明确标注这是临时方案,未来将改为动态控制函数广告列表。
行为选项由 FunctionChoiceBehaviorOptions.cs 定义:
| 属性 | JSON 字段 | 默认值 | 作用 |
|---|---|---|---|
AllowParallelCalls | allow_parallel_calls | null(采用模型默认) | 是否让模型优先并行调用多个函数 |
AllowConcurrentInvocation | allow_concurrent_invocation | false | 模型并行请求的多个函数是否允许并发执行(若函数不修改共享状态可设为true) |
AllowStrictSchemaAdherence | allow_strict_schema_adherence | false | 是否要求模型严格遵循函数 schema |
RetainArgumentTypes | (非序列化字段,实验特性SKEXP0001) | false | 函数参数是否保留类型信息(默认 SK 将参数反序列化为字符串;为true时以JsonElement保留类型) |
实践:在应用中启用函数调用
使用FunctionChoiceBehavior.Auto()的典型用法
仓库示例 FunctionCalling.cs 展示了最典型的用法——在OpenAIPromptExecutionSettings中配置行为,并传入 Kernel(因为自动调用需要 Kernel 来解析与执行函数):
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() }; Kernel kernel = new(); var result = await kernel.GetRequiredService<IChatCompletionService>() .GetChatMessageContentAsync(chatHistory, settings, kernel);其中FunctionChoiceBehavior.Auto()等价于FunctionChoiceBehavior.Auto(functions: null, autoInvoke: true),即:把 Kernel 中全部插件函数广告给模型,并在模型请求调用时自动执行。
如果只想把函数信息暴露给模型、由应用自行校验与执行(对应 ADR 决策中的"应用开发者负责校验和执行函数结果"),则显式关闭自动调用:
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = Microsoft.SemanticKernel.FunctionChoiceBehavior.Auto(autoInvoke: false) };同目录下的 AzureAIInference_FunctionCalling.cs 表明,该行为不仅是 OpenAI 专属——兼容 OpenAI 协议的 Azure AI Inference 服务同样通过FunctionChoiceBehavior.Auto()配置,印证了 ADR 中"OpenAI 专属功能收敛在 connector 包内、核心抽象保持通用"的设计意图。
函数描述的成本与数量控制
回到 ADR 记录的事实:函数描述按输入 token 计费。因此实践中的关键建议是:
- 不要无条件地把 Kernel 中所有函数广告给模型,应根据场景用
FunctionChoiceBehavior.Auto(functions: 指定函数集合, ...)或Required/None收敛候选列表; - 多轮对话中模型可能反复请求同一函数,
Required行为在第二轮起停止广告函数,ToolCallBehavior的 128 次自动调用上限,都是防止 token 失控与循环调用的内置护栏; - 在需要人工审批的场景(如敏感操作前),可先用
FunctionChoiceBehavior.None()让模型"描述"将要执行的函数,由人确认后再真正执行。
执行入口与验证测试
自动调用的完整链路(模型返回函数调用 → connector 解析 → 调用 Kernel 函数 → 回传结果给模型)由 ClientCore.ChatCompletion.cs 承载。仓库中的集成测试提供了可直接参考的验证场景:
- OpenAIChatCompletion_FunctionCallingTests.cs
- AzureOpenAIChatCompletionFunctionCallingTests.cs
- AutoFunctionInvocationFilterTests.cs(自动调用与过滤器协同的单元测试)
此外,该能力同样覆盖其他兼容 Function Calling 的 provider(如 Google Gemini 的 GeminiToolCallBehavior.cs、Mistral 的 MistralAIToolCallBehavior.cs),可见"函数选择行为抽象化"已成为 SK 多模型 connector 的通用模式。
演进脉络总结
| 阶段 | 代表产物 | 自动调用 | 位置 |
|---|---|---|---|
| ADR 决策(2023-09) | 0017-openai-function-calling.md | 明确暂不包含 | 架构决策记录 |
| 初代实现 | ToolCallBehavior.cs | EnableKernelFunctions不自动,AutoInvokeKernelFunctions自动 | OpenAI connector 包内 |
| 通用抽象 | FunctionChoiceBehavior | Auto/Required默认自动,None不自动 | 核心抽象层 |
这条演进路径清晰印证了 ADR 的决策智慧:先以最小侵入方式(不改接口、不自动执行)在 connector 内落地能力,待自动调用行为的安全性讨论成熟后,再以通用抽象的形式沉淀到核心层——既避免了破坏性变更,又保留了未来的扩展空间。
参考与延伸阅读
- 决策原文:0017-openai-function-calling.md
- 关联 ADR:0017-openai-function-calling.md 后续的 0061-function-call-behavior.md 与 0063-function-calling-reliability.md 记录了该能力后续的行为演进与可靠性设计
- 行为实现:FunctionChoiceBehavior.cs、FunctionChoiceBehaviorOptions.cs
- Connector 实现:ToolCallBehavior.cs、OpenAIPromptExecutionSettings.cs
- 可运行示例:FunctionCalling.cs
- 验证测试:OpenAIChatCompletion_FunctionCallingTests.cs
【免费下载链接】semantic-kernelIntegrate cutting-edge LLM technology quickly and easily into your apps项目地址: https://gitcode.com/GitHub_Trending/se/semantic-kernel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考