RISC-V跑机械臂:视觉闭环+大模型Agent/MCP实践
2026/9/6 6:16:15 网站建设 项目流程

RISC-V 能不能跑机器人?这个问题我过去一年被问过很多次。坦白讲,在我真正把一套机械臂视觉闭环方案在 VisionFive 2 上跑通之前,我也不敢给一个确定的答案。但完成这个项目之后,我可以很肯定地说:能跑,而且不只是跑一个点灯的 demo,是能把“视觉识别 + 大模型决策 + 机械臂执行”这一整条链路都跑起来。

这篇文章记录的是我基于 VisionFive 2、YOLOE、Agent API 和 MCP 搭建的机械臂工程实践。核心思路很简单:摄像头把画面交给 YOLOE 做目标检测,得出物体的像素坐标;坐标解算模块把像素坐标换算成机械臂基座坐标系下的三维位置;Agent API 负责理解用户的自然语言指令并规划下一步动作;最后 MCP 协议把动作意图封装成标准化的工具调用,真正去驱动机械臂。如果你正在纠结 RISC-V 的性能能不能承担边缘机器人任务,或者想知道大模型 Agent 怎么跟真实硬件结合起来,这篇文章应该能给你一个相对完整的参考。

1. RISC-V 到底能不能扛住机器人这一摊活儿

1.1 先说结论:能跑,但不是照着 x86 的思路跑

先把话说在前头:RISC-V 能跑机器人,但这里的“能跑”跟我在 x86 小主机上跑机器人,完全不是一回事。VisionFive 2 用的是 StarFive JH7110 芯片,四核 SiFive U74 处理器,主频 1.5GHz,带 FPU,还集成了一颗 IMG BXE-4-32 GPU,8GB 内存版本配合 M.2 插槽,扩展性比想象中好。这套配置放在单板计算机里不算差,但你要真把它当作 Jetson 那样的 AI 平台去用,就会很痛苦。

我的判断依据是:机器人任务可以拆成感知、决策、执行三块。执行部分完全不吃算力,机械臂的关节运动是靠舵机或伺服驱动器完成的,主控只需要发串口指令;决策部分如果走 Agent API,算力负载在云端或者局域网里的模型服务上,开发板只承担网络调用和协议解析;真正吃算力的是感知部分,也就是跑视觉模型。YOLOE 这种开放词汇检测模型,参数不小,在 RISC-V 的 CPU 上跑不可能像 x86 或者带 NPU 的板子那样流畅,但如果你对实时性的要求是“秒级反应”而不是“毫秒级跟踪”,那完全够用。

所以我的结论很明确:RISC-V 适合做机器人的主控和决策中枢,不适合做高帧率的实时感知单元。把“重计算”放在模型推理精度和帧率需求都不极端的场景下,RISC-V 是能扛住的。整个闭环跑下来,单次“看到目标—做出决策—执行抓取”大约需要 4 到 6 秒,对很多桌面级机器人任务来说已经是可以接受的节奏了。

1.2 为什么选 VisionFive 2,而不是树莓派或者 Jetson

其实这个项目里,视觉检测用树莓派或者 Jetson 都会更舒服,但我选 VisionFive 2 有自己的理由。我做这个项目不是为了追求性能上限,而是想验证一个事情:在完全不依赖特定厂商 AI 工具链的前提下,RISC-V 能不能承担一个完整的边缘机器人应用。VisionFive 2 是目前市面上最容易买到的、性能相对均衡的 RISC-V 单板机,而且它能跑完整的 Debian 系统,这决定了大部分 Linux 生态的软件栈可以直接复用。

几个平台的对比我列在下面:

项目VisionFive 2树莓派 4B/5Jetson Orin Nano
架构RISC-V 四核 U74ARM Cortex-A72/A76ARM Cortex-A78AE + GPU/NPU
AI 加速无成熟工具链CUDA + TensorRT
内存4GB/8GB2GB~8GB4GB/8GB
生态成熟度中低
功耗约 5~8W约 4~10W约 7~15W
适合机器人决策主控、轻量感知通用原型重感知、模型推理

