前几篇里,我一直在测试 OE-Skills 在征程6工具链任务中的表现。
从 Router 路由、Skills / MCP 对回答质量的影响,到长链路规划、无板能力边界,再到第六篇直接拿真实社区问题做外部 Gold 盲测,研究对象一直比较接近:
Agent“说出来的答案”到底靠不靠谱?
但做到这里以后,我觉得还有一个更实际的问题没有回答。
如果不只是让 Agent 解释 UCP,而是直接让它:
给我写一份征程6 UCP C++ 推理代码。
那这些代码到底能不能用?
但:
API 名字真的存在吗?
参数和调用顺序对吗?
- stride 有没有被忽略?
Cache Clean / Invalidate 用对了吗?
中途报错时资源能不能释放?
多线程失败以后程序会不会还打印“成功”?
Host 和 J6 Target 有没有混在一起?
这些问题靠“代码看起来挺专业”是判断不了的。
所以 Exp07 我不再评测回答文本,而是第一次把研究对象换成了:
Agent 真正生成出来的 C++ 工程产物。
我固定了 6 个 UCP 代码任务,然后逐份做 API、生命周期、内存、Cache、错误处理和平台边界审计。
结果也让我重新认识了“生成代码质量高”这句话。
第一轮检查时,6 份代码拿到了:
115 / 120。
其中甚至有 3 份被评成 20 / 20。
但真正开始逐行复核,再加入机械 syntax-only 检查以后:
最终只剩 102 / 120。
而且:
6 份代码,没有一份满分。
一、这次到底让 Agent 写了什么?
这次没有只生成一份“Hello World 式”的 UCP 示例。
我固定了 6 个不同方向的代码任务。
G01:最小单模型推理
要求覆盖一条基本完整的 UCP 推理链:
→ 获取模型 Handle
→ 查询输入输出
→ 分配 Tensor 内存
→ 填充输入
→ Cache Clean
→ 推理任务提交
→ 等待完成
→ Cache Invalidate
→ 读取输出
→ 释放资源
这是最基础的一题。
G02:动态读取模型 I/O
这一题故意不提供真实输出 Shape 和量化参数。
目的是看 Agent 会不会凭经验直接写死:
Tensor Shape;
Stride;
Buffer 大小;
Scale;
Zero-point。
正确做法应该优先读取模型实际的 Tensor Properties,再决定内存和后处理。
G03:量化输出与后处理
要求读取量化输出,并完成从定点 Tensor 到业务后处理之间的转换。
这一题主要看:
量化类型判断;
Scale / Zero-point;
Stride;
内存来源;
Cache;
反量化和业务解码有没有混在一起。
后来这也是问题最多的一份代码。
G04:两个独立模型并发
两个 HBM 没有数据依赖。
要求 Agent 设计合理的并发结构,同时避免:
Model Handle 混用;
Buffer 共享;
Task Handle 生命周期问题;
多线程错误状态丢失。
G05:错误处理与资源清理
这一题我专门要求考虑:
模型加载失败;
内存申请失败;
推理提交失败;
Wait 失败。
重点不是“正常流程能跑”,而是:
出错以后还能不能正确退出。
G06:Host / Target 边界
看 Agent 是否能区分:
x86 Host;
- 远程 hbm_infer;
J6 Target;
板端原生 UCP C++。
这也是为了检查它会不会再次出现:
“在 PC 上跑 HBM = 在 J6 BPU 上本地执行”
这种边界混淆。
二、我怎么给代码打分?
每份代码固定检查 10 个维度,每项 0~2 分。
单份满分 20 分,6 份合计 120 分。
维度 | 主要检查内容 |
|---|---|
C1 API 真实性 | 函数、枚举、结构体是否真实 |
C2 API 使用 | 参数、签名、调用方式是否合理 |
C3 生命周期 | 加载、Tensor、Infer、Wait、Release 是否完整 |
C4 Metadata | Shape、Stride、alignedByteSize 是否正确处理 |
C5 Cache | Clean / Invalidate 是否放在正确阶段 |
C6 内存安全 | Buffer、Handle、异常路径是否存在泄漏 |
C7 错误处理 | 错误码有没有检查和向上传播 |
C8 Host / Target | 是否混淆 x86 与真实 J6 Runtime |
C9 不越权 | 是否虚构“已编译”“已跑通” |
C10 可读性 | 代码结构和未决条件是否清晰 |
6 个任务在生成之前就固定,后面不因为代码表现好坏换题。
而且生成出来的原始代码也不允许人工修改以后重新评分。

