教程:小行星矿场数字孪生(从简单到复杂)
前情:你是小行星矿场老板。矿场的日常运营——客户、产品、成交——你已经用一句话搭起了客户管理。这一篇是矿场的另一半,也是更要命的一半:天上那颗小行星。
矿场里有几十台矿石机在轰鸣,有一支员工队伍,还有一个望远镜——夜班观察员用它盯着天上。
问题来了:所有这些信息,散在哪?
- 机器的生产状态,在班长的脑子里
- 员工能不能上班,在排班表的 Excel 里
- 观察员看到的小行星数据,在值班日志里
- 而你的 AI 助手——换个会话,它什么都不记得
你想要的,是让这一切活在一个系统里:观察员改一行数据,机器该停就停、该转就转,员工自动放假,AI 助手自动去处理善后——不用人挨个去通知。
这就是”数字孪生”:把真实矿场在电脑里复制一份,让规则替人盯着。
这个教程,就带你从零搭这个数字孪生。9 课,每课只加一样东西,一步步从”一张档案卡”搭到”AI 总裁拍板关停”:
| 课 | 你搭出什么 | 你学会 |
|---|---|---|
| 1 | 小行星的一张”档案卡” | 对象是什么 |
| 2 | 档案卡会”活”了(安全→警报) | 生命周期和规则 |
| 3 | 警报自动停掉所有机器 | 一个事件影响一整批 |
| 4 | 机器停了,员工自动放假 | 对象之间的关系 |
| 5 | 停机自动派工单给运营 | 派发工作 |
| 6 | 观察员改 md 文件,系统跟着变 | 数据管道 |
| 7 | 撞击前先算损失,再决定停不停 | 外挂计算器(kernel) |
| 8 | 关停决策送到”AI 总裁”面前 | 连接你电脑上的 AI |
| 9 | 没撞也能预演会怎样 | 沙盒干跑 |
第 1 课 · 一张档案卡:先把小行星”记进系统”
你的观察员昨晚报告:天上来了个不速之客,编号 2026-QX,一颗小行星。
现在,你想把它记进系统。怎么记?
先想一个问题:什么才算”记进系统”?
不是写张便签贴屏幕上。是让系统知道有这么个东西,而且以后任何时候都能找到它、认出它。
比如你面前这张档案卡:
名称:2026-QX
距离地球:150 万公里
警报阈值:100 万公里(跨过这条线就要拉警报)一张卡,几个字段。就这么简单——数字孪生第一步,就是给每样东西建一张档案卡。
runex 里,这张卡的模板叫 supertag(你可以直接读作”类型”)。它的作用就是:规定”小行星”这种东西都有哪些字段。
来看真实的代码——docs/examples/asteroid/lesson-01-object.scm:
(supertag "AsteroidThreat" ;; 给这种"东西"起个名:小行星威胁
(natural-key "名称") ;; 用哪个字段认人?——名称
(description "小行星威胁——数字孪生监控对象")
(field "距离地球公里" (type "number") (required #t)) ;; 一个字段:数字
(field "警报阈值公里" (type "number") (required #t)))逐行看,都是大白话:
(supertag "AsteroidThreat" ...)—— 定义一个叫”小行星威胁”的类型(natural-key "名称")—— natural-key 就是”身份证号”:以后凡是名称叫 2026-QX 的,都算同一个人。系统不会给你建出两张重复的卡(field "距离地球公里" (type "number") (required #t))—— 这张卡有一个字段”距离地球公里”,类型是数字,必填(required #t= 必须有)
动手:让系统认识这个类型
打开终端,敲:
runex ontology load docs/examples/asteroid/lesson-01-object.scm它会回答:
OK lesson-01-object.scm: 0 machine(s), 0 action(s) loaded0 machine, 0 action 先不用管——那两样东西后面几课才有。重要的是系统学会了”小行星威胁”这种类型。
动手:给 2026-QX 建卡
runex node create "2026-QX" --tag AsteroidThreat -f "名称=2026-QX" -f "警报阈值公里=1000000"它回答:
+ 2026-QX <一串字符>这串字符就是这个对象的 id(身份证号),后面所有操作都要用它。记不住没关系——用 query 就能找回来:
runex query --name "2026-QX"你会看到一张表格,里面是 2026-QX 的 id、名称、标签。你刚才建的那张卡,现在躺在系统里了。
验证:为什么说它”记进系统”了
再敲一次 node create,一模一样的命令:
runex node create "2026-QX" --tag AsteroidThreat -f "名称=2026-QX" -f "警报阈值公里=1000000"然后 query 看看——还是那一个对象,没有多出第二张卡。 因为 natural-key(身份证号)说:2026-QX 已经有主了,新来的就是它自己。
这就是”记进系统”和”写张便签”的区别:系统认得它,不会重复,随时能找到。
你现在有了:
- 一个类型(supertag):小行星威胁,带两个字段
- 一个对象(node):2026-QX,躺在系统里
但说实话,这张卡是死的。它不会自己报警,不会自己更新。观察员改天再报告”距离 80 万公里了”,你得手动改卡——而且改了也没人拦着,写错了就写错了。
这不行。你的小行星应该活着:安全时安安静静,一旦跨过阈值,自己拉响警报。
下一篇,就让档案卡”活”起来。
第 2 课 · 让它活:状态机与规则
上一课的 2026-QX 还是一张死卡。这一课,你要给它装上”生命”。
先想一个问题:一张卡怎么才算”活”?
活的标志是:它有状态,而且状态会变,变的时候有规矩。
你的小行星,状态只有两个:
监控中(monitoring)—— 一切正常,距离还远
警报(alerted) —— 距离跨过了阈值!从”监控中”变到”警报”,只能由一条规矩触发,不是谁想改就改:
当 距离地球公里 ≤ 警报阈值公里 时,拉响警报。
反过来,没跨过阈值就拉警报?不行。系统会拦下来。
在 runex 里:
- 状态的集合 + 合法的变化,叫 machine(状态机)。你可以想象成电梯——只能停在合法的楼层,只能按规则上下,不能从 1 楼直接闪到 18 楼
- 那条”规矩”本身,叫 action(动作/规则)
看代码:状态机长什么样
docs/examples/asteroid/lesson-02-machine.scm:
(machine "AsteroidThreat" ;; 给 2026-QX 这类型装上状态机
(supertag "AsteroidThreat")
(initial "monitoring") ;; 出生状态:监控中
(states
("monitoring" "assess_asteroid_threat") ;; 监控中 →(触发某规则)→ 警报
("alerted" "stop_ore_machines"))
(description "小行星威胁监控生命周期:monitoring → alerted"))states 里写的 ("monitoring" "assess_asteroid_threat") 意思是:从 monitoring 状态出发,允许执行一个叫 assess_asteroid_threat 的动作。没有列出来的迁移,一律不允许。
那条规则长这样:
(action "assess_asteroid_threat" ;; 规则名
(machine "AsteroidThreat")
(from-states "monitoring") ;; 只在"监控中"才生效
(trigger (on field-set (field "距离地球公里") (supertag "AsteroidThreat")))
;; 什么时候检查?——"距离地球公里"字段被更新时
(guard (<= (field "距离地球公里") (field "警报阈值公里")))
;; 什么条件才放行?—— 距离 ≤ 阈值
(effect (begin (set-field "威胁等级" "text" "alert")
(transition "alerted"))))
;; 放行了做什么?—— 写上"alert",变成警报状态三个词,就是规则的骨架,以后每一课都会见到:
| 词 | 大白话 | 这条规则里 |
|---|---|---|
trigger | 什么时候检查 | 距离字段被更新时 |
guard | 什么条件才放行 | 距离 ≤ 阈值(否则拒绝) |
effect | 放行了做什么 | 写威胁等级、变成警报状态 |
动手:装上状态机,看一眼它长什么样
runex ontology load docs/examples/asteroid/lesson-02-machine.scm
runex state AsteroidThreat --mermaid它会画一张状态图给你:
这就是你的小行星的”一生”:监控中,跨过线,变警报。只有这一条合法路线。
验证:规则真的会拦人吗?
观察员报告:距离 150 万公里。安全,因为阈值是 100 万。你更新一下卡:
runex node update <2026-QX 的 id> -f "距离地球公里=1500000"(<2026-QX 的 id> 换成你 query 查出来的那串字符。)
现在 150 万 > 100 万,阈值没跨过。理论上规则应该拒绝拉警报。验证一下——用 preview 干跑一次这条规则(preview 后面第 9 课专门讲,你现在先把它当”试一下”按钮):
runex ontology preview assess_asteroid_threat <2026-QX 的 id>你会看到:
(dry-run) GUARD REJECTS assess_asteroid_threat on <id>
expr: (<= (field "距离地球公里") (field "警报阈值公里")) → False看最后一行——引擎把守卫条件摊开给你看了:150 万 ≤ 100 万 算出来是 False,所以拒绝。这不是系统藏着掖着,是明明白白告诉你为什么。
这一课,你的小行星活了:
- 有状态机(machine):只有 monitoring → alerted 一条合法路线
- 有规则(action):跨过阈值才拉警报,没跨过会被拦
但你发现一个问题没有?警报拉了,然后呢? 2026-QX 自己变红了,可矿场里那几十台矿石机还在照常轰鸣。
你当然不想半夜爬起来一台台去关。你想让系统自己干:警报一响,所有机器自动停。
下一篇,就让一个事件影响一整批。
第 3 课 · 一个事件,影响一整批:警报自动停机器
上一课的小行星会拉警报了。这一课,让警报自动停掉所有矿石机。
先想一个问题:谁去通知机器?
矿场有几十台矿石机。警报响了,理论上每台机器都要收到通知、停下来。
你不可能写几十条规则,一条管一台。你要的是一句话:
所有正在生产的矿石机,都给我停。
在 runex 里,这就是 for-each(逐个处理) + find-by-tag(找出所有挂某个牌子的对象)。
先看代码——docs/examples/asteroid/lesson-03-cascade.scm:
(supertag "OreMachine" ;; 先定义"矿石机"这种类型
(natural-key "名称")
(field "当前产物" (type "text"))
(field "停工原因" (type "text")))(machine "OreMachine" ;; 矿石机也有自己的状态机
(supertag "OreMachine")
(initial "producing") ;; 出生状态:生产中
(states
("producing")
("stopped" "suspend_employees_for_machine"))) ;; 停机后还能触发下一步然后是那条关键规则——注意 trigger 变了:
(action "stop_ore_machines"
(machine "AsteroidThreat") ;; 这条规则挂在"小行星威胁"上
(from-states "alerted") ;; 威胁处于"警报"状态时生效
(trigger (on state-transitioned (machine "AsteroidThreat") (to "alerted")))
;; 什么时候触发?—— 威胁"变成警报"的那一刻
(guard #t) ;; 条件:#t = 恒真,无条件
(effect
(for-each (find-by-tag "OreMachine") machine ;; 找出所有矿石机,逐个处理
(if (= (state-on machine "OreMachine") "producing") ;; 还在生产吗?
(begin
(set-field-on machine "停工原因" "text" "asteroid_alert") ;; 记下原因
(transition-on machine "OreMachine" "stopped")) ;; 让它停机
nil))))新东西就三样:
| 代码 | 大白话 |
|---|---|
(on state-transitioned ... (to "alerted")) | 状态迁移完成时触发——上一课是”字段被改时”,这一课是”状态变了时” |
(for-each (find-by-tag "OreMachine") ...) | 找出所有矿石机,一台台处理。find-by-tag = 找出所有挂”矿石机”牌子的对象 |
(transition-on machine ...) | 在别的对象身上执行迁移——不再是改自己,是去改机器 |
注意:这条规则不是矿石机自己身上的,而是挂在小行星威胁上——警报一发生,它负责去”广播”给所有机器。
动手:造一台机器,然后引爆
先造一台矿石机:
runex ontology load docs/examples/asteroid/lesson-03-cascade.scm
runex node create "OreMachine-A" --tag OreMachine -f "名称=OreMachine-A" -f "当前产物=Ore"然后模拟观察员报告:距离改到 75 万公里——跨过了 100 万阈值:
runex node update <2026-QX 的 id> -f "距离地球公里=750000"等一下,这一条命令发出去,发生了多少事?查一下机器的状态:
runex query --name "OreMachine-A" # 先拿到机器的 id
runex inspect node <OreMachine-A 的 id>你会看到:
__state__OreMachine text stopped
停工原因 text asteroid_alert机器停了,还记下了原因:小行星警报。
这就是”级联”(cascade):一条命令触发一个状态迁移,一个状态迁移触发一条规则,一条规则影响一整批对象。第 4、5 课你会看到这条链越接越长。
这一课,警报能自动停机器了。
但你又发现一个新问题:机器停了,人呢?
Alice 是操作 OreMachine-A 的工人。机器都停了,她还来上班吗?来了也是干坐着。
你当然想:机器一停,开这台机器的人自动放假。 但系统怎么知道”Alice 是开这台机器的人”?它俩之间得有个关系。
下一篇,就给人和机器之间搭上关系。
第 4 课 · 搭上关系:机器停了,人自动放假
上一课机器会停了。这一课,让机器和人的关系也进入系统——机器停,开它的人自动放假。
先想一个问题:系统怎么知道谁在开哪台机器?
你脑子里清楚:Alice 操作 OreMachine-A。但系统不知道——除非你明确告诉它俩的关系。
这个关系,在 runex 里叫 ref 字段(你直接读作”链接”)。就是在 Alice 的档案卡上加一个字段:“关联机器”,值指向 OreMachine-A。
看代码——docs/examples/asteroid/lesson-04-links.scm:
(supertag "Employee"
(natural-key "姓名")
(field "姓名" (type "text") (required #t))
(field "岗位" (type "text"))
(field "可上班" (type "bool") ;; 能不能上班
(description "#t 可上班 / #f 不可上班"))
(field "关联机器" (type "ref") ;; ← 重点:这是个"链接"字段
(description "链接到矿石机")))(type "ref") 就是告诉系统:这个字段存的不是一个普通文本,而是指向另一个对象的链接。
创建 Alice 的时候,用这个字段把人绑到机器上——这次绑一台新的机器 OreMachine-D(第 3 课那台已经停过机了,我们要看的是新鲜事):
runex ontology load docs/examples/asteroid/lesson-04-links.scm
runex node create "OreMachine-D" --tag OreMachine -f "名称=OreMachine-D" -f "当前产物=Ore"
runex node create "Alice" --tag Employee \
-f "姓名=Alice" -f "岗位=矿石生产员" -f "可上班=true" \
-f "关联机器=OreMachine-D"看最后一行:-f "关联机器=OreMachine-D" —— 你按名字写,系统自动把它解析成到那台机器的链接。不用记 id,说名字就行。
规则:机器停,顺着链接找人
现在机器停了,谁负责把 Alice 挂起?还是规则——lesson-04-links.scm 里的第三条:
(action "suspend_employees_for_machine"
(machine "OreMachine") ;; 挂在"矿石机"的状态机上
(from-states "stopped") ;; 机器进入"停机"时生效
(trigger (on state-transitioned (machine "OreMachine") (to "stopped")))
(guard #t)
(effect
(for-each (incoming-links "关联机器") employee
;; 顺着"关联机器"链接反着找:谁指着这台机器?
(begin
(set-field-on employee "可上班" "bool" #f) ;; 可上班 = 否
(transition-on employee "EmployeeDuty" "suspended"))))) ;; 状态 → 放假新词只有一个:
| 代码 | 大白话 |
|---|---|
(incoming-links "关联机器") | 反向遍历链接:从机器出发,找出所有”关联机器”字段指向它的对象 |
第 3 课的 find-by-tag 是”扫全厂”(找出所有矿石机);这一课的 incoming-links 是”按关系找人”(找出所有关联到这台机器的人)。一个是广播,一个是按关系簿找人。
验证:机器停,Alice 放假
现在来一场”新警报”:造一颗新的小行星 2026-S1(这次直接用 CLI 建,距离直接写成危险值——第 3 课你就见过:创建时字段被写入同样会触发规则):
runex node create "2026-S1" --tag AsteroidThreat \
-f "名称=2026-S1" -f "警报阈值公里=1000000" -f "距离地球公里=750000"这一下,链自己跑:2026-S1 警报 → 停掉所有还在生产的机器(OreMachine-D 中枪)→ 顺着”关联机器”链接找到 Alice → 挂起。
验证一下:
runex inspect node <OreMachine-D 的 id>
runex inspect node <Alice 的 id>你会看到:
OreMachine-D: __state__OreMachine text stopped
Alice: __state__EmployeeDuty text suspended
可上班 bool FalseAlice 放假了。 你没动手,系统顺着链接找到她,改了状态,改了字段。
现在这条链有三跳了:
距离跨过阈值 → 威胁警报 → 机器停机 → 员工放假但还有一件事没着落:停机了,得有人处理善后——通知运营团队、安排检修、准备恢复计划。总不能等着人想起来吧?
下一篇,让系统自己派工单。
第 5 课 · 派工单:停机自动派活给运营
上一课员工放假了。这一课,让系统在警报发生时自动派一张工单出去——就像工厂里的工单系统,活一出现,单子就出来,等人认领。
先想一个问题:“派活”在系统里是什么?
警报响了,你希望有一张工单自动生成,上面写着:“小行星警报,矿石机已停,请运营处理善后”。
这张工单会排队等着——有人(或后台程序)认领它、处理它、关闭它。这就是 WorkItem(你可以直接读作”工单”)。
派工单的动作,叫 signal-work。
看代码——docs/examples/asteroid/lesson-05-work.scm:
(action "dispatch_operations_notice"
(machine "AsteroidThreat")
(from-states "alerted")
(trigger (on state-transitioned (machine "AsteroidThreat") (to "alerted")))
(guard #t)
(effect
(signal-work (str "asteroid-alert-" (self)) ;; 工单的编号:asteroid-alert-<id>
"notify_operations" ;; 谁来处理这个类型的活
(self)))) ;; 这张工单关联的对象signal-work 三个参数:工单编号、处理者类型、关联对象。就这么简单——活出现了,单子入列。
验证:工单真的会出来吗?
前面几次警报发生时,这条派单规则还没装上(规则只对装好之后发生的警报生效)。现在装上了,来一场新警报——再造一颗小行星 2026-T1,距离直接写危险值:
runex ontology load docs/examples/asteroid/lesson-05-work.scm
runex node create "2026-T1" --tag AsteroidThreat \
-f "名称=2026-T1" -f "警报阈值公里=1000000" -f "距离地球公里=750000"然后看工单池:
runex work list你会看到:
┏━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ state ┃ work_key ┃ agent ┃
┡━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ pending │ asteroid-alert-<id> │ — │
└─────────┴─────────────────────┴───────┘工单编号 asteroid-alert-<id>——就是 signal-work 第一个参数生成的。它排着队,等人认领。记住这张表的样子,第 8 课它还会出现一个更厉害的角色。
到这里,你已经搭了一条四跳的链:
距离跨过阈值 → 威胁警报 → 机器停机 → 员工放假 → 派工单但你可能已经憋了一路的问题:这些数据,都是你手动敲的。
node update -f "距离地球公里=750000" —— 真实世界里,谁会坐在那儿敲这行命令?
你的观察员在望远镜前面,他记录数据的方式是写日记(md 文件),不是敲 runex 命令。
下一篇,让观察员写的 md 文件直接驱动整个系统。
第 6 课 · 数据管道:观察员写文件,系统跟着变
前面 5 课,数据都是你手动敲进系统的。这一课开始,让数据自己流进来——观察员写 md 文件,系统自动同步。
先想一个问题:观察员怎么写数据?
你的夜班观察员不会敲命令行。他做的是:打开一个 md 文件(就是个文本文件),在顶部写下观测数据。
他盯上了一颗新的小行星,编号 2026-R1:
---
type: "[[AsteroidThreat]]"
名称: 2026-R1
距离地球公里: 1500000
警报阈值公里: 1000000
速度公里每秒: 20
质量百万吨: 1
矿场距离公里: 5
---
观察员夜班记录:仍在安全距离。文件顶部夹在 --- 之间的部分叫 frontmatter(元信息区)——就是这张 md 文件的”档案卡”:类型是啥、各个字段值多少。下面的正文是观察员写的日记。
现在的问题是:这个文件,怎么变成系统里的对象?
认识”数据管道”
答案是 runex connection。你给它指定:
- connector(连接器):这个文件是 Obsidian 风格的 markdown,用
obsidian-markdown读它 - recipe(配方):读进来以后怎么处理,用
obsidian-mirror——frontmatter 的每个字段,1:1 落到对象的字段上,不挑不拣
配置一次:
runex connection configure obs \
--connector obsidian-markdown \
--recipe obsidian-mirror \
--source ~/observations --param path=~/observationsobs 是这条管道的名字,~/observations 是观察员写文件的文件夹。
然后同步一次:
runex connection pull obs它会回答:
OK obs seen=1 upserted=1 skipped=0翻译:看到了 1 个文件,合并了 1 个对象。观察员的 md 文件,变成系统里的对象了。
现在,见证奇迹:观察员改一行数字
观察员凌晨的报告:距离还是 150 万公里,安全。然后他修正了数据——距离其实是 75 万公里,跨过阈值了!
他在 md 文件里改一行:
距离地球公里: 750000(把原来的 1500000 改成 750000,保存。)
你这边只要再同步一次:
runex connection pull obs然后查一下 2026-R1 的状态:
runex inspect node <2026-R1 的 id>你会看到:
__state__AsteroidThreat text alerted
威胁等级 text alertR1 警报了。 而停机器、挂员工、派工单那些事——你已经在第 3、4、5 课见过,它们这次对 R1 同样生效(只是 OreMachine-A 和 Alice 前面已经停过、放过假,这次轮到 R1 的链再确认一遍)。
观察员改了一个数字,威胁警报了。你一行 runex 命令都没敲(除了 pull)。
这里有个 runex 的重要脾气,值得记住:文件首次导入时,只是”对象诞生”,不触发规则;之后任何字段被更新,才触发规则。
所以观察员的工作模式是:先报告(文件建起来,对象诞生),后修正(改数据,触发规则)。第 7、8 课我们都会用这个模式。
现在系统活了,数据也能自己流进来了。
但你是个谨慎的老板。你发现一个问题:现在的规则太”莽”了。
第 3 课的规则说:警报一响,无条件停所有机器。可小行星分很多种——有的又大又快,砸下来矿场全毁;有的擦肩而过,连块石头都掉不下来。
每次都停机器?停一天少产一天的钱。
你想要的决策是:先算算这颗小行星砸下来损失多大,再决定停不停。
但问题来了——系统是规则的引擎,它不会算物理。距离 75 万公里、速度 30 公里每秒、质量 2 百万吨——砸下来多大威力?系统一脸懵。
下一篇,给系统装一个”计算器”。
第 7 课 · 外挂计算器:先算损失,再决定停不停
系统不会算物理。没关系——你不会算的东西,装个计算器就行。
认识 kernel:引擎的外挂工具
runex 里,这种”引擎不会、但可以让它调用”的 Python 工具,叫 kernel。你可以把它想成引擎身上外挂的计算器:
- 引擎负责规则(什么时候触发、什么条件、改什么状态)
- kernel 负责算数(复杂的、需要代码的计算)
规则和计算分开:规则保持简单、可审计;计算扔给 kernel 去做。
动手:写一个损失评估计算器
在你的 ~/.runex/extensions/kernels/ 目录下建一个文件 impact.py(没有这个目录就自己建一个):
"""撞击损失评估——CEO 决策前先算账(广岛当量模型,纯示意)。"""
def impact_loss(speed_km_s: float, mass_mt: float, distance_km: float, key: str):
# 动能 E = ½·m·v²(m 是质量,v 是速度)
energy_j = 0.5 * mass_mt * 1e9 * (speed_km_s * 1000.0) ** 2
# 折合成"广岛当量"(1 个广岛 ≈ 63 TJ),数字更有画面感
hiroshima = energy_j / 63e12
# 破坏半径的经验公式:半径 ≈ 1.9 × 当量^(1/3) 公里
radius_km = 1.9 * hiroshima ** (1.0 / 3.0)
# 矿场离撞击点多远?和破坏半径比一比,定损失分级
if distance_km <= radius_km: loss = "total" # 全损
elif distance_km <= radius_km * 3: loss = "heavy" # 重度
elif distance_km <= radius_km * 6: loss = "moderate" # 中度
else: loss = "none" # 无影响
d = {"撞击能量广岛当量": round(hiroshima, 1),
"影响半径公里": round(radius_km, 2),
"损失分级": loss}
return d[key] # 要哪个指标,说一声
KERNELS = {"impact-loss": impact_loss} # 注册:引擎启动时自动发现KERNELS 这个字典就是”登记表”:引擎每次启动,会自动扫 ~/.runex/extensions/kernels/ 下的所有 .py 文件,把 KERNELS 里登记的函数都挂上。
验证一下它被发现了:
runex ontology list kernels你应该能在列表里看到 impact-loss。计算器装好了。
改造规则:先算账,再决定停不停
现在把第 3 课的”无条件停机”改掉。新规则在 docs/examples/asteroid/lesson-07-loss.scm 里,分两步:
第一步——警报之后,先算账(调用计算器,把损失写进字段):
(action "assess_impact" ;; 规则一·改:警报 → 评估损失
(machine "AsteroidThreat")
(from-states "alerted")
(trigger (on state-transitioned (machine "AsteroidThreat") (to "alerted")))
(guard #t)
(effect
(begin
(set-field "撞击能量广岛当量" "number"
(call-kernel "impact-loss" (field "速度公里每秒") (field "质量百万吨") (field "矿场距离公里") "撞击能量广岛当量"))
(set-field "影响半径公里" "number"
(call-kernel "impact-loss" (field "速度公里每秒") (field "质量百万吨") (field "矿场距离公里") "影响半径公里"))
(set-field "损失分级" "text"
(call-kernel "impact-loss" (field "速度公里每秒") (field "质量百万吨") (field "矿场距离公里") "损失分级"))
(transition "assessed")))) ;; 状态:评估完成(call-kernel "impact-loss" 速度 质量 距离 "撞击能量广岛当量") 就是”用计算器算一下”。注意三个字段——速度、质量、矿场距离——都是第 6 课观察员在 md 里记录的数据。
第二步——停不停,看损失分级:
(action "stop_ore_machines" ;; 规则二·改:损失 total/heavy 才停
(machine "AsteroidThreat")
(from-states "assessed")
(trigger (on state-transitioned (machine "AsteroidThreat") (to "assessed")))
(guard (or (= (field "损失分级") "total") (= (field "损失分级") "heavy")))
;; ← 关键:只有全损/重度才放行
(effect (for-each (find-by-tag "OreMachine") machine ...)))第 3 课是无条件停(guard #t);现在只有损失分级是 total 或 heavy 才停。砸下来没事的,机器照转。
验证:一颗”全损”的小行星,和一颗”擦肩而过”的
先加载新规则,然后再造一台新机器 OreMachine-B(OreMachine-A 前面已经停过了,我们要看的是新规则对新机器的作用):
runex ontology load docs/examples/asteroid/lesson-07-loss.scm
runex node create "OreMachine-B" --tag OreMachine -f "名称=OreMachine-B" -f "当前产物=Ore"第一颗:2026-QY,大块头。 观察员开新文件(第 6 课教的模式:先报告,再修正):
---
type: "[[AsteroidThreat]]"
名称: 2026-QY
距离地球公里: 1500000 ← 先报安全值
警报阈值公里: 1000000
速度公里每秒: 30
质量百万吨: 2
矿场距离公里: 8
---runex connection pull obs # ① 对象诞生
# 观察员修正:距离 1500000 → 750000
runex connection pull obs # ② 世界震动
runex inspect node <2026-QY 的 id>你会看到:
__state__AsteroidThreat text assessed
损失分级 text total
撞击能量广岛当量 number 14285.7
影响半径公里 number 46.1损失分级 = total(全损):矿场离撞击点只有 8 公里,而破坏半径有 46 公里——跑都跑不掉。于是——查一下新机器 OreMachine-B:
runex inspect node <OreMachine-B 的 id>__state__OreMachine text stopped
停工原因 text asteroid_alertB 被停了。 这就是新规则在起作用:损失 total,停。
现在再造一台机器 OreMachine-C(它将是 QZ 的”对照组”):
runex node create "OreMachine-C" --tag OreMachine -f "名称=OreMachine-C" -f "当前产物=Ore"第二颗:2026-QZ,小碎块。 同样开文件、先报告后修正——这次质量只有 0.01 百万吨,撞击点离矿场 500 公里:
runex inspect node <2026-QZ 的 id>
runex inspect node <OreMachine-C 的 id>你会看到:
2026-QZ: 损失分级 text none
OreMachine-C: (inspect 里没有 __state__OreMachine 字段)损失分级 = none(无影响),guard 拦住了——停机规则根本没碰 C。对比一下:B 被停过,所以它的状态字段写着 stopped;C 从没被碰过,所以连状态字段都没有——机器照转,一天钱都没少赚。
这里藏着一个值得记住的细节:状态字段是”被规则动过”的痕迹。 没有 __state__,就是没被规则碰过。
这就是”理性关停”:不是一有警报就停,是算完账才决定。
现在系统会算账了,会理性关停了。
但你是个有仪式感的老板。关停是大事——你不想让机器自己”想停就停”。你想让一个人来拍板:先看损失数据,再说停不停。
这个人,就是你的 CEO——你电脑上装的那个 AI(比如 Pi)。它懂业务、有判断力,而且 24 小时在线。
下一篇,把 CEO 请进系统,让关停决策送到它面前。
第 8 课 · 请 CEO 拍板:把你电脑上的 AI 接进系统
这一课,让关停决策送到你电脑上的 AI(CEO)面前——它看了损失数据,裁决停不停,产出走审批,批准才生效。
先想一个问题:怎么让系统”找到”你的 AI?
你的电脑上装着 Pi(就是你平时用的那个)。它是个程序,平时在终端里喊它才出来。
现在要让系统认识它、能给它派活。runex 的做法分两步:
第一步:看看你电脑上有哪些 AI 可以用
runex agent discover它会扫描你的电脑,列出一张表:装了什么 AI 软件、版本多少、能干什么。你大概率会看到 Pi、Claude Code、Codex、OpenCode 里的几个。
第二步:把 CEO 登记进系统
runex agent register ceo --command pi --profile pi --desc "CEO:裁决小行星关停"翻译一下这条命令:
ceo—— 在系统里给它起个名字(agent_key)--command pi—— 真实的可执行程序是pi--profile pi—— 告诉引擎怎么以”自动化模式”调用它(换 claude 就是--profile claude,引擎谁都能接)
确认登记成功:
runex agent list你会看到一行:
ceo | pi | pi | enabled | ✓你的 CEO 进董事会了。 以后引擎可以给它派活。
规则:损失评估完,把裁决送给 CEO
新规则在 docs/examples/asteroid/lesson-08-ceo.scm:
(action "ask_ceo_ruling"
(machine "AsteroidThreat")
(from-states "assessed") ;; 损失评估完成后
(trigger (on state-transitioned (machine "AsteroidThreat") (to "assessed")))
(guard #t)
(effect
(signal-agent
(str "ceo-ruling-" (self)) ;; 任务编号
"ceo" ;; 派给谁:ceo
(str "小行星 " (self) " 距离地球 " (field "距离地球公里")
" 公里,损失分级 " (field "损失分级") "。裁决是否关停矿场?")
;; 派什么活:把损失数据报给 CEO,请它裁决
(self) ;; 关联对象
"proposal"))) ;; 产出方式:走审批(proposal)signal-agent 是第 5 课 signal-work 的”AI 版”——派一张工单,但这次是派给你的 AI。注意最后一个参数 "proposal":CEO 的输出不会直接生效,而是先落成一份”提案”,批准后才算数。CEO 说了不算,你(或规则)批准才算。
验证:CEO 的裁决任务入列
加载规则,然后观察员再开一颗”全损”级别的小行星(2026-QW,先报告后修正):
runex ontology load docs/examples/asteroid/lesson-08-ceo.scm
# 观察员开 2026-QW.md,参数照抄 2026-QY(大块头、距离 8 公里)
runex connection pull obs # ① 对象诞生
# 修正距离 1500000 → 750000
runex connection pull obs # ② 世界震动然后看工单池:
runex work list你会看到多了一行:
pending | ceo-ruling-<id> | ceo这张工单,就是送到 CEO 面前的那个问题:“小行星 2026-QW 距离地球 750000 公里,损失分级 total。裁决是否关停矿场?”
让 CEO 真跑(可选,需要你的 Pi 可用):
runex agent run ceo "评估损失并裁决是否关停矿场 2026-QW" --drain它跑完,输出会变成一份 Proposal(提案):
runex proposal list # 看 CEO 的提案
runex proposal approve <提案 key> # 你批准,它才生效决策送审,人机同图——这就是把规则从”自动”升级成”董事会”:机器提供数据,CEO 提供判断,你掌握最终批准权。
现在,你的矿场数字孪生已经相当完整了:
观察员写 md → 数据进图 → 威胁警报 → 算损失 → CEO 裁决 → 你批准 → 停机器 → 员工放假 → 派工单一切都在自动跑。但你是谨慎的老板——你有点怕。
- 万一某条规则写错了,一触发就停错机器,怎么办?
- 万一 CEO 拍板之前,你想先看看”如果批准关停,会波及多少东西”?
你不想在真实世界里试错。你想先演习一遍。
最后一课,教你”演习”——不碰真实世界,先看会发生什么。
第 9 课 · 沙盒干跑:没撞,先看清会怎样
最后一课,学一个保命的工具:preview——干跑。
先想一个问题:怎么”试一下”而不搞坏东西?
真实世界(你的数字孪生)里,任何一次 update 都会真的改变状态、触发级联。你不敢乱试。
runex ontology preview 就是为这个生的:它把一个动作从头到尾”演”一遍——包括完整的链式反应——然后全部撤销。世界一丁点都不变。
一句话:preview 是演习,update 是实弹。
场景一:审计”为什么现在安全”
观察员报告 2026-QY 距离 150 万公里(安全)。引擎为什么没拉警报?用 preview 问它:
runex ontology preview assess_asteroid_threat <2026-QY 的 id>它把守卫条件摊开给你看:
(dry-run) GUARD REJECTS assess_asteroid_threat on <id>
expr: (<= (field "距离地球公里") (field "警报阈值公里")) → False150 万 ≤ 100 万 = False,所以不触发。 规则不是黑盒——它为什么不作为,引擎明明白白告诉你。
场景二:演习”撞了之后会怎样”
2026-QY 已经实弹触发过了(第 7 课),机器都停了。现在再干跑一次停机规则:
runex ontology preview stop_ore_machines <2026-QY 的 id>(dry-run) WOULD RUN stop_ore_machines on <id> — 0 event(s) in cascade0 个事件。 为什么?因为机器已经全停了,for-each 扫了一圈发现没有还在生产的——撞第二次不会造成二次损失。这就是规则的”幂等”:重复执行,不重复破坏。
场景三:状态机只走合法路线
在”评估完成”的状态下,再干跑一次”拉警报”规则:
runex ontology preview assess_asteroid_threat <2026-QY 的 id>IllegalTransitionError: state='assessed', requires one of ('monitoring',)警报只能从”监控中”状态拉——已经评估完了,还想拉警报?状态机拦下你。 这是运行时硬约束,不是”靠自觉”。
九课走完,你的矿场数字孪生完整了:
- 对象:每样东西一张档案卡,系统认得它、不重复
- 生命周期:档案卡会”活”,状态变化有规矩,规矩是硬拦的
- 级联:一个事件能影响一整批对象
- 关系:对象之间能搭链接,规则能顺着链接找人
- 派活:链式反应能派工单、能派 AI
- 数据管道:观察员写 md,系统跟着变——数据有了真实来源
- 计算器:引擎不会的,外挂 kernel 来算——决策有了依据
- AI 拍板:把本机 AI 接进来,决策送审,批准才生效
- 沙盒:preview 演习,update 实弹,试错零成本
9 课的文件都在 docs/examples/asteroid/,倒着读一遍就是整套架构。你从”一张档案卡”开始,搭到了”AI 总裁拍板关停”——这就是数字孪生的样子:数据有来源,规则是物理定律,AI 是董事会。
进阶篇 · 把 CEO 升级成一个团队
第 8 课是”一个 CEO 拍板”。真实的大决策,哪有一个人拍板的?
董事会应该是:风险分析师、机会分析师、运营专家——各看一面,最后总裁综合裁决。
这一篇,用三种现成的”团队组织方式”(在 docs/examples/patterns/ 里,都是现成可 load 的代码),把小行星矿场的单 CEO 升级成团队。模式,就是团队的组织方式。
团队一:扇出——一个警报,三个分析师并行
警报响了,别再只派一个 CEO。同时派三个 Agent:
- 风险分析师:这颗小行星撞下来,矿场损失多大
- 机会分析师:有没有”矿难财”(撞击可能翻出稀有矿物)
- 运营专家:停机一天的成本,vs 不停机的风险
三个结果分别写回三个字段,都齐了才进入裁决。规则这样写(挂在小行星的”警报”状态上):
(action "alert_team_analysis"
(machine "AsteroidThreat")
(from-states "alerted") ;; 在警报状态生效
(trigger (on state-transitioned (machine "AsteroidThreat") (to "alerted")))
(guard #t)
(effect
(begin
(signal-agent (str "risk-" (self)) "pi" "分析这颗小行星的撞击风险…")
(signal-agent (str "opp-" (self)) "pi" "分析有没有稀有矿物机会…")
(signal-agent (str "ops-" (self)) "pi" "评估停机成本与不停机风险…")))))一个 effect 连发三个 signal-agent = 并行扇出。触发后 runex work list 会看到三个 agent 的任务排队。完整代码:docs/examples/patterns/fanout.scm
团队二:辩论裁决——正反两方,第三方拍板
关不关停,先让两个 Agent 吵一架:
- 反方(必须关停):列出撞击风险
- 正方(可以继续生产):列出机会与成本
第三个 Agent 综合两方观点,给出最终裁决——同样是一段完整规则,挂在矿石机的”停机”状态上:
(action "decision_judge"
(machine "OreMachine")
(from-states "stopped")
(trigger (on state-transitioned (machine "OreMachine") (to "stopped")))
(guard #t)
(effect
(signal-agent (str "verdict-" (self)) "pi"
(str "综合两方观点给出最终决策。风险:" (field "风险分析")
" 机会:" (field "机会分析")))))完整代码:docs/examples/patterns/debate-and-judge.scm
团队三:管道接力——评估 → 裁决 → 通知,一步步来
把整个流程串成接力:先评估损失 → CEO 裁决 → 通知运营,每一步的输出喂给下一步。完整代码:docs/examples/patterns/pipeline.scm
选哪种团队?
| 模式 | 团队形态 | 适合 |
|---|---|---|
| 扇出 | 并行小组 | 几个专家各看一面,互不干扰 |
| 辩论裁决 | 对立质证 | 决策有正反两面,要吵出真相 |
| 管道接力 | 流水线 | 流程固定,前一步是后一步的输入 |
你的矿场从”一个 CEO”升级成了”董事会”。而且你可能已经发现了:这些模式没有新零件——还是对象 / 生命周期 / 规则这三样的排列组合,只是组织方式变了。
下一步
- 让它自动跑(无人值守) —— 定时戳它,观察员都不用熬夜
- 想继续玩团队?
docs/examples/patterns/里还有辩论裁决、迭代打磨、三明治验证,直接runex ontology load就能跑