前阵子B站刷到小智AI机器人,入手了一个玩玩,官方的服务器显然不足以满足我的幻想,于是搭建了第三方服务器,顺便给它功能扩展了一下,到底做了点啥呢,似乎有一点意思了。
上手初体验
这个机器人似乎前阵子比较火,有不少硬件形态,有比较小巧可爱的,也有硬核直接的,但内核是不变的,如果你从来没玩过,好像长什么样,可以B站看看或淘宝啥的搜索一下。我家的比较抽象,长得像这样:

跟着电路图或者引脚说明接线,这步不是有手就行,还得有力气:)因为ESP32的针脚和面包板孔不完全匹配,需要用力强行按进去。看过B站有个哥们用锤子敲,也真是乐坏我了。供电的话我们可以随便拿个充电宝,倒也是很容易搞定、也有一定移动性。
最开始体验时,参考官方的DIY教程将设备刷好官方固件后,整体响应较快,对话流畅,不过模型数量有限,声音复刻收费,特别是如果这个不能联网做点啥,光和模型过时的数据聊天略有点无聊啊。结合当前较火的MCP功能,于是有了两个扩展目标:
- 复刻个人的声音让AI用我的语气说话
- 扩展MCP功能,让AI具备连接更多外界能力
消耗了一点闲暇时间,目前已经跑起来咯,下面简要分享一下过程。如果你也想折腾或者有好点子,欢迎一起交流。
部署专属服务器
小智官方并没有把服务器开源,但提供了交互协议等,有第三方开源服务器实现 xiaozhi-esp32-server,可供使用。要完成可以使用第三方服务器,我们做两件事:
- 将服务部署到自己的机器上。
- 修改小智的固件的两个地址,重新编译并烧录到设备上。
其实都很简单,我们需要动脑的机会不多,像我这样一步步来即可。
部署服务器
参考xiaozhi-esp32-server的Docker运行全模块教程。我推荐使用docker部署,主要是为了保持本地的干净。
准备配置文件.config.yaml
,在这个服务器中,使用了多层config来互相覆盖,我们定义的.config.yaml会覆盖默认的config.yaml。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # 在开发中,请在项目根目录创建data目录,然后在data目录创建名称为【.config.yaml】的空文件
# 然后你想修改覆盖修改什么配置,就修改【.config.yaml】文件,而不是修改【config.yaml】文件
# 系统会优先读取【data/.config.yaml】文件的配置,如果【.config.yaml】文件里的配置不存在,系统会自动去读取【config.yaml】文件的配置。
# 这样做,可以最简化配置,保护您的密钥安全。
read_config_from_api: True
manager-api:
# 你的manager-api的地址,最好使用局域网ip
url: http://xiaozhi-esp32-server-web:8002/xiaozhi
# 你的manager-api的token,就是刚才复制出来的server.secret
secret: 8e0ee7db-3928-400f-9e93-261275f91cf0
# #####################################################################################
# #############################以下是服务器基本运行配置####################################
server:
# 服务器监听地址和端口(Server listening address and port)
ip: 0.0.0.0
port: 8000
|
这里的配置不用太多,因为它的配置还有一层可以动态通过manager-api来下发更新,所以这里我们只需要配置一个manager-api的地址和secret即可。
然后我们使用docker-compose部署,在此之前你需要自行下载语音识别模型文件到models/SenseVoiceSmall/model.pt
,这些细节上面链接都有提,不啰嗦了,最终yaml如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
| # Docker安装全模块
services:
# Server模块
xiaozhi-esp32-server:
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
container_name: xiaozhi-esp32-server
depends_on:
- xiaozhi-esp32-server-db
- xiaozhi-esp32-server-redis
restart: always
networks:
- default
ports:
# ws服务端
- "8000:8000"
security_opt:
- seccomp:unconfined
environment:
- TZ=Asia/Shanghai
volumes:
# 配置文件目录
- ./data:/opt/xiaozhi-esp32-server/data
# 模型文件挂接,很重要
- ./models/SenseVoiceSmall/model.pt:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall/model.pt
# manager-api和manager-web模块
xiaozhi-esp32-server-web:
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
container_name: xiaozhi-esp32-server-web
restart: always
networks:
- default
depends_on:
xiaozhi-esp32-server-db:
condition: service_healthy
xiaozhi-esp32-server-redis:
condition: service_healthy
ports:
# 智控台
- "8002:8002"
environment:
- TZ=Asia/Shanghai
- SPRING_DATASOURCE_DRUID_URL=jdbc:mysql://xiaozhi-esp32-server-db:3306/xiaozhi_esp32_server?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&connectTimeout=30000&socketTimeout=30000&autoReconnect=true&failOverReadOnly=false&maxReconnects=10
- SPRING_DATASOURCE_DRUID_USERNAME=root
- SPRING_DATASOURCE_DRUID_PASSWORD=123456
- SPRING_DATA_REDIS_HOST=xiaozhi-esp32-server-redis
- SPRING_DATA_REDIS_PORT=6379
xiaozhi-esp32-server-db:
# image: mysql/mysql-server:latest
image: mysql:latest
container_name: xiaozhi-esp32-server-db
healthcheck:
test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ]
timeout: 45s
interval: 10s
retries: 10
restart: always
networks:
- default
ports:
- "3306:3306"
volumes:
- ./mysql/data:/var/lib/mysql
environment:
- TZ=Asia/Shanghai
- MYSQL_ROOT_PASSWORD=123456
- MYSQL_DATABASE=xiaozhi_esp32_server
- MYSQL_INITDB_ARGS="--character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
xiaozhi-esp32-server-redis:
image: redis
expose:
- 6379
container_name: xiaozhi-esp32-server-redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- default
networks:
default:
|
使用命令启动:
等服务起来了之后,我们可以使用仓库自带的test
页面测试,浏览器打开main/xiaozhi-server/test/test_page.html
,可以看到如下页面:
你最好是在这个网页测试通过,聊天、语音等都符合你的预期,再进行下一步。话说这个页面也是调试利器了,效率高不少。
这里还有个小技巧,因为我们接下来需要将ota/websocket地址烧录在固件上,如果你不确定服务未来会部署在哪台机器上,可以通过一个反向代理来转发。给个示意了解一下即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
| apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: to-xiaozhi-web
spec:
entryPoints:
- websecure
routes:
- match: Host(`web-xiaozhi.mrlin.cc`)
kind: Rule
services:
- name: web-xiaozhi
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: web-xiaozhi
spec:
ports:
- protocol: TCP
port: 80
targetPort: 8002
---
apiVersion: v1
kind: Endpoints
metadata:
name: web-xiaozhi
subsets:
- addresses:
- ip: 192.168.50.190
ports:
- port: 8002
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: to-xiaozhi-api
spec:
entryPoints:
- websecure
routes:
- match: Host(`api-xiaozhi.mrlin.cc`)
kind: Rule
services:
- name: api-xiaozhi
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: api-xiaozhi
spec:
ports:
- protocol: TCP
port: 80
targetPort: 8000
---
apiVersion: v1
kind: Endpoints
metadata:
name: api-xiaozhi
subsets:
- addresses:
- ip: 192.168.50.190
ports:
- port: 8000
|
上面我将api-xiaozhi
和web-xiaozhi
都指向了我的本地IP,这样我就可以通过api-xiaozhi.mrlin.cc
和web-xiaozhi.mrlin.cc
来访问了,未来迁移到其他机器上,只需要修改这个yaml文件即可。你不搞这一步一点问题也没有,继续用IP,注意那样就使用http/ws协议。
修改固件&编译&烧录
这里我们要进入到官方固件仓库来继续我们的操作。并且编译烧录等需要你在Windows下进行了。
我们可以参考这篇文章Windows搭建 ESP IDF 5.3.3开发环境以及编译小智大概了解整个过程;接着看这里简要而关键的固件构建过程。
简单来说,当前最新版本我们仅需要修改OTA_URL
地址即可,以往要修改websocket
地址,现在它已经通过OTA下发了。
当天我正在照着教程编译,发现不找到websocket的定义了,定睛一看刚好有提交重构了这一块,于是顺便向server那边也提了个PR,居然很快就被合并了。不过当时字段名取得不太好,我延用了固件那边的websocket_url
,后面又被修改为server.websocket
,这确实更符合项目的命名规范一些。
之后构建就几个命令:
1
2
3
4
5
6
| # 终端命令行进入xiaozhi-esp32的根目录
cd xiaozhi-esp32
# 例如我使用的板子是esp32s3,所以设置编译目标为esp32s3,如果你的板子是其他型号,请替换成对应的型号
idf.py set-target esp32s3
# 进入菜单配置
idf.py menuconfig
|
之后是烧录,将你的小智AI设备通过串口连接到电脑,然后使用idf.py build
构建,接着使用idf.py -p PORT flash
烧录。
这里的PORT根据你连接的设备不同,可能有所不同,在电脑中我的设备
查看串口,我的是COM4
。如果嫌烧录慢,可以添加参数-b 2000000
来加速。
一切成功后,同时也在上一步骤的服务器的server.websocket
中填入wss://api-xiaozhi.mrlin.cc/xiaozhi/v1/
,然后就可以打开设备连接自己的服务器开玩了。
现在你可能发现有了更多的LLM选择,有了更多的TTS(声音)等,似乎是更开放了,或许可以玩得更嗨了。
个人声音复刻
之前在玩火山引擎时,送了几次声音复刻给我,体验了一下,当我想用在小智上时,发现已经过期了,居然只有十天有效期,这也太抠了。我去腾讯云找了一下,也有免费的额度,申请下来有3个月,那么就玩玩吧。不像火山引擎的声音复刻在网页上提交声音即可,腾讯云的复刻要原始得多,需要自己使用API,所幸相关功能也在它的tccli
中,那么也就是敲命令的事,也不可能难倒我们。
命令行体验
我们可以参考文档声音复刻相关接口,使用命令行来把过程体验一下。至于如何安装tccli
,可以参考腾讯云命令行工具。
1. 获取训练文本
1
2
3
4
5
6
7
8
9
10
11
12
| ❯ tccli vrs GetTrainingText --TaskType 5 --Domain 1
{
"Data": {
"TrainingTextList": [
{
"TextId": "5f3ef960-4f11-4f4b-84e6-514692ebe0c5",
"Text": "亲 默认是发韵达快递哦 您这边可以收到韵达的货吗 "
}
]
},
"RequestId": "8c758bac-19fc-474d-9b12-2e813e834102"
}
|
这一步要我们根据提供的文本来提供一段音频,我盲猜是不是要规避用别人的声音来复刻呢?
2. 环境检测和音频质量检测
1
2
3
4
5
6
7
8
9
10
11
12
| ❯ tccli vrs DetectEnvAndSoundQuality --cli-unfold-argument --TextId "5f3ef960-4f11-4f4b-84e6-514692ebe0c5" --AudioData "$(base64 -i vrs.m4a | tr -d '\n')" --TypeId 2 --Codec m4a --TaskType 5
{
"Data": {
"AudioId": "audio0de472d6d96d488ba2159d83c10b440f",
"DetectionCode": 0,
"DetectionMsg": "Success",
"DetectionTip": []
},
"RequestId": "6218aa6d-9d45-492c-9c18-dfa342d3bd5e"
}
|
这一步看名称感觉多余,但是下一步创建任务需要这个AudioId
,所以还是得来一下。
3. 创建声音复刻任务
1
2
3
4
5
6
7
| ❯ tccli vrs CreateVRSTask --SessionId 1234 --VoiceName kevin --VoiceGender 1 --VoiceLanguage 1 --AudioIdList "[\"audio0de472d6d96d488ba2159d83c10b440f\"]" --TaskType 5
{
"Data": {
"TaskId": "fast7feffdeb04ca401583a317002dd14e7a"
},
"RequestId": "127ad8c6-a27d-470f-8dab-b9940c9594cb"
}
|
我们得到了一个任务ID。原因或许是这一步是异步的,服务器训练这个声音也需要时间。
4. 查询声音复刻任务结果
1
2
3
4
5
6
7
8
9
10
11
12
13
| ❯ tccli vrs DescribeVRSTaskStatus --TaskId fast7feffdeb04ca401583a317002dd14e7a
{
"Data": {
"TaskId": "fast7feffdeb04ca401583a317002dd14e7a",
"Status": 2,
"StatusStr": "Success",
"VoiceType": 200000000,
"ErrorMsg": "",
"ExpireTime": "2025-08-02T00:00:00+08:00",
"FastVoiceType": "WCHN-869b22949cbe4db6a344f1da8cf8a68c"
},
"RequestId": "3eb52b2d-3fa1-42f5-84f0-abf71bef32e2"
}
|
查到的结果里有几个比较关键的,VoiceType
、FastVoiceType
,前者是音色ID,后者是快速复刻时使用的音色ID,请用小本本记录下来备用。
5. 查询复刻音色信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| ❯ tccli vrs GetVRSVoiceTypeInfo --VoiceType 200000000 --TaskType 5 --FastVoiceType WCHN-869b22949cbe4db6a344f1da8cf8a68c
{
"Data": {
"VoiceType": 200000000,
"VoiceName": "kevin",
"VoiceGender": 1,
"TaskType": 5,
"TaskID": "fast7feffdeb04ca401583a317002dd14e7a",
"DateCreated": "2025-05-03T15:17:35+08:00",
"IsDeployed": true,
"ExpireTime": "2025-08-02T00:00:00+08:00",
"FastVoiceType": "WCHN-869b22949cbe4db6a344f1da8cf8a68c"
},
"RequestId": "db5709d2-4e2c-432d-8bd3-ed2eddcb6c21"
}
|
这一步可能真没啥用了,我们要的信息前面也有了,说有用的话,你大概知道这是一个男人名叫kevin的声音。
6. 使用复刻声音来合成语音
1
2
3
4
5
| ❯ tccli tts TextToVoice \
--Text "我今天带娃在meland玩,这家伙玩疯了呢" \
--SessionId 123 \
--VoiceType 200000000 \
--FastVoiceType WCHN-869b22949cbe4db6a344f1da8cf8a68c > result.json
|
返回的内容包括base64后的音频数据,我们使用jq
来提取并保存到文件:
1
| jq -r '.Audio' result.json | base64 -d > tts.wav
|
播放来听听吧,感受一下自己声音的魔力或者惊吓。
小智server支持使用复刻声音
当我使用复刻声音时,发现小智server并不支持,于是我给它添加了支持,并提交了PRfeat: 支持腾讯TTS声音一句话复刻后的合成,这次没有很快被合并,因为官方说某个字段要废弃了,又没说最新用什么方式,明儿就就腾讯云提工单。
如PR的修改所见,仅是增加了上述fast_voice_type
参数的传递即可。不管上游合不合修改,反正我本地已经用上了。有个小发现,这里server中处理不同语音合成模型的参数,UI是基于fields
的字段及其定义动态生成展示的,这还不错。顺手update一个DB,对应的UI就有新字段出来了。
1
2
3
4
| UPDATE `ai_model_provider`
SET `fields` = '[{"key":"appid","label":"应用ID","type":"string"},{"key":"secret_id","label":"Secret ID","type":"string"},{"key":"secret_key","label":"Secret Key","type":"string"},{"key":"output_dir","label":"输出目录","type":"string"},{"key":"region","label":"区域","type":"string"},{"key":"voice","label":"音色ID","type":"string"},{"key":"fast_voice_type","label":"FastVoiceType","type":"string"}]',
`update_date` = NOW()
WHERE id = 'SYSTEM_TTS_TencentTTS';
|
不知道当你发现你自己在和自己说话时是什么感受,反正我挺惊奇的,感觉自己像个神经病。明儿我还是复刻一个我孩子的声音吧,我就可以随时亲子教育了:D
MCP扩展
在这个第三方服务器的实现中,它支持了一部分MCP的能力,也即stdio
模式的mcp server。这会因为使用容器的模式,限制了它的使用。容器内可没有npx/uvx啥的,更没有chrome了。我就一个小小愿望,问它谁是最美丽的人,不对,问它昨天曼联输了个几比几,它不能实时联网找到正确答案我就不满意了。
同样的,让我们扩展它,只需要几行代码的修改就可以啦,详见PRfeat: MCP server支持使用sse模式。一般来说宿主机上使用npx/uvx/chrome等,容器内我们通过http协议使用mcp-server
还是更简单的。可是有一些mcp server人家就没实现sse咋办?一点都不困难,比如我们借助supergateway
就可以让几个模式互转。
使用supergateway
Supergateway runs MCP stdio-based servers over SSE (Server-Sent Events) or WebSockets (WS) with one command. This is useful for remote access, debugging, or connecting to clients when your MCP server only supports stdio.
比如我有这样一个perplexity-ask
的mcp server,它只支持stdio模式,那么我就可以使用supergateway来让它支持sse模式:
1
2
3
4
5
6
7
| "perplexity-ask": {
"command": "/opt/homebrew/bin/node",
"args": ["/Users/kevinlin/Source/modelcontextprotocol/perplexity-ask/dist/index.js"],
"env": {
"PERPLEXITY_API_KEY": "pplx-..."
}
}
|
使用supergateway来启动:
1
2
3
4
| export PERPLEXITY_API_KEY=pplx-... &&
npx -y supergateway \
--stdio "/opt/homebrew/bin/node .../perplexity-ask/dist/index.js" \
--port 8123
|
于是我们给小智server的mcp config就可以这么简单了:
1
2
3
4
5
| {
"perplexity-ask": {
"url": "http://your-ip:8123"
}
}
|
现在你的小智就可以更好的使用MCP server了,我试着问了一下,它说曼联最近欧联13场不败!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| {
"role": "assistant",
"tool_calls": [
{
"id": "call_202505041955402e0c441c996740fb_0",
"function": {
"arguments": "{\"messages\": [{\"role\": \"user\", \"content\": \"曼联过去几场欧联杯比赛的对手和结果?\"}]}",
"name": "perplexity_ask"
},
"type": "function",
"index": 0
}
]
},
{
"role": "tool",
"tool_call_id": "call_202505041955402e0c441c996740fb_0",
"content": "曼联在2024-2025赛季欧联杯中表现出色,以下是他们最近几场欧联杯比赛的对手和结果:\n\n最近的欧联杯比赛,曼联在半决赛首回合客场3-0完胜西甲第四名的毕尔巴鄂竞技[1][2]。这场比赛中,卡塞米罗为曼联首开纪录,随后布鲁诺·费尔南德斯梅开二度,全部进球都是在上半场完成的[2]。比赛中,毕尔巴鄂竞技的后卫丹尼尔·维维安被罚下场[2]。\n\n在四分之一决赛中,曼联对阵法国球队里昂。首回合比赛两队2-2战平[5]。次回合在老特拉福德球场进行的比赛异常激烈,经过常规时间和加时赛的鏖战,曼联最终以5-4战胜里昂,总比分7-6晋级半决赛[5]。在这场比赛中,达洛特建功,布鲁诺·费尔南德斯点射破门,最后时刻马奎尔上演读秒绝杀[5]。\n\n值得一提的是,曼联本赛季在欧联杯中保持着13场不败的强势表现[1],与他们在英超联赛中的低迷形成了鲜明对比。新帅阿莫林有望复制穆里尼奥在2016-17赛季率领曼联首个赛季就夺得欧联杯冠军的成就[2]。\n\n曼联已经一只脚踏入欧联杯决赛,决赛将在本月下旬在毕尔巴鄂的圣马梅斯球场举行[2]。\n\nCitations:\n[1] https://www.163.com/dy/article/JUKFHV4C0530UTM3.html\n[2] https://www.163.com/dy/article/JUHUQC8L05567SBP.html\n[3] https://zh.wikipedia.org/zh-cn/%E6%9B%BC%E8%81%AF2024%E5%B9%B4%E8%87%B32025%E5%B9%B4%E7%90%83%E5%AD%A3\n[4] https://m.dongqiudi.com/article/5071872.html\n[5] https://news.sina.com.cn/zx/2025-04-18/doc-inetppfi2898601.shtml\n"
},
{
"role": "assistant",
"content": "曼联在2024-2025赛季欧联杯中的表现非常出色。以下是他们最近几场欧联杯比赛的对手和结果:\n\n- 在半决赛首回合,曼联客场以3-0完胜西甲第四名的毕尔巴鄂竞技。卡塞米罗为曼联首开纪录,随后布鲁诺·费尔南德斯梅开二度,全部进球都是在上半场完成的。\n- 在四分之一决赛中,曼联对阵法国球队里昂。首回合比赛两队2-2战平。次回合在老特拉福德球场进行的比赛异常激烈,经过常规时间和加时赛的鏖战,曼联最终以5-4战胜里昂,总比分7-6晋级半决赛。\n\n曼联在本赛季欧联杯中保持着13场不败的强势表现,与他们在英超联赛中的低迷形成了鲜明对比。他们已经一只脚踏入欧联杯决赛,决赛将在本月下旬在毕尔巴鄂的圣马梅斯球场举行。"
}
|
使用云函数包装mcp server
像上面这样,它需要依赖机器安装相关的服务,换一台机器就歇菜了,并且为了转换为sse还是比较麻烦。前阵子在试用阿里云百炼平台时,发现有个有意思的扩展,对于它不支持的mcp server,可以通过云函数来自定义扩展它。我想何不借助于它来包装一下mcp server,这样就可以在任何地方使用它了。
我们可以打开https://cap.console.aliyun.com/explore?lang=MCP+Server,这里以高德地图为例,我们只要在云端部署它之后。
本地只需要一个url(填上面公网访问地址)即可使用这个mcp server了,它自动提供了sse接口。至此,你居然就将mcp server “上云”了:)审计和日志也一应俱全呢。现在你可以试一下再和小智聊天,对于地图信息,它比我们还清楚了。小声的说,地图API都提供了天气查询能力,咱也不用再找一个天气服务了呢。
尾声
这个机器人应该还有不少待开发的东西,无论软件还是硬件上。比如如果能再添加几个外设,预期会更有意思了。它本身也支持如Home Assistant等智能家居平台,可以实现如开关灯、开关窗帘等操作。
技术上也有不少地方可以进一步聊聊,比如MCP的使用姿势问题等,以及如何进一步提升对话的响应速度等。 记录一张图留念,本想放视频的,第一不好录制,第二声音这东西,你懂的。

考虑到篇幅问题,今天就到这里吧。如果你对某些地方感兴趣,欢迎留言讨论。如果你没什么想说的,点赞、分享、关注,都是对我最好的鼓励。我努力创作有价值的内容,期待未来与你再会。
我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:
