Golang结合Ollama调用MCP服务

与AI多番“较量”(交互、改错、提示),以下实现通过提示词调用MCP服务(另一种是function call)。

交互使用了本地Ollama qwen2.5。MCP服务是一个简单的时区时间功能。类似 关注MCP AI模型交互协议 • 腾图工作室,威远博客,威远工作室,Ease

package main

import (

"bytes"

"context"

"encoding/json"

"fmt"

"io"

"log"

"net/http"

"os"

"reflect"

"regexp"

"sync"

"github.com/ThinkInAIXYZ/go-mcp/client"

"github.com/ThinkInAIXYZ/go-mcp/protocol"

"github.com/ThinkInAIXYZ/go-mcp/transport"

)

// 定义 Ollama API 请求/响应结构体

type OllamaRequest struct {

Model string `json:"model"`

Messages []ChatMessage `json:"messages"`

Stream bool `json:"stream"`

}

type ChatMessage struct {

Role string `json:"role"`

Content string `json:"content"`

}

type OllamaResponse struct {

Message struct {

Content string `json:"content"`

} `json:"message"`

Error string `json:"error"`

}

// 主函数

func main() {

// 增加工具调用示例的提示词

prompt := `美国当前时间,请试用以下格式调用工具:

@tool{action: "current time", arguments: "Asia/Shanghai"}`

// 调用 Ollama

response, err := callOllama(prompt)

if err != nil {

log.Fatalf("Ollama 调用失败: %v", err)

}

// 解析并打印响应

fmt.Println("=== 模型原始响应 ===")

fmt.Println(response)

// 解析潜在的工具调用指令

if cmds := parseToolsCommand(response); len(cmds) > 0 {

fmt.Println("\n=== 检测到工具调用指令 ===")

fmt.Printf("解析结果:%+v\n", cmds) // 新增调试输出

executeToolsConcurrently(cmds)

}

}

// 调用 Ollama API

func callOllama(prompt string) (string, error) {

requestBody := OllamaRequest{

Model: "qwen2.5",

Messages: []ChatMessage{

{Role: "user", Content: prompt},

},

Stream: false,

}

jsonBody, err := json.Marshal(requestBody)

if err != nil {

return "", fmt.Errorf("JSON序列化失败: %w", err)

}

log.Printf("发送的请求体: %s", jsonBody) // 新增日志记录

resp, err := http.Post(

"http://localhost:11434/api/chat",

"application/json",

bytes.NewBuffer(jsonBody),

)

if err != nil {

return "", fmt.Errorf("HTTP请求失败: %w", err)

}

defer resp.Body.Close()

log.Printf("接收到的响应状态码: %d", resp.StatusCode) // 新增日志记录

if resp.StatusCode != http.StatusOK {

body, _ := io.ReadAll(resp.Body)

return "", fmt.Errorf("API错误 (%d): %s", resp.StatusCode, body)

}

var result OllamaResponse

if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {

return "", fmt.Errorf("JSON解析失败: %w", err)

}

if result.Error != "" {

return "", fmt.Errorf("Ollama错误: %s", result.Error)

}

return result.Message.Content, nil

}

// 解析工具调用指令(示例:@tool{...})

func parseToolsCommand(content string) []map[string]string {

// 精确匹配带引号的参数

re := regexp.MustCompile(`@tool{\s*action:\s*"([^"]+)"\s*,\s*arguments:\s*"([^"]+)"\s*}`)

matches := re.FindAllStringSubmatch(content, -1)

var commands []map[string]string

for _, match := range matches {

if len(match) < 3 {

continue

}

commands = append(commands, map[string]string{

"action": match[1],

"arguments": match[2],

})

}

return commands

}

// 并发执行工具调用(修改为使用官方客户端示例)