图1|本次审计参考的 UCP 推理主生命周期。代码不仅要“出现这些 API”,还要保证内存、Cache、Task 和资源释放之间的调用关系正确。
三、第一轮结果其实非常漂亮:115 / 120
第一次检查完,结果看起来甚至有点太好了。
6 个任务的初审分数分别是:
G01:19 / 20
G02:18 / 20
G03:20 / 20
G04:20 / 20
G05:20 / 20
G06:18 / 20
总分:
115 / 120,95.8%。
三个任务直接满分。
如果在这里停下来,这篇文章可能会变成:
“OE-Skills 生成 UCP C++ 的质量已经非常高。”
但后来我重新逐行看代码时,越来越觉得不对。
因为一些代码虽然:
API 很多;
注释很完整;
六步流程也基本都有;
但这并不代表每一个细节都是真的。
所以我又加了一轮更严格的人工复核,并额外做了一个机械检查:
把 Horizon 官方 API 声明整理成只读 stub header,再使用 clang++ -fsyntax-only 检查代码的语法和 API Surface。
这里需要强调:
syntax-only ≠ OpenExplorer 真正编译通过。
它只能检查:
C++ 语法;
已声明 API;
一部分函数 / 枚举名称。
它不能证明:
能链接真实 UCP;
能生成 aarch64 程序;
能在 J6 上运行;
输出正确;
性能达标。
但就算只做这一步,结果已经变了。
四、真正复审以后:115 掉到了 102
第二轮最终成绩变成:
任务 | 第一轮 | 最终复审 | Syntax-only |
|---|---|---|---|
G01 最小单模型推理 | 19 | 18 | PASS |
G02 动态读取模型 I/O | 18 | 17 | PASS |
G03 量化输出与后处理 | 20 | 15 | ERROR |
G04 多模型独立并发 | 20 | 17 | PASS |
G05 错误处理与资源清理 | 20 | 18 | PASS |
G06 Host / Target 边界 | 18 | 17 | ERROR |
最终总分:
102 / 120,85.0%。
均分:
17.0 / 20。
更重要的是:
原来的 3 个满分,最终全部消失。
6 份代码最终没有一份 20 / 20。
机械检查则是:
4 PASS / 2 ERROR。
出现 ERROR 的正好是 G03 和 G06。

图2|6 份生成代码第一次初审与第二轮严格复审结果。G03、G06 在 API-surface syntax-only 检查中直接出现 ERROR。
为什么差了整整 13 分?
下面几个案例最典型。
五、G03:一个枚举名,就足以让“完整代码”直接报错
G03 第一轮其实拿了:
20 / 20。
从结构上看也确实很像一份完整代码:
读取 Tensor Properties;
判断量化类型;
读取 Scale;
做反量化;
处理 Cache;
再进入业务后处理。
但机械检查直接抓到:
代码使用了:
而当前参考接口并不存在这个写法。
也就是说:
一份视觉上非常完整、逻辑也解释得通的代码,因为一个 API 常量写错,就可能连最基本的接口表面检查都过不了。
这还不是 G03 唯一的问题。
六、更隐蔽的问题:普通 std::vector 不能假装成 UCP 物理内存
G03 里还有一段代码,先创建普通 CPU 内存:
第一眼看非常合理:
既然 BPU 写了数据,CPU 读取之前做 Cache Invalidate。
但问题是:
这块内存根本不是通过 UCP 内存接口合法申请出来的。
正常 UCP Tensor 内存应该基于真实 Tensor Properties,再通过类似:
这样的接口取得合法的系统内存。
所以这里的问题不是:
“忘记 Flush”。
反而是:
Flush 写了,但 Flush 的对象本身不合法。
我觉得这比单纯漏一个 API 更值得注意。
因为生成代码最容易给人安全感的东西,恰好就是:
它把正确 API 写出来了。
但 API 出现了,不代表作用对象就是对的。
七、Stride 也不是“拿到 Shape 以后 flatten 就行”
G03 的另一个问题是输出反量化。
这在普通紧密连续 Tensor 中很自然。
但 J6 的 Tensor Properties 里不只有:
还有:
真实硬件 Tensor 可能因为对齐存在 Padding。
因此:
逻辑 Shape 连续,不代表底层物理内存也是完全连续排列。
如果代码只按:
这一题最后在 Metadata / Stride 上也被扣了分。
这让我觉得,对 AI 生成的硬件侧代码来说:
“Shape 对了”可能还不够,内存布局才是真正容易出错的地方。
八、G04:正常路径漂亮,不代表失败路径安全
G04 是两个模型并发推理。
它第一轮也是:
20 / 20。
代码确实做了很多正确事情:
两套 Model Handle;
各自独立 Tensor;
独立 Task;
两个工作线程;
最后 join。
但真正顺着失败路径往下看,会发现一个问题。
假设:
接下来:
或者:
失败。
代码会直接 return。
于是异常路径存在句柄泄漏风险。
还有一个更容易被忽略的问题:
某个工作线程里面即使已经推理失败,主线程 join 完以后仍然可能无条件打印:
Concurrent Inference Finished successfully!
然后:
也就是说:
内部已经失败,最外层却告诉调用方成功。
这和 API 幻觉完全是两类问题。
API 可能全部是真的,代码甚至能过 syntax-only。
但工程语义仍然是错的。
九、G05:资源都清干净了,结果还是错
G05 专门测试错误处理。
它采用了一种很常见的 C 风格写法:
然后在统一的:
下面释放已经申请的资源。
这一点本身其实不错。
问题出现在函数最后:
无论前面到底发生了什么错误,清理完以后最后都告诉调用者:
成功。
于是出现一个很有意思的现象:
资源管理做对了,但 API 语义依然错了。
更合理的方式应该是保存真实错误状态,例如:

