小智首批设备怎样放量?从接入名单到故障后的恢复决定
TL;DR
- 场景 :办公室 10 台同型号设备准备接入同一个第三方服务端。最直觉的做法是把这 10 个设备编号填进
allowed_devices,觉得这就圈定了首批体验者。 - 结论 :
allowed_devices在这条实现里表达的是认证豁免,不是试点筛选。一个字段叫"白名单",不代表它负责"只准这十台进入"。试点准入、固件启动自检、业务放量是三件不同的事。批量恢复需要先固定 F0/C0、再单变量替换,而不是把"恢复旧配置"与"重刷旧固件"合并成随意碰运气。 - 产出 :一份可填写的"两台到十台"放量演练表(8 行决策项)、6 条"先定位再恢复"决策图、20 行错误速查卡,覆盖服务端
6afc54a与客户端cf2024ff两个固定 commit、ESP-IDF v6.0.1 回滚语义边界。
平台版本:小智服务端
xinnan-tech/xiaozhi-esp32-servercommit6afc54a17def47578a4b3efc4680873689d3168b;小智客户端78/xiaozhi-esp32commitcf2024ff36b3af1d651a68ee582cbb91986ec4d4;ESP-IDF v6.0.1。资料检查日期 2026-09-16。本文未启动服务器、未调用模型、未烧录固件、未做真实断网测试。
你已经让一台小智设备完成了语音问答,准备把同一套配置发给办公室里的十个人。最直觉的做法是把十个设备编号填进 allowed_devices,再打开鉴权,觉得这就圈定了首批体验者。但在本文核验的第三方服务端里,这个字段的作用恰好值得停下来细读:命中的设备会跳过 token 校验;没有命中的设备,只要 token 有效,仍然可能通过认证。
这个差别直接改变放量动作。一个字段叫"白名单",不代表它负责"只准这十台进入"。如果连谁能进入这次试点都没有被控制,随后统计的成功率、故障数量和回退范围也就失去了确定对象。
本文围绕一个教学场景展开:让少量同型号设备体验普通语音问答,故障后能恢复到已知可用的组合,再决定是否增加设备。案例里的设备数量、等待时间和判定规则是作者填写的演练方案,不是运行结果。服务端依据 xinnan-tech/xiaozhi-esp32-server 固定提交 6afc54a17def47578a4b3efc4680873689d3168b;它是第三方开源后端,不能用来推断虾哥官方托管服务的内部实现。设备端的升级与恢复机制另按官方客户端源码核验。两份公开版本是分别核验的,本文没有证明它们已经实机联调兼容,也没有启动服务器、调用模型、烧录固件或做真实断网测试。
先确定这批设备凭什么进入
服务端 _handle_connection() 在创建 ConnectionHandler 前调用 _handle_auth()。认证失败时发送提示并关闭 WebSocket;成功后才创建负责会话状态、线程池和音频队列的连接处理器。顺序清楚,但认证函数内部还有一个分支。1
python
if self.allowed_devices and device_id in self.allowed_devices:
# 如果属于白名单内的设备,不校验token,直接放行
return
这段是固定源码的连续摘录,不是推荐配置。其外层还受 self.auth_enable 控制;未启用认证时,不进入这一整段 token 检查。启用后,不在免校验名单内的设备走 Authorization: Bearer ... 路径,并调用 verify_token()。因此,allowed_devices 在这条入口上表达的是认证豁免,不能单独承担试点设备筛选。1

