把Qwen2.5-7B跑在Jetson Orin上,让机器人听懂人话、看懂场景、自己规划路径完成搬运,再把全过程的实时状态推到Web端和小程序上——这是我最近折腾完的一套多模态AI大模型智能机器人全栈实践平台。真正落地后你会发现,它既是一个能跑的具身智能实验载体,也是一条从算法到工程的全栈学习链路。文章会从架构选型、大模型接入、感知导航、机械臂控制,到部署调参和常见坑位逐一梳理,适合正在做具身智能课题的学生、想转AI全栈的开发者,以及需要搭建智能搬运展示平台的工程师参考。
1. 项目整体设计与架构拆解
1.1 这不是一台普通AGV,而是一个具身智能载体
传统AGV在工厂里已经用了很多年,但本质上是“循迹机器人”:磁条、二维码、固定路线,碰到指令变化就抓瞎。你没法对它说“把西南角工位上那个红色扳手拿过来”,因为它既不理解“西南角”是哪里,也不认识“扳手”,更不知道“拿过来”对应哪组运动指令。
这套平台解决的正是这个问题。我用大模型作为机器人的“大脑”,把自然语言指令拆解成机器人能执行的动作序列;用多模态AI把摄像头画面、语音、文字提示统一理解;再用ROS 2控制底盘和机械臂去执行。这三点合在一起,就是当前讨论度非常高的“具身智能”——AI不再只停留在云端回答问题,而是有手有脚,能跟物理世界交互。
为什么叫“复合型实践平台”?因为它的复合体现在两个维度。第一是技术栈的复合:前端、后端、算法、模型部署、嵌入式控制全都有涉及;第二是软硬件耦合的复合:既要处理GPU上和模型相关的计算,又要实时处理激光雷达、电机驱动这些底层信号。对实训和教学来说,一个项目横跨十几个技术点,本身就是极好的练手素材。
从项目结构来看,我把它拆成了四个模块:感知层(激光雷达、深度相机、麦克风阵列)、决策层(本地大模型、任务解析、动作编排)、执行层(移动底盘、机械臂、夹爪)、应用层(Golang后端、Vue3管理后台、UniApp小程序)。这四个模块不是彼此独立的,而是通过消息总线串成一条完整链路,后面我会逐个说明。
1.2 全栈架构与技术栈选型背后的逻辑
以下是最终的选型清单,先给个总览:
| 层级 | 技术选型 | 核心职责 |
|---|---|---|
| 大模型推理 | Ollama + Qwen2.5-7B-Instruct / Qwen2-VL-7B | 意图理解、任务分解、多模态识别 |
| 机器人算法 | ROS 2 Humble + Nav2 + Cartographer + MoveIt | 建图、导航、机械臂运动规划 |
| 硬件 | Jetson Orin NX 16GB + 差速底盘 + 6自由度机械臂 + RPLIDAR A1 + RealSense D435i | 运行算力、移动、抓取、感知 |
| 服务端 | Golang + Gin + Redis + MySQL | 任务管理、设备管理、SSE推送 |
| 前端 | Vue3 + TypeScript + Element Plus | 管理后台、实时监控、任务面板 |
| 移动端 | UniApp | 小程序 / App 多端发布 |
| 通信 | HTTP + WebSocket + MQTT | 端到端消息流转 |
这套选型不是拍脑袋定的,每个选择都有明确理由。
大模型推理框架选Ollama,原因有三:一是本地部署,机器人任务数据不用上传云端,隐私和响应延迟都可控;二是它提供了OpenAI兼容接口,后端对接成本极低;三是GGUF量化模型在消费级设备和嵌入式设备上跑得很成熟,Qwen系列的中文指令跟随能力在同参数量里又是第一梯队。
后端为什么选Golang而不是Spring Boot或Node.js?因为这套平台的核心后端不是业务CRUD,而是高频任务调度、多设备长连接、状态同步。Golang的goroutine处理并发生来就轻,编译产物是单个二进制,部署到机器人板子上非常方便。当然,如果你的团队更熟Java或Node,用Spring Boot也完全可行——架构层面不锁死语言。
前端选Vue3 + UniApp,是为了“多端复用”。管理后台用Vue3做PC端大屏,现场操作端用UniApp编译成小程序,同一套数据接口两者共用,开发量省了三分之一以上。很多全栈实训项目特别喜欢这种组合,因为能打包展示“一套代码多端运行”的工程能力。
机器人算法层没有悬念,ROS 2是目前社区最成熟的选择。Humble版本稳定,Nav2导航栈开箱即用,Cartographer建图和MoveIt机械臂规划都有大量现成方案参考。这里要提醒一句:ROS 2的学习曲线不算平缓,如果团队完全没有ROS基础,优先跑通官方tutorials再碰机器人实机,否则排错会非常痛苦。
2. 多模态大模型接入与决策链路
2.1 模型选型:端上跑得动,又足够聪明的平衡点
先聊一个最现实的问题:机器人板子上的算力有限,到底选多大的模型?
我的经验是7B参数量是Jetson Orin NX 16GB的甜点区间。13B模型量化后不是不能跑,但生成速度会掉到每秒5个token左右,机器人现场执行任务时,用户在等回应,那种体验很糟糕。7B模型在Q4量化下,权重大约4.4GB,加上KV Cache和运行时开销,整体占用在8GB左右,还能给其他应用留出富余。
模型选型上我分了两套方案。第一套是纯文本方案,用Qwen2.5-7B-Instruct,重点处理“意图理解 + 参数提取”。用户说“把水杯放到实验室B区三号工位”,模型负责把这句话转成结构化指令,包括目标物体、目标位置、动作列表。第二套是多模态方案,用Qwen2-VL-7B,它能在画面中定位物体——当机器人到达目标区域后,可以通过摄像头画面判断“水杯到底在哪一层架子、什么颜色”,再引导机械臂抓取。
如果你没有Jetson这类专用板子,临时用一台带NVIDIA显卡的笔记本跑同一套模型也完全可以。消费级显卡如RX 6750 GRE或RTX 3060 12G,跑7B Q4量化模型毫无压力;如果想做模型微调,用QLoRA这类低秩微调方案,12GB显存也能塞下7B量级的训练。普通开发者不需要一上来就上多卡集群,先在一张显卡上把流程跑通,再考虑扩大规模。
实测下来,Orin NX上7B Q4的推理速度能做到8到12 token/s。这个数字看起来不快,但要注意使用场景:机器人任务解析不需要长篇大论,模型输出只要一两句JSON,一秒钟之内就能出结果。真正需要长文本生成的是用户问答,但问答功能完全可以放在云端的高性能GPU上,端侧只保留任务解析能力。
2.2 Prompt工程与工具调用:让模型“说人话”变成“干实事”
把大模型接进机器人,最常见的误区是把它当成聊天机器人来用。用户说一句“帮我搬个东西”,模型回一句“好的,我这就帮您搬”——然后呢?没有然后,因为这句话里没有任何可执行的结构。
我的做法是给模型定一套强约束的“任务解析协议”。系统提示词里明确告诉它:你是移动机器人的任务解析器,只输出JSON,不解释、不寒暄。同时给出几个few-shot示例,让它理解输出格式。举一个实际用的Prompt模板:
你是移动机器人任务解析器。根据用户指令输出JSON,格式如下: { "intent": "navigate | pick | place | status | unknown", "target_object": "目标物体名称,没有则为空", "target_position": "目标位置编号,例如A1、B3", "actions": ["navigate", "pick", "place"] } 约束: 1. 只输出JSON,不要任何额外文字。 2. 如果指令中缺少必要信息,intent返回unknown,并在actions中返回空数组。 3. 你只能基于给定位置编号和物体名称做映射,不要编造内容。为什么这套模板有效?因为大模型本质上是“概率续写器”,你给它结构化的预期,它就更倾向于输出结构化的结果。我试过不给few-shot直接让它输出,结果它非常喜欢在JSON外面包一层markdown格式的代码块,解析器一接就炸。加了约束之后,失败率从20%降到了2%以内。
除了固定JSON,Ollama还支持Function Calling功能。你可以把机器人的动作封装成函数注册给模型,比如navigate_to(position),grasp_object(name),place_object(position),模型根据用户语义自主决定调用哪个函数。这种模式更灵活,但排错难度也更高。我的建议是:初期先用固定JSON解析,把整条链路跑通后再升级到Function Calling,出了问题好定位。
2.3 接口层实现:SSE流式输出与结构化返回
机器人的大模型接口和普通AI应用有个重要区别:前端既要看到“思考过程”,后端又要拿到“最终结果”,两条通道不能混在一起。
我的实现方案是:前端交互界面用SSE(Server-Sent Events)流式展示模型的token输出,让用户实时看到任务状态;后端在模型完成生成后,再做一次JSON解析和校验,把可靠的结构化指令发给机器人调度模块。
SSE的好处这里多说一句。很多人习惯用WebSocket,但在这个场景里,消息流向是单向的——服务端往前端推状态,前端很少需要长连接回传数据。SSE天然基于HTTP,断线自动重连,实现代码量只有WebSocket的一半,前端原生EventSource就能接,不需要额外引入Socket.io这类库。
Golang后端核心逻辑大致如下:
func handleStream(c *gin.Context) { // 从请求参数里取用户指令 input := c.Query("input") // 构造Ollama请求体 reqBody := map[string]interface{}{ "model": "qwen2.5:7b-instruct-q4_K_M", "prompt": buildPrompt(input), "stream": true, } // 调用Ollama流式接口 resp, err := http.Post("http://localhost:11434/api/generate", "application/json", jsonBody) // 按行读取返回,通过SSE推送 c.Header("Content-Type", "text/event-stream") scanner := bufio.NewScanner(resp.Body) for scanner.Scan() { c.SSEvent("message", scanner.Text()) c.Writer.Flush() } }前端取消请求时,要用AbortController断开连接,同时后端要在goroutine里监听请求上下文取消事件,及时终止对Ollama的调用。否则会出现一种很尴尬的情况:用户已经取消任务,机器人还在继续执行。
3. 智能搬运的感知、导航与运动控制
3.1 栅格地图构建与路径规划
“全场景智能搬运”的地基,是让机器人知道自己在哪里、要去哪里、路上有没有障碍。这一环靠的是栅格地图。
栅格地图很好理解,就是把环境切成一个个小格子,每个格子标记三种状态:空闲、占用、未知。机器人用激光雷达扫描一圈环境,把墙、柱子、货架都标成占用格,把可通行区域标成空闲格,这张格子图就是机器人的“世界模型”。
建图我推荐Cartographer。ROS 2下Gmapping也能用,但Cartographer的闭环检测能力明显更强,在大一点的场地里不容易出现地图重影。建图时有几个关键点:
- 雷达安装位置要水平,倾斜会让地图失真。
- 推动或遥控机器人建图时,速度控制在0.2m/s以内,转弯要缓,急转弯会导致匹配失败。
- 建图完成后,用Nav2的map_saver工具保存地图,后续导航直接加载这张静态地图。
ros2 run nav2_map_server map_saver_cli -f ~/map导航规划我用Nav2栈。全局路径规划器用NavFn,在静态地图上算出一条从起点到目标点的全局路径;局部路径规划器用DWB Controller,在运动过程中实时避障。这里有一个特别容易踩的坑:代价地图的膨胀半径必须大于机器人半径,否则机器人会贴着墙走,机械臂容易蹭到障碍物。我调试时把膨胀半径设为机器人半径的1.3倍,安全性好了很多。
导航还要解决一个问题:用户说的“A3工位”怎么变成坐标点?我的方案是维护一个固定的工位点位表,存到JSON或数据库里。模型只负责把自然语言映射成点位编号,再用程序查表得到实际坐标。这样既不用让模型记住数字坐标,也方便现场调点位——挪一下桌子,只需要改表里的坐标,不用重新微调模型。
3.2 目标识别与机械臂抓取
搬运任务的最后一步是抓取,这也是整个系统里失败率最高的环节。我用RealSense D435i深度相机做视觉感知,先通过YOLOv8识别目标物体,再把图像坐标转换到机械臂坐标系。
这里必须做手眼标定。相机默认看到的坐标是相机坐标系下的,而机械臂运动用的是基座坐标系,两者之间的转换关系需要标定算出。简单说,就是控制机械臂末端去触碰画面中的已知点,采集多组数据后求出变换矩阵。标定没做好,抓取时机械臂会偏出几厘米,这在夹爪场景里就是致命失败。
抓取流程是典型的感知-规划-执行闭环:
- 视觉识别目标物,得到3D位置和朝向。
- 调用MoveIt的运动规划,计算机械臂从当前姿态到抓取姿态的关节轨迹。
- 夹爪下探到目标物附近,依据深度值判断是否到位,然后闭合夹爪。
- 夹爪闭合后,检测夹爪电流变化,确认是否真的抓住了目标物。
抓取容错是另一个容易被忽略的点。视觉识别出的坐标总有误差,夹爪如果做成“非要一次精准定位”的逻辑,成功率不会超过五成。我的做法是给夹爪加一个柔性下探策略:先移动到目标上方10cm位置,然后以2cm/s的速度缓慢下探,同时实时读取深度相机数据,当检测到与目标表面距离小于阈值时,立即停止下探并夹合。这套策略把抓取成功率从50%提到了85%以上。
3.3 任务状态机与多任务调度
把导航、抓取、放置这些独立动作串起来,需要一个清晰的任务状态机。我在调度模块里定义了六个状态:空闲、导航中、抓取中、搬运中、放置中、汇报中。用户下发一条新指令后,任务解析结果不是直接执行,而是先转换成状态机驱动事件。
状态机的好处是让系统知道“我现在进行到哪一步了”。搬运过程中底盘运动到一半,相机突然掉线,这时候调度模块好歹能知道机器人正处于“导航中”,可以做安全停车而不是直接傻掉。每一步状态变化都会推送给前端,用户在小程序上就能看到实时进度。
多任务并发不要贪多。我测试过同时下发三个任务让机器人自己排队调度,结果前一个任务卡在抓取容错循环里,后面两个任务全部拥堵。最终我改成“单任务执行,多任务排队”的策略,一个任务彻底结束后才从队列里取下一个。稳定率比并发方案高很多,对展示场景来说也够用了。
4. 环境部署与核心模块实操
4.1 硬件避坑与算力核算
先上一份可复用的硬件清单:
| 部件 | 型号建议 | 注意事项 |
|---|---|---|
| 主控 | Jetson Orin NX 16GB | 算力够用;8GB版本跑7B模型很紧张 |
| 雷达 | RPLIDAR A1 / A2 | A1性价比高,室内够用 |
| 相机 | Intel RealSense D435i | 带IMU,建图和视觉识别都能用 |
| 麦克风 | ReSpeaker V2阵列 | 做语音交互必须用阵列,单麦拾音太差 |
| 底盘 | 差速轮/麦克纳姆轮 | 室内平整地面差速轮足够 |
| 机械臂 | 6自由度桌面机械臂 | 注意负载,1kg级别够搬水杯 |
算力核算这笔账我认真算过一次。Qwen2.5-7B在Q4_K_M量化下,模型权重约4.4GB,KV Cache在上下文长度128时约1.5GB,ROS系统本身占用约3GB,CUDA运行时预留2GB。这些加起来已经超过10GB,所以16GB版本的Orin NX是底线。如果你还要同时跑YOLO做实时识别,GPU显存会很紧张,我的方案是让YOLO用TensorRT半精度推理,显存占用压缩到1GB以内,实测20帧以上的识别速度完全够用。
后台管理的服务器比较普通,一台4核8GB的云主机就能跑起来,Golang后端加MySQL和Redis非常轻量。重活都在机器人板卡上,云主机只负责任务中转和状态存储。
4.2 软件环境搭建步骤
完整部署流程按下面这个顺序走,出问题最少:
第一步,给Jetson刷JetPack 5.1.2,自带Ubuntu 22.04和CUDA 11.8环境。系统装完先装深度学习依赖,用conda管理Python环境,避免和ROS 2的Python依赖打架。
第二步,安装ROS 2 Humble。官方apt源安装最省事,装完建议装一个ros-dev-tools,后面编译工作空间用得上。
第三步,安装Ollama并拉取模型:
curl -fsSL https://ollama.com/install.sh | sh ollama pull qwen2.5:7b-instruct-q4_K_M ollama serve确认服务起来后,写个简单脚本测试一下接口连通性。这一步务必单独验证,别等整套系统联调时才发现模型没加载成功。
第四步,建ROS 2工作空间,编译Nav2、Cartographer、MoveIt相关依赖包。这一步极其耗时,建议提前半天到一天预留编译时间,不要等到演示当天才编译。
第五步,启动Golang后端,配置Ollama地址、Redis地址、MySQL连接串。后端启动后先跑一遍单元测试,确保任务解析接口能正常返回结构化JSON。
第六步,启动Vue3前端和UniApp壳工程,登录后台确认能通过API查到设备状态。
最后一步,把所有模块按顺序启动,跑一次端到端冒烟测试。
4.3 关键代码与联调流程
核心的联调代码分三块。第一块是后端调用Ollama做任务解析的核心逻辑:
func AnalyzeTask(input string) (*TaskPayload, error) { reqBody := map[string]interface{}{ "model": "qwen2.5:7b-instruct-q4_K_M", "prompt": buildTaskPrompt(input), "format": "json", // 强制JSON输出 "stream": false, } raw, _ := json.Marshal(reqBody) resp, err := http.Post("http://localhost:11434/api/generate", "application/json", bytes.NewBuffer(raw)) // 解析response,校验关键字段 // taskPayload包含Intent、TargetObject、TargetPosition }注意"format": "json"这个参数。Ollama支持forced JSON output,加上之后模型几乎不会再输出markdown格式的垃圾内容,解析稳定性提升非常明显。
第二块是后端向ROS 2发送导航动作。ROS 2在机器人侧跑了一个action server,后端通过一个轻量的Python桥接服务,把导航目标点转成ROS 2 Action请求:
from action_tutorials_interfaces.action import NavigateToPose from rclpy.action import ActionClient async def send_goal(position): goal_msg = NavigateToPose.Goal() goal_msg.pose.position.x = position['x'] goal_msg.pose.position.y = position['y'] goal_msg.pose.theta = position['theta'] await action_client.send_goal_async(goal_msg)第三块是机械臂抓取的核心时序。MoveIt规划出轨迹后,执行运动并等待位姿反馈。抓取要加超时保护,机械臂运动卡住超过15秒就取消当前动作回安全位,避免电机堵转烧坏。
联调流程我强烈建议从简到繁:先用postman直接给后端接口喂固定JSON,验证导航和机械臂;再让模型在线解析自然语言;最后才接前端界面做全链路展示。一步到位硬测,出了问题根本不知道源头在哪。
5. 常见问题与避坑实录
5.1 问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 模型输出的JSON无法解析 | 没有强制JSON格式,模型输出markdown或多余文字 | 加format参数,few-shot约束,后端做重试 |
| 7B模型推理特别慢 | 模型跑到CPU上了 | 检查Ollama日志,调整n_gpu_layers参数 |
| 机器人导航到错误位置 | 工位坐标映射错误或地图精度不够 | 检查点位表,重新建图 |
| 机械臂抓取总是偏一点 | 手眼标定不准确 | 重新做标定,增加柔性下探容错 |
| 前端任务状态一直pending | 状态回调链路断裂 | 用Redis发布订阅保证状态透传 |
| 多任务同时下发机器人不动 | 任务队列竞争 | 改成单任务执行模型 |
| 语音识别死活不响应 | 麦克风阵列驱动没装对 | 检查Arecord录音测试,先录一段看波形 |
| 搬运过程中机器人突然急停 | 障碍物膨胀半径太大,把自己卡住 | 调小膨胀半径,确认地图更新频率 |
5.2 几个值得注意的设计细节
第一,日志链路必须贯穿整个任务。我在每个任务生成时给一个task_id,从用户输入、模型解析、导航状态、抓取结果到最后落库,全程打点。前端十分钟能定位问题,后端也只需要一个ID过滤日志。没有任务ID,多状态并发下排查问题会痛不欲生。
第二,大模型的输出要做两次校验。第一次是格式校验,确认是合法JSON;第二次是语义校验,确认intent字段属于合法枚举值、目标物体和位置编号在系统的能力范围内。两次都通过才允许下发执行,任何一次失败都先重试一次,重试仍失败再转人工确认。这个机制能挡住绝大多数模型“胡说八道”的情况。
第三,机器人安全必须设置多重保险。底盘限速、机械臂扭矩限制、急停按钮一样都不能少。我在每次实验前都会把速度上限调到最低,先把路径跑通后再逐步提速。有一次我在没设置限速的情况下测试,机器人差点撞到实验台的桌脚,从那以后我养成了“先龟速跑,再正常速跑”的习惯。
第四,关于模型微调。如果你想让模型更懂你的工位编号、物体名称,微调是必要的。先用LoRA做参数高效微调,数据集从几十条开始就有效果,不需要准备海量数据。我在实际项目里准备了大约500条搬运指令,微调后模型对场地特有物体名称的理解明显提升,意图解析成功率从85%升到了95%以上。微调完记得导出GGUF格式再放回Ollama里跑。
6. 写在最后的一些体会
从立项到全链路跑通,这个项目我前前后后花了三个多月。最大的感触是:大模型其实是这套系统里最容易搞定的部分,真正消耗精力的是机械结构稳定性、ROS节点通信可靠性、状态机在异常场景下的恢复能力。大模型输出不稳定最多重试一次,但机械臂一次夹空可能就要人工介入扶正,这才是真麻烦。
如果让我重新做一次,我会先用一台普通笔记本电脑跑通全部软件链路,把大模型解析、后端调度、前端展示都验证完,再上真机。纯软件调试比带硬件的联调快一倍不止,而且不用忍受机械故障打断调试节奏。
这套平台目前的扩展空间还很大。后续可以做多机调度,让两台机器人协同完成不同区域的搬运任务;可以做端侧模型蒸馏,把7B模型压缩成更小的参数量部署到更低成本的硬件上;还可以在抓取环节引入强化学习,让机械臂通过试错提升在不同光照、不同角度下的抓取成功率。但无论扩展哪个方向,我都会先把当前的搬运闭环做到“连续运行一周不故障”,稳定性永远是机器人项目的生命线。如果你正在搭类似的东西,有一点我特别想强调:先把最简单的闭环跑到稳定,再往上叠花活,这比什么都重要。