news 2026/9/8 13:13:24

RISC-V开发板驱动视觉机械臂:YOLOE+Agent+MCP闭环实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RISC-V开发板驱动视觉机械臂:YOLOE+Agent+MCP闭环实践

1. 项目整体思路:为什么把这四样拼在一起

先直接回答标题里的问题:RISC-V 确实能跑机器人,但要看跑成什么样。我手里这块 VisionFive 2 是 StarFive 出的 RISC-V 开发板,四核 Cortex-A55,8GB 内存,带一个 2TOPS 左右的 NPU。说实话,这个算力在机器人视觉领域算不上豪华,但做桌面级机械臂的视觉闭环已经够用了。加上它把 PCIe、千兆网、USB 3.0 都给了,外接摄像头、舵机控制板都很方便,整套系统的扩展性比想象中好。

这个项目我给它定的目标是:用 VisionFive 2 作为唯一的主控,跑 YOLOE 做目标检测,识别桌面上的物体位置,然后把坐标通过 Agent API 交给一个自动化决策层,最后通过 MCP 协议把动作指令下发到机械臂,完成“看到-判断-执行-验证”的完整闭环。消息中间件用的是 ROS 2,但关键是整条链路的模型选择、接口设计和调试方法,这些东西跟具体硬件绑定不算太深,换到别的开发板上思路同样成立。

为什么选 YOLOE 而不是老牌的 YOLOv8?因为 YOLOE 是开放词汇检测模型,不需要针对特定物体重训一个分类头,你用文字描述目标类别,它就能在画面里找出来。对机器人项目来说这个特性太关键了,桌面上的物体种类是经常变的,如果每次都重新标注数据、重新训练,项目根本跑不起来。后面我会详细讲 YOLOE 在 RISC-V 平台上的移植和推理优化,包括我改过的预处理代码和推理参数。

至于 Agent API 和 MCP,简单理解就是让机器人拥有“听懂指令”和“调用工具”的能力。我这边用了一个本地的 Agent 框架,接收自然语言任务后,解析成结构化的动作序列,再通过 MCP Server 暴露的机械臂控制接口去执行。MCP(Model Context Protocol)可以把外部工具包成标准接口给大模型调用,我在这块板上跑了一个轻量级 MCP Server,负责把机械臂的坐标移动、夹爪开合、状态查询封装成工具,Agent 拿到视觉检测结果后自己决定下一步做什么。

这套组合最大的价值在于:它把“视觉识别”和“任务决策”彻底解耦了。视觉部分专注输出物体类别和位置,Agent 负责任务拆解,MCP 负责执行,哪一环出问题都能单独排查,不用推倒重来。

2. 视觉闭环的核心实现

2.1 先跑通 YOLOE:模型准备与推理环境搭建

YOLOE 的推理在 PC 上不算难,但放到 VisionFive 2 上就有讲究了。这块板子的 NPU 支持的算子有限,直接拿 x86 上编译好的 ONNX 模型跑,大概率会报错或者部分算子回落到 CPU,速度会很难看。我的做法是先用官方仓库把 YOLOE 导出成 ONNX,再用 npu-compiler 工具链做模型转换,量化成 INT8。

导出模型时有一个关键参数需要改:输入分辨率。官方默认是 640x640,但机械臂抓取场景里目标物体往往比较小,我建议直接上 1280x1280。代价是推理时间增加,但 VisionFive 2 的 NPU 跑 1280 输入时大概能到 4~6 FPS,对于“物体基本静止、机械臂动作本身需要一两秒”的桌面场景,完全够用。如果你对帧率有要求,可以把输入降回 640,但检测小物体的召回率会明显下降,这个取舍得根据实际任务来。

推理代码我用了 Python 版本的 rknn-toolkit2 兼容层(VisionFive 2 的 NPU 接口与 RKNN 有相似之处,但注意它不是完全兼容,需要根据 StarFive 提供的 API 改写)。核心的部分是这样的:

