Files
map-client-vue/docs/mcp-tool-calling-example.md
2025-10-15 15:07:45 +08:00

13 KiB
Raw Blame History

MCP 工具调用完整示例

概述

本文档展示 Cherry Studio 架构风格的 MCP 工具调用流程,通过"发布小红书文章"的实际例子,详细说明 AI 如何理解用户意图、生成内容、并自动调用 MCP 工具。

实现架构

核心流程

用户输入
    ↓
获取 MCP 工具 (带服务器名称前缀)
    ↓
添加 System Prompt (指导 AI 使用工具)
    ↓
AI 理解意图 + 生成内容
    ↓
AI 调用工具 (OpenAI Function Calling)
    ↓
解析工具名称 (serverName__toolName)
    ↓
执行 MCP 工具
    ↓
工具结果返回
    ↓
AI 生成友好回复

关键创新点

  1. 工具名称前缀: serverName__toolName 格式避免多服务器工具名冲突
  2. System Prompt: 详细的工具使用指南,让 AI 理解如何创作和调用
  3. 参数自动注入: AI 根据用户意图自动生成完整参数
  4. 多轮对话: 支持工具结果继续对话

完整示例:发布小红书文章

用户输入

用户: 帮我发布小红书文章,内容是:如何制作一道酸菜鱼

步骤 1: 获取 MCP 工具

假设连接了名为 xiaohongshu 的 MCP 服务器,提供以下工具:

{
  "name": "public_content",
  "description": "发布内容到小红书平台",
  "inputSchema": {
    "type": "object",
    "properties": {
      "title": {
        "type": "string",
        "description": "文章标题,吸引眼球且相关"
      },
      "content": {
        "type": "string",
        "description": "文章正文Markdown 格式"
      },
      "tags": {
        "type": "array",
        "description": "标签列表3-5个",
        "items": { "type": "string" }
      },
      "category": {
        "type": "string",
        "description": "分类,如美食、生活、旅游等"
      }
    },
    "required": ["title", "content", "tags", "category"]
  }
}

步骤 2: 转换为 OpenAI 格式(带前缀)

// chatService.ts - convertToolsToOpenAIFormat()
{
  type: 'function',
  function: {
    name: 'xiaohongshu__public_content',  // 添加服务器前缀
    description: '发布内容到小红书平台',
    parameters: { ...inputSchema }
  }
}

步骤 3: 生成 System Prompt

// chatService.ts - createSystemPromptWithTools()
你是一个智能助手,可以使用以下工具完成任务:

 xiaohongshu__public_content
  描述: 发布内容到小红书平台
  参数:
  - title [必填]: 文章标题,吸引眼球且相关
  - content [必填]: 文章正文,Markdown 格式
  - tags [必填]: 标签列表,3-5
  - category [必填]: 分类,如美食、生活、旅游等

使用指南:
1. 当用户需要完成某个任务时,请分析哪个工具最合适
2. 如果需要发布内容(如文章、笔记等),请根据用户意图创作完整的内容
3. 为内容生成合适的标题、正文、标签等所有必需参数
4. 自动调用相应工具,将生成的内容作为参数传递
5. 根据工具执行结果,给用户友好的反馈

注意事项:
- 保持内容质量和平台特色
- 标签要相关且有吸引力
- 分类要准确
- 如果工具执行失败,给出明确的错误说明和建议

当前连接的 MCP 服务器: xiaohongshu

步骤 4: 发送请求到 LLM

// modelServiceManager.ts - sendChatRequestStream()
const request = {
  model: 'gpt-4',
  messages: [
    {
      role: 'system',
      content: '你是一个智能助手,可以使用以下工具...'  // System Prompt
    },
    {
      role: 'user',
      content: '帮我发布小红书文章,内容是:如何制作一道酸菜鱼'
    }
  ],
  tools: [
    {
      type: 'function',
      function: {
        name: 'xiaohongshu__public_content',
        description: '发布内容到小红书平台',
        parameters: { ... }
      }
    }
  ],
  tool_choice: 'auto',
  stream: true
}

步骤 5: AI 理解 + 生成内容 + 调用工具

AI 响应SSE 流式返回):

// 第一部分AI 思考过程(可选)
{
  "choices": [{
    "delta": {
      "content": "好的,我来帮你创作一篇关于酸菜鱼制作的小红书文章并发布。"
    }
  }]
}