从这个表能看出来,单论 AI 推理,VisionFive 2 肯定排在后面,但它有一个其他平台没有的价值:它代表了一种从指令集层面就可以自主掌握的路线。这个价值在项目工程上可能体现得不那么直接,但对软件栈的长期演进来说意义很大。如果你只是想要一个能快速出效果的机械臂项目,选树莓派 5 或者 Jetson 会省心得多;如果你想知道 RISC-V 的边界到底在哪,VisionFive 2 是目前最合适的试验场。

2. 完整架构设计:从摄像头到机械臂的一条闭环链路

2.1 数据流和任务流怎么串起来

在写任何代码之前,我先把整个系统的数据流画在纸上。摄像头固定在机械臂工作台正上方,拍摄范围覆盖机械臂的作业区域。每一轮任务开始后,摄像头采集一帧画面,交给 YOLOE 做目标检测,输出所有目标的类别、置信度和 2D 边界框。接着坐标解算模块利用预先标定的映射关系,把目标的像素坐标换算成机械臂基座坐标系下的 X、Y、Z 坐标。

到这里,感知部分告一段落。接下来是决策部分:用户的指令,比如“把红色的方块挪到左边的框里”,会被发送给 Agent API。Agent 不是直接输出关节角度,而是结合视觉模块给出的目标列表和位置信息,推理出应该调用哪个工具、传入什么参数。这个工具调用请求通过 MCP 协议传递给 MCP Server,MCP Server 里封装了机械臂的底层控制函数,最终把抽象的动作指令翻译成串口命令发给机械臂。

机械臂执行完动作之后,摄像头会再次采集画面,确认目标是否被正确抓取或放置。如果位置误差超过阈值,系统会自动进行微调。这就是视觉闭环的核心:不是“看一次就完事”,而是“边执行边确认”。整个链路里,RISC-V 开发板承担了视觉推理、坐标换算、Agent 调度和 MCP 通信,负载不轻,但没有超出它的能力范围。

2.2 视觉检测为什么用 YOLOE,而不是训练一个 YOLOv8

目标检测部分,我一开始的选择其实是 YOLOv8,因为它在边缘设备上的推理速度和生态成熟度都是顶级的。但后来我换成了 YOLOE,原因很实际:YOLOE 是一个开放词汇检测模型,它不依赖固定的类别列表,而是可以通过文本描述或者视觉提示来检测任意物体。这意味着机械臂面对新物体的时候,不需要重新训练模型,只需要换一句话或者给一张参考图。

在机器人应用里,这一点太关键了。我的工作场景不是固定的流水线,而是“今天抓螺丝,明天抓积木”这样的多变任务。如果用 YOLOv8,每增加一个物体类别,就要重新准备数据集、训练、导出,每次都是几个小时的活。用 YOLOE 之后,我只需要在系统配置里加一句 prompt,或者给一个示例图片,检测头就能找到目标。

YOLOE 的模型结构比传统 YOLO 复杂一些,在 RISC-V 上跑起来会慢一点,但这是值得的。它的核心模块是 T2VP,也就是文本到视觉提示的融合模块,模型会把文本特征先映射成视觉提示,再和多尺度图像特征融合,最后用提示引导检测头进行预测。模型也有不同尺寸,工程上我选了 YOLOE-S,因为它在精度和推理速度之间相对平衡,在纯 CPU 推理的条件下也能接受。YOLOE-L 的精度更高,但推理时间会翻倍,我测下来不太适合这个场景。

2.3 Agent API 和 MCP 在系统里的角色划分

关于 Agent 和 MCP,很多朋友容易混在一起。我在这套系统里的理解是:Agent API 是“大脑”,MCP 是“神经系统”。Agent 负责理解自然语言、分析环境状态、决定下一个动作,但它不知道机械臂具体怎么走、夹爪怎么开合;MCP Server 则把机械臂的能力封装成一个一个标准的工具,比如 move_to、gripper,Agent 只需要按照 MCP 协议调用这些工具,剩下的机械细节由工具内部处理。

这种分层最大的好处是安全。如果让大模型直接输出机械臂的关节角度,只要模型有一次幻觉,机械臂就可能做出危险动作。但通过 MCP 工具层,代码可以在真正执行之前检查坐标范围、速度限制,甚至可以加急停逻辑。说白了,模型只做“项目经理”,真正的操作工是那一个个封装好的 MCP 工具函数。