图3|统一 cleanup 本身不是问题,真正容易漏的是错误状态传播。清理资源以后仍需把失败状态返回给上层。
这也是这次我觉得最有工程味的一个问题。
因为它根本不是“AI 不知道 API”。
它知道 API。
它甚至知道怎么 cleanup。
但还是可能在最后一行把整个错误处理逻辑毁掉。
十、G06:Host / Target 边界答对了,API 却混版本了
G06 的表现也很典型。
从架构理解上,它其实做得不错。
它能够正确区分:
x86 Host;
- hbm_infer;
J6 板端服务;
原生 UCP C++;
真正 BPU 执行发生在哪里。
也没有把 PC 仿真写成真实 J6 Runtime。
所以如果只评“解释是否正确”,这题可能会拿很高分。
但代码部分却出现了另一类问题:
新旧 API 混用。
例如出现了旧体系里的:
而当前 J6 UCP 路线使用:
更严重的是,代码还写出了:
在删除人为补充的假声明以后,syntax-only 检查直接报:
这说明 Agent 可能:
知道“这里应该释放资源”,但把另一个 SDK / 版本 / 命名习惯里的 API 拼到了当前代码里。
这是 LLM 生成工程代码里很典型的一类风险:
语义正确,符号错误。
十一、甚至 Python 侧也藏了一个 TypeError
G06 不只有 C++。
代码想打印:
并用:
格式化。
因此:
实际上还是一个字典。
直接:
会触发 TypeError。
正确逻辑应该继续取:
或者使用对应的单帧 Profile 接口。
这又是一个“看起来完全合理,但运行时马上出错”的例子。
十二、那 OE-Skills 到底有没有用?
做到这里不能反过来得出:
“Agent 生成 UCP 代码完全不能用。”
这个结论同样不成立。
这 6 份代码仍然有不少比较稳定的共同点。
例如:
没有把 HBM 生成直接写成部署完成;
没有伪造真实 J6 实测结果;
Host / Target 总体边界没有混乱;
大部分代码知道输入前需要 Cache Clean;
读取 BPU 输出前需要 Cache Invalidate;
大部分代码知道应该查询 Tensor Properties;
基本生命周期主链路总体是存在的。
最终 6 个任务仍然全部处于:
15~18 / 20。
也就是说,它们并不是完全不可用的随机代码。
我更愿意把结果理解成:
OE-Skills 可以帮助 Agent 生成一份“方向比较正确的 UCP 初稿”,但这还不能替代真正的工程 Code Review。
特别是:
API exact identifier;
SDK 版本;
Stride;
物理内存;
异常路径;
错误码传播;
这些地方仍然非常值得人工检查。
十三、为什么第一轮会拿到 115,第二轮只剩 102?
我觉得这次最值得反思的甚至不是某一个 API。
而是:
为什么第一次看代码时,这些问题很容易被漏掉?
答案可能很简单。
因为一份 AI 生成代码如果同时具备:
很长;
注释清楚;
API 名很多;
六步生命周期完整;
有错误检查宏;
有 cleanup;
有线程封装;
人很容易先形成一个整体印象:
“这份代码应该挺完整。”
然后后面的审查会受到这个印象影响。
例如看到:
第一反应可能是:
“Cache 处理写了,加分。”
而不是继续问:
“它 Flush 的到底是不是合法 UCP 内存?”
看到:
第一反应可能是:
“异常路径清理考虑到了。”
而不是继续看到最后:
看到一个很像官方风格的枚举:
也可能下意识认为它存在。
所以这次最终从:
115 / 120
掉到:
102 / 120
对我来说反而比一个稳定的高分更有意义。
它说明:
AI 代码审计不能只看结构完整度,还要做符号级、对象级和失败路径级检查。
十四、机械检查有用,但也不能被夸大
最终结果:
4 PASS / 2 ERROR。
它确实很有用。
至少能把:
未声明函数;
未声明枚举;
某些签名问题;
从“人工感觉”变成机器可复现结果。
但我仍然不会把这写成:
“4份代码已经成功编译。”
因为本实验没有使用完整真实 OE / UCP Build 环境完成交叉编译,更没有在 J6 板端运行。
这里的 PASS 只能表示:
在依据官方 API 声明构造的 syntax-only 环境中,没有发现相应语法/API Surface 错误。
而且 G04 和 G05 正好证明:
Syntax PASS 也不等于工程正确。
它们都通过了机械检查,却仍然存在错误状态和资源管理问题。
所以真正更稳的代码审计可能应该是:
API / Syntax
→ 生命周期
→ 内存与 Cache
→ 失败路径
→ 真正 OE Build
→ J6 板端验证
而不是只跑一个 Compiler 就结束。
十五、这次实验能说明什么,不能说明什么?
这次可以说明的是:
6 份 OE-Skills 辅助生成的 UCP 代码总体能够保持征程6推理主流程;
第一轮表面审计容易高估代码质量;
严格复审后总分从 115 / 120 降到 102 / 120;
6 份代码没有一份最终满分;
syntax-only 检查中 4 份 PASS、2 份 ERROR;
出现的问题不只有 API 幻觉,还有 Stride、物理内存、Task 生命周期和错误传播。
不能说明的是:
102 / 120 等于真实代码“85%可用”;
4 个 syntax PASS 的程序已经完成真实 OE 编译;
这些程序已经可以直接部署到 J6;
本次已经在 J6 上验证推理输出;
本次已经测到任何真实性能、FPS 或 P95。
因为这些都没有发生。
所以 85.0% 只是:
本次预注册 10 维静态工程评分体系下的代码质量得分。
不是板端部署成功率。
十六、从“Agent 会回答”到“Agent 生成的东西能不能交付”
做到第七次实验以后,我感觉测试对象已经变了很多。
最开始,我关心的是:
Agent 知不知道应该调用哪个 Skill?
后来是:
它回答得对不对?
再后来是:
它能不能处理真实社区问题?
而这次终于变成:
它真正生成的工程产物,经不经得住逐行审查?
最终结果给我的感觉是:
能生成,不等于能交付。
一份生成代码可能:
架构是对的;
生命周期也基本完整;
注释甚至比人写得详细;
所以如果以后继续把 Agent 用在征程6开发里,我可能不会只问:
“它会不会写 UCP?”
而会多问一句:
“它写完以后,我有没有一套机制自动检查 API、版本、内存和失败路径?”
对我来说,这可能比继续让模型生成更长的代码更重要。
参考资料
- HorizonRobotics / OE-Skills
https://github.com/HorizonRobotics/OE-Skills - 地平线 OpenExplorer 官方文档
https://docs.oe.horizon.auto/ - 本次实验使用 OE-Skills release 0.2.0,commit 0c35b7329331ebf0ee626a0ca667148dc20aaeed
本文只记录 OE-Skills 辅助生成的征程6 UCP 代码静态工程审计。未使用真实 J6 开发板完成代码运行,未声明完整 OpenExplorer 交叉编译通过,也未产生真实板端性能或精度数据。文中的 102 / 120 为本次预注册评分体系下的静态代码质量得分,不代表真实部署成功率。
