Skip to Content
文档🚀 指南教程:小行星矿场数字孪生(从简单到复杂)

教程:小行星矿场数字孪生(从简单到复杂)

前情:你是小行星矿场老板。矿场的日常运营——客户、产品、成交——你已经用一句话搭起了客户管理。这一篇是矿场的另一半,也是更要命的一半:天上那颗小行星。

矿场里有几十台矿石机在轰鸣,有一支员工队伍,还有一个望远镜——夜班观察员用它盯着天上。

问题来了:所有这些信息,散在哪?

  • 机器的生产状态,在班长的脑子里
  • 员工能不能上班,在排班表的 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) loaded

0 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 False

Alice 放假了。 你没动手,系统顺着链接找到她,改了状态,改了字段。


现在这条链有三跳了:

距离跨过阈值 → 威胁警报 → 机器停机 → 员工放假

但还有一件事没着落:停机了,得有人处理善后——通知运营团队、安排检修、准备恢复计划。总不能等着人想起来吧?

下一篇,让系统自己派工单。

第 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=~/observations

obs 是这条管道的名字,~/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 alert

R1 警报了。 而停机器、挂员工、派工单那些事——你已经在第 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_alert

B 被停了。 这就是新规则在起作用:损失 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 "警报阈值公里")) → False

150 万 ≤ 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 cascade

0 个事件。 为什么?因为机器已经全停了,for-each 扫了一圈发现没有还在生产的——撞第二次不会造成二次损失。这就是规则的”幂等”:重复执行,不重复破坏。

场景三:状态机只走合法路线

在”评估完成”的状态下,再干跑一次”拉警报”规则:

runex ontology preview assess_asteroid_threat <2026-QY id>
IllegalTransitionError: state='assessed', requires one of ('monitoring',)

警报只能从”监控中”状态拉——已经评估完了,还想拉警报?状态机拦下你。 这是运行时硬约束,不是”靠自觉”。


九课走完,你的矿场数字孪生完整了:

  1. 对象:每样东西一张档案卡,系统认得它、不重复
  2. 生命周期:档案卡会”活”,状态变化有规矩,规矩是硬拦的
  3. 级联:一个事件能影响一整批对象
  4. 关系:对象之间能搭链接,规则能顺着链接找人
  5. 派活:链式反应能派工单、能派 AI
  6. 数据管道:观察员写 md,系统跟着变——数据有了真实来源
  7. 计算器:引擎不会的,外挂 kernel 来算——决策有了依据
  8. AI 拍板:把本机 AI 接进来,决策送审,批准才生效
  9. 沙盒: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 就能跑
Last updated on