news 2026/7/11 20:58:21

04-让AI学会行动-FunctionCalling函数调用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
04-让AI学会行动-FunctionCalling函数调用

unction Calling的工作原理
很多人误解Function Calling是AI直接调用了函数。实际上,AI并不执行任何代码,它只是"告诉"你它想调用哪个函数、传什么参数。真正的执行逻辑由你的程序完成。

完整的交互流程如下:

┌──────────┐ ①用户提问 ┌──────────┐
│ │ ──────────────────→ │ │
│ 你的 │ │ 大模型 │
│ 程序 │ ②模型返回: │ │
│ │ “我想调用 │ │
│ │ GetWeatherInfo │ │
│ │ (city=广州)” │ │
│ │ ←────────────────── │ │
│ │ │ │
│ │ ③执行本地函数 │ │
│ │ GetWeatherInfo │ │
│ │ (“广州”) │ │
│ │ → 得到天气数据 │ │
│ │ │ │
│ │ ④把函数结果回传 │ │
│ │ ──────────────────→ │ │
│ │ │ │
│ │ ⑤模型基于结果 │ │
│ │ 生成最终回答 │ │
│ │ ←────────────────── │ │
└──────────┘ └──────────┘
关键认知:模型做了决策(调什么函数、传什么参数),你的程序做了执行(实际调用函数),模型再做总结(基于结果生成回答)。

定义AI可调用的函数
在C#中,定义一个AI可调用函数只需要两步:

  1. 编写函数并用[Description]描述
    [Description(“Get weather information for the specified city”)]
    string GetWeatherInfo(
    [Description(“City name, for example: GuangDong”)] string city)
    {
    // 模拟天气API调用
    var weatherData = new
    {
    city,
    temperature = “30°C”,
    condition = “Sunny”,
    humidity = “65%”,
    windSpeed = “10 km/h”
    };

    return JsonSerializer.Serialize(weatherData);
    }
    [Description]特性至关重要——模型正是通过这些描述来理解:

这个函数是做什么的?
参数应该怎么填?
描述越准确,模型的调用决策越正确。

  1. 注册为AI工具
    var options = new ChatOptions
    {
    Tools = [AIFunctionFactory.Create(GetWeatherInfo)]
    };
    AIFunctionFactory.Create()会反射函数签名和Description特性,自动生成符合OpenAI Tool协议的工具描述JSON。

启用自动函数调用
默认的IChatClient不会自动执行函数调用。我们需要用ChatClientBuilder包装一层:

IChatClient client =
new OpenAI.Chat.ChatClient(
model,
new ApiKeyCredential(apiKey),
new OpenAI.OpenAIClientOptions { Endpoint = new Uri(baseUrl) })
.AsIChatClient();

// 关键:用UseFunctionInvocation()启用自动函数执行
using var functionCallingChatClient = new ChatClientBuilder(client)
.UseFunctionInvocation()
.Build();
UseFunctionInvocation()做了什么?它注册了一个中间件管道:

拦截模型的响应
检测是否包含函数调用请求
如果有,自动执行对应的本地函数
把函数结果回传给模型
返回模型的最终回答
整个过程对开发者完全透明——你只需要像普通对话一样调用GetStreamingResponseAsync。

完整的Function Calling对话
var options = new ChatOptions
{
Tools = [AIFunctionFactory.Create(GetWeatherInfo)]
};

List chatMessages =
[
new ChatMessage(ChatRole.System, “你是一个有用的AI助手,请用中文回答用户的问题。”)
];

