🦞 一只用 AI Agent 搭副业产线的程序员


上篇我们写了 Agent 的骨架——感知-决策-执行循环。但循环里最关键的一步没有展开:AI 怎么知道该调用哪个函数?

很多人以为 Function Calling 是 API 自带的功能,点了开关就行。其实不是。Function Calling 本质上就是「你告诉 AI 有哪些函数可以用,AI 在合适的时机告诉你该调哪个」。

这篇我们把这个过程拆到骨头。手写一遍,你就彻底懂了。


Function Calling 不是魔法,是一份说明书

整个过程分 4 步:

① 你定义 Tool Schema(函数的说明书)
② 你把 Schema 塞进 API 请求
③ AI 返回时告诉你「我要调用 tool_xxx,参数是...」
④ 你执行函数,把结果再发回给 AI

没有哪一步是 API 自动帮你做的。API 只负责第 3 步——告诉你该调什么。第 1、2、4 步全是你自己写的代码。


完整实现

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strings"
	"time"
)

// ───────── 1. 定义 Tool Schema ─────────

type Tool struct {
	Name        string                 `json:"name"`
	Description string                 `json:"description"`
	Parameters  map[string]interface{} `json:"parameters"`
}

type ToolResult struct {
	ToolName string
	Success  bool
	Data     string
	Error    string
}

// 定义一个「获取天气」的工具
var weatherTool = Tool{
	Name:        "get_weather",
	Description: "获取指定城市的当前天气信息",
	Parameters: map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"city": map[string]interface{}{
				"type":        "string",
				"description": "城市名称,例如 '北京' 或 'Shanghai'",
			},
			"unit": map[string]interface{}{
				"type":        "string",
				"enum":        []string{"celsius", "fahrenheit"},
				"description": "温度单位,默认 celsius",
			},
		},
		"required": []string{"city"},
	},
}

// 定义一个「发送邮件」的工具
var emailTool = Tool{
	Name:        "send_email",
	Description: "发送一封邮件到指定地址",
	Parameters: map[string]interface{}{
		"type": "object",
		"properties": map[string]interface{}{
			"to": map[string]interface{}{
				"type":        "string",
				"description": "收件人邮箱地址",
			},
			"subject": map[string]interface{}{
				"type":        "string",
				"description": "邮件主题",
			},
			"body": map[string]interface{}{
				"type":        "string",
				"description": "邮件正文",
			},
		},
		"required": []string{"to", "subject", "body"},
	},
}

// ───────── 2. 构建 API 请求 ─────────

type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

type ChatRequest struct {
	Model    string    `json:"model"`
	Messages []Message `json:"messages"`
	Tools    []Tool    `json:"tools"`
}

type ToolCallRequest struct {
	ID   string `json:"id"`
	Type string `json:"type"`
	Func struct {
		Name      string `json:"name"`
		Arguments string `json:"arguments"` // JSON 字符串
	} `json:"function"`
}

type ChatResponse struct {
	Choices []struct {
		Message struct {
			Role      string            `json:"role"`
			Content   string            `json:"content"`
			ToolCalls []ToolCallRequest `json:"tool_calls"`
		} `json:"message"`
	} `json:"choices"`
}

func callLLMWithTools(messages []Message, tools []Tool) (ChatResponse, error) {
	reqBody := ChatRequest{
		Model:    "deepseek-v4-pro",
		Messages: messages,
		Tools:    tools,
	}
	data, _ := json.Marshal(reqBody)

	req, _ := http.NewRequest("POST",
		"https://api.deepseek.com/anthropic/v1/chat/completions",
		bytes.NewReader(data))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("DEEPSEEK_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return ChatResponse{}, fmt.Errorf("API 调用失败: %w", err)
	}
	defer resp.Body.Close()

	body, _ := io.ReadAll(resp.Body)
	var chatResp ChatResponse
	if err := json.Unmarshal(body, &chatResp); err != nil {
		return ChatResponse{}, fmt.Errorf("解析响应失败: %w", err)
	}
	return chatResp, nil
}

// ───────── 3. 执行本地函数 ─────────

func executeWeatherTool(args map[string]interface{}) ToolResult {
	city, _ := args["city"].(string)
	unit := "celsius"
	if u, ok := args["unit"].(string); ok {
		unit = u
	}

	// 模拟天气查询(实际项目在这里调真实的天气 API)
	weatherData := fmt.Sprintf(
		`{"city": "%s", "temperature": 22, "condition": "晴", "humidity": 55, "unit": "%s"}`,
		city, unit,
	)
	return ToolResult{ToolName: "get_weather", Success: true, Data: weatherData}
}

func executeEmailTool(args map[string]interface{}) ToolResult {
	to, _ := args["to"].(string)
	subject, _ := args["subject"].(string)
	body, _ := args["body"].(string)

	fmt.Printf("📧 模拟发送邮件:\n  收件人: %s\n  主题: %s\n  正文: %s\n", to, subject, body)
	return ToolResult{
		ToolName: "send_email",
		Success:  true,
		Data:     fmt.Sprintf(`{"status": "sent", "to": "%s"}`, to),
	}
}