// 第二部分:工具调用
{
  "choices": [{
    "delta": {
      "tool_calls": [
        {
          "id": "call_abc123",
          "type": "function",
          "function": {
            "name": "xiaohongshu__public_content",
            "arguments": {
              "title": "🐟 超详细家常酸菜鱼做法10分钟学会",
              "content": "# 酸菜鱼制作教程\n\n## 所需食材\n- 草鱼1条(约1.5kg)\n- 酸菜200g\n- 姜片、蒜瓣适量...\n\n## 制作步骤\n\n### 1. 处理鱼肉\n...",
              "tags": ["美食教程", "酸菜鱼", "家常菜", "川菜", "烹饪技巧"],
              "category": "美食"
            }
          }
        }
      ]
    },
    "finish_reason": "tool_calls"
  }]
}

步骤 6: 解析工具名称

// chatService.ts - executeToolCalls()
const fullFunctionName = 'xiaohongshu__public_content'
const parts = fullFunctionName.split('__')

if (parts.length !== 2) {
  console.error('工具名称格式错误')
  return
}

const [serverName, toolName] = parts
// serverName = 'xiaohongshu'
// toolName = 'public_content'

步骤 7: 执行 MCP 工具

// MCPClientService.ts - callTool()
const result = await mcpClient.callTool(
  'xiaohongshu',  // serverId
  'public_content',  // toolName (不带前缀)
  {
    title: '🐟 超详细家常酸菜鱼做法10分钟学会',
    content: '# 酸菜鱼制作教程\n\n## 所需食材...',
    tags: ['美食教程', '酸菜鱼', '家常菜', '川菜', '烹饪技巧'],
    category: '美食'
  }
)

// MCP Server 响应:
{
  "success": true,
  "article_id": "xhs_2024_001",
  "url": "https://www.xiaohongshu.com/discovery/item/xhs_2024_001",
  "views": 0,
  "likes": 0
}

步骤 8: 工具结果返回 AI

// chatService.ts - 继续对话
const messages = [
  {
    role: 'system',
    content: '...'  // System Prompt
  },
  {
    role: 'user',
    content: '帮我发布小红书文章,内容是:如何制作一道酸菜鱼'
  },
  {
    role: 'assistant',
    tool_calls: [{
      id: 'call_abc123',
      type: 'function',
      function: {
        name: 'xiaohongshu__public_content',
        arguments: '{"title":"🐟 超详细家常酸菜鱼做法10分钟学会",...}'
      }
    }]
  },
  {
    role: 'tool',
    tool_call_id: 'call_abc123',
    name: 'xiaohongshu__public_content',  // 保持原名称(带前缀)
    content: JSON.stringify({
      success: true,
      article_id: 'xhs_2024_001',
      url: 'https://www.xiaohongshu.com/discovery/item/xhs_2024_001'
    })
  }
]

// 再次调用 LLM

步骤 9: AI 生成友好回复

{
  "choices": [{
    "delta": {
      "content": "✅ 文章已成功发布到小红书!\n\n📝 标题:🐟 超详细家常酸菜鱼做法10分钟学会\n🔗 链接https://www.xiaohongshu.com/discovery/item/xhs_2024_001\n\n你的酸菜鱼教程已经上线啦记得定期查看浏览和点赞数据哦~ 🎉"
    },
    "finish_reason": "stop"
  }]
}

关键代码实现

1. System Prompt 生成 (chatService.ts)

private createSystemPromptWithTools(tools: any[], serverName: string): string {
  const toolDescriptions = tools.map(tool => {
    const func = tool.function
    const params = func.parameters?.properties || {}
    const required = func.parameters?.required || []
    
    const paramDesc = Object.entries(params).map(([name, schema]: [string, any]) => {
      const isRequired = required.includes(name)
      const requiredMark = isRequired ? '[必填]' : '[可选]'
      return `  - ${name} ${requiredMark}: ${schema.description || schema.type}`
    }).join('\n')
    
    return `• ${func.name}\n  描述: ${func.description}\n  参数:\n${paramDesc || '  无参数'}`
  }).join('\n\n')

  return `你是一个智能助手,可以使用以下工具完成任务:

${toolDescriptions}

使用指南:
1. 当用户需要完成某个任务时,请分析哪个工具最合适
2. 如果需要发布内容(如文章、笔记等),请根据用户意图创作完整的内容
3. 为内容生成合适的标题、正文、标签等所有必需参数
4. 自动调用相应工具,将生成的内容作为参数传递
5. 根据工具执行结果,给用户友好的反馈

注意事项:
- 保持内容质量和平台特色
- 标签要相关且有吸引力
- 分类要准确
- 如果工具执行失败,给出明确的错误说明和建议

当前连接的 MCP 服务器: ${serverName}`
}

2. 工具名称转换 (chatService.ts)

