用 Tool Calling 构建智能机票查询 Agent
让 LLM 从“只会聊天”变成“会干活”——通过调用外部工具扩展能力边界
一、Tool Calling 是什么?
Tool Calling(工具调用) 是一种让大语言模型(LLM)能够调用外部函数或 API 的技术模式。简单说,就是让 AI 不仅能“动脑”(推理、决策),还能“动手”去查数据库、做计算、调接口,然后把真实结果告诉你。
为什么需要它?
|
LLM的短板 |
例子 |
后果 |
|
知识时效性差(无法获取实时信息) |
“帮我查5月30日北京到上海最便宜的机票” |
LLM训练数据有截止日期,无法获取实时航班价格,只能凭过时数据猜测,导致信息不准确。 |
|
事实性错误/幻觉严重(不会精确计算) |
“算出含税总价” |
LLM本质是概率生成,对税费计算这类精确任务容易出错,产生幻觉,给出错误的总价。 |
|
无环境交互能力(无法访问内部数据/系统) |
“我的订单出票了吗?” |
LLM只能输出文本,无法查询数据库或调用API,无法获取真实的订单状态。 |
Tool Calling(工具调用) 就是解决这三大短板的钥匙。
二、业务场景:机票查询 Agent
User Input(用户输入):
“帮我查一下 5 月 30 日北京到上海最便宜的机票,并算出含税总价”
问题拆解(Task Decomposition)
这个需求不是一句 SQL 或接口能搞定的,它本质上是多步推理 + 多次工具调用:
- 查航班列表 →
search_flights - 找最便宜航班 → LLM 推理
- 查税费规则 →
get_tax_rate - 计算含税总价 →
calc_total_price - 返回结构化结果
👉 这就是典型 Tool Calling + ReAct 场景
三、定义工具(Tool Registry)
后端需要预先定义可供 LLM 调用的工具,告诉 LLM“你有这些工具可以用”。
// ===== Define Tool Interface(定义工具接口)=====
// ToolInterface(工具接口):所有工具必须实现这些方法
type ToolInterface interface {
Name() string // Tool name(工具名称)
Description() string // Tool description(工具描述)
Parameters() map[string]ParameterSpec // Input parameters(输入参数定义)
Execute(args map[string]interface{}) (interface{}, error) // Execute logic(执行逻辑)
}
// ParameterSpec(参数规格):定义每个参数的类型和约束
type ParameterSpec struct {
Type string `json:"type"` // Parameter type (string, number)
Description string `json:"description"` // Parameter description
Required bool `json:"required"` // Is it required?
}
1️⃣ 航班查询工具(search_flights)
type SearchFlightsTool struct{}
func (t *SearchFlightsTool) Name() string {
return "search_flights" // Tool name used by LLM
}
func (t *SearchFlightsTool) Description() string {
return "Search for available flights by departure, destination, and date"
}
func (t *SearchFlightsTool) Parameters() map[string]ParameterSpec {
return map[string]ParameterSpec{
"from": {Type: "string", Description: "Departure city(出发城市)", Required: true},
"to": {Type: "string", Description: "Destination city(目的地城市)", Required: true},
"date": {Type: "string", Description: "Travel date in YYYY-MM-DD format", Required: true},
}
}
func (t *SearchFlightsTool) Execute(args map[string]interface{}) (interface{}, error) {
// Extract parameters(提取参数)
from, _ := args["from"].(string)
to, _ := args["to"].(string)
date, _ := args["date"].(string)
// Mock data(模拟数据)—— 真实项目这里会调数据库或 API
flights := []map[string]interface{}{
{"flight": "CA123", "price": 800},
{"flight": "MU456", "price": 650},
{"flight": "FM789", "price": 720},
}
return flights, nil
}
2️⃣ 税费查询工具(get_tax_rate)
真实机票的“含税总价”不是简单的“票价 × 税率”,而是:含税总价 = 票价 + 机建费 + 燃油附加费。
type GetTaxRateTool struct{}
func (t *GetTaxRateTool) Name() string {
return "get_tax_rate"
}
func (t *GetTaxRateTool) Description() string {
return "Get the tax rate (airport fee + fuel fee) for a specific route"
}
func (t *GetTaxRateTool) Parameters() map[string]ParameterSpec {
return map[string]ParameterSpec{
"from": {Type: "string", Description: "Departure city(出发城市)", Required: true},
"to": {Type: "string", Description: "Destination city(目的地城市)", Required: true},
}
}
func (t *GetTaxRateTool) Execute(args map[string]interface{}) (interface{}, error) {
// 国内航线固定:机建费50元,燃油费按距离(这里简化固定140元)
return map[string]float64{
"airport_fee": 50.0, // 机场建设费
"fuel_fee": 140.0, // 燃油附加费
}, nil
}
3️⃣ 价格计算工具(calc_total_price)
type CalcTotalPriceTool struct{}
func (t *CalcTotalPriceTool) Name() string {
return "calc_total_price"
}
func (t *CalcTotalPriceTool) Description() string {
return "Calculate total price including tax: base_price + airport_fee + fuel_fee"
}
func (t *CalcTotalPriceTool) Parameters() map[string]ParameterSpec {
return map[string]ParameterSpec{
"base_price": {Type: "number", Description: "Base ticket price(基础票价)", Required: true},
"airport_fee": {Type: "number", Description: "Airport construction fee(机场建设费)", Required: true},
"fuel_fee": {Type: "number", Description: "Fuel surcharge(燃油附加费)", Required: true},
}
}
func (t *CalcTotalPriceTool) Execute(args map[string]interface{}) (interface{}, error) {
basePrice, _ := args["base_price"].(float64)
airportFee, _ := args["airport_fee"].(float64)
fuelFee, _ := args["fuel_fee"].(float64)
total := basePrice + airportFee + fuelFee // Formula(计算公式)
return total, nil
}
四、Tool Registry(工具注册中心)
注册中心是用来管理所有工具的“菜单”,告诉 LLM 有哪些工具可用。
// ===== Tool Registry(工具注册中心)=====
type ToolRegistry struct {
tools map[string]ToolInterface // Store tools by name
mu sync.RWMutex // Read-write lock(读写锁)
}
func NewRegistry() *ToolRegistry {
return &ToolRegistry{
tools: make(map[string]ToolInterface),
}
}
func (r *ToolRegistry) Register(tool ToolInterface) error {
r.mu.Lock()
defer r.mu.Unlock()
name := tool.Name()
if _, exists := r.tools[name]; exists {
return fmt.Errorf("tool %s already registered(工具已存在)", name)
}
r.tools[name] = tool
return nil
}
func (r *ToolRegistry) GetTool(name string) (ToolInterface, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
tool, exists := r.tools[name]
return tool, exists
}
五、核心循环:LLM 调用 + 工具执行
这是最核心的部分——LLM 决定调哪个工具,Go 后端负责执行,然后把结果返回给 LLM 继续推理。
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/sashabaranov/go-openai" // OpenAI client(兼容DeepSeek、Ollama)
)
// ===== Tool Executor(工具执行器)=====
type ToolExecutor struct {
registry *ToolRegistry
}
func NewToolExecutor(registry *ToolRegistry) *ToolExecutor {
return &ToolExecutor{registry: registry}
}
func (e *ToolExecutor) ExecuteTool(toolName string, args map[string]interface{}) (interface{}, error) {
// 1. Look up tool(查找工具)
tool, exists := e.registry.GetTool(toolName)
if !exists {
return nil, fmt.Errorf("tool not found(工具未找到): %s", toolName)
}
// 2. Execute(执行工具)
result, err := tool.Execute(args)
if err != nil {
return nil, fmt.Errorf("tool execution failed(工具执行失败): %w", err)
}
return result, nil
}
// ===== Define Tools for OpenAI(定义OpenAI工具格式)=====
var tools = []openai.Tool{
{
Type: "function",
Function: &openai.FunctionDefinition{
Name: "search_flights",
Description: "Search for available flights by departure, destination, and date",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"from": map[string]interface{}{"type": "string", "description": "Departure city"},
"to": map[string]interface{}{"type": "string", "description": "Destination city"},
"date": map[string]interface{}{"type": "string", "description": "Date YYYY-MM-DD"},
},
"required": []string{"from", "to", "date"},
},
},
},
{
Type: "function",
Function: &openai.FunctionDefinition{
Name: "get_tax_rate",
Description: "Get airport fee and fuel surcharge for a route",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"from": map[string]interface{}{"type": "string"},
"to": map[string]interface{}{"type": "string"},
},
"required": []string{"from", "to"},
},
},
},
{
Type: "function",
Function: &openai.FunctionDefinition{
Name: "calc_total_price",
Description: "Calculate total price: base_price + airport_fee + fuel_fee",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"base_price": map[string]interface{}{"type": "number"},
"airport_fee": map[string]interface{}{"type": "number"},
"fuel_fee": map[string]interface{}{"type": "number"},
},
"required": []string{"base_price", "airport_fee", "fuel_fee"},
},
},
},
}
// ===== Execute Tool Call(执行工具调用)=====
func executeToolCall(tc openai.ToolCall, executor *ToolExecutor) string {
var args map[string]interface{}
json.Unmarshal([]byte(tc.Function.Arguments), &args)
result, err := executor.ExecuteTool(tc.Function.Name, args)
if err != nil {
return fmt.Sprintf(`{"error": "%s"}`, err.Error())
}
data, _ := json.Marshal(result)
return string(data)
}
// ===== Main Loop(主循环)=====
func main() {
ctx := context.Background()
// Setup registry and executor(初始化注册中心和执行器)
registry := NewRegistry()
registry.Register(&SearchFlightsTool{})
registry.Register(&GetTaxRateTool{})
registry.Register(&CalcTotalPriceTool{})
executor := NewToolExecutor(registry)
// Initialize LLM client(初始化LLM客户端)
client := openai.NewClient(os.Getenv("OPENAI_API_KEY"))
// User input(用户输入)
userMsg := "帮我查5月30日北京到上海最便宜的机票,并算出含税总价"
messages := []openai.ChatCompletionMessage{
{Role: openai.ChatMessageRoleUser, Content: userMsg},
}
// ReAct Loop(推理-行动循环):最多5轮
for i := 0; i < 5; i++ {
// Call LLM(调用LLM)
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
Model: "gpt-4o", // or "deepseek-chat" / "ollama/llama3"
Messages: messages,
Tools: tools, // Tell LLM what tools are available(告诉LLM有哪些工具)
})
if err != nil {
log.Fatal("LLM call failed:", err)
}
msg := resp.Choices[0].Message
messages = append(messages, msg)
// No tool calls → final answer(没有工具调用,说明LLM给出了最终答案)
if len(msg.ToolCalls) == 0 {
fmt.Println("\n=== Final Answer(最终回答)===")
fmt.Println(msg.Content)
break
}
// Process each tool call(处理每个工具调用)
for _, tc := range msg.ToolCalls {
fmt.Printf("\n🧠 LLM called tool: %s\n", tc.Function.Name)
fmt.Printf(" Arguments(参数): %s\n", tc.Function.Arguments)
// Execute tool(执行工具)
result := executeToolCall(tc, executor)
fmt.Printf(" Observation(结果): %s\n", result)
// Send result back to LLM(将结果返回给LLM继续推理)
messages = append(messages, openai.ChatCompletionMessage{
Role: openai.ChatMessageRoleTool,
Content: result,
ToolCallID: tc.ID,
})
}
}
}
六、执行流程可视化
User Input(用户输入)
↓
LLM 推理 → 决定调哪个工具
↓
Tool Call(工具调用请求) → JSON 格式,包含工具名和参数
↓
Tool Executor(Go后端执行)
↓
Observation(结果返回给 LLM)
↓
LLM 继续推理 → 决定下一步或生成最终回答
用案例走一遍
|
步骤 |
LLM 思考 |
Tool Call |
Observation(结果) |
|
1 |
"先查航班" |
|
三个航班,最便宜MU456,票价650 |
|
2 |
"查税费" |
|
机建费50,燃油费140 |
|
3 |
"算总价" |
|
840 |
|
4 |
"生成回答" |
— |
MU456,票价650,机建50,燃油140,含税总价840元 |
七、核心认知
Tool Calling(工具调用)的本质不是 AI 技术,而是一种架构模式,把后端系统变成可被 LLM 调度的工具网络。
架构分层
|
层级 |
角色 |
说明 |
|
LLM |
Scheduler(调度器) |
决定调哪个工具、传什么参数 |
|
Tool Registry |
Service Catalog(服务目录) |
列出所有可用工具 |
|
Tool Executor |
Execution Layer(执行层) |
实际干活(查DB、调API、做计算) |
|
Observation |
Result Feedback(结果反馈) |
把执行结果送回 LLM |
与 MCP 的关系
MCP(Model Context Protocol,模型上下文协议)是 Tool Calling 的标准化升级版,把工具定义统一为 schema,通过 MCP Server 统一管理。
八、总结
这篇通过机票查询的案例,展示了如何用 Go 语言实现 Tool Calling(工具调用):
- 定义工具(Tool Registry):告诉 LLM 有哪些能力
- LLM 调度:LLM 决定调哪个工具、传什么参数
- 后端执行(Tool Executor):Go 代码实际干活
- 结果反馈(Observation):把结果返回 LLM 继续推理
- 循环直到完成(ReAct Loop)
写在你代码里的每一个函数(查航班、算价格、查税率)都变成"工具",LLM 看着工具列表决定"现在该用哪个",然后你的 Go 代码去执行,结果回来 LLM 再回答用户。这就是 Tool Calling 的核心价值。
更多推荐


所有评论(0)