
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 GPU8GB 内存版本配合 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/NPUAI 加速无成熟工具链无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 ServerMCP 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、gripperAgent 只需要按照 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_thres0.35, iou_thres0.45): self.session ort.InferenceSession( onnx_path, providers[CPUExecutionProvider], sess_optionsort.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, dtypenp.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_thres0.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_attempts3): 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, timeout0.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 fMOVJ {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 fGRIP {pos}\n arm.write(cmd.encode()) resp arm.readline().decode().strip() return resp if __name__ __main__: mcp.run(transportstdio)这个 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_urlhttp://localhost:8000/v1, api_keyEMPTY) 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_asciiFalse)}}, {role: user, content: user_instruction} ] resp client.chat.completions.create( modelyour-model-name, messagesmessages, toolstools, tool_choiceauto, temperature0.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.3GB8GB 版本跑起来没压力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 上你必须自己动手处理算子兼容、架构编译、性能调优这些底层问题。这个过程虽然折磨人但做完之后你对系统的理解会上升一个层次。这个项目的价值不光是跑通了一个机械臂更是完整地摸清了从视觉感知到大模型决策再到硬件执行这条链路的每一条边。