import numpy as np from starfive_npu import Model # 加载转换后的模型 model = Model("yoloe_int8.nb") def preprocess(frame): # 保持宽高比缩放,不足部分填充灰色 h, w = frame.shape[:2] scale = min(1280 / h, 1280 / w) nh, nw = int(h * scale), int(w * scale) resized = cv2.resize(frame, (nw, nh)) canvas = np.full((1280, 1280, 3), 114, dtype=np.uint8) canvas[:nh, :nw] = resized # 归一化到 0~1,并转为 NHWC 格式 tensor = canvas.astype(np.float32) / 255.0 tensor = np.expand_dims(tensor, axis=0) return tensor, scale, nh, nw def infer(frame): tensor, scale, nh, nw = preprocess(frame) # 推理结果: boxes, scores, classes, texts boxes, scores, classes, texts = model.run(tensor) # 坐标映射回原始图像尺寸 boxes[:, [0, 2]] /= scale boxes[:, [1, 3]] /= scale return boxes, scores, classes, texts

这里有个容易踩的坑:YOLOE 的后处理跟 YOLOv5/v8 不一样,它在模型输出里直接带了文本嵌入的相似度分支,所以 classes 和 texts 是成对出现的。我在前期调试时一直用 YOLOv8 的 decode 方式去解析输出,导致所有检测框都是乱的,后来仔细看了输出张量的形状才定位到问题。如果你自己导模型,务必确认输出节点是哪几个,别想当然。

2.2 从像素坐标到机械臂抓取坐标

模型输出的是像素坐标系里的目标框,而机械臂需要的是三维空间坐标,这一步需要做坐标转换。我没有用复杂的标定工具,而是用了一个相对粗暴但实际很稳的方法:先固定摄像头位置,然后让机械臂末端分别移动到画面中的几个已知点位,记录下像素坐标和机械臂坐标的对应关系,再用透视变换矩阵拟合。

这个方法的前提是摄像头和机械臂底座的位置相对固定,所以我在项目里设计了一个固定支架,把摄像头装在机械臂正上方俯视工作台,这样既减少了遮挡问题,透视变换的误差也小。

import cv2 import numpy as np # 标定数据: 像素坐标 -> 机械臂底座坐标系坐标 pixel_pts = np.array([[320, 240], [960, 240], [320, 720], [960, 720]], dtype=np.float32) robot_pts = np.array([[150, 150], [450, 150], [150, 450], [450, 450]], dtype=np.float32) # 计算透视变换矩阵 M = cv2.getPerspectiveTransform(pixel_pts, robot_pts) def pixel_to_robot(px, py): point = np.array([px, py, 1.0], dtype=np.float32) rx, ry, rw = M @ point return rx / rw, ry / rw

注意,这里的机械臂坐标是我简化后的二维平面坐标,真实项目还要加上夹爪张开高度和物体高度估计。桌面高度固定,所以 Z 轴基本是个常数,但如果你抓的是圆柱体或球体,需要额外估计物体的高度,否则夹爪会撞到桌面上。我后来加了简单的激光测距模块,直接读物体顶面的高度,比纯视觉估计可靠得多。

2.3 视觉闭环里面的关键延时控制

视觉闭环最常见的问题不是识别不出来,而是“识别出来了但手臂已经错过了”。这里最关键的是要控制整个闭环的延迟预算。我实测过,在 VisionFive 2 上 YOLOE 单帧推理大约是 200 到 250 毫秒,加上图像采集、坐标变换、MCP 消息转换,全部走完接近 400 毫秒。如果机械臂动作本身要 1 秒,这个延迟其实是可以接受的。

但如果你的项目里物体是在移动的,400 毫秒延迟就会导致明显的位置偏差。我是怎么解决的?两个手段并举:第一,把检测频率降下来,但控制频率提上去。检测模块跑 5 FPS,但机械臂控制走单独的线程,每 50 毫秒根据最近一次目标坐标做一次 PID 跟踪。第二,给目标框加一个轻量级的卡尔曼滤波,预测物体在下一个控制周期可能的位置,这样即使视觉帧率低,控制端也能平稳跟踪。

