与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