[Go]GopherAI项目学习记录:MCP功能拓展

一个网页AI聊天,图像识别项目,使用到的技术栈有,Gin框架,GORM,rabbitmq,redis,eino框架,Vue框架等

项目地址:https://github.com/youngyangyang04/GopherAI


GopherAI第二版在第一版的基础上扩展了RAG服务,文件上传服务,MCP服务调用工具,TTS语言识别功能,下面进行一一梳理

MCPServer

首先是定义了用于接收wttr.in返回的JSON的结构体,解析后可获取天气,气温,湿度等数据

//wttr.in JSON 响应结构
type WttrResponse struct {
    CurrentCondition []struct {
        TempC         string `json:"temp_C"`
        Humidity      string `json:"humidity"`
        WindspeedKmph string `json:"windspeedKmph"`
        WeatherDesc   []struct {
            Value string `json:"value"`
        } `json:"weatherDesc"`
    } `json:"current_condition"`

    NearestArea []struct {
        AreaName []struct {
            Value string `json:"value"`
        } `json:"areaName"`
    } `json:"nearest_area"`
}

以及标准响应结构

//统一对外天气结构
type WeatherResponse struct {
    Location    string  `json:"location"`
    Temperature float64 `json:"temperature"`
    Condition   string  `json:"condition"`
    Humidity    int     `json:"humidity"`
    WindSpeed   float64 `json:"windSpeed"`
}

WeatherAPIClient工具类,用于调用天气API

//Weather API Client
type WeatherAPIClient struct{}

func NewWeatherAPIClient() *WeatherAPIClient {
    return &WeatherAPIClient{}
}

类的调用天气API并将返回数据封装成刚刚的结构体的函数

//Weather API Client
type WeatherAPIClient struct{}

func NewWeatherAPIClient() *WeatherAPIClient {
    return &WeatherAPIClient{}
}

func (c *WeatherAPIClient) GetWeather(ctx context.Context, city string) (*WeatherResponse, error) {
    apiURL := fmt.Sprintf(                          // 构造URL
        "https://wttr.in/%s?format=j1&lang=zh",
        city,
    )

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)    // 构造HTTP请求报文
    if err != nil {
        return nil, fmt.Errorf("create request failed: %w", err)
    }

    client := &http.Client{}
    resp, err := client.Do(req)                                                 // 发送HTTP请求
    if err != nil {
        return nil, fmt.Errorf("http request failed: %w", err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)                                          // 读取返回数据
    if err != nil {
        return nil, fmt.Errorf("read response failed: %w", err)
    }

    var wttrResp WttrResponse
    if err := json.Unmarshal(body, &wttrResp); err != nil {                     // 将JSON转为结构体
        return nil, fmt.Errorf("json parse failed: %w", err)
    }

    if len(wttrResp.CurrentCondition) == 0 {
        return nil, fmt.Errorf("no weather data")
    }

    cc := wttrResp.CurrentCondition[0]

    temp, _ := strconv.ParseFloat(cc.TempC, 64)
    humidity, _ := strconv.Atoi(cc.Humidity)
    wind, _ := strconv.ParseFloat(cc.WindspeedKmph, 64)

    location := city
    if len(wttrResp.NearestArea) > 0 &&
        len(wttrResp.NearestArea[0].AreaName) > 0 {
        location = wttrResp.NearestArea[0].AreaName[0].Value
    }

    condition := "未知"
    if len(cc.WeatherDesc) > 0 {
        condition = cc.WeatherDesc[0].Value
    }

    return &WeatherResponse{
        Location:    location,
        Temperature: temp,
        Condition:   condition,
        Humidity:    humidity,
        WindSpeed:   wind,
    }, nil
}

随后就是MCP服务器部分

