文章目录前言1. 先搞清楚谁在给谁递工具1.1 工具不是出厂自带1.2 同一个工具名不同的灵魂1.3 列表里没有工具先别骂模型1.4 只看第一页不算看完了2. 描述里写先查状态执行路径可没查2.1 参数检查倒是真的在2.2 Schema 写了 integer不等于严格拒绝小数3. 一次音量调用真正走过哪些地方3.1 true 是文本不是布尔3.2 构造结果 ≠ 远端收到4. true 到底证明什么4.1 重启工具true 只是我安排了5. user_only看不见不等于禁止6. 用一张表把接通和效果连起来7. 错误速查卡P.S. 无意间发现了一个巨牛的人工智能教程非常通俗易懂对AI感兴趣的朋友强烈推荐去看看 传送门https://blog.csdn.net/qq_34419312前言对着一台小智 ESP32 说声音调小一点服务端那边回手就是一个text: true。这个瞬间你要不要给用户回一句好的音量已经调小了建议先把手从回车键上挪开。这个 true 的完整意思是我一个回调函数成功把SetOutputVolume()叫过来人家干完活回去了我顺利走到return true。至于扬声器到底小没小——老实说没人问过我。设备接进 Agent 这条链路最容易被压缩掉的就是这一段工具出现在列表里、请求被接受、回调执行完、物理效果真的发生这是四件事不是一件事的四种说法。就像他下班了“他到家了”“他洗完澡了”“他发朋友圈了”中间任何一环断了你都只能说清自己亲眼看到的那一环。底下所有结论都钉在78/xiaozhi-esp32固定提交6240b777aaa2bc0cad43a4ce25b30de23f36ad00上核验日期 2026-09-11。证据来自官方客户端源码示例请求是照实现写的不是抓包更不是我拿设备实测——我先招了免得你看到后面热血沸腾然后找我索赔。1. 先搞清楚谁在给谁递工具设备没有听懂你那句话。它收到的是结构化调用然后去执行一个注册好的 C 回调。换个说法你不是在跟设备聊天你是在给设备下订单设备照着订单执行。执行到哪一步看订单内容也看门店实力。这条链路里McpServer是工具提供方后端是 MCP 客户端。这个角色划分值得画个重点ESP32 不是听懂人话、再自己决定改哪个寄存器的大聪明。它是接到一张写着音量设为 40的工单、转身就干的打工仔。1.1 工具不是出厂自带Application启动时调用AddCommonTools()和AddUserOnlyTools()一个工具注册同时绑定名称、描述、参数和回调。音量工具叫self.audio_speaker.set_volume参数volume是整数范围 0 到 100。注意这个范围说的是音量参数范围不是分贝数。它没承诺调到 40 就真的小了一半。分贝是声学的事不是工单的事。至于 101 想干嘛别急2.1 有它好看的。1.2 同一个工具名不同的灵魂固定版本里bread-compact-wifi 这块板子根据编译配置选NoAudioCodecSimplex或NoAudioCodecDuplex。工具名是同一个底层实现可能完全不同。就像老张这个称呼你在物业喊和在派出所喊走出来的人大概率不是同一个。1.3 列表里没有工具先别骂模型AddCommonTools()只有拿到非空 backlight 时才注册亮度工具主题和摄像头工具还缩在HAVE_LVGL编译条件里。接入后发现少一个工具第一反应应该是这固件到底注册了什么而不是模型是不是没听懂。模型你礼貌吗我连工具都看不见你让我调用空气1.4 只看第一页不算看完了tools/list带着nextCursor就说明还有下一页。这个分页来自固件构造返回消息的大小控制不是设备最多只能有 N 个工具的硬上限。分页只是消息放不下了不是设备看不起你。只看第一页就宣布工具不存在跟只看了第一集就宣布这剧烂尾了一样草率。2. 描述里写先查状态执行路径可没查音量工具的注册描述长这样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.翻译当前音量未知时得先查self.get_device_status再调这个工具。对调小一点这种需求这很有意义——工具收的是目标值不是再小一点这种相对量。假设查到 50决定设成 40这是调用方可以做的推理。50 和 40 是我随手举的例子不是固件内置的固定步长。但回调长这样[board](constPropertyListproperties)-ReturnValue{autocodecboard.GetAudioCodec();codec-SetOutputVolume(properties[volume].valueint());returntrue;}它没检查你前面查过状态了吗也没顺手帮你查一次。描述是建议回调是执行。如果后端真需要这个顺序得在编排和调用记录里自己验证——不能因为描述里有个 must就以为设备内置了一个纪律委员。就像外卖平台的请确认收货地址它只是温馨提示不会替你在下单前检查地址。你填错了它照送送到哪算哪。2.1 参数检查倒是真的在DoToolCall()按注册参数逐个读请求必需参数缺了就报错整数值走上下界检查。你传 101会走进超过最大值的异常分支回调根本不会执行。说明一下这是源码分支推演我没真跑这个请求——问就是怕烧板子板子贵。2.2 “Schema 写了 integer不等于严格拒绝小数”这个版本对整数参数先cJSON_IsNumber()再读valueint。所以别把声明里写了 integer当成实现会严格拒绝所有小数。评估自定义工具参数声明和实际解析代码要一起看——声明是 PPT代码是现实。3. 一次音量调用真正走过哪些地方假设后端完成了发现决定把音量设为 40。发给设备的内层请求长这样{jsonrpc:2.0,id:17,method:tools/call,params:{name:self.audio_speaker.set_volume,arguments:{volume:40}}}这是内层 payload不是完整的 WebSocket 或 MQTT 消息。小智协议外面还套了一层type: mcpApplication收到后把 payload 交给McpServer::ParseMessage()。解析器检查 JSON-RPC 版本、方法、参数这个版本还要求请求 id 必须是数字——id 不是数字连门都进不去。进DoToolCall()先按名字找工具再构造校验参数最后把调用安排到应用主任务app.Schedule([this,id,tool_iter,argumentsstd::move(arguments)](){try{ReplyResult(id,(*tool_iter)-Call(arguments));}catch(conststd::exceptione){ESP_LOGE(TAG,tools/call: %s,e.what());ReplyError(id,e.what());}});注意这段是把调用丢进主任务队列没给每个工具开独立线程。主循环取出排队的任务再执行。所以读自定义工具时得继续往下看回调到底干了啥是同步干完还是又安排了一件稍后再说的事。这个区别后面重启工具会教做人。3.1 true 是文本不是布尔McpTool::Call()拿到回调返回的布尔值后把它编码成文本内容所以结果里是字符串true不是顶层 JSON 布尔还带着isError: false。布尔 false 也会被编成false同时保留isError: false。结论业务返回值是业务的事包装层错误标记是包装层的事。收到false别急着喊工具出错了先看isError——它可能只是平静地告诉你这次业务结果是假的。3.2 构造结果 ≠ 远端收到ReplyResult()接着调Application::SendMcpMessage()发送本身也是安排到主任务的。“结果构造好了和远端收到了之间隔着一条队列。就像隔着一条河你喊听到了吗”回声还没回来。联调时要把 id17 的请求和远端实际收到的同 id 响应配对。这种网络实测我没有你要测了记得告诉我结果。4. true 到底证明什么音量回调里的 true至多说明它调完SetOutputVolume()后正常走到了 return。而SetOutputVolume()是虚函数——虚函数翻译成人话就是基类说具体怎么干子类自己看着办。你只看基类就等于只听部门经理说放心我们很专业具体干活的到底是谁你还没见着。官方基类实现更新output_volume_并写入设置。相应地WifiBoard::GetDeviceStatusJson()里音量状态来自codec-output_volume()。这给联调提供一个便宜的回读调用完再查一次设备状态看看软件记录值符不符合预期。但软件回读不是声学测量。它证明不了扬声器接线、功放状态、声学增益更证明不了你的耳朵。软件回读回答我设成了 40声学测量才回答你的耳朵觉得小没小——前者是考勤打卡后者是绩效面谈别混。4.1 重启工具true 只是我安排了self.reboot的回调先app.Schedule()安排任务等一秒再app.Reboot()然后回调自己返回 true。这个 true 的意思是我已安排重启工作不是设备已经重启完了。这相当于领导跟你说这事我安排好了——你该等的不是这句话是事办完的证据。重启的完成证据是设备重新上线、跑到预期状态而不是一个成功文本。等一个 true 就想宣布重启完成跟等一句好的收到就宣布快递已经送到你手里了是一回事。而且别急着推断调用方一定先收到 true 再看设备重启。响应发送也要排队重启工作和发送工作谁先谁后、连接还送不送得出结果都得沿调度路径验证。5. user_only看不见不等于禁止有些硬件动作不该让模型随便碰。固定版本把重启、升级固件注册为 user-only工具定义里加audience: [user]。默认工具列表会跳过if(!list_user_only_tools(*it)-user_only()){it;continue;}tools/list带withUserTools: true时会包含它们。但再看DoToolCall()查找条件是工具名相等方法内部没有依据user_only()再拦一次。注意把结论收紧这只能说明到达这个分发函数的调用没在这儿被拦截。它不能证明外头谁都能连上设备也不能证明云端没有身份校验、界面确认或调用限制——那些要各自的服务端和接入证据说了算。对想加电机、继电器的开发者直接结论别把默认不给模型列出写成设备强制禁止模型调用。动作需要操作者确认或硬件互锁就指出检查在哪执行、什么条件会拒绝再验证请求确实经过那里。本文源码里没有新增设备的实现更没验证过电机继电器动作安全——你要真上继电器先想想家里还有没有别的家具。6. 用一张表把接通和效果连起来拿音量工具当首个接入样例可以留这么一张记录。它是照前面源码写的验收模板还没执行每行回答一个具体问题检查位置需要留下什么能回答什么实际固件与发现板卡、版本、完整 tools/list、音量参数定义当前设备是否注册并公开了目标工具调用方决策用户要求、此前状态、目标音量、请求 id调小一点如何被解释成一个目标值设备参数与回调合法值和越界值的处理记录、实际 codec 类型请求是否到达预期实现越界是否在执行前被拒绝响应与软件回读同 id 的实际响应、后续状态中的音量值回调报告了什么设备软件状态是否符合预期产品要求的效果在指定板卡和播放条件下观察实际输出未验证时明确留空软件设置是否达到了这次需求的物理效果排查逻辑也顺了工具没出现在完整列表回注册和板卡条件越界参数还执行回解析与检查true 返回了但软件状态不符查实际 codec 实现和状态读取路径软件值正确但听感不对去看硬件输出条件。每一步失败都有明确的下一站。以后换硬件工具先把回调返回时已经完成了什么和什么证据算动作完成这两格重写一遍。这两格写清楚Agent 才知道什么时候能说办完了什么时候只能说我交给设备了。7. 错误速查卡症状根因定位修复工具返回text: true就向用户回复音量已调小true 只到SetOutputVolume()之后的 return 语句看 mcp_server.cc L60-64 注册回调区分回调完成与硬件动作完成按需追加软件回读 / 声学验证看到完整列表里没有目标工具就归因于模型没理解工具受编译条件与板卡对象状态影响看 mcp_server.cc L33-78 注册条件先查完整tools/list含nextCursor分页再核对实际固件只看第一页tools/list就宣布工具不存在列表分页由消息大小控制看 mcp_server.cc L33-78 nextCursor 返回收到nextCursor继续请求下一页bread-compact-wifi板卡声学输出符合预期同一工具名覆盖不同 codec 实现看 compact_wifi_board.cc L172-183 codec 选择按实际 board 与编译配置核对同工具名 ≠ 相同声学输出描述里写must call ... first就当设备强制顺序描述是建议强制由外部编排/调用记录验证看 mcp_server.cc L128-169 回调实际行为描述里 must 不构成设备强制按需求另加检查整数 101 直接执行了回调DoToolCall按参数上下界提前拒看 mcp_server.cc L361-571 解析路径越界走异常分支回调未执行先记录请求 id 参数值Schema 写 integer就严格拒绝所有小数当前实现先cJSON_IsNumber()再读valueint看 mcp_server.cc L361-571 解析代码自定义工具评估要同时读参数声明和解析代码业务返回false推出工具出错业务返回值与包装层错误标记不同看 mcp_server.h L290-291 / L303-304 包装检查isError字段false也可isError: falsetext: true被当作 JSON 布尔布尔被编码为文本字符串看 mcp_server.h L290-291 编码解析时把text当字符串读不要用 JSON 布尔解码写完响应就以为远端已收到ReplyResult也走app.Schedule队列看 application.cc L676-680 / L1316-1326 发送路径用同 id 请求-响应配对验证网络回调返回值就认定动作完成回调可能只安排了后续工作看 mcp_server.cc L128-169 回调实现区分同步做完与安排了一项稍后工作看后续状态self.reboot返回 true 就报告设备已重启回调只安排 1 秒后的app.Reboot()任务看 reboot 工具注册回调完成证据应是设备重新上线 预期运行状态user_only工具默认列表里看不到就视为已禁止DoToolCall内部不再做拦截看 mcp_server.cc L361-571 分发函数仅说明到达该函数的调用没被拦截强制需在编排/服务端默认列表里不出现 “云端已做限制”列表过滤只控制模型可见性看 mcp_server.cc L33-78 列表构造不能用列表可见性代替云端身份校验 / 操作者确认加电机/继电器工具直接复用user_only就行硬件互锁应在另一层执行看 mcp_server.cc L361-571 内部指出互锁具体在哪里、条件如何再验证请求经过仅看回调成功就宣布 Agent 完成缺软件回读与效果验证看 audio_codec.cc L40-46 / wifi_board.cc L303-308 状态读取增加 5 行验收表中的响应与软件回读和产品要求的效果两行以上来自一个被 true 骗过的过来人。别问我是怎么知道的——问就是我在等设备重新上线。P.S. 无意间发现了一个巨牛的人工智能教程非常通俗易懂对AI感兴趣的朋友强烈推荐去看看传送门https://blog.csdn.net/qq_34419312