
边界:什么该交给模型
大模型擅长判断,不擅长数数。问它「一个没有值表的 2 bit 信号大概是什么意思」,它能给出有用的推断;让它核对 379 个信号在 256 bit 的帧里有没有两位重叠,你会得到一个看起来像答案的东西。
这就是整个设计前提。工程任务天然分两类,混在一起两边都会退化:
| 该交给模型 | 该交给代码 |
|---|---|
| 判断这几种检查里哪个更要紧 | 两个信号是否占同一位 |
| 把一条发现翻译成它导致的现象 | 遍历 379 个信号比对位区间 |
| 看出 changelog 和文件对不上 | 从注释里提取 CRC DataID |
| 决定先跑什么后跑什么 | 每次都算出同一个正确答案 |
Skill 就是这条缝。它要提供足够结构,让模型知道该做什么;又要足够确定,让数字永远不取决于模型。把这条缝划准,才是真正的工程活 —— 下面的内容基本都是围绕它。
必须可复现的 → 进代码。需要上下文和判断的 → 留给模型。一个让模型去做算术的 skill 不算 skill,只能算祈祷。
领域知识落成确定性代码
第一个决策不是「写个解析器」,而是「审计到底需要哪些数据」。这个问题决定架构,而答案把最省事的现成方案排除掉了。
cantools 是个好库,我在别处也用。但它是为解码设计的,而它丢弃的恰好是审计需要的四类数据:
| 审计需要的数据 | cantools .messages |
本工具 |
|---|---|---|
报文周期 GenMsgCycleTime | ✗ | ✓ |
值表 VAL_(枚举文字含义) | 部分 | ✓ |
注释 CM_ | ✗ | ✓ |
报文/信号属性 BA_ | ✗ | ✓ |
丢掉这些不是细节。没有值表,2 bit 信号只读到数字 2,你不知道它代表 On 还是 Fault;没有周期,就没法和时间预算核对。在审计里,元数据就是正文。
所以解析器手写:纯标准库,格式定义的每个字段都保留。
把规范里最容易错的那部分写进代码
DBC 看着像按行组织的格式,其实不是。对正确性影响最大的一处细节是 Motorola 位序:39|8@0+ 不是「从 39 位往后读」,起始位指的是最高位,游标朝 LSB 方向走。
这是领域经验真正兑现的地方。这类知识模型并非完全不知道,而是你不能承受它每次运行时重新推导、且可能推得不一样。
Skill 形态:为加载它的读者而写
工具能跑了,然后是一个和代码无关的设计问题:skill 文件不是文档。它的读者是模型,而模型在特定时刻、特定预算下加载它。
核心是 Step 0 — 判定模式:一张「用户原话 → 子命令」的对照表,并明确写上不要反问。这是整个文件里杠杆最高的一段,因为它省掉的是每一次使用都要付的一轮往返。
# 用户原话 → 子命令
# ──────────────────────────────────────────────────────────────────────
「审计 / 检查 / 有没有问题」 → lint
「对比 / 升级改了什么 / 两版差异」 → diff
「解这帧 / 这串字节是什么值」 → decode
「乱码 / GBK / 中文变问号」 → fix-encoding
# 「检查」和「对比」同时出现 → 先跑 diff
# 已经上线的东西变了,比还没上线的东西有瑕疵严重
第二个设计动作,是大多数 skill 都缺的一节:工具查不出来的东西。头部声明了却一条都不发的节点;changelog 声称新增、文件里却没有的报文;被批量填成同一个值的属性。lint 永远报不出来 —— 而不写下来,「全绿」就会被读成「矩阵没问题」。
一个 skill 的价值不只是它自动化了什么,还在于它多精确地告诉模型自动化到哪为止。这条边界决定了它是工具还是拐杖。
能力设计:朴素 diff 表达不了什么
第二个能力是版本对比。最直觉的实现是按 CAN ID 建索引再遍历两张表 —— 它能跑,也确实产出输出。真正的问题是:这份输出能不能表达真正会让整车网络失效的那种改动。
所以比对分两趟:先按名字跨 ID 配对,报一条 DIF-ID,并抑制那对误导性的增删。同一套推理也得出「信号重命名按位定义配对」—— 因为名字正是变了的东西,位定义是唯一还能锚定它的线索。
藏在自由文本里的那条发现
校验和的 DataID 不是结构化字段,它躺在信号注释的自由文本里:
CM_ SG_ 466 VIU_Info_Checksum "CRC16 checksum (DataID: 0x61D2)";
结构化比对看到的是「注释变了」,归档为 info。但接收方用这个 ID 校验 CRC:改了它,每一帧都被拒收。现象不是报错,而是没反应 —— 模块干脆不理你,然后发送方被查了一整天。所以工具用正则把 ID 提出来,一变就升到 error。
RE_DATAID = re.compile(r"data\s*id\s*[::]?\s*(0x[0-9A-Fa-f]+)", re.I)
# 两边都有 DataID 且不同 → 不是 DIF-CMT,升为 DIF-CRC (error)
if old_id and new_id and old_id != new_id:
return DiffFinding("DIF-CRC", "error", ...)
两个案例是同一条洞察:工程产物里最危险的信息往往不在 schema 里,而在注释、字符串、命名约定里。只读 schema 的工具,会在坏文件上报成功。
质量体系:让结论可被验证
工具的结论如果没人能验证,那它就不值得信任,只是「很有自信」而已。三个机制,各自针对一类失效:
| 失效类型 | 机制 |
|---|---|
| 代码算错 | 测试建立在答案已知的样例上,而不是「它跑起来了」 |
| 文档和代码脱节 | 双向断言:文档写了没实现 → 红;实现了没写文档 → 也红 |
| 模型用错 | 隔离安装、清空会话、自然语言提问,再查它实际执行的命令 |
用断言代替承诺
文档会静默漂移。一张写着「12 类检查」而代码只有 11 个的表,比没有表更糟,因为它让报告看起来很完整。所以这个数字被断言,而且是双向的:
documented = set(re.findall(r"`([A-Z]{3}-\d{3})`", skill_md))
implemented = set(re.findall(r'code="([A-Z]{3}-\d{3})"', source))
assert documented - implemented == set() # 文档超前 → 红
assert implemented - documented == set() # 文档滞后 → 红
同一条规则也覆盖文档里关于示例的每个数字:严重度分布、缺陷类别数。一旦某个检查改动导致示例表现变化,CI 直接红,而不是让文档悄悄变错。
def test_broken_covers_ten_codes(self):
_, findings = self._lint(EXAMPLES / "demo_broken.dbc")
codes = {f["code"] for f in findings}
self.assertEqual(len(codes), 10) # 数字对不上就红
self.assertEqual(codes, EXPECTED_SET) # 集合也要相等
样例兼作环境自检
两个示例 DBC 随 skill 一起分发,所以 agent 在碰真实文件之前可以先自检环境:
python3 <skill>/scripts/dbc_analyze.py lint <skill>/examples/demo.dbc # 期望 0 error 0 warn
python3 <skill>/scripts/dbc_analyze.py lint <skill>/examples/demo_broken.dbc # 期望 10 类缺陷全报
干净样例的位分配由生成器按算法计算而非手填,破损样例则刻意命中每一族检查。前者不再干净、或后者不能全报,就说明有回归 —— 不需要人去看。
验收:让模型真的去用
以上这些都只证明脚本能跑,不证明 skill 可用。因为 skill 不等于脚本 —— 它是脚本、加上指令、加上模型对指令的理解。
所以验收放在隔离 profile 里、清空会话、用自然语言驱动,然后检查 agent 实际执行了哪些命令:
# 隔离 profile:状态进 ~/.openclaw-skilltest,workspace 也独立
openclaw --profile skilltest skills install <skill-dir>
# 真实用户措辞,不是命令
openclaw --profile skilltest agent --local --agent main \
-m "帮我检查一下 hood_v14.dbc 这个通信矩阵,有什么问题"
| 提问 | agent 自己选的命令 |
|---|---|
| 「检查这个矩阵有什么问题」 | info → fix-encoding → lint → messages → signals --frame |
| 「这两个有什么不同」 | 两边都转码,再 diff 旧 新,基线顺序正确 |
第二行才是关键。Step 2 写的是「编码先于一切」,agent 把这条原则自己推广到了两个文件 —— 而文档里从没写「diff 时两边都要转码」。这就是「照着清单做」和「照着规则做」的区别。
装在仓库旁边什么都证明不了 —— 相对路径可能碰巧能通。唯一有意义的测试,是未来用户会做的那一次:干净环境,仓库不在旁边。
成果与可复用的方法论
用同一车型网络、相隔一版的两份真实矩阵实测:10 error / 49 warn / 42 info —— 反向对比报出同样的 10 个 error,说明这份比对是对称的,不是单边偏斜。
$ dbc_analyze.py diff EHX_Car_FLM_v1.0.dbc EHX_VIU1_HOOD_v1.4.dbc
old 21 messages / 379 signals
new 26 messages / 297 signals
[E] DIF-CRC VIU_Info_Checksum: checksum data id changed (0x1D2)
0x61D2 -> 0x41D2
[E] DIF-ID NM_Autosar_FLM_R: CAN id changed (0x457 -> 0x46B)
[E] DIF-ID NM_Autosar_FLM_L: CAN id changed (0x45C -> 0x46A)
[E] DIF-REN signal renamed VIU1_SourceNodeIdentifier -> VIU1_NodeAddress
summary: 10 error, 49 warn, 42 info, 101 total
最危险的三条在普通 diff 里全都看不见:一个变了的 CRC DataID,和两条名字没变、地址变了的网络管理报文。所有接收方都得重新编译。这就是「为领域设计」而不是「为格式设计」的回报。
方法论,五行
| 步骤 | 要回答的问题 |
|---|---|
| 1 · 切边界 | 哪些必须可复现,哪些需要判断? |
| 2 · 落代码 | 领域里有什么,是模型每次都会推得不一样的? |
| 3 · 定形态 | 先写 Step 0。模型需要什么才不用反问? |
| 4 · 划边界 | 它永远看不见什么,该由谁去核? |
| 5 · 证明 | 文档里每个结论有测试断言吗?模型在干净环境里选得对命令吗? |
这五步里没有一步是「多写代码」。其中四步都是在决定什么不该自动化,以及如何让剩下的结论可被验证。这部分才是能迁移到下一个 skill 的东西。
代码、测试与完整设计
纯标准库,MIT 许可。示例矩阵全部合成 —— 不含任何真实车型、供应商或客户矩阵里的人名。
github.com/autosar-skills/autosar-agent-skills常见问题
skill 和一段 prompt 的区别在哪?
prompt 是说服,skill 是约束。skill 有 Step 0,先判定走哪个分支再动手; 有带代价说明的执行顺序(颠倒会怎样);有「要么被测试断言、要么明确列为查不出来」的输出。 如果去掉模型结果也不变,那它本来就是个工具,该落到代码里。
为什么不干脆每次让模型现写解析器?
因为模型每次都会把同一批领域事实重新推一遍,结论不一样。 位几何公式、PduR 路由链、哪家厂商用哪个容器名 —— 这些是事实,不是判断。 写进代码一次,模型就不会在某个周二推错;错了也是测试失败,而不是一段看起来对的话。
以说明文字为主的 skill 怎么测?
能机械校验的部分用断言,其余的明确列出来。 文档里的检查码必须与代码能实际产生的码双向相等 —— 这条抓到过一个「文档写了但代码从未 emit」的码。 测试覆盖不到的,必须单列成「查不出来、需人工核对」,而这恰恰是多数 skill 文档会漏掉的一段。
为什么 SKILL.md 写成执行手册,而不是 README?
因为它的读者是模型,不是人。模型不需要被告知设计为什么好,它需要知道走哪个分支、按什么顺序。 说明性内容下放到 README.md 或 references/,按需加载。 一个快速判断法:SKILL.md 里找不到 Step 0,八成是写给人看的。