
语音大模型指挥机器人LLM Agent × ROS2 端到端实战你对着麦克风说左转九十度Gazebo 里的小车真的转了 90°你问前面有障碍物吗机器人用激光雷达的真实数据开口回答你。全程离线没有一个请求发到云端。这篇文章记录这个系统从零到闭环的完整实战。一、这个系统长什么样先看最终形态的数据流麦克风 ──▶ voice_input ──/voice_text──▶ llm_agent ──/robot_command──▶ robot_controller ──▶ Gazebo 差速车silero VAD 切段 │ │SenseVoice 离线识别 │ Qwen2.5-3B │ 20Hz 心跳发布(sherpa-onnx int8) │ Function Calling │ stop 最高优先级│ │└─ 查询类走 /query_sensor 服务 ──▶ state_query读雷达/里程计说一句话走完这条链VAD 断句 0.5s ASR 语音识别 122ms LLM 理解 457ms 控制下发 50ms ≈ 1.16 秒后车开始动热态实测7 条动作指令的标准差只有 ±0.05s。整个系统是这样一天天长出来的实验主题干了什么关键产出1-3ROS2 基础环境搭建、话题/服务编程、Action launchmy_package、action_demo4传感器 TF激光雷达/里程计数据流接入TF 坐标变换/scan /odom 数据通路5造车自写 URDF 差速小车 Gazebo 一键启动2 轮差速车 激光雷达不依赖 turtlebot36大脑原型LLM Agent 设计Function Calling 拒绝规则 对话记忆llm_core.py纯 Python未接 ROS7★1 文字闭环建包 robot_agentLLM 输出转 RobotCommand.msgcontroller 20Hz 心跳控制终端输入向前走半米 → 车前进 0.5m8★2 语音闭环silero VAD 自动断句 SenseVoice 离线识别接入 /voice_text说左转九十度 → 车左转 90°端到端 1.16s9★3 感知问答state_query 节点把 /scan /odom 汇总成服务llm_agent 双推理回答“前方有障碍物吗” → 基于真实雷达数据回答10开口说话matcha TTS 离线合成 回声门控防自问自答死循环问答闭环 ≈2.2s 说完→出声11分布式多机Jetson 跑耳脑PC 跑身体环境DDS 跨机发现推理在端侧、仿真在主机llama.cpp 后端迁移12-14打磨交付计划一键 launch 收编全部节点、性能压榨、录屏从开五个终端到一条命令三个 ★ 是项目的三个验收里程碑文字闭环 → 语音闭环 → 感知问答每一步的下游都零改动——这就是下一节要说的两层契约在还债。技术选型一句话概括全离线、全端侧、小模型。组件选型为什么是它ASR 语音识别)SenseVoice int8~230MB中文友好CPU 实时数字识别准VAD 自动断句)silero VAD2MB说完停顿 0.5s 自动断句免按键识别LLMQwen2.5-3BQ4_K_M 量化原生 Function Calling3B 够用LLM 运行时Ollama / llama.cpp一条命令起服务OpenAI 兼容 API仿真Gazebo Fortress 自写 URDF不依赖 turtlebot3车是自己的通信ROS2 Humble默认 Fast-DDS话题/服务/消息契约工业标准为什么不联网用 GPT三个理由机器人要求低延迟云端往返至少加 500ms、离线可用真机在没网的厂房里也得干活、以及最重要的——这是端侧大模型部署这个岗位的完整预演。二、核心设计两层契约本文最重要的一个决定动手写第一行代码前先定了一个架构规矩LLM 层输出灵活的 JSONROS 层只认强类型消息两层之间只有 llm_agent 一个转换点。LLM 世界灵活 ROS 世界严格───────────────── ─────────────────“action”: “move_forward” ─┐“distance”: 0.5 ├─▶ RobotCommand.msgJSON模型想怎么吐就怎么吐 │ action/distance/angle/sensor_type┘ 编译期类型检查ros2 interface show 自文档这个决定在后续每个阶段都兑付了红利实验8 接语音时ASR 只是把谁往 /voice_text 发消息换了人下游零改动实验9 加感知问答时只是给 llm_agent 多挂了一个服务调用controller 零改动实验10 加 TTS 时只是订阅了已有的 /agent_answer 话题全链路零改动实验11 换 LLM 后端时Ollama → llama.cpp只改了 llm_core 一层下游还是零改动。每次零改动都是当初这一条架构规矩在还债。分布式/机器人系统里消息契约的设计价值 后面所有阶段的返工成本。三、三大功能模块的实现要点3.1 耳朵VAD 离线 ASR实验8语音输入节点就干一件事** continuous 听音说完自动断句识别成文字发到 /voice_text**。每 32ms 读一块音频喂给 VADVAD 说说完了就切出一段送 ASRdata, _ stream.read(512) # 512样本 16kHz 32msself.vad.accept_waveform(data[:, 0])if not self.vad.empty():segment self.vad.front.samples # 一句完整的话self.recognize_and_publish(segment) # SenseVoice → /voice_text三个实战细节值得说VAD 参数就是体验静默 0.5s 判定说完太短会把停顿切成两句话太长响应变慢短于 0.25s 的声音当噪声丢掉键盘声、咳嗽单段最长 8s 兜底。SenseVoice 会吐控制标签偶尔输出 |zh||NEUTRAL| 这类东西一行正则清洗掉。发布后 1s 冷却同句话的尾音常被 VAD 切成第二段冷却期直接丢弃。3.2 大脑Function Calling 一堆工程兜底实验7-9LLM 层不是简单的调个 API5 个工具前进/左转/右转/停止/查传感器背后是一整套工程化处理链用户输入│├─ ① ASR 同音纠错 ──── “左传→左转”确定性替换零token零延迟├─ ② 危险指令拦截 ──── “飞到月球” → 直接拒绝不进LLM零token├─ ③ 指令缓存查询 ──── 重复指令直接命中LRU缓存FC≈1ms vs 457ms│├─ ④ LLM Function Calling温度0.1保证稳定│ ├─ 输出参数再校验 ── distance 必须在0.05~10米模型输出不可全信│ ├─ 查询类FC1 → 调/query_sensor真实服务 → FC2 生成自然语言回答│ └─ 没触发工具但像查询── 追加提醒强推一次工具调用nudge重试│└─ ⑤ 全程写对话历史20轮上限── 支撑再走远一点这种指代为什么兜底这么多因为 3B 小模型的输出就是会漂。实测踩过的坑Qwen2.5-3B 偶尔丢 required 的 type 参数兜底成 “all”、偶尔吐出带冒号的 key “type:”归一化一下、感知问答后下一轮会模仿纯文字回答跳过工具nudge 重试解决。小模型不是不行是要用工程手段把它的不稳定磨平。 这套规则前置 输出校验 失败重试的结构比换 7B 大模型便宜得多也快得多。3.3 嘴巴TTS 回声门控实验10问答闭环的最后一块机器人开口回答。选型走了一圈弯路——melo TTS 实测不像普通话语速2倍音量1/10配置全对仍异常果断换 matchaRTF 0.0874.5s 音频 0.4s 生成。最有意思的设计是回声门控机器人有嘴有耳扬声器的声音会被麦克风重新听到 → ASR 把自己的回答识别成新指令 → 自问自答死循环演示现场翻车名场面。全双工需要 AEC 回声消除工程量大。我们的解法是时间片半双工tts_node 播报前: 发 /tts_statetrue → voice_input 丢弃一切音频段tts_node 播完後: 再静音 0.3s房间混响尾巴→ 发 false恢复收音代价是播报期间用户不能打断收益是零依赖、零误触发。消费级硬件上先保证演示可靠再谈体验升级。四、延迟数字会说话用自研的延迟采集器订阅 /latency_diag /cmd_vel零侵入实测动作类指令说完 → 车动指令ASRFC端到端首次指令~122ms~158-457ms1.16s ± 0.05缓存命中~122ms1ms0.64s两个数字值得品缓存命中把 FC 从 457ms 干到 1ms——重复指令左转90度说第二遍完全绕过 LLM端到端直降 0.5s。但感知查询永远不缓存因为现在的情况必须每次真实查询1.16s 里最大头是 VAD 的 0.5s 断句等待——这是听懂你说完了的必要成本砍不得。问答类指令说完 → 开口回答VAD 0.5s ASR 0.12s FC1 0.45s 服务 0.02s FC2 0.7s TTS生成 0.4s ≈ 2.2s问答要两次 LLM 推理先生成工具调用再基于工具结果组织语言所以比动作类慢约 1 秒。优化空间明明白白把 FC2 换成模板化播报能省 0.7s牺牲一点自然度——工程取舍。拒绝类指令“飞到月球”FC ≈ 1ms零 token。 危险指令在进 LLM 之前就被关键词规则拦下这是整条链路里最快的智能。五、实验11拆成两台机器分布式实战单机闭环只是开始。真正的目标形态推理在端侧仿真在主机——Jetson 跑耳朵大脑这是真机本体的样子PC 跑身体环境Gazebo 仿真扬声器。═════ Jetson Orin Nano端侧智能═════麦克风 → voice_input → llm_agent ⇄ llama.cpp(Qwen2.5-3B GGUF)▲ ││ /tts_state ├─/robot_command──┐═════ WiFi · DDS 跨机发现 ═══════════════════════╪═══│ ▼扬声器 ← tts_node ←──/agent_answer robot_controller → Gazebo划分逻辑不是按算力切的是按信息在源头压缩/scan 雷达数据一帧 115KB/s如果直接跨机传是灾难。state_query 放在 PC 本地把大流量消化掉跨机的只有文本消息总流量 1KB/s降两个数量级音频同理麦克风在 Jetson就地 VAD识别跨机只传 20 字节的文本。跨机联调踩坑实录九连坑这里挑最有价值的五个坑 1配置写了不存在的中间件。 脚本里 export RMW_IMPLEMENTATIONrmw_cyclonedds_cpp系统里根本没装 CycloneDDSros2 命令全部秒退。教训配置声明的环境变量先 ls /opt/ros/humble/lib/ 验证库存在再写。最终跨机直通用 Humble 默认的 Fast-DDS——CycloneDDS 既然没装就别在配置里声明它。坑 2最阴的坑——配置时好时坏。 Jetson 节点明明在跑PC 发消息永远 Waiting且换窗口时好时坏。真凶是 ~/.bashrc 第 186 行硬编码 ROS_DOMAIN_ID1新开终端就被覆盖回域 1而节点活在域 0。DOMAIN_ID 是隔离墙不同域的节点互相完全不可见。“时好时坏” 强烈怀疑环境变量污染不是玄学网络。坑 3conda 陷阱。 Jetson 上 pip 装包死活不生效——which python3 发现在 miniconda 环境里包全进了 conda 的 site-packages而 ros2 run 用系统 Python。更深的雷miniconda base 是 Python 3.13ROS2 Humble 只支持 3.10colcon 产物编到了 3.13 目录里 ROS2 根本加载不了。修法专用 3.10 环境 rm -rf build install log 全量重建 empy 锁死 3.3.4 版本。坑 4调了半天 FC服务器根本不是我以为的那个。 llama.cpp 后端 FC 全部失败折腾半天模型配置最后 ps aux | grep 8000 一看——8000 端口驻留着之前的 MLC LLM 服务。怀疑后端行为不对时先确认端口上的真身一分钟的事。坑 5文本模式 Function Calling。 换真 llama.cpp 后tool_calls 字段一直 null但模型 content 里赫然是完整的工具调用 JSON——模型完全理解意图只是用文本形式输出。解法是双模式解析文本抠 JSON → 标准 tool_calls → 纯文字回答三级降级。FC 的正确格式不止一种判断 FC 是否工作要看模型输出了什么不能只盯 tool_calls 字段。连带的深坑手工构建的 tool_calls 写进对话历史时漏了 “type”: “function” 字段——本轮解析不读 type 所以第一条能过下一轮把历史发回服务器被完整校验500。第二条才出错类问题第一嫌疑人永远是跨请求的持久状态。换后端后的实测指标数值FC 延迟Jetson llama.cpp1000–1650 msPC Ollama 是 ~457ms慢约 2–3.5 倍重复指令缓存依然 ≈1ms跨后端零损耗指令识别压测 10/10多轮对话正常模型内存~2GBQ4_K_M 量化Orin 8GB 很从容六、复盘这套系统教会我的事契约先行是分布式系统的通货膨胀对冲。 每个阶段的下游零改动都在偿还实验7 定下两层契约时的设计红利。反过来凡是当时没定契约的地方比如 TTS 门控信号后面都补了一轮协调成本。小模型的可控性 大模型的能力上限对指令场景而言。 3B 模型 厚实的工程兜底规则拦截/参数校验/缓存/nudge 重试比 7B 裸跑更快、更稳、更省内存。端侧部署的哲学是模型负责智能工程负责可靠。延迟优化先测量再动手。 1.16s 的构成里最大头是 VAD 的 0.5s必要成本其次是 LLM 的 457ms缓存可消。没有延迟打点和自动采集器这些数字全靠猜。环境问题是分布式联调的主战场。 两天九坑业务代码零改动——每个坑都是 Python 版本、环境变量、端口真身这类基础设施层的问题。排查速度表值得贴墙上症状第一反应import 报错which python3对比构建产物路径的 Python 版本后端行为诡异ps aux | grep 端口确认真身“第二条才出错”跨请求持久状态历史/缓存节点互相看不见cat /proc//environ 对比 DOMAIN_ID时好时坏环境变量污染查 bashrc