func NewMCPServer() *server.MCPServer {
    weatherClient := NewWeatherAPIClient()      // 天气API结构体

    mcpServer := server.NewMCPServer(           // 创建MCP服务器
        "weather-query-server",
        "1.0.0",
        server.WithToolCapabilities(true),
        server.WithLogging(),
    )

    mcpServer.AddTool(                          // 添加工具
        mcp.NewTool(
            "get_weather",
            mcp.WithDescription("获取指定城市的天气信息"),
            mcp.WithString(
                "city",
                mcp.Description("城市名称,如 Beijing、上海"),
                mcp.Required(),
            ),
        ),
        // 工具handler函数
        func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
            args := request.GetArguments()
            city, ok := args["city"].(string)
            if !ok || city == "" {
                return nil, fmt.Errorf("invalid city argument")
            }

            weather, err := weatherClient.GetWeather(ctx, city)
            if err != nil {
                return nil, err
            }

            resultText := fmt.Sprintf(
                "城市: %s\n温度: %.1f°C\n天气: %s\n湿度: %d%%\n风速: %.1f km/h",
                weather.Location,
                weather.Temperature,
                weather.Condition,
                weather.Humidity,
                weather.WindSpeed,
            )

            return &mcp.CallToolResult{
                Content: []mcp.Content{
                    mcp.TextContent{
                        Type: "text",
                        Text: resultText,
                    },
                },
            }, nil
        },
    )

    return mcpServer
}

// StartServer 启动MCP服务器
// httpAddr: HTTP服务器监听的地址(例如":8080")
func StartServer(httpAddr string) error {
    mcpServer := NewMCPServer()

    httpServer := server.NewStreamableHTTPServer(mcpServer)
    log.Printf("HTTP MCP server listening on %s/mcp", httpAddr)
    return httpServer.Start(httpAddr)
}

MCPClient

客户端结构体封装

// MCPClient 是MCP客户端的封装
// 它提供了一个类对象接口来与MCP服务器交互
type MCPClient struct {
    c *client.Client
}

创建MCP客户端

// NewMCPClient 创建一个新的MCP客户端实例
// httpURL: HTTP传输的URL
func NewMCPClient(httpURL string) (*MCPClient, error) {
    fmt.Println("正在初始化HTTP客户端...")
    // 创建HTTP传输
    httpTransport, err := transport.NewStreamableHTTP(httpURL)
    if err != nil {
        return nil, fmt.Errorf("创建HTTP传输失败: %w", err)
    }

    // 使用传输创建客户端
    c := client.NewClient(httpTransport)

    return &MCPClient{c: c}, nil
}

初始化客户端

// Initialize 初始化客户端
func (m *MCPClient) Initialize(ctx context.Context) (*mcp.InitializeResult, error) {
    // 设置通知处理程序(回调函数)
    m.c.OnNotification(func(notification mcp.JSONRPCNotification) {
        fmt.Printf("收到通知: %s\n", notification.Method)
    })

    // 初始化客户端
    fmt.Println("正在初始化客户端...")
    initRequest := mcp.InitializeRequest{}
    initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
    initRequest.Params.ClientInfo = mcp.Implementation{
        Name:    "MCP-Go Weather Client",
        Version: "1.0.0",
    }
    initRequest.Params.Capabilities = mcp.ClientCapabilities{}

    serverInfo, err := m.c.Initialize(ctx, initRequest)
    if err != nil {
        return nil, fmt.Errorf("初始化失败: %w", err)
    }

    // 显示服务器信息
    fmt.Printf("连接到服务器: %s (版本 %s)\n",
        serverInfo.ServerInfo.Name,
        serverInfo.ServerInfo.Version)

    return serverInfo, nil
}

调用工具

// CallTool 调用MCP工具
func (m *MCPClient) CallTool(ctx context.Context, toolName string, args map[string]any) (*mcp.CallToolResult, error) {
    callToolRequest := mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name:      toolName,
            Arguments: args,
        },
    }

    result, err := m.c.CallTool(ctx, callToolRequest)
    if err != nil {
        return nil, fmt.Errorf("调用工具失败: %w", err)
    }

    return result, nil
}

// CallWeatherTool 调用get_weather工具
func (m *MCPClient) CallWeatherTool(ctx context.Context, city string) (*mcp.CallToolResult, error) {
    fmt.Printf("正在查询城市 %s 的天气...\n", city)

    callToolRequest := mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name: "get_weather",
            Arguments: map[string]any{
                "city": city,
            },
        },
    }

    result, err := m.c.CallTool(ctx, callToolRequest)
    if err != nil {
        return nil, fmt.Errorf("调用工具失败: %w", err)
    }

    return result, nil
}

