go利用 openai 实现本地调用工具
最近刷 老马X 的时候,经常看到大家提到 “MCP”,一开始我还以为是某种新模型,深入了解后才发现——MCP 本质上是 AI 的“助手系统”。
打个比方就明白了:你是老板,有目标有想法,但精力有限,什么都自己干不现实。于是你请了很多员工来帮你做事,有的处理数据,有的写文案,有的搞图像语音,配合高效、各司其职。这些“员工”就是 MCP —— 专注处理不同任务的小工具、小模型或代理模块。
技术上讲,MCP(Multi-Component Prompt / Multi-Capability Proxy)是在大模型基础上接入多个外部能力模块,让 AI 能完成更复杂的任务。
我看 OpenAI 的 Go SDK 也实现了类似的 Tool Calling,试了一下,感觉和 MCP 思路差不多。不过如果想玩得更自由、更强大,个人还是建议用 Python,扩展性更好、生态也更成熟。
注:如果你没有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
}
最终效果:
