go利用 openai 实现本地调用工具

224 阅读4分钟

go利用 openai 实现本地调用工具

最近刷 老马X 的时候,经常看到大家提到 “MCP”,一开始我还以为是某种新模型,深入了解后才发现——MCP 本质上是 AI 的“助手系统”。

打个比方就明白了:你是老板,有目标有想法,但精力有限,什么都自己干不现实。于是你请了很多员工来帮你做事,有的处理数据,有的写文案,有的搞图像语音,配合高效、各司其职。这些“员工”就是 MCP —— 专注处理不同任务的小工具、小模型或代理模块。

技术上讲,MCP(Multi-Component Prompt / Multi-Capability Proxy)是在大模型基础上接入多个外部能力模块,让 AI 能完成更复杂的任务。

我看 OpenAI 的 Go SDK 也实现了类似的 Tool Calling,试了一下,感觉和 MCP 思路差不多。不过如果想玩得更自由、更强大,个人还是建议用 Python,扩展性更好、生态也更成熟。

image-20250609170525037

注:如果你没有openiApiKey 又没有魔法,建议你去 https://github.com/chatanywhere/GPT_API_free?tab=readme-ov-file 获取免费,或者购买,也是很便宜,国内可以直接调用访问openai客户端

1.实例化 openai客户端

package models
​
import (
    "bufio"
    "context"
    "encoding/json"
    "fmt"
    "os""github.com/openai/openai-go"
    "github.com/openai/openai-go/option"
    "github.com/openai/openai-go/packages/param""openaiTools/tools"
)
​
const (
    key     = "你的key"
    baseUrl = "请求地址"
)
​
type openaiModel struct {
    Client      openai.Client
    Context     context.Context
    MessageList []openai.ChatCompletionMessageParamUnion
    Tools       *tools.ToolHub
}
​
var OpenAiModel = openaiModel{}
​
func (ai *openaiModel) Start() {
    ai.init()
    scanner := bufio.NewScanner(os.Stdin)
    fmt.Println("🟢(输入 'exit' 退出)")
​
    for {
        fmt.Print("🐒 用户:")
        if !scanner.Scan() {
            fmt.Println("❌ 读取输入失败,请重试。")
            break
        }
        input := scanner.Text()
        if input == "exit" {
            fmt.Println("👋 再见!感谢使用小爱助手。")
            break
        }
        ai.ask(input)
    }
}
​
func (ai *openaiModel) init() {
    ai.Client = ai.newClient()
    ai.Context = context.Background()
    ai.MessageList = []openai.ChatCompletionMessageParamUnion{
        openai.SystemMessage("你的名字叫小爱,你是我们的助手"),
    }
    ai.Tools = tools.NewToolHub()
}
​
func (ai *openaiModel) newClient() openai.Client {
    return openai.NewClient(
        option.WithAPIKey(key),
        option.WithBaseURL(baseUrl),
    )
}
​
func (ai *openaiModel) ask(question string) {
    ai.MessageList = append(ai.MessageList, openai.UserMessage(question))
    fmt.Println("🤖 小爱回复:")
    fmt.Print("💬 ")
​
    if err := ai.handleToolCallLoop(); err != nil {
        fmt.Printf("\n❌ 错误:%v\n", err)
    }
}
​
func (ai *openaiModel) handleToolCallLoop() error {
    for {
        params := openai.ChatCompletionNewParams{
            Messages:    ai.MessageList,
            Temperature: openai.Float(0.7),
            Model:       openai.ChatModelGPT4o,
            Tools:       ai.Tools.ChatTollParam,
        }
​
        stream := ai.Client.Chat.Completions.NewStreaming(ai.Context, params)
        acc := openai.ChatCompletionAccumulator{}
        var toolCalls []openai.ChatCompletionMessageToolCallParam
        for stream.Next() {
            chunk := stream.Current()
            acc.AddChunk(chunk)
            //判断有没有工具调用
            if finishedToolCall, ok := acc.JustFinishedToolCall(); ok {
                toolCall := openai.ChatCompletionMessageToolCallParam{
                    ID:   finishedToolCall.ID,
                    Type: "function",
                    Function: openai.ChatCompletionMessageToolCallFunctionParam{
                        Name:      finishedToolCall.Name,
                        Arguments: finishedToolCall.Arguments,
                    },
                }
                toolCalls = append(toolCalls, toolCall)
            }
​
            if len(chunk.Choices) > 0 && chunk.Choices[0].Delta.Content != "" {
                fmt.Print(chunk.Choices[0].Delta.Content)
            }
        }
​
        if err := stream.Err(); err != nil {
            return fmt.Errorf("流式处理错误: %w", err)
        }
​
        if len(toolCalls) == 0 {
            break
        }
​
        for _, toolCall := range toolCalls {
            var args map[string]interface{}
            if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &args); err != nil {
                return fmt.Errorf("解析工具参数失败: %w", err)
            }
​
            //调用工具
            result, err := ai.useExtendTools(toolCall.Function.Name, args)
            if err != nil {
                return fmt.Errorf("工具调用失败: %w", err)
            }
​
            // 添加工具调用的 assistant 消息
            ai.MessageList = append(ai.MessageList, openai.ChatCompletionMessageParamUnion{
                OfAssistant: &openai.ChatCompletionAssistantMessageParam{
                    Role:      "assistant",
                    ToolCalls: toolCalls,
                },
            })
​
            // 添加工具响应消息
            ai.MessageList = append(ai.MessageList, openai.ChatCompletionMessageParamUnion{
                OfTool: &openai.ChatCompletionToolMessageParam{
                    Role: "tool",
                    Content: openai.ChatCompletionToolMessageParamContentUnion{
                        OfString: param.NewOpt(result),
                    },
                    ToolCallID: toolCall.ID,
                },
            })
        }
    }
​
    fmt.Println()
    return nil
}
​
func (ai *openaiModel) useExtendTools(name string, args map[string]interface{}) (string, error) {
    var resultStr string
    tool, exists := ai.Tools.Tools[name]
    if !exists {
        return "", fmt.Errorf("工具 '%s' 不存在", name)
    }
    result := tool.Request(args)
    switch v := result.(type) {
    case string:
        resultStr = v
    default:
        resJson, err := json.Marshal(v)
        if err != nil {
            return "", fmt.Errorf("工具结果序列化失败: %w", err)
        }
        resultStr = string(resJson)
    }
    return resultStr, nil
}
​

2.配置工具

index.go

package tools
​
import "github.com/openai/openai-go"type Tool interface {
    Name() string
    Request(params map[string]interface{}) interface{}
    RegisterTool(hub *ToolHub)
    RequireFiled() []string
    GetTool() openai.ChatCompletionToolParam
}
​
type ToolHub struct {
    Tools         map[string]Tool
    ChatTollParam []openai.ChatCompletionToolParam
}
​
func NewToolHub() *ToolHub {
    hub := &ToolHub{
        Tools:         make(map[string]Tool),
        ChatTollParam: make([]openai.ChatCompletionToolParam, 0),
    }
    hub.Register()
    return hub
}
​
func (t *ToolHub) Register() {
    WeatherToolApi.RegisterTool(t)
}
​

weather.go