提取结果文本

// GetToolResultText 获取工具结果中的文本内容
func (m *MCPClient) GetToolResultText(result *mcp.CallToolResult) string {
    var text string
    for _, content := range result.Content {
        if textContent, ok := content.(mcp.TextContent); ok {
            text += textContent.Text + "\n"
        }
    }
    return text
}

MCP main.go

这个main.go既可以当server又可以当client,取决于命令行传入的参数

func main() {
    // 定义命令行标志
    mode := flag.String("mode", "", "运行模式: server 或 client")
    httpAddr := flag.String("http-addr", ":8081", "HTTP服务器地址")
    city := flag.String("city", "", "要查询天气的城市名称")
    flag.Parse()

    if *mode == "" {
        fmt.Println("Error: 您必须指定模式使用--mode (server 或 client)")
        flag.Usage()
        os.Exit(1)
    }

    if *mode == "server" {
        // 启动服务器
        fmt.Println("启动MCP服务器...")
        if err := mcpserver.StartServer(*httpAddr); err != nil {
            log.Fatalf("服务器错误: %v", err)
        }
    } else if *mode == "client" {
        // 运行客户端
        if *city == "" {
            fmt.Println("Error: 您必须指定城市名称使用--city")
            flag.Usage()
            os.Exit(1)
        }

        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()

        // 创建客户端
        httpURL := "http://localhost:8081/mcp"
        mcpClient, err := mcpclient.NewMCPClient(httpURL)
        if err != nil {
            log.Fatalf("创建客户端失败: %v", err)
        }
        defer mcpClient.Close()

        // 初始化客户端
        if _, err := mcpClient.Initialize(ctx); err != nil {
            log.Fatalf("初始化失败: %v", err)
        }

        // 执行健康检查
        if err := mcpClient.Ping(ctx); err != nil {
            log.Fatalf("健康检查失败: %v", err)
        }

        // 调用天气工具
        result, err := mcpClient.CallWeatherTool(ctx, *city)
        if err != nil {
            log.Fatalf("调用工具失败: %v", err)
        }

        // 显示天气结果
        fmt.Println("\n天气查询结果:")
        fmt.Println(mcpClient.GetToolResultText(result))

        fmt.Println("\n客户端初始化成功。正在关闭...")
    }
}

MCPModel实现

在common/aihelper/model.go中新增了AIModel接口的MCP实现,结构体封装了基本的Eino提供的chatmodel和一个上面写的MCP客户端以及用户名和MCP服务器地址(这里硬编码为localhost:8081)

// MCPModel MCP模型实现,集成MCP服务
type MCPModel struct {
    llm        model.ToolCallingChatModel
    mcpClient  *client.Client
    username   string
    mcpBaseURL string
}

创建MCPModel,内部创建了chatmodel,设置了MCP服务器地址

// NewMCPModel 创建MCP模型实例
func NewMCPModel(ctx context.Context, username string) (*MCPModel, error) {
    key := os.Getenv("OPENAI_API_KEY")
    conf := config.GetConfig()
    modelName := conf.RagModelConfig.RagChatModelName
    baseURL := conf.RagModelConfig.RagBaseUrl

    // 创建LLM
    llm, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{
        BaseURL: baseURL,
        Model:   modelName,
        APIKey:  key,
    })
    if err != nil {
        return nil, fmt.Errorf("create mcp model failed: %v", err)
    }

    mcpBaseURL := "http://localhost:8081/mcp"

    return &MCPModel{
        llm:        llm,
        mcpBaseURL: mcpBaseURL,
        username:   username,
    }, nil
}

获取模型内部的MCP客户端