MCP 本身的优势在于标准化。它定义了工具发现的接口、参数格式、返回格式,Agent 侧可以通过 MCP Client 动态获取工具能力列表,而不是把工具参数硬编码在系统提示词里。这种动态发现机制在大模型场景里非常有用,因为模型每次调用工具前都会拿到最新的、结构化的工具定义,大幅降低了参数格式混乱的概率。

3. 视觉闭环实现:YOLOE 部署和坐标解算

3.1 把 YOLOE 模型搬到 VisionFive 2 上的准备

模型部署的第一步是把 YOLOE 导出成 ONNX 格式。直接加载 PyTorch 模型在 VisionFive 2 上做推理不是不行,但 PyTorch 在 RISC-V 上的性能表现不理想,内存占用也高。ONNX Runtime 对 RISC-V 的支持虽然没有 ARM 那么完善,但基本算子都能跑,而且可以通过多线程配置提高 CPU 利用率。

导出 ONNX 的时候要注意一个关键点:固定输入尺寸。YOLOE 的可变尺寸导出在 ONNX Runtime 里会引入额外的 Resize 和动态维度处理,在 RISC-V 上非常容易踩算子不兼容的坑。我最后直接把输入固定为 640x640,推理时用 letterbox 的方式把原始图像缩放并填充到目标尺寸,这样既避免了动态维度问题,也保持了检测精度。

板子上的环境我用的是 Python 3.10 + onnxruntime。ONNX Runtime 在 RISC-V 上需要安装对应架构的 wheel 包,直接用 pip install onnxruntime 大概率会拉到 ARM 或 x86 的包,这一点必须额外注意。装好之后,先跑一个随机输入的张量做基准测试,确认模型加载和推理链路是通的,再接入摄像头数据。

3.2 核心代码:YOLOE 目标检测推理

下面是我在板子上实际使用的推理代码,做了一些简化,但核心流程完整。代码的核心思路是:从摄像头读取画面,做 letterbox 预处理,调用 ONNX Runtime 推理,再解析输出框进行非极大值抑制。

import cv2 import numpy as np import onnxruntime as ort class YOLOEDetector: def __init__(self, onnx_path, conf_thres=0.35, iou_thres=0.45): self.session = ort.InferenceSession( onnx_path, providers=["CPUExecutionProvider"], sess_options=ort.SessionOptions(), ) self.conf_thres = conf_thres self.iou_thres = iou_thres self.input_size = 640 self.class_names = [] def letterbox(self, img): h, w = img.shape[:2] scale = min(self.input_size / w, self.input_size / h) nw, nh = int(round(w * scale)), int(round(h * scale)) resized = cv2.resize(img, (nw, nh)) canvas = np.full((self.input_size, self.input_size, 3), 114, dtype=np.uint8) dx, dy = (self.input_size - nw) // 2, (self.input_size - nh) // 2 canvas[dy:dy + nh, dx:dx + nw] = resized return canvas, scale, dx, dy def preprocess(self, img): canvas, scale, dx, dy = self.letterbox(img) blob = canvas.astype(np.float32) / 255.0 blob = np.transpose(blob, (2, 0, 1))[None] return blob, scale, dx, dy def postprocess(self, output, scale, dx, dy): # output shape 依据导出模型而定,常见为 [1, num_anchors, 4 + num_cls] preds = output[0][0] boxes, scores, classes = [], [], [] for pred in preds: cls_scores = pred[4:] cls_id = int(np.argmax(cls_scores)) score = float(cls_scores[cls_id]) if score < self.conf_thres: continue x1, y1, x2, y2 = pred[:4] # 去除 letterbox 填充并映射回原图坐标 x1, x2 = (x1 - dx) / scale, (x2 - dx) / scale y1, y2 = (y1 - dy) / scale, (y2 - dy) / scale boxes.append([x1, y1, x2, y2]) scores.append(score) classes.append(cls_id) keep = self.nms(boxes, scores) return [(boxes[i], scores[i], classes[i]) for i in keep] @staticmethod def nms(boxes, scores, iou_thres=0.45): if not boxes: return [] boxes = np.array(boxes) scores = np.array(scores) x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] areas = (x2 - x1) * (y2 - y1) order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) inter = np.maximum(0.0, xx2 - xx1) * np.maximum(0.0, yy2 - yy1) iou = inter / (areas[i] + areas[order[1:]] - inter) inds = np.where(iou <= iou_thres)[0] order = order[inds + 1] return keep

