(原) Go结合Ollama调用MCP服务

原创文章,请后转载,并注明出处。

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

交互使用了本地Ollama qwen2.5。MCP服务是一个简单的时区时间功能。类似于此文章中实现的SSE时间服务。

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)
	}
}
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

相关文章