接下来应该分开回答两个问题:这条连接有没有持有与当前身份相符的凭证;即使凭证有效,这台设备是否属于本轮允许进入的群体。前者是认证,后者是试点准入。设备自行报出的 ID 也不等于硬件可信身份,不能因此宣称具备防克隆能力。小范围试点可以用受控网络、入口侧规则或已经实现并验证的设备分组约束后者。选哪个取决于现有部署条件,没必要为十台设备先搭一个新管理平台。但不能把额外准入能力写成这个字段已经提供的功能。
案例选择最简单的边界:先只在隔离的体验网络中允许两台自有设备接入,同一批服务实例不开放给其他网络;认证打开,免校验名单保持空。网络隔离由部署人员实际配置并检查,本文没有执行这项操作。两台都完成指定试验后,再把另外八台纳入同一受控范围。"两台到十台"只是为这个办公室场景选的试点规模,不是项目官方容量建议。
认证本身也需要负例。只验证自己的有效凭证能连上,说明不了无效输入能否被拒绝。准备一台同样属于测试者的设备,分别使用缺失凭证、已知无效凭证,再恢复有效凭证。前两次应走认证失败并关闭的路径,最后一次才进入业务连接。记录是否创建了业务会话与关闭原因,不保存完整 token。若免校验名单里仍有该设备,负例就测不到 token 分支,需要先消除这个前提差异。
AuthManager 的签名内容包含 client_id、设备标识和时间戳,验证时重新计算签名并检查时间差。更改参与签名的标识而沿用原 token,不能期待自动通过。其构造函数又把未提供、零或负的有效期回落到三十天;所以"填零立即过期"也不是这一实现的语义。2 这些细节决定试点换机、重新配置后应核对什么,不能被一句"已经加了鉴权"覆盖。
两台设备能回答,仍不足以决定扩到十台
能进入服务只是第一段证据。两台设备先各自发起同一个普通问答,目的是确认被分配的模型、音频链和输出方式可用;随后才让两台重叠发起请求,观察它们相互影响的方式。串行试用两遍,不能替代两路同时活动。这里也不把普通聊天的结论推广到带工具、长记忆或其他音频 provider 的路径。
有两个很容易被误读成容量保证的实现细节。ConnectionHandler 创建 ThreadPoolExecutor(max_workers=5),同时建立 queue.Queue() 形式的报告队列和 ASR 音频队列。前者是这个连接使用的线程池并发设置,既不是五台设备上限,也不是五个模型请求一定能同时完成;后者没有在构造时传入 maxsize,不能从初始化代码得出它已提供固定的音频积压保护。3
继续增加连接,线程、队列、实际 provider 连接、内存和模型供应商额度可能在不同位置出现压力。这里只能据代码确定应观察的资源,无法靠参数乘法算出整机能承受多少台。就算线程池有五个工作线程,任务还可能阻塞在远程接口,队列和其他协程也不会因此自动获得统一容量限制。
案例选择四个时间点记录一条请求:设备结束发言、服务端出现有效识别文本、第一段合成音频进入发送、设备实际开始播放。前两段延迟变长与最后一段变长指向不同的调查对象。时间戳来自不同机器时不能未经校时直接相减,最省事的第一轮可以在同一台采集机上记录客户端事件与服务端转发事件,并明确它们只是代理观察;真正的设备发声仍用独立录音或设备播放事件确认。收到发送日志不能直接填入"用户已听见"。
除了耗时,还记录尝试次数与失败阶段。每台发起多少次、多少次得到完整可听的回答、多少次在连接、识别、生成或播放处失败,应有同一分母。重试后终于成功的请求,保留首次失败与重试次数,避免把重试成本藏进"最终成功"。没有足够样本时,逐次列出结果比给出看似稳定的尾延迟更诚实。
这种记录仍不是要给文章制造一套漂亮指标。若第二台加入后,第一台的识别文本出现时间不变,但首段音频明显后移,下一步应查该轮生成、合成与排队;若有效文本迟迟没有到达,先回到音频接收和识别路径。试点记录的价值是决定下一次只改哪个变量,而不是先给整个系统贴上"慢"或"可扩展"的标签。
把一次断开做完整,再讨论增加设备
真实使用中的难题往往在第一轮演示结束后出现。设备没有电源重启,却丢了网络;WebSocket 已经关闭,远程生成还在结束;连接重新建立,用户再次说了同一句话。此时,仅看新连接数回升会遗漏旧连接的残余工作。
当前服务端 close() 会尝试释放 VAD 的连接资源、清理缓存、停止相关任务、清空队列、关闭 WebSocket 和 ASR/TTS,再以 shutdown(wait=False) 结束线程池管理。3 这些是具体的清理动作,应该认可它们已经存在。但这个调用没有提供"等待全部正在运行的工作结束"的完成证明,也不能据此推出上游模型取消了生成或停止计费。日志写"连接资源已释放",仍应与实际任务、连接和内存变化相互核对。

超时也有多个触发条件。连接构造时,二道超时阈值按 close_connection_no_voice_time 加六十秒计算;检查任务按周期比较最后活动时间,需要绑定的连接则使用首次活动时间。默认配置中的数值与实际关闭时刻不能机械画等号,周期调度、活动更新及其他关闭路径都会影响观察。3 因而,在演练表里只写"等待一百二十秒必然释放"并没有完整依据。
案例在两台设备中只对 B 操作一次受控断网,A 继续正常问答,避免同时切断两台后无法区分个体与公共故障。先在 B 空闲时断开并恢复网络,再在另一轮 B 的回答过程中做同样操作。两种情况分别回答连接能否恢复、正在发生的业务会留下什么。不要把它们合并成一个"断网重连通过"。
断网后记录 B 的旧会话是否停止接收新业务、服务端是否进入清理、新连接得到什么会话标识,以及恢复后首次问答是否完整。客户端恢复行为必须按实际固件、网络和协议验证,不能因为服务器支持接受新连接,就推断设备一定会自动回来。若设备需要用户重新唤醒才能恢复,这也可以是首轮试点接受的行为,但必须明确告诉体验者,而不是把手动恢复计成自动恢复。
同时观察 A 是否还正常,旧 B 的工作是否继续占用资源。如果新 B 已恢复而旧 B 的上游请求仍活着,本轮先停止增加设备,查清取消或自然结束的实际边界;不必立刻宣布发生内存泄漏。判定泄漏需要对照断开前后、等待中的任务以及多次重复后的回落趋势,一次高水位不足以定性。对首轮放量而言,暂时无法解释的持续占用已经足以让规模保持不变。
固件被确认有效,与语音业务恢复是两次决定
网络恢复之外,首批设备还会遇到升级后的退路问题。小智官方客户端固定提交 cf2024ff36b3af1d651a68ee582cbb91986ec4d4 的 ActivationTask() 先调用 CheckNewVersion(),之后才 InitializeProtocol()。在版本检查成功、执行到相应分支时,前者会调用 MarkCurrentVersionValid(),随后才检查是否还有激活码或挑战需要处理。4
这个顺序给出了比"支持 OTA"更有用的信息:当前固件被标记有效的时点,不能直接当作一次完整语音问答通过。它发生在这里的协议初始化之前,更不能据它宣称云端识别、生成、合成和扬声器播放已经全部验收。代码还存在版本检查失败达到重试上限后返回、升级成功后重启等路径,因此也不能说每次启动都会无条件执行确认。
确认函数本身也有限定。运行在 factory 分支时直接返回;读取分区状态失败时返回;只有状态为 ESP_OTA_IMG_PENDING_VERIFY 才调用 esp_ota_mark_app_valid_cancel_rollback()。5 状态名表示新应用正在等待启动有效性确认,不是一个业务服务可用性标签。
官方客户端的 sdkconfig.defaults 启用了应用回滚选项,但默认文件不能证明某块已烧录设备的实际构建配置。组件要求的 ESP-IDF 最低版本为 6.0.1,本文据此使用固定的 ESP-IDF v6.0.1 文档解释机制,并不声称手上的设备已经用该 SDK 编译。6
在启用回滚机制的前提下,ESP-IDF 让新应用首次启动进入待验证状态。应用完成自检后确认有效;如果仍未确认就发生重启,后续启动会将该候选标为中止,并按可启动镜像条件选择回退路径。显式自检失败也有标记无效并回退的接口。7 真正演练前,仍要核对实际分区布局、回滚配置和可用旧镜像,不能假设任意出厂设备都能退到希望的版本。

因此,这个试点分开保留两种恢复动作。固件启动自检关注设备本地能够有限时间内判定的能力,回退依赖真实构建与镜像条件;业务放量关注新组合能否完成指定语音任务,失败时先停止扩批,并视原因恢复服务端配置或设备版本。本文没有修改客户端的确认时机,也没有把新的云端验收逻辑写成现有功能。
尤其不建议简单地把"确认固件有效"推迟到云端所有接口都成功。网络中断、凭证失效或模型服务故障,未必能通过退回旧固件修复。若把外部依赖失败直接解释为本地固件损坏,反而可能把可启动的设备带入反复恢复。需要追加自检时,应先说明测试对象、最长等待和失败出口,再在可恢复的开发设备上验证。
案例把设备固件记作 F0,服务端配置记作 C0,二者都是操作人员在实施时填写的版本标识,不对应一份本文制造的可下载镜像。先验证 F0/C0 组合;尝试新固件时保持 C0,尝试新配置时保持 F0。避免一轮同时换固件、模型、音频参数和网络,让"恢复旧配置"与"重刷旧固件"变成随意碰运气。原先正常的组合也要保留实际文件、构建信息和部署方式,仅记住一个版本名不足以恢复。
一次可以执行、也可以明确停下的试点
下面把前面的决策填进同一份教学记录。所有结果均待实际运行填写;表中已填的是实验安排与决定规则。它主要回答"下一批能不能进入",不是一张宣称项目已经生产可用的成绩单。
| 项目 | 本轮安排 | 观察与决定 |
|---|---|---|
| 参与范围 | 同型号自有设备 A、B;通过后再增加八台;受控网络,认证开启,免校验列表空 | 实施人员先证实外部设备不能进入该试点实例;做不到就停在准备阶段 |
| 固定组合 | F0/C0;普通聊天;同一模型与音频设置 | 实施时填入实际固件提交、构建配置、服务端提交、配置摘要;凭据只记录引用 |
| 认证负例 | 测试设备依次使用缺失、无效、有效凭证 | 前两次被拒绝,恢复有效凭证才进入业务会话;记录会话是否建立,不记录完整凭证 |
| 基线请求 | A、B 各做五次固定问题,再做五轮双设备重叠问答 | 每次记录发言结束、有效识别文本、音频发送、设备播放与失败点;不能以串行成功替代重叠结果 |
| 网络恢复 | A 保持工作;B 分别在空闲与回答中受控断网十秒后恢复 | 十秒是演练输入;记录旧会话、新会话、人工动作和首次完整回答,不预填自动重连成功 |
| 清理观察 | 断开后观察上游活动请求、线程、队列与内存;以实际配置解释等待窗口 | 如果旧工作持续占用且无法解释,暂停扩批并查清;短时高水位不直接定性为泄漏 |
| 升级退路 | 开发设备先核对实际回滚开关、分区、旧镜像与有线恢复入口 | 未证明退路可用,不把新固件分发给另外八台;测试不在唯一可用设备上冒险执行 |
| 业务恢复 | F0/C0 基线留存;每轮只换固件或服务配置之一 | 外部模型或网络故障先停扩批并定位,不直接触发固件降级 |
| 本轮判定 | 指定请求全部取得可解释结果,恢复步骤可复现,资源残留已解释 | 出现未解释失败就留在两台;全部完成才人工决定扩到十台,扩批后重复同样过程 |

