1. 项目缘起为什么要在机器人上跑本地语音LLM最近在折腾我的Reachy Mini机器人一个很酷的开源双臂机器人平台。它本身已经具备不错的视觉和运动能力但交互方式主要还是靠预设的程序或者远程API调用。我一直想给它加上一个更“自然”的交互入口——语音。想象一下你直接对它说“把那个红色的方块拿给我”它就能理解并执行这体验感一下子就上来了。市面上现成的语音助手方案很多比如接个智能音箱的API或者用云端的语音转文本STT和大语言模型LLM服务。但把这些方案用在机器人上有几个硬伤首先是延迟云端API的网络往返时间在机器人实时控制场景下是不可接受的一个指令等个一两秒体验就全毁了。其次是隐私和成本机器人摄像头和麦克风采集的音频、视频数据全部上传到第三方对于很多实验室、创客空间或者有数据安全顾虑的场景来说是个大问题。最后是可控性云端模型是个黑盒你很难针对机器人特定的指令集比如“向左转30度”、“夹爪力度设为0.5牛”去做定制化的优化和调试。所以本地部署就成了一个必然的选择。把语音识别和语言模型都跑在机器人“体内”的计算单元上实现端到端的低延迟、高隐私的智能交互。我手头的Reachy Mini配套的计算单元是reComputer Mini这是一款基于NVIDIA Jetson Orin Nano模组的小型工控机性能对于边缘AI应用来说相当不错。这个项目的核心目标就是把一个轻量级的语音LLM pipeline从云端“搬”到这台reComputer Mini上让Reachy Mini真正能“听懂人话”并基于理解做出响应。2. 硬件与软件栈选型为什么是它们在开始动手之前得先把“用什么”和“为什么用”这两个问题搞清楚。硬件是给定的reComputer Mini但软件栈的每一个组件都需要仔细考量。2.1 核心硬件reComputer Mini (Jetson Orin Nano) 的能力边界reComputer Mini的核心是NVIDIA Jetson Orin Nano模组。我用的这款是8GB内存的版本。对于部署LLM来说内存是首要的硬约束。一个参数量为7B70亿的模型以FP16精度加载大约需要14GB的显存这显然超出了Orin Nano的能力。因此我们的模型选型必须瞄准更小的参数规模比如1B到3B左右的模型或者使用量化技术如INT4, INT8来大幅降低显存占用。Orin Nano的CPU是ARM架构的GPU则基于NVIDIA Ampere架构拥有少量但效能不错的CUDA核心支持TensorRT等加速库。这意味着我们虽然不能跑动辄百亿参数的大模型但针对小模型进行合理的优化后获得可用的推理速度比如每秒生成10-20个token是完全可行的。另一个优势是功耗和体积它非常适合作为机器人的“大脑”集成。2.2 模型服务框架为什么选择OllamaOllama近半年在开发者社区里火得不行它本质上是一个简化本地大模型部署和运行的工具。相比于直接去Hugging Face下载模型文件然后用Transformers库加载Ollama提供了几个关键优势开箱即用一条命令就能拉取、运行一个模型自动处理模型格式GGUF、上下文长度等配置。统一的API通过简单的REST API默认端口11434提供模型交互这极大简化了应用程序比如我们的机器人控制程序与模型之间的集成。我们不需要关心模型底层的加载和推理细节。活跃的社区和丰富的模型库Ollama官方维护了一个模型库ollama.com/library里面有大量预量化好的热门模型如Llama 3、Qwen、DeepSeek等而且很多都提供了从1.5B到8B不等的多种尺寸方便我们根据硬件能力选择。对ARM平台的支持Ollama提供了Linux ARM64的安装包这是能在Jetson上运行的前提。当然Ollama也不是唯一选择。像llama.cpp、text-generation-webui等也是优秀的本地部署方案。但Ollama在易用性和API标准化方面做得最好特别适合我们这种需要将LLM能力快速集成到另一个系统机器人系统中的场景。2.3 语音处理流水线设计一个完整的“语音指令-机器人动作”流程需要以下几个步骤语音采集通过USB麦克风或reComputer Mini的音频接口采集音频流。语音识别ASR将音频流实时转换为文本。这里也需要一个本地、轻量级的模型。我选择了OpenAI开源的Whisper模型的“tiny”或“base”版本。虽然Whisper本身不算特别小但其tiny版本约39M参数在Orin Nano上实时运行是可行的准确度对于近场、指令式语音也足够。文本理解与生成LLM将识别出的文本指令送入本地部署的Ollama服务中的小语言模型。这个模型需要完成两项任务一是理解用户的自然语言指令如“请轻轻拿起桌上的杯子”二是将其转换成结构化的、Reachy Mini SDK能够理解的命令或参数如arm.move_to(x, y, z)gripper.open()。这里涉及到“提示词工程”Prompt Engineering我们需要精心设计一个系统提示词System Prompt告诉LLM它现在是一个机器人控制专家并且输出格式必须是固定的JSON。指令执行我们的主控程序用Python编写解析LLM输出的JSON调用Reachy Mini的Python SDKreachy-sdk来执行相应的动作。整个流水线的瓶颈很可能会在ASR和LLM推理环节。因此模型选型和量化等级的选择至关重要。3. 实战部署一步步搭建本地语音LLM系统理论说完了接下来是实操环节。我会尽量详述每一步包括可能遇到的坑和解决办法。3.1 基础环境准备在reComputer Mini上安装OllamareComputer Mini预装了Ubuntu 20.04或22.04。首先通过SSH连接到设备。第一步安装OllamaOllama提供了便捷的一键安装脚本。但由于网络原因直接从官网下载可能会非常慢甚至失败。这里就需要用到国内镜像源这是第一个实战技巧。# 方法一使用中科大镜像源加速安装推荐 curl -fsSL https://ollama.com/install.sh | OLLAMA_HOSThttps://mirrors.ustc.edu.cn/ollama sh这条命令的关键在于设置OLLAMA_HOST环境变量指向中科大的镜像站这样下载安装包和后续拉取模型都会走国内CDN速度有质的飞跃。安装完成后启动Ollama服务并设置为开机自启# 启动服务 sudo systemctl start ollama # 设置开机自启 sudo systemctl enable ollama # 查看服务状态 sudo systemctl status ollama如果状态显示为active (running)说明服务启动成功。第二步验证安装并运行第一个模型Ollama服务启动后默认会在本地11434端口提供一个API服务。我们先拉取一个超小模型进行测试比如TinyLlama约1.1B参数。# 拉取模型同样会走镜像源速度较快 ollama pull tinyllama # 运行模型并与它交互 ollama run tinyllama在出现的提示符后输入Hello看看它是否能正常回复。这一步是为了验证Ollama基础功能是否正常。一个关键配置修改Ollama模型存储路径默认情况下Ollama会把模型下载到~/.ollama/models目录。对于Jetson设备其系统盘空间通常有限。我们可以将其修改到挂载的大容量SD卡或USB存储设备上。首先查看Ollama的服务配置文件sudo systemctl edit ollama.service这会打开一个临时编辑器在其中添加以下内容将/path/to/your/large/disk替换为你的大容量存储路径[Service] EnvironmentOLLAMA_MODELS/path/to/your/large/disk/ollama-models保存退出后重新加载systemd配置并重启Ollamasudo systemctl daemon-reload sudo systemctl restart ollama之后所有通过ollama pull下载的模型都会存储在新的路径下。3.2 语音识别模块集成本地WhisperOllama负责LLM我们还需要一个ASR引擎。这里选择faster-whisper它是Whisper的一个CTranslate2实现推理速度更快内存占用更少非常适合边缘设备。安装faster-whisper及其依赖# 更新包列表 sudo apt update # 安装Python3和pip如果尚未安装 sudo apt install python3-pip -y # 安装faster-whisper pip3 install faster-whisper在Jetson ARM平台上直接pip安装faster-whisper可能会因为需要编译一些C依赖而失败。一个更稳妥的方法是使用NVIDIA JetPack SDK中已经优化过的库或者安装预编译的wheel包。如果遇到困难可以回退到使用原始的openai-whisper包pip install openai-whisper虽然速度慢一点但兼容性最好。编写一个简单的语音识别脚本创建一个Python文件asr_server.py实现一个持续监听麦克风、进行实时语音识别的服务。import queue import sounddevice as sd import numpy as np from faster_whisper import WhisperModel # 配置参数 MODEL_SIZE tiny # 可选 tiny, base, small。tiny最快base折中。 DEVICE cpu # 在Orin Nano上先用CPU。如果CUDA配置好可尝试cuda COMPUTE_TYPE int8 # 量化类型降低内存和计算量 # 加载模型 print(fLoading Whisper {MODEL_SIZE} model...) model WhisperModel(MODEL_SIZE, deviceDEVICE, compute_typeCOMPUTE_TYPE) # 音频参数 SAMPLE_RATE 16000 CHUNK_DURATION 3 # 每次处理3秒的音频块 CHUNK_SIZE int(SAMPLE_RATE * CHUNK_DURATION) audio_queue queue.Queue() def audio_callback(indata, frames, time, status): 音频回调函数将数据放入队列 if status: print(fAudio callback status: {status}) audio_queue.put(indata.copy()) def transcribe_audio(audio_data): 转录音频数据 audio_np np.concatenate(audio_data, axis0).astype(np.float32).flatten() # 使用Whisper进行识别 segments, info model.transcribe(audio_np, beam_size5, languagezh) text .join([seg.text for seg in segments]).strip() return text # 开始录音流 print(开始监听请说话...) stream sd.InputStream(callbackaudio_callback, channels1, samplerateSAMPLE_RATE, blocksizeCHUNK_SIZE) stream.start() try: while True: # 收集一定时长的音频 audio_chunks [] for _ in range(int(2 / CHUNK_DURATION)): # 例如收集2秒音频再识别 audio_chunks.append(audio_queue.get()) if audio_chunks: text transcribe_audio(audio_chunks) if text: # 只有识别到内容才打印 print(f识别结果: {text}) # 这里可以将text发送给LLM处理模块 except KeyboardInterrupt: print(\n停止监听) finally: stream.stop() stream.close()这个脚本创建了一个简单的实时语音识别循环。你需要先通过pip install sounddevice安装音频库。注意在Jetson上使用USB麦克风时可能需要通过arecord -l查看设备ID并在sd.InputStream中指定device参数。3.3 LLM模型选型与提示词工程这是项目的核心智能部分。我们需要一个足够小、足够快但理解能力和指令跟随能力又不错的模型。模型选择经过测试以下几个模型在Orin Nano 8GB上表现较为均衡使用Ollama的q4_0或q8_0量化版本Qwen2.5-1.5B-Instruct通义千问的小尺寸版本中文理解好指令跟随能力强响应速度快。Llama 3.2-1B-InstructMeta出品英文指令处理非常出色代码生成和结构化输出能力在同尺寸中领先。Phi-3-mini-3.8B微软的模型虽然参数稍大3.8B但因其卓越的“小身材大能量”特性经过q4_0量化后仍可在Orin Nano上运行且综合能力很强。我最终选择了Qwen2.5-1.5B-Instruct因为我们的指令主要是中文且它对结构化JSON输出的遵循性很好。用Ollama拉取它ollama pull qwen2.5:1.5b-instruct-q4_0设计系统提示词System Prompt提示词的质量直接决定了LLM能否输出我们想要的、可解析的指令。我们的目标是让LLM成为一个“机器人指令翻译官”。创建一个robot_prompt.txt文件内容如下你是一个机器人控制助手。你的任务是将用户的自然语言指令翻译成Reachy Mini机器人可以执行的、结构化的JSON命令。 Reachy Mini机器人有以下可控制部件 - 左臂 (left_arm) - 右臂 (right_arm) - 头部 (head可以转动) - 左夹爪 (left_gripper) - 右夹爪 (right_gripper) 可能的动作类型action包括 - move_to: 将机械臂末端移动到指定坐标[x, y, z]单位米。坐标系原点在机器人底座中心。 - move_joints: 控制机械臂的关节角度[j1, j2, j3, j4, j5, j6, j7]单位弧度。 - look_at: 控制头部看向某个坐标[x, y, z]。 - open_gripper: 打开夹爪。 - close_gripper: 闭合夹爪。 - set_gripper_force: 设置夹爪力度单位牛顿。 你输出的必须是且仅是一个合法的JSON对象格式如下 { actions: [ { part: left_arm, // 或 right_arm, head, left_gripper, right_gripper action: move_to, // 动作类型 parameters: { // 动作参数根据action类型不同而不同 x: 0.2, y: 0.1, z: 0.3 } } // ... 可以包含多个顺序执行的动作 ] } 如果用户的指令不明确、无法实现或存在安全风险如让机器人伤害自己或他人请在JSON中返回一个错误信息格式如下 { error: 具体的错误描述信息 } 现在请处理用户的指令。这个提示词明确了机器人的能力边界、输出格式并加入了安全校验。我们将把这个提示词作为每次对话的“系统指令”发送给Ollama。3.4 集成与主控程序编写现在我们需要编写一个主控Python程序它需要做三件事从ASR模块获取文本指令。将文本指令和系统提示词组合发送给Ollama API。解析Ollama返回的JSON调用Reachy SDK执行动作。首先安装Reachy SDK和requests库pip3 install reachy-sdk requests主控程序robot_llm_controller.py的核心逻辑如下import json import requests import time from reachy_sdk import ReachySDK from reachy_sdk.trajectory import goto # 假设我们从asr_server.py通过某种方式如队列、Socket获取文本 # 这里简化处理从标准输入读取模拟指令 import sys class RobotLLMController: def __init__(self, ollama_hosthttp://localhost:11434, model_nameqwen2.5:1.5b-instruct-q4_0): self.ollama_url f{ollama_host}/api/generate self.model_name model_name # 加载系统提示词 with open(robot_prompt.txt, r, encodingutf-8) as f: self.system_prompt f.read() # 连接到Reachy Mini假设机器人IP是192.168.1.100 try: self.reachy ReachySDK(host192.168.1.100) print(成功连接到Reachy Mini) except Exception as e: print(f连接Reachy失败: {e}) self.reachy None def ask_llm(self, user_input): 向Ollama发送请求获取结构化指令 payload { model: self.model_name, prompt: user_input, system: self.system_prompt, # 关键传入系统提示词 stream: False, options: { temperature: 0.1, # 低温度让输出更确定、更遵循格式 num_predict: 150 # 限制生成长度避免废话 } } try: response requests.post(self.ollama_url, jsonpayload, timeout30) response.raise_for_status() result response.json() return result[response] except requests.exceptions.RequestException as e: return json.dumps({error: fLLM请求失败: {e}}) except (KeyError, json.JSONDecodeError) as e: return json.dumps({error: fLLM响应解析失败: {e}}) def parse_and_execute(self, llm_response): 解析LLM的JSON响应并执行机器人动作 try: command json.loads(llm_response) except json.JSONDecodeError: print(f无法解析LLM响应为JSON: {llm_response}) return if error in command: print(fLLM返回错误: {command[error]}) # 可以在这里让机器人通过语音或灯光提示错误 return if actions not in command or not isinstance(command[actions], list): print(f响应中缺少有效的actions列表: {command}) return if self.reachy is None: print(未连接到机器人仅模拟执行) # 模拟执行打印动作 for action in command[actions]: print(f模拟执行: {action}) return # 实际执行动作 for action_spec in command[actions]: part action_spec.get(part) action action_spec.get(action) params action_spec.get(parameters, {}) try: robot_part getattr(self.reachy, part, None) if robot_part is None: print(f未找到机器人部件: {part}) continue if action move_to: # 注意reachy_sdk的移动API可能需要使用goto或特定方法 # 这里是一个示例实际API请查阅SDK文档 target_pos [params.get(x, 0), params.get(y, 0), params.get(z, 0)] # 假设robot_part有goto方法这是一个简化示例 # goto({robot_part: target_pos}, duration2.0) print(f移动 {part} 到位置 {target_pos}) elif action open_gripper: robot_part.open() print(f打开 {part}) elif action close_gripper: robot_part.close() print(f闭合 {part}) # ... 处理其他动作类型 else: print(f不支持的动作类型: {action}) except Exception as e: print(f执行动作 {action} 于 {part} 时出错: {e}) time.sleep(0.5) # 动作间短暂停顿 def run(self): 主循环从输入获取指令处理执行 print(机器人语音LLM控制器已启动。请输入指令或从ASR模块获取:) while True: # 这里替换为从ASR模块获取文本例如 # user_text asr_module.get_latest_transcription() user_text input( ).strip() if user_text.lower() in [quit, exit, q]: break if not user_text: continue print(f用户指令: {user_text}) llm_output self.ask_llm(user_text) print(fLLM原始输出:\n{llm_output}) self.parse_and_execute(llm_output) if __name__ __main__: controller RobotLLMController() controller.run()这个程序是一个框架。你需要将获取用户输入的部分input( )替换为与你的asr_server.py的实际集成比如通过一个共享的线程安全队列queue.Queue或者一个简单的WebSocket/RPC接口。同时执行机器人动作的部分需要根据reachy-sdk的实际API进行完善。4. 性能调优与踩坑实录把整个系统跑起来只是第一步让它跑得“流畅可用”才是挑战的开始。以下是几个关键的调优点和我踩过的坑。4.1 模型量化与推理速度的权衡Ollama拉取的模型标签如q4_0、q8_0就代表了量化等级。数字越小如q4模型体积越小推理速度越快但精度损失也越大可能导致模型“智力下降”输出胡言乱语或无法遵循指令格式。实测对比在Orin Nano上qwen2.5:1.5b-instruct-q4_0 模型文件约900MB推理速度约15 tokens/秒。大部分指令能正确理解并输出JSON但偶尔会在JSON外生成多余的解释文字需要我们在程序里做后处理如用正则表达式提取第一个{}之间的内容。qwen2.5:1.5b-instruct-q8_0 模型文件约1.5GB推理速度约10 tokens/秒。输出格式更稳定几乎不会出现格式错误理解能力也稍强。qwen2.5:1.5b-instruct-fp16 模型文件约3GB内存占用过高在8GB设备上与其他服务如Whisper同时运行极易导致OOM内存溢出不推荐。结论与建议对于机器人指令这种对格式要求严苛、但语义相对简单的任务q4_0量化在速度和内存占用上优势明显。为了应对其偶尔的格式错误必须在主控程序中加入健壮的输出清洗和解析逻辑。例如import re def extract_json_from_response(llm_output): # 尝试匹配第一个完整的JSON对象 match re.search(r\{[^{}]*\}, llm_output) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass # 如果匹配失败返回错误 return {error: LLM输出无法解析为JSON}4.2 内存管理避免OOM杀手Jetson Orin Nano 8GB的内存是共享的GPU和CPU共用。同时运行Ollama加载LLM、Whisper语音识别、Python主程序以及Reachy SDK的通信模块内存压力很大。监控与优化手段使用tegrastats监控NVIDIA提供了tegrastats工具可以实时查看CPU、GPU、内存、功耗等信息。运行sudo tegrastats重点关注RAM和SWAP的使用情况。为Ollama设置运行限制可以通过修改Ollama的系统服务文件限制其可用的CPU和内存。编辑/etc/systemd/system/ollama.service.d/override.conf如果没有则创建[Service] MemoryMax4G # 限制Ollama进程最多使用4GB内存 CPUQuota80% # 限制CPU使用率修改后执行sudo systemctl daemon-reload sudo systemctl restart ollama。卸载不用的模型Ollama默认会将最近使用的模型留在内存中。如果切换模型记得用ollama rm model-name卸载旧的模型释放内存。优化Whisper模型使用tiny版本而非base版本并使用int8量化compute_typeint8可以节省近百MB内存。4.3 延迟优化从语音到动作的流水线整个系统的端到端延迟从说完话到机器人开始动是体验的关键。延迟主要来自三部分ASR处理时间、LLM推理时间、机器人运动规划时间。实测数据与优化ASR延迟使用Whisper-tiny处理3秒音频块在Orin Nano的CPU上约需0.8-1.2秒。优化方向使用更小的音频块如1秒但会降低识别精度或者尝试使用GPU运行Whisperdevicecuda但这需要配置好CUDA和CTranslate2的GPU支持可能会增加复杂性。LLM推理延迟对于一条简单的指令“拿起杯子”Qwen2.5-1.5B-q4_0生成约50个token的JSON响应需要3-4秒。这是最大的瓶颈。优化方向提示词精简尽可能缩短系统提示词的长度减少不必要的描述。使用num_predict参数在Ollama API请求中明确限制生成token的数量避免模型“说废话”。考虑流式响应Ollama API支持流式响应stream: true。虽然对于JSON输出我们需要完整的响应才能解析但流式可以让我们在模型生成完第一个}JSON对象结束后就立即开始解析和执行而不是等整个响应结束这可以节省一点时间。流水线并行一个重要的技巧是不要让程序串行地等待每一步完成。可以采用生产者-消费者模式ASR模块持续识别一旦识别出一句完整的话例如检测到静音端点就立刻放入队列主控程序从队列中取指令发送给LLM在LLM推理的同时机器人可以执行上一条指令的后续动作如果安全的话。这样可以将ASR和LLM的等待时间重叠一部分。4.4 稳定性与错误处理在实际演示中最怕的就是机器人因为一个未处理的异常而“僵住”或者做出危险动作。必须加入的健壮性设计LLM输出格式校验如前所述必须有强大的JSON解析和fallback机制。指令安全性过滤在系统提示词中要求LLM检查安全风险是不够的。在主控程序中必须对解析出的动作进行二次校验。例如检查move_to的坐标是否在机器人的工作空间限位内检查close_gripper的力度是否超过最大值。超时与重试对Ollama API的请求设置超时如30秒并加入重试逻辑最多2-3次。网络或模型加载可能出现瞬时问题。看门狗Watchdog编写一个简单的看门狗线程定期检查ASR、LLM服务以及机器人连接是否健康。如果某个服务卡死可以尝试自动重启或进入安全模式如让机器人回到初始姿态。急停机制务必保留一个物理急停按钮或一个优先级最高的语音指令如“停下”该指令直接发送给机器人底层控制器绕过整个LLM流水线确保在任何情况下都能立即停止所有运动。5. 效果评估与未来展望部署完成后我进行了一系列测试。对于“抬起左臂”、“张开右手”、“看着我的手”这类简单明确的指令系统反应时间在5-7秒左右从说完到开始动其中LLM推理占了大头。虽然离“实时”还有距离但对于展示、教育或非紧急的辅助场景已经具备了很强的可玩性和实用性。更复杂的指令如“用左手拿起你面前的蓝色积木然后放到右边的盒子里”LLM能够分解成多个动作look_at定位move_to接近close_gripper抓取 再次move_to移动open_gripper释放并输出一个包含多个动作对象的JSON数组。这证明了小模型在特定任务提示词的引导下具备一定的任务规划能力。未来的优化方向非常明确专用小型模型微调现在的通用小模型是“万金油”。我们可以收集一批“人类指令-机器人动作序列”的配对数据在Qwen2.5-1.5B这样的基座模型上进行LoRA微调。微调后的模型会极度擅长将特定领域的自然语言映射为结构化指令格式错误率会大幅下降推理速度也可能因为任务更专注而间接提升。硬件加速探索深入研究Jetson平台上的TensorRT-LLM或NVIDIA Triton Inference Server将Ollama替换为这些工业级推理服务器可能获得更好的性能和资源管理。多模态输入目前只用了语音。Reachy Mini有摄像头完全可以接入一个本地部署的轻量级视觉语言模型VLM如MiniCPM-V或Qwen-VL的小尺寸版本。实现“看着这个红色的方块把它推下去”这类需要视觉 grounding 的指令。上下文记忆让机器人能记住之前的对话和状态实现多轮交互。这需要在主控程序中维护一个对话历史并在每次请求时将其作为上下文传递给LLM。这个项目从构思到实现最大的感触是在边缘设备上部署AI应用永远是在性能、精度、功耗和成本之间做精巧的平衡。没有完美的方案只有针对特定场景的最优解。通过Ollama这样的工具我们得以将曾经需要庞大算力的LLM能力塞进一个巴掌大的机器人“大脑”里这本身就是一件充满成就感的事情。当你对着机器人说出指令并看到它真的尝试去理解并执行时那种感觉远比调用一个云端API要来得真实和有趣得多。
基于Jetson Orin Nano与Ollama的机器人本地语音LLM部署实战
1. 项目缘起为什么要在机器人上跑本地语音LLM最近在折腾我的Reachy Mini机器人一个很酷的开源双臂机器人平台。它本身已经具备不错的视觉和运动能力但交互方式主要还是靠预设的程序或者远程API调用。我一直想给它加上一个更“自然”的交互入口——语音。想象一下你直接对它说“把那个红色的方块拿给我”它就能理解并执行这体验感一下子就上来了。市面上现成的语音助手方案很多比如接个智能音箱的API或者用云端的语音转文本STT和大语言模型LLM服务。但把这些方案用在机器人上有几个硬伤首先是延迟云端API的网络往返时间在机器人实时控制场景下是不可接受的一个指令等个一两秒体验就全毁了。其次是隐私和成本机器人摄像头和麦克风采集的音频、视频数据全部上传到第三方对于很多实验室、创客空间或者有数据安全顾虑的场景来说是个大问题。最后是可控性云端模型是个黑盒你很难针对机器人特定的指令集比如“向左转30度”、“夹爪力度设为0.5牛”去做定制化的优化和调试。所以本地部署就成了一个必然的选择。把语音识别和语言模型都跑在机器人“体内”的计算单元上实现端到端的低延迟、高隐私的智能交互。我手头的Reachy Mini配套的计算单元是reComputer Mini这是一款基于NVIDIA Jetson Orin Nano模组的小型工控机性能对于边缘AI应用来说相当不错。这个项目的核心目标就是把一个轻量级的语音LLM pipeline从云端“搬”到这台reComputer Mini上让Reachy Mini真正能“听懂人话”并基于理解做出响应。2. 硬件与软件栈选型为什么是它们在开始动手之前得先把“用什么”和“为什么用”这两个问题搞清楚。硬件是给定的reComputer Mini但软件栈的每一个组件都需要仔细考量。2.1 核心硬件reComputer Mini (Jetson Orin Nano) 的能力边界reComputer Mini的核心是NVIDIA Jetson Orin Nano模组。我用的这款是8GB内存的版本。对于部署LLM来说内存是首要的硬约束。一个参数量为7B70亿的模型以FP16精度加载大约需要14GB的显存这显然超出了Orin Nano的能力。因此我们的模型选型必须瞄准更小的参数规模比如1B到3B左右的模型或者使用量化技术如INT4, INT8来大幅降低显存占用。Orin Nano的CPU是ARM架构的GPU则基于NVIDIA Ampere架构拥有少量但效能不错的CUDA核心支持TensorRT等加速库。这意味着我们虽然不能跑动辄百亿参数的大模型但针对小模型进行合理的优化后获得可用的推理速度比如每秒生成10-20个token是完全可行的。另一个优势是功耗和体积它非常适合作为机器人的“大脑”集成。2.2 模型服务框架为什么选择OllamaOllama近半年在开发者社区里火得不行它本质上是一个简化本地大模型部署和运行的工具。相比于直接去Hugging Face下载模型文件然后用Transformers库加载Ollama提供了几个关键优势开箱即用一条命令就能拉取、运行一个模型自动处理模型格式GGUF、上下文长度等配置。统一的API通过简单的REST API默认端口11434提供模型交互这极大简化了应用程序比如我们的机器人控制程序与模型之间的集成。我们不需要关心模型底层的加载和推理细节。活跃的社区和丰富的模型库Ollama官方维护了一个模型库ollama.com/library里面有大量预量化好的热门模型如Llama 3、Qwen、DeepSeek等而且很多都提供了从1.5B到8B不等的多种尺寸方便我们根据硬件能力选择。对ARM平台的支持Ollama提供了Linux ARM64的安装包这是能在Jetson上运行的前提。当然Ollama也不是唯一选择。像llama.cpp、text-generation-webui等也是优秀的本地部署方案。但Ollama在易用性和API标准化方面做得最好特别适合我们这种需要将LLM能力快速集成到另一个系统机器人系统中的场景。2.3 语音处理流水线设计一个完整的“语音指令-机器人动作”流程需要以下几个步骤语音采集通过USB麦克风或reComputer Mini的音频接口采集音频流。语音识别ASR将音频流实时转换为文本。这里也需要一个本地、轻量级的模型。我选择了OpenAI开源的Whisper模型的“tiny”或“base”版本。虽然Whisper本身不算特别小但其tiny版本约39M参数在Orin Nano上实时运行是可行的准确度对于近场、指令式语音也足够。文本理解与生成LLM将识别出的文本指令送入本地部署的Ollama服务中的小语言模型。这个模型需要完成两项任务一是理解用户的自然语言指令如“请轻轻拿起桌上的杯子”二是将其转换成结构化的、Reachy Mini SDK能够理解的命令或参数如arm.move_to(x, y, z)gripper.open()。这里涉及到“提示词工程”Prompt Engineering我们需要精心设计一个系统提示词System Prompt告诉LLM它现在是一个机器人控制专家并且输出格式必须是固定的JSON。指令执行我们的主控程序用Python编写解析LLM输出的JSON调用Reachy Mini的Python SDKreachy-sdk来执行相应的动作。整个流水线的瓶颈很可能会在ASR和LLM推理环节。因此模型选型和量化等级的选择至关重要。3. 实战部署一步步搭建本地语音LLM系统理论说完了接下来是实操环节。我会尽量详述每一步包括可能遇到的坑和解决办法。3.1 基础环境准备在reComputer Mini上安装OllamareComputer Mini预装了Ubuntu 20.04或22.04。首先通过SSH连接到设备。第一步安装OllamaOllama提供了便捷的一键安装脚本。但由于网络原因直接从官网下载可能会非常慢甚至失败。这里就需要用到国内镜像源这是第一个实战技巧。# 方法一使用中科大镜像源加速安装推荐 curl -fsSL https://ollama.com/install.sh | OLLAMA_HOSThttps://mirrors.ustc.edu.cn/ollama sh这条命令的关键在于设置OLLAMA_HOST环境变量指向中科大的镜像站这样下载安装包和后续拉取模型都会走国内CDN速度有质的飞跃。安装完成后启动Ollama服务并设置为开机自启# 启动服务 sudo systemctl start ollama # 设置开机自启 sudo systemctl enable ollama # 查看服务状态 sudo systemctl status ollama如果状态显示为active (running)说明服务启动成功。第二步验证安装并运行第一个模型Ollama服务启动后默认会在本地11434端口提供一个API服务。我们先拉取一个超小模型进行测试比如TinyLlama约1.1B参数。# 拉取模型同样会走镜像源速度较快 ollama pull tinyllama # 运行模型并与它交互 ollama run tinyllama在出现的提示符后输入Hello看看它是否能正常回复。这一步是为了验证Ollama基础功能是否正常。一个关键配置修改Ollama模型存储路径默认情况下Ollama会把模型下载到~/.ollama/models目录。对于Jetson设备其系统盘空间通常有限。我们可以将其修改到挂载的大容量SD卡或USB存储设备上。首先查看Ollama的服务配置文件sudo systemctl edit ollama.service这会打开一个临时编辑器在其中添加以下内容将/path/to/your/large/disk替换为你的大容量存储路径[Service] EnvironmentOLLAMA_MODELS/path/to/your/large/disk/ollama-models保存退出后重新加载systemd配置并重启Ollamasudo systemctl daemon-reload sudo systemctl restart ollama之后所有通过ollama pull下载的模型都会存储在新的路径下。3.2 语音识别模块集成本地WhisperOllama负责LLM我们还需要一个ASR引擎。这里选择faster-whisper它是Whisper的一个CTranslate2实现推理速度更快内存占用更少非常适合边缘设备。安装faster-whisper及其依赖# 更新包列表 sudo apt update # 安装Python3和pip如果尚未安装 sudo apt install python3-pip -y # 安装faster-whisper pip3 install faster-whisper在Jetson ARM平台上直接pip安装faster-whisper可能会因为需要编译一些C依赖而失败。一个更稳妥的方法是使用NVIDIA JetPack SDK中已经优化过的库或者安装预编译的wheel包。如果遇到困难可以回退到使用原始的openai-whisper包pip install openai-whisper虽然速度慢一点但兼容性最好。编写一个简单的语音识别脚本创建一个Python文件asr_server.py实现一个持续监听麦克风、进行实时语音识别的服务。import queue import sounddevice as sd import numpy as np from faster_whisper import WhisperModel # 配置参数 MODEL_SIZE tiny # 可选 tiny, base, small。tiny最快base折中。 DEVICE cpu # 在Orin Nano上先用CPU。如果CUDA配置好可尝试cuda COMPUTE_TYPE int8 # 量化类型降低内存和计算量 # 加载模型 print(fLoading Whisper {MODEL_SIZE} model...) model WhisperModel(MODEL_SIZE, deviceDEVICE, compute_typeCOMPUTE_TYPE) # 音频参数 SAMPLE_RATE 16000 CHUNK_DURATION 3 # 每次处理3秒的音频块 CHUNK_SIZE int(SAMPLE_RATE * CHUNK_DURATION) audio_queue queue.Queue() def audio_callback(indata, frames, time, status): 音频回调函数将数据放入队列 if status: print(fAudio callback status: {status}) audio_queue.put(indata.copy()) def transcribe_audio(audio_data): 转录音频数据 audio_np np.concatenate(audio_data, axis0).astype(np.float32).flatten() # 使用Whisper进行识别 segments, info model.transcribe(audio_np, beam_size5, languagezh) text .join([seg.text for seg in segments]).strip() return text # 开始录音流 print(开始监听请说话...) stream sd.InputStream(callbackaudio_callback, channels1, samplerateSAMPLE_RATE, blocksizeCHUNK_SIZE) stream.start() try: while True: # 收集一定时长的音频 audio_chunks [] for _ in range(int(2 / CHUNK_DURATION)): # 例如收集2秒音频再识别 audio_chunks.append(audio_queue.get()) if audio_chunks: text transcribe_audio(audio_chunks) if text: # 只有识别到内容才打印 print(f识别结果: {text}) # 这里可以将text发送给LLM处理模块 except KeyboardInterrupt: print(\n停止监听) finally: stream.stop() stream.close()这个脚本创建了一个简单的实时语音识别循环。你需要先通过pip install sounddevice安装音频库。注意在Jetson上使用USB麦克风时可能需要通过arecord -l查看设备ID并在sd.InputStream中指定device参数。3.3 LLM模型选型与提示词工程这是项目的核心智能部分。我们需要一个足够小、足够快但理解能力和指令跟随能力又不错的模型。模型选择经过测试以下几个模型在Orin Nano 8GB上表现较为均衡使用Ollama的q4_0或q8_0量化版本Qwen2.5-1.5B-Instruct通义千问的小尺寸版本中文理解好指令跟随能力强响应速度快。Llama 3.2-1B-InstructMeta出品英文指令处理非常出色代码生成和结构化输出能力在同尺寸中领先。Phi-3-mini-3.8B微软的模型虽然参数稍大3.8B但因其卓越的“小身材大能量”特性经过q4_0量化后仍可在Orin Nano上运行且综合能力很强。我最终选择了Qwen2.5-1.5B-Instruct因为我们的指令主要是中文且它对结构化JSON输出的遵循性很好。用Ollama拉取它ollama pull qwen2.5:1.5b-instruct-q4_0设计系统提示词System Prompt提示词的质量直接决定了LLM能否输出我们想要的、可解析的指令。我们的目标是让LLM成为一个“机器人指令翻译官”。创建一个robot_prompt.txt文件内容如下你是一个机器人控制助手。你的任务是将用户的自然语言指令翻译成Reachy Mini机器人可以执行的、结构化的JSON命令。 Reachy Mini机器人有以下可控制部件 - 左臂 (left_arm) - 右臂 (right_arm) - 头部 (head可以转动) - 左夹爪 (left_gripper) - 右夹爪 (right_gripper) 可能的动作类型action包括 - move_to: 将机械臂末端移动到指定坐标[x, y, z]单位米。坐标系原点在机器人底座中心。 - move_joints: 控制机械臂的关节角度[j1, j2, j3, j4, j5, j6, j7]单位弧度。 - look_at: 控制头部看向某个坐标[x, y, z]。 - open_gripper: 打开夹爪。 - close_gripper: 闭合夹爪。 - set_gripper_force: 设置夹爪力度单位牛顿。 你输出的必须是且仅是一个合法的JSON对象格式如下 { actions: [ { part: left_arm, // 或 right_arm, head, left_gripper, right_gripper action: move_to, // 动作类型 parameters: { // 动作参数根据action类型不同而不同 x: 0.2, y: 0.1, z: 0.3 } } // ... 可以包含多个顺序执行的动作 ] } 如果用户的指令不明确、无法实现或存在安全风险如让机器人伤害自己或他人请在JSON中返回一个错误信息格式如下 { error: 具体的错误描述信息 } 现在请处理用户的指令。这个提示词明确了机器人的能力边界、输出格式并加入了安全校验。我们将把这个提示词作为每次对话的“系统指令”发送给Ollama。3.4 集成与主控程序编写现在我们需要编写一个主控Python程序它需要做三件事从ASR模块获取文本指令。将文本指令和系统提示词组合发送给Ollama API。解析Ollama返回的JSON调用Reachy SDK执行动作。首先安装Reachy SDK和requests库pip3 install reachy-sdk requests主控程序robot_llm_controller.py的核心逻辑如下import json import requests import time from reachy_sdk import ReachySDK from reachy_sdk.trajectory import goto # 假设我们从asr_server.py通过某种方式如队列、Socket获取文本 # 这里简化处理从标准输入读取模拟指令 import sys class RobotLLMController: def __init__(self, ollama_hosthttp://localhost:11434, model_nameqwen2.5:1.5b-instruct-q4_0): self.ollama_url f{ollama_host}/api/generate self.model_name model_name # 加载系统提示词 with open(robot_prompt.txt, r, encodingutf-8) as f: self.system_prompt f.read() # 连接到Reachy Mini假设机器人IP是192.168.1.100 try: self.reachy ReachySDK(host192.168.1.100) print(成功连接到Reachy Mini) except Exception as e: print(f连接Reachy失败: {e}) self.reachy None def ask_llm(self, user_input): 向Ollama发送请求获取结构化指令 payload { model: self.model_name, prompt: user_input, system: self.system_prompt, # 关键传入系统提示词 stream: False, options: { temperature: 0.1, # 低温度让输出更确定、更遵循格式 num_predict: 150 # 限制生成长度避免废话 } } try: response requests.post(self.ollama_url, jsonpayload, timeout30) response.raise_for_status() result response.json() return result[response] except requests.exceptions.RequestException as e: return json.dumps({error: fLLM请求失败: {e}}) except (KeyError, json.JSONDecodeError) as e: return json.dumps({error: fLLM响应解析失败: {e}}) def parse_and_execute(self, llm_response): 解析LLM的JSON响应并执行机器人动作 try: command json.loads(llm_response) except json.JSONDecodeError: print(f无法解析LLM响应为JSON: {llm_response}) return if error in command: print(fLLM返回错误: {command[error]}) # 可以在这里让机器人通过语音或灯光提示错误 return if actions not in command or not isinstance(command[actions], list): print(f响应中缺少有效的actions列表: {command}) return if self.reachy is None: print(未连接到机器人仅模拟执行) # 模拟执行打印动作 for action in command[actions]: print(f模拟执行: {action}) return # 实际执行动作 for action_spec in command[actions]: part action_spec.get(part) action action_spec.get(action) params action_spec.get(parameters, {}) try: robot_part getattr(self.reachy, part, None) if robot_part is None: print(f未找到机器人部件: {part}) continue if action move_to: # 注意reachy_sdk的移动API可能需要使用goto或特定方法 # 这里是一个示例实际API请查阅SDK文档 target_pos [params.get(x, 0), params.get(y, 0), params.get(z, 0)] # 假设robot_part有goto方法这是一个简化示例 # goto({robot_part: target_pos}, duration2.0) print(f移动 {part} 到位置 {target_pos}) elif action open_gripper: robot_part.open() print(f打开 {part}) elif action close_gripper: robot_part.close() print(f闭合 {part}) # ... 处理其他动作类型 else: print(f不支持的动作类型: {action}) except Exception as e: print(f执行动作 {action} 于 {part} 时出错: {e}) time.sleep(0.5) # 动作间短暂停顿 def run(self): 主循环从输入获取指令处理执行 print(机器人语音LLM控制器已启动。请输入指令或从ASR模块获取:) while True: # 这里替换为从ASR模块获取文本例如 # user_text asr_module.get_latest_transcription() user_text input( ).strip() if user_text.lower() in [quit, exit, q]: break if not user_text: continue print(f用户指令: {user_text}) llm_output self.ask_llm(user_text) print(fLLM原始输出:\n{llm_output}) self.parse_and_execute(llm_output) if __name__ __main__: controller RobotLLMController() controller.run()这个程序是一个框架。你需要将获取用户输入的部分input( )替换为与你的asr_server.py的实际集成比如通过一个共享的线程安全队列queue.Queue或者一个简单的WebSocket/RPC接口。同时执行机器人动作的部分需要根据reachy-sdk的实际API进行完善。4. 性能调优与踩坑实录把整个系统跑起来只是第一步让它跑得“流畅可用”才是挑战的开始。以下是几个关键的调优点和我踩过的坑。4.1 模型量化与推理速度的权衡Ollama拉取的模型标签如q4_0、q8_0就代表了量化等级。数字越小如q4模型体积越小推理速度越快但精度损失也越大可能导致模型“智力下降”输出胡言乱语或无法遵循指令格式。实测对比在Orin Nano上qwen2.5:1.5b-instruct-q4_0 模型文件约900MB推理速度约15 tokens/秒。大部分指令能正确理解并输出JSON但偶尔会在JSON外生成多余的解释文字需要我们在程序里做后处理如用正则表达式提取第一个{}之间的内容。qwen2.5:1.5b-instruct-q8_0 模型文件约1.5GB推理速度约10 tokens/秒。输出格式更稳定几乎不会出现格式错误理解能力也稍强。qwen2.5:1.5b-instruct-fp16 模型文件约3GB内存占用过高在8GB设备上与其他服务如Whisper同时运行极易导致OOM内存溢出不推荐。结论与建议对于机器人指令这种对格式要求严苛、但语义相对简单的任务q4_0量化在速度和内存占用上优势明显。为了应对其偶尔的格式错误必须在主控程序中加入健壮的输出清洗和解析逻辑。例如import re def extract_json_from_response(llm_output): # 尝试匹配第一个完整的JSON对象 match re.search(r\{[^{}]*\}, llm_output) if match: try: return json.loads(match.group()) except json.JSONDecodeError: pass # 如果匹配失败返回错误 return {error: LLM输出无法解析为JSON}4.2 内存管理避免OOM杀手Jetson Orin Nano 8GB的内存是共享的GPU和CPU共用。同时运行Ollama加载LLM、Whisper语音识别、Python主程序以及Reachy SDK的通信模块内存压力很大。监控与优化手段使用tegrastats监控NVIDIA提供了tegrastats工具可以实时查看CPU、GPU、内存、功耗等信息。运行sudo tegrastats重点关注RAM和SWAP的使用情况。为Ollama设置运行限制可以通过修改Ollama的系统服务文件限制其可用的CPU和内存。编辑/etc/systemd/system/ollama.service.d/override.conf如果没有则创建[Service] MemoryMax4G # 限制Ollama进程最多使用4GB内存 CPUQuota80% # 限制CPU使用率修改后执行sudo systemctl daemon-reload sudo systemctl restart ollama。卸载不用的模型Ollama默认会将最近使用的模型留在内存中。如果切换模型记得用ollama rm model-name卸载旧的模型释放内存。优化Whisper模型使用tiny版本而非base版本并使用int8量化compute_typeint8可以节省近百MB内存。4.3 延迟优化从语音到动作的流水线整个系统的端到端延迟从说完话到机器人开始动是体验的关键。延迟主要来自三部分ASR处理时间、LLM推理时间、机器人运动规划时间。实测数据与优化ASR延迟使用Whisper-tiny处理3秒音频块在Orin Nano的CPU上约需0.8-1.2秒。优化方向使用更小的音频块如1秒但会降低识别精度或者尝试使用GPU运行Whisperdevicecuda但这需要配置好CUDA和CTranslate2的GPU支持可能会增加复杂性。LLM推理延迟对于一条简单的指令“拿起杯子”Qwen2.5-1.5B-q4_0生成约50个token的JSON响应需要3-4秒。这是最大的瓶颈。优化方向提示词精简尽可能缩短系统提示词的长度减少不必要的描述。使用num_predict参数在Ollama API请求中明确限制生成token的数量避免模型“说废话”。考虑流式响应Ollama API支持流式响应stream: true。虽然对于JSON输出我们需要完整的响应才能解析但流式可以让我们在模型生成完第一个}JSON对象结束后就立即开始解析和执行而不是等整个响应结束这可以节省一点时间。流水线并行一个重要的技巧是不要让程序串行地等待每一步完成。可以采用生产者-消费者模式ASR模块持续识别一旦识别出一句完整的话例如检测到静音端点就立刻放入队列主控程序从队列中取指令发送给LLM在LLM推理的同时机器人可以执行上一条指令的后续动作如果安全的话。这样可以将ASR和LLM的等待时间重叠一部分。4.4 稳定性与错误处理在实际演示中最怕的就是机器人因为一个未处理的异常而“僵住”或者做出危险动作。必须加入的健壮性设计LLM输出格式校验如前所述必须有强大的JSON解析和fallback机制。指令安全性过滤在系统提示词中要求LLM检查安全风险是不够的。在主控程序中必须对解析出的动作进行二次校验。例如检查move_to的坐标是否在机器人的工作空间限位内检查close_gripper的力度是否超过最大值。超时与重试对Ollama API的请求设置超时如30秒并加入重试逻辑最多2-3次。网络或模型加载可能出现瞬时问题。看门狗Watchdog编写一个简单的看门狗线程定期检查ASR、LLM服务以及机器人连接是否健康。如果某个服务卡死可以尝试自动重启或进入安全模式如让机器人回到初始姿态。急停机制务必保留一个物理急停按钮或一个优先级最高的语音指令如“停下”该指令直接发送给机器人底层控制器绕过整个LLM流水线确保在任何情况下都能立即停止所有运动。5. 效果评估与未来展望部署完成后我进行了一系列测试。对于“抬起左臂”、“张开右手”、“看着我的手”这类简单明确的指令系统反应时间在5-7秒左右从说完到开始动其中LLM推理占了大头。虽然离“实时”还有距离但对于展示、教育或非紧急的辅助场景已经具备了很强的可玩性和实用性。更复杂的指令如“用左手拿起你面前的蓝色积木然后放到右边的盒子里”LLM能够分解成多个动作look_at定位move_to接近close_gripper抓取 再次move_to移动open_gripper释放并输出一个包含多个动作对象的JSON数组。这证明了小模型在特定任务提示词的引导下具备一定的任务规划能力。未来的优化方向非常明确专用小型模型微调现在的通用小模型是“万金油”。我们可以收集一批“人类指令-机器人动作序列”的配对数据在Qwen2.5-1.5B这样的基座模型上进行LoRA微调。微调后的模型会极度擅长将特定领域的自然语言映射为结构化指令格式错误率会大幅下降推理速度也可能因为任务更专注而间接提升。硬件加速探索深入研究Jetson平台上的TensorRT-LLM或NVIDIA Triton Inference Server将Ollama替换为这些工业级推理服务器可能获得更好的性能和资源管理。多模态输入目前只用了语音。Reachy Mini有摄像头完全可以接入一个本地部署的轻量级视觉语言模型VLM如MiniCPM-V或Qwen-VL的小尺寸版本。实现“看着这个红色的方块把它推下去”这类需要视觉 grounding 的指令。上下文记忆让机器人能记住之前的对话和状态实现多轮交互。这需要在主控程序中维护一个对话历史并在每次请求时将其作为上下文传递给LLM。这个项目从构思到实现最大的感触是在边缘设备上部署AI应用永远是在性能、精度、功耗和成本之间做精巧的平衡。没有完美的方案只有针对特定场景的最优解。通过Ollama这样的工具我们得以将曾经需要庞大算力的LLM能力塞进一个巴掌大的机器人“大脑”里这本身就是一件充满成就感的事情。当你对着机器人说出指令并看到它真的尝试去理解并执行时那种感觉远比调用一个云端API要来得真实和有趣得多。