视觉线程 : 摄像头 -> YOLOE 检测 -> 输出目标坐标(约5Hz) 控制线程 : 读取最新坐标 -> 卡尔曼预测 -> 发给 MCP Server(约20Hz) MCP 线程 : 接收动作指令 -> 机械臂控制指令

这个结构看着简单,但实际工程里线程间的数据传输很容易出错。我的建议是:不要直接共享可变对象,用带锁的消息队列传递不可变数据。坐标数据量很小,哪怕是 Python 的 queue.Queue 也够用,关键是别在多个线程里同时改同一个 numpy 数组,我因为这个问题排查了整整一个下午。

3. Agent API 与 MCP 的衔接

3.1 用 MCP Server 给机械臂开一个“工具接口”

MCP 的核心思想是:把外部设备或软件的能力抽象为标准的工具(tools),大模型通过标准协议去调用这些工具,而不是每次都用自然语言把指令发给控制系统。我在这里做了一个轻量级的 MCP Server,暴露了三个工具:move_to、gripper、get_status。每个工具都有自己的输入参数说明,Agent 调用时会自动带上参数。

其实不用把 MCP Server 想得太复杂,它的本质就是一个 JSON-RPC 服务。客户端发来一个请求,服务端解析方法名和参数,然后执行对应的 Python 函数,把结果返回去。我在 VisionFive 2 上用的 FastAPI 实现,因为开发效率高、调试方便,运行时占用的资源也完全可以接受。

from fastapi import FastAPI from pydantic import BaseModel import serial app = FastAPI() arm = serial.Serial('/dev/ttyUSB0', 115200) class MoveRequest(BaseModel): x: float y: float z: float speed: float = 100.0 class GripperRequest(BaseModel): action: str # "open" or "close" @app.post("/move_to") def move_to(req: MoveRequest): # 转换坐标并生成机械臂控制指令 cmd = f"G0 X{req.x} Y{req.y} Z{req.z} F{req.speed}\n" arm.write(cmd.encode()) return {"status": "ok", "cmd": cmd} @app.post("/gripper") def gripper(req: GripperRequest): action = 1 if req.action == "open" else 0 cmd = f"M3 {action}\n" arm.write(cmd.encode()) return {"status": "ok", "cmd": cmd} @app.get("/get_status") def get_status(): arm.write(b"M114\n") resp = arm.readline().decode().strip() return {"status": "ok", "position": resp}

这里需要说明,MCP 协议和普通的 HTTP API 并不矛盾。MCP 更多是站在“大模型工具调用”这一层,它规定了工具发现的格式、参数 schema 怎么描述、请求响应怎么封装。如果你做的系统不是给大模型用的,直接用 HTTP API 也完全没有问题。我这边实际是两层都做了:MCP Server 负责和大模型通信,HTTP API 负责和前端控制台通信,底层调用同一套函数。

3.2 Agent 如何自主决定抓取哪个物体

视觉部分把桌面上的物体都标记出来了,但具体抓哪个、先抓哪个、抓完放哪里,这些需要 Agent 来决策。我这里用的是一个简单的 Agent 框架,给它一个系统提示词,描述当前环境里有机械臂、夹爪、摄像头,以及工作台上可能出现的物体类别,然后让它接收自然语言任务。

一个典型的任务流程是这样的:用户说“把红色的杯子放到左侧托盘里”,Agent 需要先从视觉模块拿最新的检测结果,找到类别为“cup”且颜色接近红色的目标框,然后调用 move_to 移动到该物体上方,再调用 gripper 夹取,最后移动到托盘位置放下。

在这个流程里,Agent API 的作用是把视觉模块的输出包装成语义化的环境状态。视觉模块只负责输出“画面中有哪些物体、坐标是多少”,而“哪个是任务需要的目标”这个决策交给了 Agent。这里我用了一个小技巧:把 YOLOE 输出的文本嵌入结果直接作为环境描述的一部分返回给 Agent,这样 Agent 不需要依赖硬编码的物体列表。

用户任务: "把红色杯子放到左侧托盘里" Agent 解析: 1. 查询环境状态 -> 获取物体列表 [{id: 1, category: "cup", color: "red", pos: (320, 425)}] 2. 筛选目标 -> 选择 id=1 3. 调用 move_to(x=320, y=425, z=50) 4. 调用 gripper(action="close") 5. 调用 move_to(x=100, y=100, z=50) 6. 调用 gripper(action="open") 任务完成

为了不让 Agent 乱调参数,我在 MCP Server 里对坐标范围做了边界检查,超出工作台的坐标直接返回错误。这个看似简单的校验,在实际调试中帮了大忙,否则机械臂很容易就去撞限位了。

4. 硬件调试中踩过的坑

4.1 算力、内存和 IO 的实际瓶颈

VisionFive 2 的 CPU 在跑 YOLOE 预处理和后处理时,性能比我想象的要好,真正拖后腿的反而是内存带宽。1280x1280x3 的输入做归一化和 resize 时,频繁的内存拷贝会把耗时拉得很高。我一开始直接用 Python 的 cv2.resize,一帧预处理要 80 毫秒,后来改成 OpenCV 的 UMat 走 GPU(其实是 NPU 的加速路径),预处理的耗时降到了 40 毫秒左右。

另外一个容易被忽略的瓶颈是 USB 摄像头帧率。理论上是 30 FPS,但在 RISC-V 板子上实际跑到 15 FPS 就不错了,原因不是摄像头不行,而是 USB 控制器和驱动栈对高带宽传输支持不太好。我的解决方法是把摄像头设成 MJPG 格式而不是 YUYV,同样分辨率下带宽需求直接减半。代价是 CPU 解码要多花一些时间,但总体帧率是明显提升的。

如果你对实时性要求更高,建议直接用 CSI 接口的摄像头,走 MIPI 传输,带宽和延迟都会好很多。不过我那边 CSI 摄像头驱动在 VisionFive 2 上还有点问题,暂时先用 USB 顶着,后面会换。

4.2 MCP Server 超时与状态不一致

我这边实际遇到的一个隐蔽问题:Agent 调用 MCP 工具时如果超时,机械臂可能已经执行了动作,但 Agent 因为没收到返回结果就重试,导致同一个动作被执行两次。这个问题非常危险,轻则夹爪空夹,重则机械臂直接撞到限位。

我的解决思路是在每个工具接口里加了一个 request_id 参数。Agent 在调用时生成一个唯一 ID,MCP Server 收到后如果发现相同 ID 已经执行过,就直接返回上一次的结果,不再重复执行。这个方案实现成本很低,但让整个系统的容错性好了一个量级。

还有一个问题是夹爪状态同步。机械臂执行完夹取动作后,如果目标物体太滑或者太重,夹爪未必真的夹住了。我在夹爪上加了一个电流检测模块,夹紧瞬间电流会突变,如果电流没有变化就说明没夹住,这个状态会通过 get_status 接口返回给 Agent,Agent 会决定是否需要重新尝试。这一步让我想起了做工业机器人的人常说的“末端执行器反馈”,没有这一层反馈,视觉闭环严格来说是不成立的。

4.3 避坑速查表

问题现象可能原因解决办法
YOLOE 推理结果全是乱框后处理解析用了 YOLOv8 的格式确认输出节点,YOLOE 有独立的文本相似度输出
预处理耗时过高多次内存拷贝用 UMat 走硬件加速路径,避免 numpy 中转
USB 摄像头帧率低摄像头输出格式带宽占用过大改用 MJPG 格式,降低 USB 带宽需求
Agent 重复执行动作MCP 工具调用超时后重试工具接口增加 request_id 幂等控制
机械臂抓取失败但无感知缺少夹爪状态反馈加电流检测或力传感器,回传状态
NPU 模型转换失败某些算子不支持换用更基础的算子组合,或降级到 CPU

5. 项目可以扩展的两个方向

5.1 多自由度机械臂的避障规划

我现在做的这套系统本质上是“点到点运动”,没有考虑中间路径上的障碍物。如果要抓取的位置旁边有别的物体,机械臂直接从初始位置直线运动过去,很可能撞到旁边的物体。一个可行的扩展是在 Agent 层加入路径规划模块,比如把工作台栅格化,用 A* 或 RRT 算法生成一条无碰撞路径,然后拆成多个航点传给机械臂。这个扩展对算力的要求不高,但对代码结构会有一定改动,因为运动控制要从“单点”改成“轨迹”模式。