private convertToolsToOpenAIFormat(mcpTools: any[], serverName: string): any[] {
  return mcpTools.map(tool => ({
    type: 'function',
    function: {
      name: `${serverName}__${tool.name}`,  // 添加服务器前缀
      description: tool.description || '',
      parameters: tool.inputSchema || {
        type: 'object',
        properties: {},
        required: []
      }
    }
  }))
}

3. 工具名称解析 (chatService.ts)

private async executeToolCalls(
  conversation: Conversation,
  toolCalls: any[],
  model: string | undefined,
  onChunk: (chunk: string) => void,
  mcpServerId: string
): Promise<void> {
  for (const toolCall of toolCalls) {
    const fullFunctionName = toolCall.function.name
    const args = JSON.parse(toolCall.function.arguments)
    
    console.log('🔧 执行工具调用:', {
      fullName: fullFunctionName,
      id: toolCall.id,
      arguments: args
    })
    
    // 解析 serverName__toolName 格式
    const parts = fullFunctionName.split('__')
    if (parts.length !== 2) {
      console.error('❌ 工具名称格式错误,应为 serverName__toolName:', fullFunctionName)
      continue
    }
    
    const toolName = parts[1]
    console.log('🎯 提取工具名称:', toolName)
    
    try {
      // 调用 MCP 工具(使用不带前缀的工具名)
      const result = await this.mcpClient.callTool(
        mcpServerId,
        toolName,  // 使用原始工具名
        args
      )
      
      // 添加工具结果到消息历史(使用完整名称)
      const toolResultMessage: Message = {
        id: Date.now().toString(),
        role: 'tool',
        content: JSON.stringify(result),
        timestamp: new Date(),
        status: 'success',
        toolCallId: toolCall.id,
        toolName: fullFunctionName  // 保持完整名称
      }
      
      conversation.messages.push(toolResultMessage)
      this.saveConversations()
      
      // 继续对话
      await this.callModelStream(conversation, model, onChunk, mcpServerId)
      
    } catch (error) {
      console.error('❌ 工具执行失败:', error)
      // 错误处理...
    }
  }
}

测试场景

场景 1: 发布文章

用户: 帮我发布一篇关于"春季穿搭指南"的小红书笔记

AI 处理:
1. 识别需要使用 xiaohongshu__public_content 工具
2. 创作完整文章(标题、正文、标签、分类)
3. 调用工具发布
4. 返回发布结果和链接

场景 2: 多工具选择

假设有多个 MCP 服务器:

- xiaohongshu__public_content (发布小红书)
- weibo__post_status (发布微博)
- notion__create_page (创建 Notion 页面)
用户: 帮我把这篇文章同时发到小红书和微博

AI 处理:
1. 理解需要两个工具
2. 为小红书创作合适格式的内容
3. 为微博创作合适格式的内容(字数限制)
4. 依次调用两个工具
5. 返回两个平台的发布结果

场景 3: 错误处理

用户: 发布文章到小红书,标题是"测试"

AI 处理:
1. 识别内容不完整
2. 提示用户补充正文内容
3. 等待用户补充后再调用工具

优势总结

1. 智能参数生成

  • AI 自动创作内容,无需用户逐一填写参数
  • 符合平台特色(小红书风格 vs 微博风格)

2. 工具名称隔离

  • serverName__toolName 避免多服务器冲突
  • 清晰的工具来源

3. 友好的用户体验

  • 自然语言输入:"帮我发布..."
  • 自动处理所有技术细节
  • 结果友好呈现

4. 可扩展性

  • 轻松添加新 MCP 服务器
  • 支持任意数量和类型的工具
  • System Prompt 自动生成

5. 多轮对话支持

  • 工具结果自动传回 AI
  • 可以追问、修改、重试

对比 Cherry Studio

特性 mcp-client-vue Cherry Studio
工具名称格式 serverName__toolName serverName__toolName
System Prompt 自动生成 自动生成
参数自动注入 AI 生成 AI 生成
多轮对话 完整支持 完整支持
流式响应 SSE 真流式 真流式
错误处理 完善 完善
UI 界面 Vue 3 + Naive UI Electron + React

下一步优化

  1. 批量工具调用: 同时调用多个工具
  2. 工具调用历史: 记录和展示工具调用日志
  3. 工具执行超时: 防止长时间阻塞
  4. 工具权限控制: 敏感操作需要用户确认
  5. 工具调用缓存: 避免重复调用

相关文件

  • /web/src/services/chatService.ts - 核心服务
  • /web/src/services/modelServiceManager.ts - 模型管理
  • /web/src/services/MCPClientService.ts - MCP 客户端
  • /web/src/components/Chat/ChatLayout.vue - UI 组件

版本: v1.0.2+
更新时间: 2024-01
作者: MCP Client Vue Team