func executeToolsConcurrently(commands []map[string]string) {

var wg sync.WaitGroup

results := make(chan string, len(commands))

for _, cmd := range commands {

wg.Add(1)

go func(command map[string]string) {

defer wg.Done()

log.Printf("准备创建 SSE 传输客户端,请求工具: %s,参数: %s", command["action"], command["arguments"])

// 创建 SSE 传输客户端

transportClient, err := transport.NewSSEClientTransport("http://127.0.0.1:8080/sse")

if err != nil {

log.Printf("创建传输客户端失败: %v", err)

results <- fmt.Sprintf("创建传输客户端失败: %v", err)

return

}

defer transportClient.Close()

log.Printf("准备初始化 MCP 客户端,请求工具: %s,参数: %s", command["action"], command["arguments"])

// 初始化 MCP 客户端

mcpClient, err := client.NewClient(transportClient)

if err != nil {

log.Printf("创建 MCP 客户端失败: %v", err)

results <- fmt.Sprintf("创建 MCP 客户端失败: %v", err)

return

}

defer mcpClient.Close()

// 构造 CallToolRequest

req := &protocol.CallToolRequest{

Name: command["action"],

RawArguments: []byte(fmt.Sprintf(`{"timezone": "%s"}`, command["arguments"])),

}

log.Printf("准备调用工具: %s,参数: %s", command["action"], command["arguments"])

// 调用工具

result, err := mcpClient.CallTool(context.Background(), req)

if err != nil {

log.Printf("调用工具失败: %v", err)

results <- fmt.Sprintf("调用工具失败: %v", err)

return

}

log.Printf("工具调用成功,开始处理结果,请求工具: %s,参数: %s", command["action"], command["arguments"])

log.Printf("工具调用返回的原始内容: %+v", result.Content)

// 处理结果

var hasResult bool

for _, content := range result.Content {

// 尝试通过反射获取 Text 字段

if reflectValue := reflect.ValueOf(content); reflectValue.Kind() == reflect.Struct {

if textField := reflectValue.FieldByName("Text"); textField.IsValid() && textField.Kind() == reflect.String {

hasResult = true

results <- textField.String()

}

}

}

if !hasResult {

log.Printf("工具调用成功,但没有有效的结果返回,请求工具: %s,参数: %s", command["action"], command["arguments"])

results <- "工具调用成功,但没有有效的结果返回"

}

}(cmd)

}

go func() {

wg.Wait()

close(results)

}()

fmt.Println("工具执行结果:")

for res := range results {

fmt.Printf("• %s\n", res)

}

}

// 初始化测试(可选)

func init() {

// 验证Ollama服务是否可用

resp, err := http.Get("http://localhost:11434")

if err != nil || resp.StatusCode != http.StatusOK {

fmt.Println(`

⚠️ 请先启动 Ollama 服务:

1. 下载安装:https://ollama.ai/

2. 启动服务:ollama serve

3. 下载模型:ollama pull qwen2.5`)

os.Exit(1)

}

}

代码中加了很多反馈信息。返回结果如下:

=== 模型原始响应 ===

看起来您想要获取美国当前的时间,但您提供的参数是指定了时区为中国上海(Asia/Shanghai)。如果您想了解美国当前的时间,特别是某个特定城市如纽约、洛杉矶等,请明确指定相应的时区。下面是一个正确的调用示例:

@tool{action: "current time", arguments: "America/New_York"}

这将返回美国东部时间的当前时间。

如果您需要其他城市的美国时间,请替换“America/New_York”为相应的时间区域标识符,例如“America/Los_Angeles”代表洛杉矶。如果您确实想查询中国的上海时间,可以使用您提供的参数:

@tool{action: "current time", arguments: "Asia/Shanghai"}

这将返回中国上海当前的时间。请根据您的具体需求选择合适的时区。

=== 检测到工具调用指令 ===

解析结果:[map[action:current time
arguments:America/New_York] map[action:current time arguments:Asia/Shanghai]]

工具执行结果:

2025/04/22 22:07:36 准备创建 SSE 传输客户端,请求工具: current time,参数: Asia/Shanghai

2025/04/22 22:07:36 准备初始化 MCP 客户端,请求工具: current time,参数: Asia/Shanghai

2025/04/22 22:07:36 准备创建 SSE 传输客户端,请求工具: current time,参数: America/New_York

2025/04/22 22:07:36 准备初始化 MCP 客户端,请求工具: current time,参数: America/New_York

2025/04/22 22:07:36 准备调用工具: current time,参数: Asia/Shanghai

2025/04/22 22:07:36 准备调用工具: current time,参数: America/New_York

2025/04/22 22:07:36 工具调用成功,开始处理结果,请求工具: current time,参数: Asia/Shanghai

2025/04/22 22:07:36 工具调用返回的原始内容: [{Annotated:{Annotations:<nil>} Type:text Text:当前时间是 2025-04-22 22:07:36.3608727 +0800 CST}]

• 当前时间是 2025-04-22 22:07:36.3608727 +0800 CST

2025/04/22 22:07:36 工具调用成功,开始处理结果,请求工具: current time,参数: America/New_York

2025/04/22 22:07:36 工具调用返回的原始内容: [{Annotated:{Annotations:<nil>} Type:text Text:当前时间是 2025-04-22 10:07:36.3613879 -0400 EDT}]

• 当前时间是 2025-04-22 10:07:36.3613879 -0400 EDT

举报