2.5. semantic kernel中FunctionCalling

ChatCompletion最强大功能之一就是能够从模型中调用函数。这让你可以基于现有代码创建聊天机器人,实现自动化业务流程、生成代码片段等功能。

在 Semantic Kernel 中,函数调用的过程被大幅简化:它会自动将你的函数和参数描述给模型,并处理模型与代码之间的沟通。了解这一机制对优化代码、充分发挥函数调用的优势至关重要。

自动函数调用的工作原理

函数调用的工作原理可以分为以下几个步骤:

步骤

描述

1. 序列化函数

将所有可用的函数(及其输入参数)使用 JSON Schema 进行序列化。

2. 发送消息和函数到模型

将序列化后的函数(以及当前聊天历史)作为输入发送到模型。

3. 模型处理输入

模型处理输入,并生成响应。响应可以是普通的聊天消息,也可以是一个或多个函数调用。

4. 处理模型返回的响应

如果返回的是普通聊天消息,直接返回给调用方。如果返回的是函数调用,则解析出函数名称及其参数。

5. 执行函数

使用提取出的函数名称和参数,在 Kernel 中调用相应的函数。

6. 返回函数结果

函数调用的结果会作为聊天历史的一部分发送回模型。然后,步骤 2-6 会重复执行,直到模型返回普通的聊天消息或达到最大迭代次数。

模拟函数调用

除了用户(User)、助手(Assistant)和系统(System)这几种角色外,你还可以使用工具(Tool)角色来模拟函数调用。这对于让 AI 学习如何使用插件,或者在对话中提供额外的背景信息非常有帮助。比如,如果你想让 AI 了解用户的信息(比如过敏情况),但又不希望用户每次都手动输入,也不想让 AI 反复询问,你可以用 工具(Tool) 角色直接把这些信息提供给 AI。

下面是一个示例,我们通过模拟调用“用户插件”,把用户的过敏信息直接提供给助手,让 AI 知道用户的饮食限制。

模拟函数调用在提供当前用户的详细信息时特别有用。如今的大型语言模型(LLM)对用户信息非常敏感。即使你在系统消息中提供了用户信息,LLM 仍然可能选择忽略它。但如果你通过用户消息(User Message)或工具消息(Tool Message)提供这些信息,LLM 更有可能正确使用它。

// 添加一个来自助手的模拟函数调用
chatHistory.Add(
    new() {
        Role = AuthorRole.Assistant,
        Items = [
            new FunctionCallContent(
                functionName: "get_user_allergies", // 函数名称:获取用户过敏信息
                pluginName: "User", // 插件名称:用户(User)
                id: "0001", // 调用 ID
                arguments: new () { {"username", "laimonisdumins"} } // 参数:用户名 laimonisdumins
            ),
            new FunctionCallContent(
                functionName: "get_user_allergies", // 函数名称:获取用户过敏信息
                pluginName: "User", // 插件名称:用户(User)
                id: "0002", // 调用 ID
                arguments: new () { {"username", "emavargova"} } // 参数:用户名 emavargova
            )
        ]
    }
);

// 添加来自工具角色的模拟函数返回结果
chatHistory.Add(
    new() {
        Role = AuthorRole.Tool,
        Items = [
            new FunctionResultContent(
                functionName: "get_user_allergies", // 函数名称:获取用户过敏信息
                pluginName: "User", // 插件名称:用户(User)
                id: "0001", // 调用 ID
                result: "{ \"allergies\": [\"peanuts\", \"gluten\"] }" // 返回结果:用户对花生和麸质过敏
            )
        ]
    }
);
chatHistory.Add(
    new() {
        Role = AuthorRole.Tool,
        Items = [
            new FunctionResultContent(
                functionName: "get_user_allergies", // 函数名称:获取用户过敏信息
                pluginName: "User", // 插件名称:用户(User)
                id: "0002", // 调用 ID
                result: "{ \"allergies\": [\"dairy\", \"soy\"] }" // 返回结果:用户对乳制品和大豆过敏
            )
        ]
    }
);

在模拟工具(Tool)返回结果时,必须始终提供与之对应的函数调用 ID。这是让 AI 理解返回结果上下文的重要信息。某些大型语言模型(LLM),比如 OpenAI,如果缺少 ID 或 ID 与函数调用不匹配,可能会抛出错误。

函数调用的示例

C# 版的函数调用示例,模拟了一个书籍订购插件(OrderBookPlugin),它提供了添加书籍到购物车、从购物车中移除书籍、获取购物车内容等功能。

配置Azure OpenAI

Semantic Kernel 中的函数调用

Semantic Kernel(SK)提供了一个强大的函数调用机制,它封装了与模型交互的复杂细节,让你可以更专注于业务逻辑的实现。SK 自动处理函数注册、参数绑定、模型调用等步骤,大大简化了开发流程。

✅Function Calling 全流程原始步骤(如果你不用 SK) 这里假设你在使用 OpenAI Function Calling(或 Azure OpenAI)时,手动处理每一步,并对比 SK 替你做了哪些工作。

🧱 原始流程步骤 vs Semantic Kernel 封装对比

步骤
Function Calling 原始做法(自己做)
SK 帮你封装

1. 函数注册与描述构建

• 手动用 JSON 构建每个函数的 schema(name、description、parameters) • 指定类型、必填字段、枚举等

• 自动反射 [KernelFunction] 和参数描述 • 构建 JSON Schema

2. 组织聊天历史

• 自己构造 messages 列表:用户消息、助手回复、函数调用请求

• 用 ChatHistory 自动管理 • 自动记录调用内容

3. 将函数定义与聊天记录发给模型

• 把函数定义 + 聊天记录打包到 HTTP 请求体 • 发送给 /v1/chat/completions

• GetChatMessageContentAsync(...) 一句完成

4. 解析模型返回,判断是否为函数调用

• 检查 finish_reason == "function_call" • 提取函数名、参数 JSON

• 自动检查响应结构 • 自动识别是否为工具调用

5. 参数绑定

• 把 JSON 字符串手动 JsonConvert.Deserialize() • 处理类型不匹配 / 丢失字段

• 自动转换为 C# 参数类型 • 支持默认值、必填校验

6. 执行对应的函数逻辑

• 自己手动查找函数(基于名称) • 调用函数 + try/catch

• 自动找到已注册插件函数并调用

7. 错误处理与恢复机制

• 捕获错误 • 生成 fallback 消息 • 通知模型

• 自动捕获异常,返回默认失败响应

8. 函数结果回写到聊天历史

• 自己构造 tool 角色的响应 JSON • 添加到 messages 中

• 自动封装为 FunctionResultContent 并追加

9. 模型继续生成最终回复

• 再次发送新的消息上下文给模型 • 包含 tool result + prompt

• GetChatMessageContentAsync 自动完成

10. 多轮调用管理

• 手动判断是否继续调用函数或结束对话

• 由 SK 自动判断并进行多轮对话或结束流程

Semantic Kernel 帮你自动处理了函数注册、上下文维护、参数绑定、模型调用、函数执行和结果回写,让你几乎只需要写业务逻辑函数,剩下的复杂流程都由它接管了。这大大简化了函数调用的开发流程,让你可以更专注于业务逻辑的实现,而不是底层的细节处理。

Last updated