package tools
​
import (
    "encoding/json"
    "fmt"
    "github.com/openai/openai-go"
    "io/ioutil"
    "log"
    "net/http"
    "net/url"
)
​
type WeatherTool struct {
    apikey string
}
type LocationResponse struct {
    Code     string     `json:"code"`
    Location []Location `json:"location"`
}
​
type Location struct {
    Name    string `json:"name"`
    ID      string `json:"id"`
    Country string `json:"country"`
}
​
type WeatherResponse struct {
    Code string `json:"code"`
    Now  Now    `json:"now"`
}
​
type Now struct {
    Temp      string `json:"temp"`      // 温度
    FeelsLike string `json:"feelsLike"` // 体感温度
    Text      string `json:"text"`      // 天气状况(如"晴")
    WindDir   string `json:"windDir"`   // 风向
    WindScale string `json:"windScale"` // 风力等级
}
​
var WeatherToolApi = &WeatherTool{
    apikey: "和风天气apiKey",
}
​
func (w *WeatherTool) Name() string {
    return "weather"
}
​
func (w *WeatherTool) RegisterTool(hub *ToolHub) {
    name := w.Name()
    hub.Tools[name] = w
    hub.ChatTollParam = append(hub.ChatTollParam, w.GetTool())
}
​
func (w *WeatherTool) RequireFiled() []string {
    return []string{"location"}
}
​
func (w *WeatherTool) GetTool() openai.ChatCompletionToolParam {
    return openai.ChatCompletionToolParam{
        Function: openai.FunctionDefinitionParam{
            Name:        w.Name(),
            Description: openai.String("这是查询天气的工具"),
            Parameters: openai.FunctionParameters{
                "type": "object",
                "properties": map[string]interface{}{
                    "location": map[string]string{
                        "type": "string",
                    },
                },
                "required": w.RequireFiled(),
            },
        },
    }
}
​
func (w *WeatherTool) Request(params map[string]interface{}) interface{} {
    requiredFields := w.RequireFiled()
    extracted := make(map[string]string)
    for _, field := range requiredFields {
        if val, ok := params[field]; ok {
            extracted[field] = fmt.Sprintf("%v", val)
        }
    }
    city := extracted["location"]
    id, err := w.getLocationID(city)
    if err != nil {
        return fmt.Sprint("获取城市代码失败")
    }
    res, err := w.getWeather(id)
    if err != nil {
        return fmt.Sprint("获取城市天气失败")
    }
    return fmt.Sprintf("📍%s 温度 %s 体感温度 %s 风向 %s 风力等级为 %s", city, res.Temp, res.FeelsLike, res.WindDir, res.WindScale)
}
​
func (w *WeatherTool) http(requestUrl string) ([]byte, error) {
    resp, err := http.Get(requestUrl)
    if err != nil {
        log.Println("http get error:", err)
        return nil, err
    }
    defer resp.Body.Close()
​
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Println("http read error:", err)
        return nil, err
    }
    return body, nil
}
​
// 获取城市 LocationID
func (w *WeatherTool) getLocationID(city string) (string, error) {
    encodedCity := url.QueryEscape(city)
    requestUrl := fmt.Sprintf("https://geoapi.qweather.com/v2/city/lookup?location=%s&key=%s", encodedCity, w.apikey)
    result, err := w.http(requestUrl)
    if err != nil {
        log.Println("请求城市id失败")
        return "", err
    }
    var locationResp LocationResponse
    if err := json.Unmarshal(result, &locationResp); err != nil {
        return "", fmt.Errorf("解析城市信息失败: %v", err)
    }
    if len(locationResp.Location) == 0 {
        return "", fmt.Errorf("未找到城市: %s", city)
    }
​
    return locationResp.Location[0].ID, nil
}
​
// 查询实时天气
func (w *WeatherTool) getWeather(locationID string) (*Now, error) {
    requestUrl := fmt.Sprintf("https://api.qweather.com/v7/weather/now?location=%s&key=%s", locationID, w.apikey)
    result, err := w.http(requestUrl)
    if err != nil {
        log.Println("查询天气失败")
        return nil, err
    }
    var weatherResp WeatherResponse
    if err := json.Unmarshal(result, &weatherResp); err != nil {
        return nil, fmt.Errorf("解析天气数据失败: %v", err)
    }
​
    if weatherResp.Code != "200" {
        return nil, fmt.Errorf("天气API返回错误: %s", weatherResp.Code)
    }
​
    return &weatherResp.Now, nil
}
​

最终效果:

image-20250609172234147