5.2 让多个 Agent 协作

目前的 Agent 是单线程的,一次只处理一个任务。如果把任务拆成“看”和“动”两个 Agent,一个 Agent 只负责视觉分析和目标确认,另一个 Agent 负责运动控制,它们之间通过消息队列通信,系统的模块化程度会更高。更进一步,两个 Agent 可以通过 MCP 互相调用对方的工具,这样一来,整个系统就有点像一个微型的多智能体机器人集群了。我目前只在模拟环境里验证过这个思路,离真正落地还有一段距离,但值得继续搞。


个人说点实在的:做这个项目最大的收获不是跑通了某个框架,而是把“视觉识别”和“硬件控制”这条链路上所有的细节都过了一遍。从模型选型到坐标标定,从 MCP 协议到机械臂反馈,每一步都有很多“想当然”和“实际不是那么回事”的地方。RISC-V 跑机器人这个题目,现在看已经不是一个“能不能”的问题,而是“怎么跑得更稳、更聪明”的问题。VisionFive 2 这块板子的算力上限摆在那里,但通过合理的架构和优化,桌面级视觉机械臂完全可以在它上面跑起来。后续如果再给我一次机会,我会把重点放在夹爪的力反馈和路径规划上,这两块是当前系统最薄弱的环节,也是真正决定一个机器人能不能干活的最终底线。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:12:47

源端并行解析 × 目标端多通道入库:KFS 同步架构深度解读

源端并行解析 目标端多通道入库:KFS 同步架构深度解读 一、为什么"增量同步"成了数据库的生死线十年前做数据同步,工程师最在意的是"能不能把数据搬过去";今天做数据同步,工程师最在意的是"能不能跟得上…

作者头像 李华
网站建设 2026/9/8 13:11:00

移动端AI聊天引擎搭建:SSE流式输出与WebView打包实战

之前做移动端 AI 聊天产品时,最让我头疼的不是大模型本身,而是手机端的流式渲染、软键盘弹出、WebView 缓存不一致这些细节。这次我把整个免费 AI 聊天引擎的手机端从零搭了出来,不废话,直接分享一套能跑通的最小闭环,…

作者头像 李华
网站建设 2026/9/8 13:10:57

千笔与灵感AI横评:谁更懂MBA论文写作全流程?

先说个开场白。我这两周把市面上叫得上名字的AI论文平台几乎都跑了一遍,最终锁定了两个最有代表性的放在一起做深度横评——千笔专业学术智能体,和灵感AI。理由很简单:一个是垂直学术场景的智能体方案,一个是通用AI写作平台里呼声…

作者头像 李华
网站建设 2026/9/8 13:10:55

免费自托管AI聊天引擎:手机端Web界面部署与API调用实践

能自己托管、能塞进手机浏览器、又能对外提供 API 的免费 AI 聊天引擎,其实比想象中更难得。这次完成的手机端,就是把原本只能在电脑上操作的聊天引擎,重新包了一层适合移动端的 Web 界面:同一套后端,手机和电脑都能访…

作者头像 李华
网站建设 2026/9/8 13:10:55

AI写作如何去掉机器味?资深编辑拆解humanizer人性化改写方法论

"humanizer"这个词最近在内容创作圈子里越来越热,但翻来覆去能看到的大多是工具广告和软件评测,真正讲清楚"它到底在解决什么问题、底层逻辑是什么、怎么才能做好"的内容少之又少。我做了几年内容代笔和自媒体运营,前前后…

作者头像 李华
网站建设 2026/9/8 13:09:45

大模型应用开发实战:从API调用到生产级AI Agent

在实际开展 AI 应用开发之前,很多人的体验是“玩了 AI 才知道”:学一些概念、调通一次 API,就像吃了一份清淡养胃的简餐;真正把大模型接进业务系统,让它自主调用工具、处理上下文、稳定地对外提供接口,才是…

作者头像 李华