func executeTool(toolName string, argsJSON string) ToolResult {
	var args map[string]interface{}
	if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
		return ToolResult{ToolName: toolName, Success: false, Error: fmt.Sprintf("参数解析失败: %v", err)}
	}

	switch toolName {
	case "get_weather":
		return executeWeatherTool(args)
	case "send_email":
		return executeEmailTool(args)
	default:
		return ToolResult{ToolName: toolName, Success: false, Error: "未知工具"}
	}
}

// ───────── 4. 完整的 Agent 循环 ─────────

func main() {
	messages := []Message{
		{Role: "system", Content: "你是一个生活助手。当用户询问天气时,调用 get_weather 工具。当需要发送邮件时,调用 send_email 工具。"},
		{Role: "user", Content: "北京今天天气怎么样?如果气温超过 20 度,发一封邮件给 boss@company.com,主题'今日天气提醒',正文'北京今天很暖和'。"},
	}

	tools := []Tool{weatherTool, emailTool}
	maxSteps := 10

	for step := 0; step < maxSteps; step++ {
		resp, err := callLLMWithTools(messages, tools)
		if err != nil {
			fmt.Printf("❌ 第 %d 步出错: %v\n", step+1, err)
			break
		}

		if len(resp.Choices) == 0 {
			fmt.Println("❌ 空响应")
			break
		}

		msg := resp.Choices[0].Message

		// 有工具调用 → 执行
		if len(msg.ToolCalls) > 0 {
			for _, tc := range msg.ToolCalls {
				fmt.Printf("🔧 AI 请求调用: %s(%s)\n", tc.Func.Name, tc.Func.Arguments)

				// 记录 AI 的工具调用请求
				messages = append(messages, Message{
					Role:    "assistant",
					Content: fmt.Sprintf("调用工具 %s", tc.Func.Name),
				})

				// 执行本地函数
				result := executeTool(tc.Func.Name, tc.Func.Arguments)

				// 把结果发回给 AI
				resultMsg := fmt.Sprintf("工具 %s 返回: %s", result.ToolName, result.Data)
				if !result.Success {
					resultMsg = fmt.Sprintf("工具 %s 执行失败: %s", result.ToolName, result.Error)
				}
				messages = append(messages, Message{Role: "user", Content: resultMsg})
			}
			continue
		}

		// 没有工具调用 → 最终答案
		fmt.Println("\n✅ AI 最终回复:")
		fmt.Println(msg.Content)
		break
	}
}

关键细节拆解

1. Tool Schema 就是函数的说明书

var weatherTool = Tool{
    Name:        "get_weather",
    Description: "获取指定城市的当前天气信息",  // 这句话最重要
    Parameters: map[string]interface{}{ ... },  // JSON Schema 格式
}

AI 不知道你的代码。它只会根据你的 DescriptionParameters 判断该不该用这个工具。Description 写得好不好,决定了 AI 能不能正确地选对工具。

2. AI 只做「决策」,不做「执行」

AI 返回的是:

{
  "tool_calls": [{
    "function": {
      "name": "get_weather",
      "arguments": "{\"city\": \"北京\"}"
    }
  }]
}

它说「我建议调 get_weather,参数是北京」。真正执行 get_weather 的是你的代码。 AI 没有权限访问你的文件系统、网络或数据库——除非你给它工具。

3. 结果反馈是关键

messages = append(messages, Message{
    Role: "user",
    Content: fmt.Sprintf("工具 %s 返回: %s", result.ToolName, result.Data),
})

执行完函数后,把结果格式化成文本塞回消息历史。AI 读到结果后,才能决定下一步。


一个真实的坑:参数幻觉

有一次我定义了一个工具 search_database,参数 query 的描述我偷懒写成 "搜索条件"

AI 传回来的参数是:{"query": "最近一周销量最高的产品"}。这是个自然语言句子,不是 SQL。

教训:Tool 的 Description 和参数定义要精确到「机器可执行的级别」。 写法则是——如果参数是 SQL,就在描述里写明「SQL 查询语句,例如 SELECT * FROM orders WHERE…」,别给 AI 自由发挥的空间。


一眼看懂的流程

你的代码                           AI API
   │                                 │
   ├─ 定义 Tool Schema               │
   ├─ 发送 [messages, tools] ───────→│
   │                                 ├─ 分析用户意图
   │                                 ├─ 决定用哪个 tool
   │   ←──── [tool_calls] ──────────┤
   ├─ 解析 tool call                 │
   ├─ 执行本地函数                    │
   ├─ 发送 [result] ────────────────→│
   │                                 ├─ 理解结果
   │                                 ├─ 决定是否继续
   │   ←──── [final_answer] ────────┤
   ├─ 输出给用户                      │

总结

Function Calling 不是 API 替你执行的。它只是 AI 说「我建议调这个函数」——剩下的都是你的代码在做。

理解了这一层,你就能写出完全受控的 Agent。下一篇我们解决一个工程问题:当你有 10 个 Tool 时,怎么设计统一的接口?怎么处理超时?怎么让错误不炸掉整个 Agent?

关注我,别错过。


🦞 一只用 AI Agent 搭副业产线的程序员

全平台同名:虾哥不加班
需要定制 AI 工具?来聊聊 → lob_ai

源码:GitHub - lobster-bujiaban

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