// getMCPClient 获取或创建MCP客户端
func (m *MCPModel) getMCPClient(ctx context.Context) (*client.Client, error) {
    if m.mcpClient == nil {
        // 创建MCP客户端
        httpTransport, err := transport.NewStreamableHTTP(m.mcpBaseURL)
        if err != nil {
            return nil, fmt.Errorf("create mcp transport failed: %v", err)
        }

        m.mcpClient = client.NewClient(httpTransport)

        // 初始化MCP客户端
        initRequest := mcp.InitializeRequest{}
        initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION
        initRequest.Params.ClientInfo = mcp.Implementation{
            Name:    "MCP-Go AIHelper Client",
            Version: "1.0.0",
        }
        initRequest.Params.Capabilities = mcp.ClientCapabilities{}

        if _, err := m.mcpClient.Initialize(ctx, initRequest); err != nil {
            return nil, fmt.Errorf("mcp client initialize failed: %v", err)
        }
    }
    return m.mcpClient, nil
}

要让AI懂得调用MCP工具,首先需要设计一套prompt

// buildFirstPrompt 构建第一次调用的提示词
func (m *MCPModel) buildFirstPrompt(query string) string {
    return fmt.Sprintf(`你是一个智能助手,可以调用MCP工具来获取信息。

可用工具:
- get_weather: 获取指定城市的天气信息,参数: city(城市名称,支持中文和英文,如北京、Shanghai等)

重要规则:
1. 如果需要调用工具,必须严格返回以下JSON格式:
{
  "isToolCall": true,
  "toolName": "工具名称",
  "args": {"参数名": "参数值"}
}
2. 如果不需要调用工具,直接返回自然语言回答
3. 请根据用户问题决定是否需要调用工具

用户问题: %s

请根据需要调用适当的工具,然后给出综合的回答。`, query)
}

// buildSecondPrompt 构建第二次调用的提示词
func (m *MCPModel) buildSecondPrompt(query, toolName string, args map[string]interface{}, toolResult string) string {
    return fmt.Sprintf(`你是一个智能助手,可以调用MCP工具来获取信息。

工具执行结果:
工具名称: %s
工具参数: %v
工具结果: %s

用户问题: %s

请根据工具结果和用户问题,给出最终的综合回答。`, toolName, args, toolResult, query)
}

设计了一个AIToolCall结构体,用于表示是否需要工具调用,调用的工具名称,调用参数

// AIToolCall 表示AI工具调用请求
type AIToolCall struct {
    IsToolCall bool                   `json:"isToolCall"`
    ToolName   string                 `json:"toolName"`
    Args       map[string]interface{} `json:"args"`
}

解析AI响应的函数,返回上面的AIToolCall结构体

// parseAIResponse 解析AI响应,检查是否包含工具调用
func (m *MCPModel) parseAIResponse(response string) (*AIToolCall, error) {
    // 尝试解析为JSON
    var toolCall AIToolCall
    if err := json.Unmarshal([]byte(response), &toolCall); err == nil {
        return &toolCall, nil
    }

    // 如果不是JSON,检查是否包含工具调用关键词
    if strings.Contains(response, "get_weather") {
        // 尝试提取城市名称
        city := m.extractCityFromResponse(response)
        if city != "" {
            return &AIToolCall{
                IsToolCall: true,
                ToolName:   "get_weather",
                Args:       map[string]interface{}{"city": city},
            }, nil
        }
    }

    // 不是工具调用
    return &AIToolCall{IsToolCall: false}, nil
}

// extractCityFromResponse 从响应中提取城市名称
// 直接从AI返回的JSON中提取城市,不预留城市列表
func (m *MCPModel) extractCityFromResponse(response string) string {
    // 尝试从JSON中提取城市
    var toolCall AIToolCall
    if err := json.Unmarshal([]byte(response), &toolCall); err == nil {
        if args, ok := toolCall.Args["city"].(string); ok {
            return args
        }
    }

    // 如果JSON解析失败,尝试从文本中提取城市名称
    // 这部分可以根据实际需要扩展,但不再预留固定城市列表
    return ""
}

调用工具并返回结果