while (true)
{
Console.Write("\n你: ");
var userInput = Console.ReadLine();

if (userInput.Equals("exit", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine("再见!"); break; } chatMessages.Add(new ChatMessage(ChatRole.User, userInput)); Console.Write("\nAI: "); StringBuilder sb = new StringBuilder(); await foreach (var update in functionCallingChatClient .GetStreamingResponseAsync(chatMessages, options)) { foreach (var item in update.Contents) { if (item is TextReasoningContent textReasoning) { Console.Write(textReasoning.Text); // 思考过程 } else if (item is TextContent text) { Console.Write(text.Text); // 最终回答 sb.Append(text.Text); } } }

}
代码和之前的对话程序几乎一样!唯一的区别是传入了options(包含工具列表)。函数调用的复杂逻辑全部被UseFunctionInvocation()封装了。

透视Function Calling的底层通信
项目中提供了一个基于HarmonyLib的HTTP拦截器,它通过Hook HttpClient的构造函数,自动在所有HTTP请求中注入日志记录,无需修改业务代码。使用方式很简单,在代码入口加一行:

HttpClientInterceptor.StartInterception();
开启后你就能在控制台看到Function Calling的完整请求和响应报文(详见06-番外篇)。下面我们来看看Function Calling实际经历了两次API请求:

第一次请求:模型决策
请求体(关键部分):

{
“model”: “glm-5.1”,
“messages”: [
{ “role”: “system”, “content”: “你是一个有用的AI助手…” },
{ “role”: “user”, “content”: “广州今天天气怎么样?” }
],
“tools”: [
{
“type”: “function”,
“function”: {
“name”: “GetWeatherInfo”,
“description”: “Get weather information for the specified city”,
“parameters”: {
“type”: “object”,
“properties”: {
“city”: {
“type”: “string”,
“description”: “City name, for example: GuangDong”
}
},
“required”: [“city”]
}
}
}
]
}
注意tools数组——这就是模型"看到的"可用工具列表。

响应体(关键部分):

{
“choices”: [{
“message”: {
“role”: “assistant”,
“tool_calls”: [{
“id”: “call_xxx”,
“type”: “function”,
“function”: {
“name”: “GetWeatherInfo”,
“arguments”: “{“city”:“广州”}”
}
}]
}
}]
}
模型没有直接回答,而是返回了一个tool_calls——它决定调用GetWeatherInfo,参数是city=广州。

中间执行:本地函数
SDK自动执行GetWeatherInfo(“广州”),得到:

{“city”:“广州”,“temperature”:“30°C”,“condition”:“Sunny”,“humidity”:“65%”,“windSpeed”:“10 km/h”}
第二次请求:结果回传
{
“messages”: [
{ “role”: “system”, “content”: “…” },
{ “role”: “user”, “content”: “广州今天天气怎么样?” },
{ “role”: “assistant”, “tool_calls”: […] },
{ “role”: “tool”, “tool_call_id”: “call_xxx”,
“content”: “{“city”:“广州”,“temperature”:“30°C”,…}” }
]
}
注意新增的tool角色消息——这就是函数执行结果。模型基于这个结果生成最终回答。

第二次响应:最终回答
模型这次不再请求调用函数,而是直接用自然语言总结:

“广州今天天气晴朗,气温30°C,湿度65%,风速10km/h,适合户外活动。”

多个函数的场景
你可以同时注册多个函数,模型会智能选择:

[Description(“获取指定城市的天气信息”)]
string GetWeatherInfo([Description(“城市名称”)] string city) { … }

[Description(“搜索网页信息”)]
string SearchWeb([Description(“搜索关键词”)] string query) { … }

[Description(“计算数学表达式”)]
string Calculate([Description(“数学表达式”)] string expression) { … }

var options = new ChatOptions
{
Tools = [
AIFunctionFactory.Create(GetWeatherInfo),
AIFunctionFactory.Create(SearchWeb),
AIFunctionFactory.Create(Calculate)
]
};
模型会根据用户的问题自动选择合适的函数——甚至可能在一个回答中调用多个函数。

最佳实践
Description的编写原则
// ❌ 太简略,模型可能误解
[Description(“Get weather”)]
string GetWeather(string city) { … }

// ✅ 清晰描述功能和参数
[Description(“获取指定城市的实时天气信息,包括温度、湿度、天气状况等”)]
string GetWeatherInfo(
[Description(“城市名称,如:北京、上海、广州,支持中文城市名”)]
string city) { … }
函数设计的注意事项
原则 说明
返回值用JSON字符串 模型更容易解析结构化的JSON
函数要幂等 模型可能在推理过程中多次调用同一函数
做好错误处理 函数失败时返回错误信息而非抛异常,模型能理解错误并调整回答
参数尽量简单 避免复杂的嵌套对象参数
从Function Calling到Agent
Function Calling是构建AI Agent的基石。Agent = LLM + Memory + Tools。我们目前已经拥有了:

✅ LLM对话(第一篇)
✅ 记忆系统(第二、三篇)
✅ 工具调用(本篇)
一个基本的Agent已经初具雏形。但Function Calling还有一个局限:工具必须在编译时定义。如果你想让AI动态接入各种第三方工具呢?

这就是下一篇文章的主角——MCP协议。

小结

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 20:57:47

MySQL 备份

逻辑备份: 把数据库转换成 SQL原数据库: user id name 1 Tom 2 Jack备份以后: CREATE TABLE user(id INT,name VARCHAR(20) ); INSERT INTO user VALUES(1,Tom); INSERT INTO user VALUES(2,Jack);恢复: mysql ↓ 执行SQL ↓ 恢复数据运用工具: mysqldum…

作者头像 李华
网站建设 2026/7/11 20:57:33

Gin 绑定 Query 参数踩坑记:为什么我的 json 标签不生效?

背景 在使用 Gin 开发 RESTful API 时,我们经常需要从 URL 的查询字符串(Query String)中获取参数,比如 GET /api/search?keywordgin&page1。Gin 提供了便捷的绑定方法,但如果你和我一样,习惯性地给结…

作者头像 李华
网站建设 2026/7/11 20:57:29

鸿蒙实战:路由参数传递与接收

鸿蒙实战:路由参数传递与接收 前言(必读) 图:鸿蒙实战:路由参数传递与接收(第 74 篇) 运行效果截图(HarmonyOS NEXT) 页面跳转时携带参数是导航系统的核心能力——「从…

作者头像 李华
网站建设 2026/7/11 20:57:08

如何定制自己的LFS系统:openEuler课程中的个性化配置技巧

如何定制自己的LFS系统:openEuler课程中的个性化配置技巧 【免费下载链接】lfs-course This reposiroty will provide the lfs course which has been created in openEuler-practice-courses and migrated here. 项目地址: https://gitcode.com/openeuler/lfs-co…

作者头像 李华
网站建设 2026/7/11 20:56:28

GitKraken 9.0 企业级团队协作实战:5人团队3个月分支管理效率提升40%

GitKraken 9.0 企业级团队协作实战:5人团队3个月分支管理效率提升40% 在中小型技术团队中,代码版本控制常常成为制约开发效率的瓶颈。我们团队曾面临这样的困境:5名开发人员每月平均产生200次提交,却要花费近30%的工作时间处理分支…

作者头像 李华
网站建设 2026/7/11 20:55:53

Godot PCK文件解包全攻略:从原理到实战资源提取

1. 项目概述:为什么我们需要解包Godot的PCK文件? 如果你是一名游戏开发者、Mod制作者,或者单纯对游戏内部构造充满好奇,那么“解包PCK文件”这个操作对你来说绝对不陌生。尤其是在使用Godot引擎开发的游戏中, .pck 文…

作者头像 李华