ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

小智AI的MCP交互全流程拆解:从ESP32到WebSocket工具调用

小智AI的MCP交互全流程拆解:从ESP32到WebSocket工具调用 1. 小智AI的MCP交互链路到底长什么样小智AI在ESP32上跑MCP工具调用本质是把设备本身当成一个能力提供方设备通过WebSocket连上小智AI服务器把自己的工具清单报上去用户说一句话服务器侧的模型决定调哪个工具、传什么参数再把调用请求顺着同一条WebSocket推回设备设备本地执行完把结果回传最后模型根据结果生成语音回复。整条链路里ESP32既是音频终端也是MCP Server。这套东西适合谁如果你手上有一块ESP32-S3开发板跑过小智AI的固件想让把音量调到80查一下设备状态这类指令真正落到硬件动作上而不是只停留在聊天层面那MCP就是那条把语言变成操作的通道。它和外部MCP服务器的最大区别在于工具执行发生在设备本地省掉了设备→服务器→外部MCP→服务器→设备的额外网络往返实测能省下150到300毫秒。我试过在本地把这条链路完整跑通一遍踩过的坑主要集中在工具注册的JSON-RPC格式和WebSocket消息分帧上。下面按连接建立→工具注册→语音触发→工具调用→结果回传→语音播报的顺序把每一步的协议细节和可复制配置拆开讲最后给一套能直接对着抓包日志排查的验证方法。2. TaoToken 前置准备拿到模型侧调用凭证小智AI服务器侧的意图理解和工具决策依赖大模型能力如果你要自建或替换这一层需要先准备好模型调用的API Key。TaoToken在这里的角色是提供统一的模型接入入口让你不用为每个模型单独对接一套鉴权。操作路径很直接打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。API的Base URL统一用 https://taotoken.net/api 注意这个地址后面不要加UTM参数否则部分SDK会把查询串拼进请求路径导致404。拿到Key之后建议先在模型对话页面 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 里发一条测试消息确认Key有效、额度正常。这一步别跳过我见过太多人把Key填进ESP32固件后调不通最后发现是Key本身没激活。如果你打算长期跑编码类或Agent类任务比如让模型持续生成工具调用决策可以看下Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按套餐走比按量计费更可控。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有针对不同语言SDK的示例ESP32侧用HTTP客户端调REST接口时可以直接参考。需要说明的是TaoToken只负责模型调用这一层ESP32和小智AI服务器之间的WebSocket连接、MCP协议握手是设备固件和服务器各自实现的两者不要混在一起理解。3. 可复制的MCP配置骨架与WebSocket联调3.1 连接建立设备侧WebSocket初始化ESP32启动后先完成音频编解码、显示等外设初始化然后注册MCP工具最后发起WebSocket连接。连接地址形如// 设备侧连接小智AI服务器 void Application::Initialize() { // 外设初始化省略 #if CONFIG_IOT_PROTOCOL_MCP McpServer::GetInstance().AddCommonTools(); // 注册工具 #endif protocol_-Connect(); // 内部发起WebSocket连接 }WebSocket的URL结构是wss://api.xiaozhi.me/mcp/device/{device_id}其中device_id是设备唯一标识。连接建立后服务器会主动下发tools/list请求来拉取设备能力这一步不需要设备主动发起。3.2 工具注册tools/list 的请求与响应服务器发来的请求是标准JSON-RPC 2.0格式{ jsonrpc: 2.0, id: 1, method: tools/list, params: {} }设备侧在McpServer::HandleRequest里解析method字段命中tools/list就构造工具数组返回。每个工具必须包含name、description、inputSchema三个字段其中inputSchema是JSON Schema用来告诉模型参数类型和取值范围if (strcmp(method-valuestring, tools/list) 0) { cJSON* response cJSON_CreateObject(); cJSON* result cJSON_CreateObject(); cJSON* tools cJSON_CreateArray(); cJSON* volume_tool cJSON_CreateObject(); cJSON_AddStringToObject(volume_tool, name, self.audio_speaker.set_volume); cJSON_AddStringToObject(volume_tool, description, Set the volume of the audio speaker. If the current volume is unknown, you must call self.get_device_status tool first and then call this tool.); cJSON* input_schema cJSON_CreateObject(); cJSON_AddStringToObject(input_schema, type, object); cJSON* properties cJSON_CreateObject(); cJSON* volume_prop cJSON_CreateObject(); cJSON_AddStringToObject(volume_prop, type, integer); cJSON_AddNumberToObject(volume_prop, minimum, 0); cJSON_AddNumberToObject(volume_prop, maximum, 100); cJSON_AddItemToObject(properties, volume, volume_prop); cJSON_AddItemToObject(input_schema, properties, properties); cJSON_AddItemToObject(volume_tool, inputSchema, input_schema); cJSON_AddItemToArray(tools, volume_tool); cJSON_AddItemToObject(result, tools, tools); cJSON_AddItemToObject(response, result, result); char* response_str cJSON_Print(response); protocol_-SendMCPResponse(response_str); free(response_str); cJSON_Delete(response); }设备返回的工具列表长这样注意required字段要显式声明否则模型可能生成缺参数的调用{ jsonrpc: 2.0, id: 1, result: { tools: [ { name: self.get_device_status, description: Provides the real-time information of the device..., inputSchema: {type: object, properties: {}} }, { name: self.audio_speaker.set_volume, description: Set the volume of the audio speaker..., inputSchema: { type: object, properties: { volume: {type: integer, minimum: 0, maximum: 100} }, required: [volume] } } ] } }3.3 工具调用tools/call 的完整往返用户说把音量调到80ESP32采集音频、Opus编码后发给服务器服务器做ASR得到文本模型分析意图后生成工具调用请求{ jsonrpc: 2.0, id: 2, method: tools/call, params: { name: self.audio_speaker.set_volume, arguments: {volume: 80} } }设备侧命中tools/call分支取出name和arguments执行本地动作if (strcmp(method-valuestring, tools/call) 0) { auto params cJSON_GetObjectItem(json, params); auto tool_name cJSON_GetObjectItem(params, name); auto arguments cJSON_GetObjectItem(params, arguments); if (strcmp(tool_name-valuestring, self.audio_speaker.set_volume) 0) { auto volume cJSON_GetObjectItem(arguments, volume); int volume_value volume-valueint; auto board Board::GetInstance(); auto codec board.GetAudioCodec(); codec-SetOutputVolume(volume_value); auto display board.GetDisplay(); if (display) { display-ShowNotification(音量: std::to_string(volume_value)); } cJSON* response cJSON_CreateObject(); cJSON* result cJSON_CreateObject(); cJSON_AddBoolToObject(result, success, true); cJSON_AddNumberToObject(result, volume, volume_value); cJSON_AddStringToObject(result, message, 音量设置成功); cJSON_AddItemToObject(response, result, result); char* response_str cJSON_Print(response); protocol_-SendMCPResponse(response_str); free(response_str); cJSON_Delete(response); } }回传结果{ jsonrpc: 2.0, id: 2, result: { success: true, volume: 80, message: 音量设置成功 } }服务器拿到结果后生成回复文本好的已将音量调整到80TTS合成音频推回设备设备在OnIncomingAudio里入队OnAudioOutput里Opus解码后写I2S播放。3.4 协议层与异步处理MCP消息的收发统一走Protocol类避免在业务代码里直接操作WebSocketclass Protocol { public: void SendMCPResponse(const std::string response) { websocket_-send(response); } void OnMCPRequest(const std::string request) { McpServer::GetInstance().HandleRequest(request); } };工具执行可能涉及I2C、I2S等阻塞操作建议放到后台任务里别卡住WebSocket接收线程void McpServer::HandleRequest(const std::string request) { background_task_-Schedule([this, request]() { ProcessMCPRequest(request); }); }4. 验证请求与成功结果联调时不要一上来就对着麦克风喊先用WebSocket客户端手动发JSON-RPC把协议层单独验证通过。第一步用wscat或Postman的WebSocket功能连上wss://api.xiaozhi.me/mcp/device/{device_id}连接成功后服务器会自动发tools/list请求。你手动回一条工具列表观察服务器是否继续下发其他消息。第二步手动构造tools/call请求发过去看设备串口日志里是否打印出工具名和参数。如果设备侧有显示屏应该能看到音量: 80的通知。第三步检查设备回传的JSON里id是否和请求一致。JSON-RPC靠id做请求响应配对id对不上服务器会一直等表现为设备执行了但AI没反应。成功的结果是串口日志依次出现tools/list handled、tools/call: self.audio_speaker.set_volume、volume set to 80同时扬声器音量实际变化服务器侧返回TTS音频设备播放好的已将音量调整到80。整条链路延迟实测在500到1250毫秒之间其中ASR占200到500毫秒模型决策100到300毫秒本地工具执行只要10到50毫秒。5. 本篇常见错误排查5.1 tools/list 返回后服务器无响应最常见的原因是JSON里少了jsonrpc字段或值不是2.0。有些cJSON示例代码只加了result忘了顶层协议版本服务器解析直接丢弃。检查cJSON_AddStringToObject(response, jsonrpc, 2.0)是否在构造response时第一时间加上。另一个原因是inputSchema写成了input_schema或parameters。MCP规范里字段名是驼峰inputSchema大小写敏感写错模型就看不到参数定义自然不会发起调用。5.2 工具调用请求到了但设备不执行先看method字段比对是不是用了strcmp且字符串完全一致。tools/call中间是斜杠不是点写成tools.call就永远命中不了。再看arguments的解析。如果模型传的是{volume: 80}字符串而不是数字valueint会取到0。稳妥做法是用cJSON_IsNumber判断类型字符串则用atoi转换并在工具描述里明确写type: integer。5.3 回传结果后AI不生成语音检查回传JSON的id是否和请求的id一致。服务器用id做异步配对id不一致会认为调用超时。另外result必须是对象不能直接是布尔值或字符串否则服务器解析失败。如果设备侧用了后台任务异步处理注意request字符串的生命周期。lambda捕获的是引用还是拷贝捕获引用的话函数返回后request已析构后台任务拿到的是野指针。改成按值捕获[this, request]。5.4 WebSocket频繁断连ESP32的WebSocket心跳间隔要小于服务器超时时间。如果服务器30秒无数据就断设备侧心跳设成20秒。另外注意发送大块TTS音频时不要和MCP消息共用同一个发送缓冲容易互相阻塞。建议音频走独立通道MCP消息走控制通道。6. 继续深入的方向把上面这套跑通之后你可以往两个方向扩展。一是增加工具数量比如self.get_device_status返回电量、WiFi信号、固件版本让模型在调音量前先查状态。二是把工具执行结果结构化除了success和message把实际生效的值也带上方便模型做二次判断。模型侧如果要做更复杂的多轮工具编排比如先查状态再调音量最后播报建议用支持function calling的模型并在系统提示里写清楚工具调用顺序约束。TaoToken的模型对话入口 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 可以直接测这类多轮决策接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有function calling的请求示例。长期跑Agent类任务的话Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 的额度模型更适合持续调用。最后提醒一句MCP工具描述里的文字会直接影响模型的调用决策description写得越具体模型选错工具的概率越低。我踩过的坑是description写得太笼统模型把调音量理解成调亮度后来在描述里加了audio speaker限定词才稳定。
返回列表