固定问题可以选"用一句话说明今天的测试内容",并关闭会让请求走其他业务的额外工具或意图分流。选择这种输入,是为了减少试验变量;它不能代表工具调用、长回答或复杂口音已经覆盖。若实际产品依赖这些能力,它们应作为后续独立场景补测,而不是从基础问答通过中借结论。
这份表的五次与五轮,只是让首轮记录保持可操作,不构成统计置信度或线上服务目标。办公室体验者可以接受偶发人工恢复,无人值守终端可能完全不能接受,因此上线标准要来自使用场景。当前案例采用更保守的首轮规则:小样本里任何未解释失败都会阻止扩批;这保证了决策明确,却不证明扩大到更多设备后没有新问题。

再看一个如何填写结果的示范判断,但不要把它当作已发生的测试:假设 B 恢复网络后重新建立会话,第一次问答成功,A 全程正常,却发现旧 B 的生成请求迟迟未结束。此时可以填写"连接恢复成功,残余请求待解释",整轮结论仍是"保持两台"。既不抹掉恢复成功的事实,也不为了一个成功答案继续扩大成本与资源影响。下一步针对旧请求自然结束或取消机制补证,完成后重做这一行即可。
相反,如果升级后的设备本地自检正常,只有远端凭证失效,恢复措施应优先回到凭证与服务配置。把固件刷回 F0 仍携带同一无效凭证,通常没有解决该故障的因果依据。这个例子说明为什么最终产物需要保留"失败在哪一段、撤回什么、重验什么"三项关联,而不能只有一栏笼统的"已回滚"。

首批放量结束时,交付物就是这份带实际版本、逐次观察和明确决定的记录。它可以得出"继续到十台",也可以得出"先修认证配置""先解释旧请求残留"或"先证明升级退路"。只要决定能回到原始观察,暂缓扩批同样是有效结果;一台设备答出第一句话,则仍只是这条工程路径的起点。
核查依据
- 服务端
xinnan-tech/xiaozhi-esp32-servercommit6afc54a17def47578a4b3efc4680873689d3168b:6 个 GitHub blob 链接全部 HTTP 200(核心 3 个 + 超时 3 个)。 - 客户端
78/xiaozhi-esp32commitcf2024ff36b3af1d651a68ee582cbb91986ec4d4:5 个 GitHub blob 链接全部 HTTP 200。 - ESP-IDF
espressif/esp-idfv6.0.1:1 个文档 blob 链接 HTTP 200。 - 6 张图均
curl -fsSL下载到/tmp/blog-imgs13/01-06并已read工具解读(页眉 XIAOZHI / 恢复观察与扩批决定,2026-09-16 核验)。 - 资料检查日期 2026-09-16;本文未启动服务器、未调用模型、未烧录固件、未做真实断网测试。所有次数(5 次 / 5 轮)、时长(10 秒断网)、版本标识(F0/C0/F1/C1)与判定结果均为作者填写的演练方案。