// callMCPTool 调用MCP工具
func (m *MCPModel) callMCPTool(ctx context.Context, client *client.Client, toolName string, args map[string]interface{}) (string, error) {
    callToolRequest := mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name:      toolName,
            Arguments: args,
        },
    }

    result, err := client.CallTool(ctx, callToolRequest)
    if err != nil {
        return "", fmt.Errorf("mcp tool call failed: %v", err)
    }

    // 提取工具结果文本
    var text string
    for _, content := range result.Content {
        if textContent, ok := content.(mcp.TextContent); ok {
            text += textContent.Text + "\n"
        }
    }

    return text, nil
}

最后就是使用上面的众多工具函数写出的GenerateResponse接口函数,流程如下:

  • 首先获取用户最后一条消息,封装成第一次调用工具的提示词格式,并创建更新后的schema切片
  • 第一次调用AI,使用刚刚的schema切片,这样会告诉ai去判断是否需要调用工具,并封装成JSON格式返回
  • 解析第一次AI调用的回复,拿出JSON
  • 如果AI认为不需要使用工具,直接返回第一次生成的结果;如果AI认为需要,就帮AI调用,最后把结果封装进第二次AI调用的提示词里,再次调用AI并返回最终结果
// GenerateResponse 生成响应,集成MCP工具
func (m *MCPModel) GenerateResponse(ctx context.Context, messages []*schema.Message) (*schema.Message, error) {
    if len(messages) == 0 {
        return nil, fmt.Errorf("no messages provided")
    }

    // 获取最后一条消息
    lastMessage := messages[len(messages)-1]
    query := lastMessage.Content

    // 第一次调用AI:告诉AI使用固定的JSON格式
    firstPrompt := m.buildFirstPrompt(query)
    firstMessages := make([]*schema.Message, len(messages))
    copy(firstMessages, messages)
    firstMessages[len(firstMessages)-1] = &schema.Message{
        Role:    schema.User,
        Content: firstPrompt,
    }

    // 调用LLM生成第一次响应
    firstResp, err := m.llm.Generate(ctx, firstMessages)
    if err != nil {
        return nil, fmt.Errorf("mcp first generate failed: %v", err)
    }
    log.Println("first resp is ", firstResp)
    // 解析AI响应
    aiResult := firstResp.Content
    toolCall, err := m.parseAIResponse(aiResult)
    if err != nil {
        log.Printf("Failed to parse AI response: %v", err)
        return firstResp, nil
    }

    // 情况1:AI不调用工具,直接返回响应
    if !toolCall.IsToolCall {
        log.Println("toolCall IsToolCall is false ", firstResp)
        return firstResp, nil
    }
    log.Println("toolCall IsToolCall is true ", firstResp)
    // 情况2:AI要调用工具
    // 获取MCP客户端
    mcpClient, err := m.getMCPClient(ctx)
    if err != nil {
        log.Printf("MCP client error: %v", err)
        return firstResp, nil
    }

    // 调用MCP工具
    toolResult, err := m.callMCPTool(ctx, mcpClient, toolCall.ToolName, toolCall.Args)
    if err != nil {
        log.Printf("MCP tool call failed: %v", err)
        return firstResp, nil
    }

    // 第二次调用AI:将工具结果告诉AI
    secondPrompt := m.buildSecondPrompt(query, toolCall.ToolName, toolCall.Args, toolResult)
    secondMessages := make([]*schema.Message, len(messages))
    copy(secondMessages, messages)
    secondMessages[len(secondMessages)-1] = &schema.Message{
        Role:    schema.User,
        Content: secondPrompt,
    }

    // 调用LLM生成最终响应
    finalResp, err := m.llm.Generate(ctx, secondMessages)

    if err != nil {
        return nil, fmt.Errorf("mcp second generate failed: %v", err)
    }
    log.Println("最终响应为:", finalResp)
    return finalResp, nil
}

Factory注册

// MCP 模型(集成MCP服务)
    f.creators["3"] = func(ctx context.Context, config map[string]interface{}) (AIModel, error) {
        username, ok := config["username"].(string)
        if !ok {
            return nil, fmt.Errorf("MCP model requires username")
        }
        return NewMCPModel(ctx, username)
    }
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