# OpenAI Function Calling 实战:让 AI 调用外部工具的完整指南
你是否想过,AI 不仅仅是对话,还能帮你查天气、搜资料、操作数据库?Function Calling 就是让 LLM 具备这种能力的关键技术。本文会从头讲起,带你一步步实现 AI 调用外部工具的功能。
## 背景介绍
GPT-4 这样的语言模型,本质上是个概率预测机——它根据训练数据预测下一个最可能出现的字。这种能力让生成文章、代码变得轻而易举,但模型本身并不具备执行能力。你问它”今天北京天气怎么样”,它只能根据训练数据猜测,而无法告诉你真实的天气情况。
这时候就需要 Function Calling(函数调用)机制。简单来说,这个功能的流程是:用户提问 -> 模型判断需要调用哪个函数 -> 返回函数签名给前端 -> 前端执行实际调用 -> 把结果返回给模型 -> 模型生成最终回答。
这个设计很巧妙:模型只负责”思考”需要什么工具,真正执行交给外部程序。这样既保留了语言模型的理解能力,又获得了实际操作的能力。
## 问题描述
很多开发者在集成 Function Calling 时会遇到几个典型问题。
一是函数定义格式错误。OpenAI 对 JSON Schema 有严格要求,参数类型必须是标准格式。二是流式输出的处理。Function Calling 通常需要多轮交互,怎么在流式输出中正确处理。三是结果转换。函数返回的 JSON 怎么格式化后传给模型才能得到最好的回答。
接下来我们用 Go 语言实现一个完整的 demo,功能是让 AI 帮助查询天气。
## 详细步骤
### 第一步:定义函数签名
函数定义的核心是 JSON Schema。我们需要告诉模型这个函数叫什么、做什么、参数是什么格式。
// weather_functions.go
package main
import (
"encoding/json"
"fmt"
"strings"
"github.com/sashabaranov/go-openai"
)
// 天气查询函数定义
var weatherFunctions = []openai.FunctionDefinition{
{
Name: "get_weather",
Description: "查询指定城市的天气信息",
Parameters: json.RawMessage(`{
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,如 北京、上海、Seattle"
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "温度单位,摄氏或华氏",
"default": "celsius"
}
},
"required": ["city"]
}`),
},
}
这里有几个关键点。Description 会直接展示给模型,影响它是否选择这个函数,一定要描述清晰。parameters 部分遵循 JSON Schema 标准,type、description 都是必填的。required 数组声明哪些参数必须有,没填的话模型就不会选择这个函数。
### 第二步:实现函数注册和处理
模型返回函数选择后,前端需要找到对应的处理函数并执行。
// function_registry.go
package main
import (
"fmt"
"math/rand"
"time"
)
// FunctionRegistry 保存所有可用函数
var FunctionRegistry = map[string]func(args map[string]interface{}) (string, error){
"get_weather": getWeather,
}
// 天气查询的实际实现
func getWeather(args map[string]interface{}) (string, error) {
city, ok := args["city"].(string)
if !ok {
return "", fmt.Errorf("city 参数缺失")
}
unit := "celsius"
if u, ok := args["unit"].(string); ok {
unit = u
}
// 模拟天气查询(实际项目中替换为真实 API 调用)
temp := rand.Intn(15) + 15 // 15-30 度随机
if unit == "fahrenheit" {
temp = temp*9/5 + 32
}
weather := []string{"晴", "多云", "阴", "小雨"}
cond := weather[rand.Intn(len(weather))]
return fmt.Sprintf(`{"city": "%s", "temperature": %d, "unit": "%s", "condition": "%s"}`,
city, temp, unit, cond), nil
}
注册表的设计很直接:用函数名作为 key,value 是实际处理函数。实际项目中,这里应该替换为调用 Weather API、搜狗搜索等真实服务。
### 第三步:多轮对话处理
Function Calling 通常需要多轮交互。这里是个简化版的处理流程:
// chat_with_functions.go
package main
import (
"context"
"fmt"
"log"
"github.com/sashabaranov/go-openai"
)
func chatWithFunction(ctx context.Context, client *openai.Client, messages []openai.ChatMessage) error {
// 第一轮:发送消息,获取模型响应
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: "gpt-4-turbo",
Messages: messages,
Functions: weatherFunctions,
FunctionCall: "auto",
})
if err != nil {
return fmt.Errorf("创建聊天失败: %w", err)
}
message := resp.Choices[0].Message
// 检查模型是否选择了函数调用
if message.FunctionCall.Name == "" {
// 普通对话,直接输出
fmt.Println("助手:", message.Content)
return nil
}
// 模型选择了函数,提取函数名和参数
funcName := message.FunctionCall.Name
var funcArgs map[string]interface{}
if err := json.Unmarshal([]byte(message.FunctionCall.Arguments), &funcArgs); err != nil {
return fmt.Errorf("解析函数参数失败: %w", err)
}
fmt.Printf("模型选择调用函数: %s, 参数: %v\n", funcName, funcArgs)
// 查找并执行函数
if handler, ok := FunctionRegistry[funcName]; ok {
result, err := handler(funcArgs)
if err != nil {
return fmt.Errorf("执行函数失败: %w", err)
}
fmt.Printf("函数返回结果: %s\n", result)
// 将函数结果添加到对话历史,继续请求模型
messages = append(messages, message)
messages = append(messages, openai.ChatMessage{
Role: "function",
Name: funcName,
Content: result,
})
// 第二轮:获取最终回复
finalResp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: "gpt-4-turbo",
Messages: messages,
})
if err != nil {
return fmt.Errorf("获取最终回复失败: %w", err)
}
fmt.Println("助手:", finalResp.Choices[0].Message.Content)
return nil
}
return fmt.Errorf("未找到函数: %s", funcName)
}
这里的核心是多轮对话。第一轮把用户问题发给模型,模型判断需要调用天气函数;第二轮把函数执行结果发回模型,让它根据结果生成回答。
### 第四步:运行测试
把以上代码组合成完整的入口文件:
// main.go
package main
import (
"context"
"fmt"
"log"
"os"
"github.com/sashabaranov/go-openai"
)
func main() {
apiKey := os.Getenv("OPENAI_API_KEY")
if apiKey == "" {
log.Fatal("请设置 OPENAI_API_KEY 环境变量")
}
client := openai.NewClient(apiKey)
ctx := context.Background()
// 测试用例
messages := []openai.ChatMessage{
{
Role: "user",
Content: "北京今天天气怎么样?",
},
}
err := chatWithFunction(ctx, client, messages)
if err != nil {
log.Fatal(err)
}
}
运行前先设置环境变量:
export OPENAI_API_KEY=sk-你的API密钥
go run main.go
## 运行结果
一次典型的运行输出:
模型选择调用函数: get_weather, 参数: map[city:北京 unit:celsius]
函数返回结果: {"city": "北京", "temperature": 22, "unit": "celsius", "condition": "多云"}
助手: 根据查询结果,北京今天天气多云,气温22摄氏度。建议带伞出门。
整个过程耗时约 2-3 秒,主要消耗在 API 调用上。实际应用中可以通过缓存、并发请求等方式优化。
## 总结
Function Calling 为 AI 应用打开了新世界的大门。它让语言模型从”只会说”变成了”真的能做”。关键要点:
一是函数定义要准确。JSON Schema 的格式必须符合规范,description 要清晰到模型能理解。二是注册表设计要灵活。把函数实现和模型调用分离,方便后续扩展。三是多轮对话处理好。模型可能连续调用多个函数,每次都要把结果喂回去。
进阶方向可以探索:多个函数并行调用,流式输出中实时展示调用进度,用 Tool Calls 实现 RAG 检索增强等。这些内容我们以后再展开。