实测下来,YOLOE-S 模型在 VisionFive 2 上处理一帧 640x640 的图像,纯 CPU 推理大概在 300 到 600 毫秒这个范围,也就是 1 到 3 FPS 左右。帧率不高,但对我这个应用场景足够:机械臂的运动通常要一两秒,视觉检测的瓶颈不明显。如果希望提升速度,可以考虑把模型量化到 INT8,但这需要额外的校准数据,我在第一版里没有做。

3.3 像素坐标到机械臂基座坐标:不依赖复杂标定的方案

目标检测输出的只是图像上的 2D 坐标,机械臂需要的是基座坐标系下的 3D 坐标。最正规的做法是手眼标定,求出相机坐标系到机械臂基座坐标系的变换矩阵。但我这里摄像头固定在工作台上方,角度固定,目标物体基本都在一个平面高度上,所以可以用一个更简单实用的方案:先求一张单应性矩阵,把像素坐标映射到工作台平面坐标,然后加上固定的高度值。

单应性矩阵的求解只需要四个点。我把一个 A4 棋盘格放在机械臂的工作台上,记录四个角点在图像里的像素坐标,同时用机械臂末端对准这四个角点,记录它们的机械臂基座坐标。然后用最小二乘求出一个 3x3 的单应矩阵。这部分代码如下:

import numpy as np def compute_homography(pixel_pts, robot_pts): # pixel_pts: 图像上四个点的坐标 # robot_pts: 机械臂基座坐标系下对应四个点的坐标 A = [] for (u, v), (x, y) in zip(pixel_pts, robot_pts): A.append([-u, -v, -1, 0, 0, 0, u * x, v * x, x]) A.append([0, 0, 0, -u, -v, -1, u * y, v * y, y]) A = np.asarray(A) _, _, Vt = np.linalg.svd(A) H = Vt[-1].reshape(3, 3) return H def pixel_to_robot(u, v, H, z_height): p = np.array([u, v, 1.0]) q = H @ p x, y = q[0] / q[2], q[1] / q[2] return x, y, z_height

这里 z_height 是目标物体放置平面的高度。如果目标物体不在同一个平面,比如在一个小盒子里,这个简单方案就不够用了,需要加深度相机或者做真正的 3D 标定。对于第一版工程验证,平面假设已经够用。有一点要特别注意:单应性矩阵的精度高度依赖四个点的标定精度,机械臂对准角点的时候尽量用末端指针,减少人为误差。

3.4 闭环控制逻辑:抓不到就调整,而不是盲目重试

视觉闭环的实现不复杂,但逻辑上要做一些防抖和收敛判断。我的控制循环大致是这样的:

def pick_object(target_desc, max_attempts=3): for attempt in range(max_attempts): frame = camera.read() targets = detector.detect(frame) obj = select_target(targets, target_desc) if obj is None: return "目标未找到" x, y, z = pixel_to_robot(obj.cx, obj.cy, H, Z_HEIGHT) # 调用 MCP 工具,让机械臂移到目标上方 result = agent_arm_call("pick", x, y, z) # 执行后重新检测,确认目标是否已被抓取 frame = camera.read() targets = detector.detect(frame) if not select_target(targets, target_desc): return "抓取成功" # 如果还在,可能位置有偏差,进行微调 time.sleep(0.5) return "达到最大尝试次数"

这里有几个细节值得说。第一,目标检测的结果直接用来做成败判断,而不是依赖机械臂的反馈,这比单纯的“发指令-等完成”可靠得多。第二,每次机械臂动作之后给一个短暂的稳定时间,避免因为画面抖动导致重复检测误判。第三,目标框中心点最好做一个指数移动平均,防止单帧抖动导致坐标跳变。简单来说,如果上一帧的中心点是 (u1, v1),当前帧是 (u2, v2),那实际用的坐标是 alpha * u2 + (1 - alpha) * u1 这样的平滑值。alpha 取 0.5 左右比较合适。

4. Agent API + MCP 决策层搭建

4.1 先让 Agent 能够“看见”环境

