原生 Function Calling 与结构化输出

Agent 与工具一章的 ReAct 模式依靠提示词驱动模型输出"行动指令"再自行解析。现代模型提供了原生的工具调用能力(Function Calling / Tool Use):工具声明随请求发送,模型以结构化字段返回调用意图,无需脆弱的文本解析。langchaingo 通过 llms.WithTools 暴露该能力。

定义工具

工具以 llms.Tool 声明,参数用 JSON Schema 描述:

package main

import "github.com/tmc/langchaingo/llms"

var availableTools = []llms.Tool{
    {
        Type: "function",
        Function: &llms.FunctionDefinition{
            Name:        "getCurrentWeather",
            Description: "查询指定城市的当前天气",
            Parameters: map[string]any{
                "type": "object",
                "properties": map[string]any{
                    "location": map[string]any{
                        "type":        "string",
                        "description": "城市名称,例如:Beijing",
                    },
                },
                "required": []string{"location"},
            },
        },
    },
    {
        Type: "function",
        Function: &llms.FunctionDefinition{
            Name:        "convertCurrency",
            Description: "按实时汇率换算货币金额",
            Parameters: map[string]any{
                "type": "object",
                "properties": map[string]any{
                    "amount": map[string]any{"type": "number", "description": "金额"},
                    "from":   map[string]any{"type": "string", "description": "源货币代码"},
                    "to":     map[string]any{"type": "string", "description": "目标货币代码"},
                },
                "required": []string{"amount", "from", "to"},
            },
        },
    },
}
Tip

Description 与参数描述直接决定模型选择工具、填充参数的准确度,应使用模型易于理解的自然语言,并明确取值格式。FunctionDefinition.Strict 可启用部分供应商支持的结构化输出严格模式。

工具调用循环

完整流程分四步:发送请求 → 执行模型选择的工具 → 将结果回传 → 获取最终回答。关键在于维护完整的消息历史

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"

    "github.com/tmc/langchaingo/llms"
    "github.com/tmc/langchaingo/llms/openai"
)

func main() {
    ctx := context.Background()
    llm, err := openai.New()
    if err != nil {
        log.Fatal(err)
    }

    history := []llms.MessageContent{
        llms.TextParts(llms.ChatMessageTypeHuman, "北京现在多少度?适合穿外套吗?"),
    }

    // 第一次调用:模型决定调用哪个工具
    resp, err := llm.GenerateContent(ctx, history, llms.WithTools(availableTools))
    if err != nil {
        log.Fatal(err)
    }
    choice := resp.Choices[0]

    // 将 AI 消息(含工具调用意图)加入历史
    assistant := llms.TextParts(llms.ChatMessageTypeAI, choice.Content)
    for _, tc := range choice.ToolCalls {
        assistant.Parts = append(assistant.Parts, tc)
    }
    history = append(history, assistant)

    // 执行模型请求的每个工具调用
    for _, tc := range choice.ToolCalls {
        var result string

        switch tc.FunctionCall.Name {
        case "getCurrentWeather":
            var args struct {
                Location string `json:"location"`
            }
            if err := json.Unmarshal([]byte(tc.FunctionCall.Arguments), &args); err != nil {
                log.Fatal(err)
            }
            // 实际项目在此调用真实 API;此处返回模拟数据
            result = fmt.Sprintf("%s 当前 12°C,多云转晴,风力 3 级", args.Location)

        default:
            result = "未知工具"
        }

        // 将工具结果以 Tool 角色回传历史
        history = append(history, llms.MessageContent{
            Role: llms.ChatMessageTypeTool,
            Parts: []llms.ContentPart{
                llms.ToolCallResponse{
                    ToolCallID: tc.ID,
                    Name:       tc.FunctionCall.Name,
                    Content:    result,
                },
            },
        })
    }

    // 第二次调用:模型基于工具结果生成最终回答
    resp, err = llm.GenerateContent(ctx, history, llms.WithTools(availableTools))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.Choices[0].Content)
}

核心类型一览:

类型字段作用
llms.ToolTypeFunction *FunctionDefinition工具声明
llms.ToolCallIDFunctionCall.NameFunctionCall.Arguments模型返回的调用意图
llms.ToolCallResponseToolCallIDNameContent工具执行结果回传
Warning

OpenAI 兼容接口要求 ToolCallResponse.ToolCallIDtc.ID 一一对应,缺失或错配会导致 400 错误。Arguments 是 JSON 字符串而非结构体,必须反序列化后再使用,且对解析失败保持容错。

生产实现建议将工具注册表抽象为 map[string]func(ctx, args string) (string, error),用统一分发器替代 switch,新增工具只需注册。

与 ReAct Agent 的取舍

维度原生 Function CallingReAct(agents 包)
可靠性结构化字段,无文本解析失败风险依赖提示词输出格式,可能解析失败
模型要求需模型支持工具调用(主流模型均已支持)任意模型可用
控制粒度自行管理循环与消息历史agents.Executor 开箱即用
多步复杂推理需自行编排框架内置迭代逻辑

简单工具场景优先原生调用;需要复杂推理链、多工具编排时用 Agent,或在两者之上自建循环。

JSON Mode:可靠的结构化输出

要求模型输出 JSON 时,开启 llms.WithJSONMode() 强制响应格式,再配合 JSON 解析

type Review struct {
    Score   int    `json:"score"`
    Summary string `json:"summary"`
}

func extractReview(ctx context.Context, llm llms.Model, code string) (*Review, error) {
    prompt := "评审以下 Go 代码。以 JSON 对象输出,字段:score(1-10 整数)、summary(一句话)。只输出 JSON,不要其他文本。\n" + code

    resp, err := llm.GenerateContent(ctx,
        []llms.MessageContent{llms.TextParts(llms.ChatMessageTypeHuman, prompt)},
        llms.WithJSONMode(),
    )
    if err != nil {
        return nil, err
    }

    var review Review
    if err := json.Unmarshal([]byte(resp.Choices[0].Content), &review); err != nil {
        return nil, fmt.Errorf("解析模型输出失败: %w", err)
    }
    return &review, nil
}
Tip

JSON Mode 只是"保证输出是合法 JSON",不校验 schema。要求字段精确匹配时,可将 JSON Schema 作为参数定义传给 WithTools 并将 ToolChoice 强制指向它(供应商支持时),或使用支持 Structured Outputs 的模型参数。

在 HTTP 服务中的整合

工具调用循环耗时不可控,接入工作池超时控制可保障服务稳定性:

http.HandleFunc("POST /api/agent", func(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
    defer cancel()

    // 循环内的每次 GenerateContent 均携带 ctx;
    // 并发执行互不依赖的工具调用时使用 errgroup 限流
    answer, err := runToolLoop(ctx, llm, question) // 即上文封装的完整循环
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadGateway)
        return
    }
    _ = json.NewEncoder(w).Encode(map[string]string{"answer": answer})
})

小结

  • llms.WithTools + JSON Schema 参数定义实现原生工具调用,输出为结构化的 ToolCall
  • 工具调用必须维护完整消息历史,工具结果以 ChatMessageTypeTool 角色 + ToolCallResponse 回传。
  • JSON Mode(WithJSONMode)保证合法 JSON 输出;schema 级校验需供应商的 Structured Outputs 支持。
  • 简单场景原生调用更可靠,复杂推理链用 Agent;两者都应置于超时与限流保护之下。