在大模型介入之前,我先想明白一个问题:Agent 需要看到什么信息才能做出决策。直接把原始图像塞给多模态大模型,在 RISC-V 上是不可行的,一方面是图像传输和推理延迟不可控,另一方面是多模态 API 的成本和时延都不理想。所以我选择让视觉模块先做一轮检测,把结构化的环境信息整理出来,再交给 Agent。

结构化的环境信息长这样:

{ "objects": [ {"id": 1, "name": "red_block", "position": [120.5, 88.3, 15.0], "size": 35.0}, {"id": 2, "name": "blue_block", "position": [45.2, 200.1, 15.0], "size": 35.0} ], "workspace_bounds": {"x": [-250, 250], "y": [-250, 250], "z": [0, 150]}, "gripper_state": "open" }

这份 JSON 作为系统消息的一部分传给 Agent。Agent 看到的是:当前工作台上有哪些物体、分别在什么坐标、机械臂状态如何。用户发来的指令是自然语言,比如“把蓝色方块放到红色方块的右边”。Agent 的任务是把这句话拆解成具体的执行序列:先移动到蓝色方块上方,抓取,再移动到红色方块右侧的空位,放下。

这里的关键是:Agent 只能基于结构化的工具调用去影响世界。它没有能力直接控制电机,只能选择“调用 move_to”还是“调用 gripper”,参数必须符合 MCP 工具的 JSON Schema。这样即使模型理解错了,也只是选错了工具参数,不会产生直接的危险动作。

4.2 核心代码:用 MCP Server 封装机械臂工具

MCP 的工具层是整个决策系统里最关键的一环。我选择用官方 Python SDK 的 FastMCP 模块来实现,写起来非常快,本质上就是装饰器注册函数。每个函数就是一个工具,参数类型和注释会被自动转换成工具描述,Agent 侧可以动态获取这些工具列表。

下面是一个简化后的 MCP Server 代码,我删掉了一些和具体硬件相关的细节,保留了核心的封装模式:

from mcp.server.fastmcp import FastMCP import serial mcp = FastMCP("robot_arm_server") arm = serial.Serial("/dev/ttyUSB0", 115200, timeout=0.5) @mcp.tool() def move_to(x: float, y: float, z: float, speed: float = 80.0) -> str: """移动机械臂末端到指定基座坐标。 Args: x: 基座坐标系 X 坐标,单位 mm,范围 [-250, 250] y: 基座坐标系 Y 坐标,单位 mm,范围 [-250, 250] z: 基座坐标系 Z 坐标,单位 mm,范围 [0, 300] speed: 运动速度百分比,范围 [1, 100] """ if not (-250 <= x <= 250 and -250 <= y <= 250 and 0 <= z <= 300): return "ERR: coordinate out of range" cmd = f"MOVJ {x:.1f} {y:.1f} {z:.1f} {speed:.0f}\n" arm.write(cmd.encode()) resp = arm.readline().decode().strip() return resp @mcp.tool() def gripper(open: bool) -> str: """开合机械臂夹爪。 Args: open: True 表示松开夹爪,False 表示夹紧 """ pos = "OPEN" if open else "CLOSE" cmd = f"GRIP {pos}\n" arm.write(cmd.encode()) resp = arm.readline().decode().strip() return resp if __name__ == "__main__": mcp.run(transport="stdio")

这个 Server 跑起来之后,就以 stdio 的方式提供标准输入输出通道,任何 MCP Client 都可以连接它、发现 move_to 和 gripper 这两个工具、并按 schema 调用。这里我没有用 Serial 以外的网络传输,是因为 stdio 在本地集成时最简单稳定,Agent 进程和 MCP Server 进程由同一套脚本拉起,调试也方便。如果你有远程调用的需求,可以改成 SSE transport,但配置复杂度会上升。

机械臂的底层协议我这里用的是一个抽象的 ASCII 指令格式。你换成自己手头的机械臂 SDK 完全没问题,MCP Server 这一层恰恰就是为了屏蔽这种硬件差异而存在的。

4.3 Agent 如何调用这些 MCP 工具

Agent 侧我采用了一个比较直接的方案:在 Agent 进程中通过 MCP Client 连接本地 Server,拿到工具列表后,转换成 OpenAI 兼容的 function calling 格式,再交给大模型 API。代码结构大致如下:

import json from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY") system_prompt = """ 你是一个桌面机械臂控制助手。工作台上有若干物体,每个物体都有唯一的 id、名称和 3D 坐标。 你的任务是根据用户指令,调用 move_to 和 gripper 工具完成任务。 注意: 1. 所有坐标单位是毫米,必须确保在 [-250,250] 的工作范围内。 2. 第一步先移动到目标物体正上方,第二步抓取,第三步移动到放置位置,第四步松开。 3. 移动过程中尽量使用合适的 speed 参数,避免速度过快导致物体滑落。 """ tools = [ { "type": "function", "function": { "name": "move_to", "description": "移动机械臂末端到指定基座坐标", "parameters": { "type": "object", "properties": { "x": {"type": "number", "description": "X 坐标 mm"}, "y": {"type": "number", "description": "Y 坐标 mm"}, "z": {"type": "number", "description": "Z 坐标 mm"}, "speed": {"type": "number", "description": "速度百分比"} }, "required": ["x", "y", "z"] } } }, { "type": "function", "function": { "name": "gripper", "description": "开合夹爪", "parameters": { "type": "object", "properties": { "open": {"type": "boolean", "description": "True 松开,False 夹紧"} }, "required": ["open"] } } } ] def agent_plan(user_instruction, env_state): messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": f"当前环境:{json.dumps(env_state, ensure_ascii=False)}"}, {"role": "user", "content": user_instruction} ] resp = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools, tool_choice="auto", temperature=0.1 ) return resp.choices[0].message

拿到模型的响应之后,解析 tool_calls,再逐一调用对应的 MCP 工具函数,并把执行结果追加到对话历史里。这个循环一直持续到模型认为任务完成,或者达到最大调用次数。

这里我要特别强调一个细节:工具参数的 JSON Schema 一定要写准确,尤其是 required 字段和类型定义。大模型对工具 schema 的敏感度非常高,一旦 required 字段缺失或者类型含糊,模型就会频繁生成畸形参数,导致调用失败。我一开始把 move_to 的 speed 参数写成可选但没写 default,模型有时候传 speed: "fast" 这种字符串,直接被 schema 拒绝。后来给每个数值参数都标清了单位和范围,情况才稳定下来。

4.4 安全兜底:不能把机械臂的命运完全交给大模型

用大模型控制物理设备,安全问题怎么强调都不为过。我的设计原则是:模型可以提方案,但代码做最终裁决。具体来说,有三个层面的兜底。

第一层在 MCP 工具内部,也就是前面代码里已经体现的坐标范围检查。move_to 函数执行前会校验输入坐标是否在工作区间内,如果模型生成了超出范围的坐标,工具直接返回错误,不会真的给机械臂发指令。第二层是急停逻辑,我在机械臂的串口通信线程里加了一个特殊的急停指令,一旦检测到连续两条工具调用失败,自动触发急停并复位机械臂。第三层是操作日志,每一步工具调用的入参、出参、耗时全部记录到本地文件里,出问题的时候可以复盘到底是模型理解错了,还是工具执行出错。

很多初次做 Agent 硬件控制的朋友容易忽略这一点,觉得模型输出什么就执行什么,反正出了错再改。但真实机械臂不比软件,走错了就是物理碰撞,轻则任务失败,重则损坏设备。所以安全兜底不是可选项,而是必选项。

5. 实测性能与瓶颈分析

5.1 一次完整抓取任务的耗时拆解

我这边把一次“识别并抓取物体”的全流程拆成了几个阶段,分别在日志里打了时间戳。一轮典型任务的时间分布大概是这样:

阶段耗时范围说明
摄像头取帧30~80msUSB 摄像头 640x480 分辨率
YOLOE 推理300~600ms640x640 输入,CPU 推理
坐标解算1~3ms单应性矩阵映射
Agent 决策500~2000ms局域网内模型 API 调用
机械臂执行1000~3000ms移动加夹爪动作
复检确认300~600ms第二次 YOLOE 推理

整体加起来,一次完整抓取大约 2.5 到 6 秒。可以看到,最大的时间开销其实不在 RISC-V 的推理上,而在 Agent API 的网络往返和机械臂本身的执行速度上。这说明在现有的工程架构里,VisionFive 2 的性能没有成为明显的短板,真正的瓶颈在决策延迟和机械执行时间。

这也给我一个启发:如果以后要优化整个系统,优先级不是去压缩 YOLOE 推理时间,而是想办法让 Agent 决策更快,比如把模型部署到更靠近开发板的地方、或者用小一点的模型、或者让 Agent 在动作序列层面做一次性的规划,而不是每一步都来回调用。

5.2 CPU、内存和温度表现

我观察了系统连续运行一小时后的资源占用情况。YOLOE 推理时,四个 U74 核心基本满负载,CPU 占用会冲到 90% 以上。内存方面,Python 进程加 ONNX Runtime 加模型权重,总共占用了大概 2.3GB,8GB 版本跑起来没压力,4GB 版本会有点紧但是能跑。板的温度在 60 到 70 度之间,需要加一个散热片或者小风扇。

让我比较意外的是,Agent API 的请求阶段和机械臂运动阶段几乎不消耗 CPU 资源,0.1% 都不到,这说明 RISC-V 平台在 IO 密集和串口控制这类任务上非常轻松。系统真正的计算热点只有视觉检测那一块,其他地方基本都在等网络和等机械臂。

5.3 和 Jetson、x86 小主机的对比结论

这个项目做完之后,我对 RISC-V 在机器人场景里的定位有了一个更清醒的认识。如果把同样一套代码放到 Jetson Orin Nano 上,YOLOE 推理时间可以从 500ms 级别降到 50ms 级别,整个闭环的节奏会快很多。放到 x86 小主机上,推理时间也能轻松控制在 100ms 内。理论上,Jetson 和 x86 在性能上全面碾压 VisionFive 2。

但对比不能只看算力。RISC-V 平台带来的软件栈可控性、指令集自主性,这是其他平台不具备的。而且我在这个项目里验证了一个关键事实:只要视觉模型导出成 ONNX,不依赖特定厂商的加速库,RISC-V 就能跑起来。换句话说,RISC-V 虽然目前不是机器人领域性能最优解,但它已经是一个可行的开放选项。随着 RISC-V 的 AI 工具链逐步完善,加上未来带 NPU 的 RISC-V 芯片陆续出现,这个选项的价值会越来越大。

6. 踩坑实录和常见问题速查

6.1 系统安装与基础镜像的坑

给 VisionFive 2 装系统的时候,最容易出问题的是 U-Boot 和启动介质的概念。很多从树莓派转过来的朋友,习惯把镜像直接用 dd 写进 SD 卡就完事,但 VisionFive 2 的官方镜像在第一次启动时会做分区调整和启动文件的初始化,直接 dd 老镜像会出现启动到一半卡死的问题。我的建议是严格按照官方 Wiki 的流程走,先用专用工具烧录,然后插入 SD 卡上电,第一次启动等它自动完成初始化再断电。

还有一个和 Docker 相关的坑:板子自带的 Debian 系统里的 Docker 版本可能比较老,而 MCP 或 Agent 相关的一些镜像要求较新的容器版本。遇到 Pull 下来镜像启动报错的情况,先检查 Docker 版本。升级 Docker 本身在 RISC-V 上也可能遇到依赖缺失,我当时是直接改用系统的 Python 虚拟环境跑 MCP Server,绕开了容器层的问题。

6.2 ONNX Runtime 和模型算子不兼容

这是我在视觉检测部分遇到的最头疼的问题。YOLOE 从 PyTorch 导出 ONNX 后,不是所有算子都能被 RISC-V 上的 ONNX Runtime 完整支持。我遇到过一个很典型的情况:整个模型导出和加载都没问题,但推理到某个节点直接报“NotImplemented”,一看日志是某个自定义算子没有 CPU 实现。

排查思路有几个方向。第一,尽量把模型导出为 ONNX opset 13 以下,新版本 opset 的一些算子兼容性在 RISC-V 上更差。第二,检查模型里是否使用了像 torch.split、torch.repeat_interleave 这类容易被导出成复杂组合算子的操作,尽量在导出前把它们换成更基础的算子。第三,实在不行就手动改造模型图,用 ONNX GraphSurgeon 或者 onnxruntime 自带的图优化工具把不兼容节点替换成等价实现。

这类问题的排查过程非常消耗耐心,所以我强烈建议在部署到板子上之前,先在 x86 机器上把 ONNX 模型完整跑一遍,确认没有兼容性问题,再搬到 VisionFive 2 上。板子上的报错信息往往更少,调试效率更低。

6.3 MCP 连接和 Agent 调用的坑

MCP 这块遇到的最常见的问题是 stdio transport 的进程生命周期管理。如果用 systemd 或者 nohup 拉起 MCP Server,一旦父进程退出,stdin/stdout 管道就会断裂,Agent 侧会一直等到超时。后来我自己写了一个简单的守护脚本,让 Agent 进程主动拉起 MCP Server,并在链路空闲时做心跳检测,发现问题自动重启子进程。

另一个和 Agent 调用相关的坑是上下文膨胀。每轮 Agent 循环都会把上一次的 tool_calls 和工具结果追加到 messages 里,几轮之后上下文长度会快速膨胀。MCP 工具执行的成败结果往往是一大段文本,模型读起来也费 token。我的做法是让 MCP Server 的工具返回尽量精简的结果,比如只返回 OK 或者 ERR,详细的机械臂反馈单独写日志。这样既提高了 Agent 的准确率,也控制了模型 API 的调用成本。

6.4 串口通信和机械臂控制的坑

串口通信是另一个容易忽视的坑。板子的用户可能不在 dialout 组里,导致 Python 打不开 /dev/ttyUSB0,这个时候能在代码层面看到 PermissionError,但很多人第一反应是去查串口波特率,浪费时间。直接把用户加入 dialout 组然后重新登录就行。

还有机械臂的串口应答问题。有些机械臂的串口协议在收到指令后会回一个很长的状态包,如果不调用 readline 把这些数据读出来,下一次写指令就会因为缓冲区满而失败。所以我在 MCP Server 里对每一条串口指令都做了同步的读写配对,并且加了超时重发机制。连续两次超时就返回错误,而不是无限期等待,否则机械臂卡住了系统毫无感知。

6.5 典型问题速查表

现象可能原因解决办法
系统启动卡死镜像版本过旧或 SD 卡兼容性差重新按官方流程烧录,换一张 SD 卡
pip 安装 onnxruntime 报错拉到了错误架构的包从 RISC-V 源安装对应 wheel
模型推理报 NotImplementedONNX 算子不兼容降低 opset、替换算子、预跑验证
Agent 调用工具超时MCP Server 进程退出或 stdin 断裂守护进程自动拉起 MCP Server
机械臂收到指令没动串口缓冲区残留数据每次发送后同步读取应答并清缓冲
坐标偏差大单应性矩阵标定点不准重新标定,检查机械臂末端对准精度
检测抖动导致误判目标位置在多帧之间跳动加指数移动平均,提高检测置信度阈值

7. 后续可以往哪个方向扩展

我的下一步计划是在现有架构上增加深度相机,把平面单应性映射升级成真正的 3D 视觉定位。这样机械臂就可以处理堆叠物体的抓取,而不只是平面上单个物体的抓取。YOLOE 本身支持视觉 prompt,我后面还想在界面上加一个“选中即抓取”的功能:人工在画面里点一个物体,YOLOE 以这个点的视觉特征作为 prompt,把同类目标都检测出来。

另外一个我比较看好的方向是把 MCP 工具做得更细,比如把“搜索目标”“规划路径”“防碰撞检测”都拆成独立的 MCP 工具,让 Agent 有更多的决策余地。这样做可控性会更强,Agent 也能在更复杂的任务里展现出真正的规划能力。

最后分享一个心得:RISC-V 目前的生态确实还不算完善,但正是因为不完善,做这类项目才会逼着你把每一层软件栈都吃透。在 x86 上,很多问题被各种成熟的工具链和文档掩盖了;到了 RISC-V 上,你必须自己动手处理算子兼容、架构编译、性能调优这些底层问题。这个过程虽然折磨人,但做完之后你对系统的理解会上升一个层次。这个项目的价值,不光是跑通了一个机械臂,更是完整地摸清了从视觉感知到大模型决策再到硬件执行这条链路的每一条边。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询