← 全部文章
应用搭建 开发者 已发布 · · 作者 ObjectStack Team

「本体即软件」到底是什么意思?可执行业务本体的完整定义

这句话是 ObjectStack 的定位,这篇是它的定义:可执行业务本体由六类核心构成,有四种投影、三个「不是」,还有一条分辨核心与投影的测试。

「本体即软件」到底是什么意思?可执行业务本体的完整定义
  • 本体
  • 可执行业务本体

先给结论:「本体即软件。」是 ObjectStack README 的第一句话,紧跟其后的那一行就是定义:「一份可执行的业务本体。AI 编写它,运行时运行它,Agent 操作它,你拥有它。」可执行业务本体,是你的业务里那份由运行时直接执行的定义。它的核心是六类东西——对象与字段、关系、动作、权限、流程、Agent 与工具定义——编译成一个工件,数据库、API、UI 和 Agent 工具都从这个工件推导出来。视图、仪表盘、应用和翻译是它的投影,不是它的一部分。它不是知识表示意义上的本体,不是架在你现有系统之上的语义层,也不是哲学意义上的形式本体。代码不会消失,它搬进了运行时:钩子、动作体、CEL 和受限 JSX。

这就是全部主张。本文接下来把它拆开:这句话为什么说「即」而不说「描述」;六类核心;投影,以及一条区分二者的测试;三个「不是」;代码去了哪里;以及这句话最终归结成的四个承诺。来源是 README 和它链接的概念页,引用而非转述;再加上同一仓库里自带的 CRM 示例,测量而非描述。

这句话为什么说「即」

大多数企业本体是在描述软件。它们挨着真正跑业务的系统——一个客户模型、一张「什么连着什么」的图、一份对「营收」的受控定义——而底下的系统照常运转,毫不知情。这种描述有用,但也是可选的:删掉它,什么都不会停。

ObjectStack 这句话写成一个等式,是因为关系正好反过来:定义本身就是在跑的那个东西。删掉本体,应用就没有了——没有表、没有接口、没有界面、没有可供 Agent 调用的工具。概念页用一行写出整条流水线:

objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server

你在 objectstack.config.ts 里编写类型化元数据;它编译成 objectstack.json 工件;运行时读取工件,推导出其余一切。README 给同一张图的说明更短:「一份类型化定义 → 数据库 · REST API · 客户端 SDK · UI · MCP 工具。」这里没有第二个工件在「当」软件、而本体只是谈论它。这就是「即」的含义,下面的一切都由此推出。

六类核心

概念页把核心定义为六类东西。下表中间一列保留它的原话,右边一列指向每一类在自带 CRM 示例(ObjectStack 仓库里的 examples/app-crm)中的位置,这样定义可以对照一份真实的定义来核验,而不是对照一句口号。

核心它声明什么在自带 CRM 示例中
对象与字段业务中的实体及其类型化属性——校验、公式、默认值、文件src/objects/:客户、联系人、线索、商机、商机明细、活动
关系对象之间的查找(lookup)与主从(master-detail)链接商机上的 Field.lookup('crm_account', …);商机明细是它的主从子对象
动作用户或 Agent 可以对一条记录发起的操作,以及它们显示的条件src/actions/convert-lead.action.ts,只在线索尚未转化时可见
权限谁可以读、建、改、删什么——RBAC、行级与字段级安全、共享src/security/sales-positions.ts:一个 crm_sales_user 权限集,以及一个只能新增线索的访客档案
流程业务过程——DAG 流程、记录触发器、定时任务、审批、工作流src/flows/convert-lead.flow.ts,一个屏幕流程:创建客户与商机,再把线索标记为已转化
Agent 与工具定义应用自带的 AI Agent、它们可以调用的工具,以及塑造它们的技能本示例未声明。每个对象本来就是一个 MCP 工具;动作用 ai: { exposed: true } 主动开放

一个测量,用的是 README 自己在「自己量一量」下面印出来的那条命令:在提交 9f9510f2,CRM 示例的定义是 31 个文件、1,917 行 TypeScript。这些文件装着六个对象、三个视图、一个仪表盘、一个应用、一条线索转化流程、一个动作、一个钩子、两个权限集、三个岗位、一个翻译包、一个页面和种子数据——整个应用,是一份你读得完的 diff。这个数字的意义不在于它抽象地「小」,而在于一个 Agent 能把整份定义一次装下,并从定义本身回答「我改了这里会弄坏什么?」——README 把这叫作「小到 AI 能整个装下的应用」。

核心同时也是——按构造就是——属于你的那部分。概念页写道:「它是一份定义,开放且有版本。它以普通 TypeScript 的形式住在你的仓库里,可以作为 diff 审阅,按 Apache-2.0 规范并授权——不是一张锁在厂商系统里、只能通过该厂商控制台触达的图。」

投影,以及「删除测试」

视图、仪表盘、应用和翻译用同样的元数据编写、由同一道门禁校验,但它们不是本体的一部分。概念页称之为投影,并逐一用一个分句解释:「列表视图把一个对象的字段和权限投影到一张网格上;仪表盘把对象上的查询投影到图表上;应用把对象、视图和动作组合成一个可导航的产品;翻译把每一个标签投影到一种语言。」

六类核心经 ObjectStack 运行时编译成数据库、API、控制台 UI 和 MCP 工具;视图、仪表盘、应用和翻译作为投影位于本体框之外;钩子、动作体、CEL 和受限 JSX 在运行时的围栏内运行

让这个区分可操作的是这一句:「改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。」把它变成一条测试,你就能一步给项目里任何一份定义归类。姑且叫它删除测试:

删掉这份定义。如果数据库结构、API,或者用户与 Agent 能做的事变了,它就是核心。如果消失的只是一个页面、一张图、一个菜单或一个标签,它就是投影。

拿 CRM 示例来对照。删掉 src/views/opportunity.view.ts,商机对象、它的字段、它的查找关系和它的权限集都还在;REST 接口照常应答;MCP 工具照样存在。删掉 src/objects/opportunity.object.ts,视图没了可投影的东西,流程没了可创建的东西,权限集点名的对象也不复存在。前者是投影,后者才是软件。

这个区分为什么值得起个名字,同一页也说了:「这个区分划定了 Agent 为了推理业务必须装下的范围:是核心,而不是每一个屏幕。」一个在审阅审批阈值改动的 Agent,需要把对象、权限和流程摆在眼前;它不需要恰好把结果画成图表的每一个仪表盘。

它不是的三样东西

「本体」这个词从其他领域带来了几十年的含义,README 的定义有相当一部分篇幅是在说哪些含义在这里不适用。否定有三个,而且各不相同。

它不是那样东西是什么为什么这里不同
知识表示意义上的本体OWL、RDF 和知识图谱描述一个领域,带有类继承和公理,并由推理引擎推理没有对象继承、没有公理、没有推理器。定义先被校验——由 Zod schema 和 os validate——然后被运行。abstract 这个键正是因此从规范中移除(ADR-0049)
架在你现有系统之上的语义层在你已经运行的系统之上的一层只读映射:它描述业务,但不运行业务本体就是系统:数据库、API、UI 和 Agent 工具都从它推导出来。联邦接入外部数据源是可以的,默认只读,而且还处于早期
哲学意义上的形式本体对「一般而言什么存在」的主张它不作此类主张。它是一家企业的应用,被定义到足以运行的精确程度

三列对比:知识表示本体被描述并被推理;语义层是现有系统之上的只读映射;可执行业务本体先被校验、再被运行成数据库、API、UI 和 Agent 工具

第一个否定最容易被忽略,因为它是一个设计选择,不是一个局限。推理引擎是第二个真相来源:它在运行时推导出的事实,从未写在任何审阅者读得到的地方。ObjectStack 的规则来自概念页,正好相反:「运行时不做任何推理:跑的就是被校验过的东西。」如果一条权限不在 diff 里,它就不在系统里。

第二个否定对任何在评估平台的人都重要,因为「语义层」这个词正在同一场对话里被用来指两件不同的事。语义层可以是对「一个数字意味着什么」的出色定义——与知识图谱的比较给了它公允的辩护。它是一个名词层。可执行业务本体带着动词:动作、权限和流程决定什么可以被做、由谁来做、接下来发生什么。文档也注明,他们在一个更窄的意义上使用「语义层」,指报表与仪表盘绑定的分析数据集层;那个窄义讲的是报表,不是这句话的意思。

第三个否定最短,也最诚实。这个格式不对世界作主张。它主张的是:一家企业的应用可以被陈述到足够精确,让运行时能执行它、让审阅者能整个读完它。

代码去了哪里

一份被执行而非被推理的定义,仍然要表达 schema 表达不了的东西:成交时自动跳到 100% 的概率,线索转化后消失的按钮,带版式的落地页。README 的句子是「代码不会消失:它搬进了运行时,成为钩子、动作体、CEL 和受限 JSX」,概念页给出了理由:这四样都「在运行时的权限与审计围栏内运行」,而且「没有一样散落在一个本体随后还得去描述的代码库里」。

CRM 示例里的一个钩子,是挂在对象生命周期上的一个函数:

export const OpportunityStageHook = defineHook({
  name: 'opportunity_stage_probability',
  object: 'crm_opportunity',
  events: ['beforeInsert', 'beforeUpdate'],
  priority: 100,
  handler: async (ctx: HookContext) => {
    const input = ctx.input as { stage?: string; probability?: number };
    if (input.stage === 'closed_won') input.probability = 100;
    if (input.stage === 'closed_lost') input.probability = 0;
  },
});

CEL 是公式、谓词和策略的表达式语言。同一个示例用它写一个计算字段,也用它写一个动作的可见性:

expected_revenue: Field.formula({
  label: 'Expected Revenue',
  expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`,
}),
visible: 'has(record.status) && record.status != "converted"',

受限 JSX 是自定义页面的写法,而「受限」正是重点:它「被解析成一棵服务端驱动的 UI 树,永远不会作为代码执行」。做这件事的包在 README 里被列为一个只解析、从不执行的编译器。动作体是第四个去处,承载一个动作被调用时在服务端做的工作。

四处的模式是同一个:允许代码,但代码不是业务所在的地方。业务住在六类核心里,代码在这六类所划定的围栏内运行,接受与一次点击或一次工具调用相同的权限和审计。

先校验,再运行

运行时对工件做的事可以用两个动词描述,概念页是有意同时使用它们的。它推导:「可互换驱动之上的数据库结构、一套生成的 REST 与实时 API、控制台的服务端驱动 UI,以及一个 MCP 服务器」。它也强制执行:「RBAC、行级与字段级安全、共享和审计,对一次 REST 请求、一次控制台点击和一次 Agent 工具调用一视同仁。」

README 用一个对象演示第一个动词。声明一张工单:

import { ObjectSchema, Field } from '@objectstack/spec/data';

export const Ticket = ObjectSchema.create({
  name: 'support_desk_ticket',
  label: 'Ticket',
  sharingModel: 'private',            // org-wide default — the security gate requires it
  fields: {
    subject: Field.text({ label: 'Subject', required: true, searchable: true }),
    status: Field.select({
      label: 'Status',
      required: true,
      options: [
        { label: 'Open', value: 'open', color: '#3B82F6', default: true },
        { label: 'Resolved', value: 'resolved', color: '#10B981' },
      ],
    }),
    due_date: Field.date({ label: 'Due Date' }),
  },
});

然后「REST API 在对象存在的那一刻就存在了——没有控制器要写」:

curl http://localhost:3000/api/v1/data/support_desk_ticket

注意带注释的那行 sharingModel: 'private'。安全姿态是对象的一部分,门禁要求它必须在:os validate「拒绝那些能通过类型检查、却会在运行时静默失败的元数据:悬空绑定、错误的 CEL 谓词、缺失的安全姿态」。这是校验的那一半。运行的那一半是,从这个对象推导出的每一个表面——表、接口、控制台网格、MCP 工具——在每一次调用时都对照同一套权限检查,所以一个通过 MCP 触达这张工单的 Agent,看到的正是拥有相同权限的人会看到的东西。

四个承诺,以「可执行」领头

README 把这句话压缩成四个词:「可执行 · AI 可编写 · Agent 可操作 · 归你所有」。每一个都是一个有机制支撑的主张,顺序也不是装饰——第一个是其余三个的立足点。

可执行。 定义就是在跑的那个东西。上面两节的全部内容都是这个承诺:一个工件、推导出的表面、每次调用都强制执行、运行时不做推理。没有它,「AI 可编写」就只是 AI 在写文档。

AI 可编写。「Agent 写的是类型化元数据——不是一个代码库。门禁拒绝那些会在运行时静默失败的东西,Agent 在你看到之前就把它改好了。」脚手架「安装 AI 技能包并写入一份 AGENTS.md,所以你的 Agent 一开始就装载了协议规则」。README 的四道门——类型化、已校验、已审阅、受治理——是 Agent 的错误到不了生产的原因,也是它说明这件事能成立的理由:「Agent 的错误变成了有定位的、可纠正的文本,它自己能读、能改」。

Agent 可操作。「对象是工具,动作是工具,权限决定 Agent 可以调用什么,审计记录它做了什么。」运行中的应用「以 MCP 服务器的形式在 /api/v1/mcp 提供它——默认开启」,Agent「在与人相同的权限和 RLS 之下」操作它。由 Agent 写出的同一份本体,随后由 Agent 使用,经过同一道围栏。

归你所有。「这个仓库里的一切都是开放栈——协议、微内核、SDK、CLI 和生产运行时,Apache-2.0,没有开放核心的小字。」而定义本身「以普通 TypeScript 的形式住在你的仓库里,在你的版本控制里有版本,可以作为 diff 审阅——不是一张锁在厂商系统里的图」。本体是你握着的一个文件,不是你租来的一个租户。

它解决不了什么

一篇定义文章应该说清定义在哪里止步。

它不会让建模变容易。决定商机是什么、谁可以关闭一个商机、超过 30% 的折扣需要什么,仍然是一如既往缓慢、有争议、靠人的工作;格式只决定结果是否小到能整个读完、是否严到能在编写时就拒绝错误。

它不推理。如果你的问题确实需要对领域模型做推断——类层级、推导出的事实、一个能发现后果的引擎——那是本文明确说「不是」的那种知识表示本体。去选那个工具,别指望这一个长出推理器来。

它不是架在你现有系统之上的一层。联邦接入外部数据源默认只读,并被描述为早期阶段,所以一份架在遗留数据库之上的可执行本体,从只读模型开始,然后一个对象一个对象地争取写入。把内部系统迁到元数据是这条路的诚实版本,包括那些答案是「根本不要迁」的情形。

还有,新颖的逻辑仍然是代码。钩子和动作体是它的去处,在围栏之内,而它们仍然是需要人审阅的代码。本体让这些代码更小、有边界;它不会让代码消失,README 也原话这么说。

托管,或在你自己的基础设施上

README 描述了上面的循环——编码 Agent 在你的仓库里写元数据,并通过 MCP 操作运行中的应用——然后说:「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise);无论哪个版本,你都可以导出本体,拿到开源的 ObjectStack 运行时上运行。这正是第四个承诺以产品边界的形式说出来:无论本体跑在哪个宿主上,它都是你的。

从 Agent 路径开始

上面没有一件事需要你凭信仰接受定义;README 自己的五分钟路径就能产出一份你读得懂的定义。

npm create objectstack@latest my-app && cd my-app

在你的编码 Agent 里打开项目,描述需求。README 的例子是一个支持台:一个 ticket 对象,带主题、描述、一个优先级选择和一个状态选择;一个只在尚未解决的工单上显示的 Resolve 动作;一个「未解决工单」列表视图和一个 Support 导航组;做完以后跑 npm run validate。然后预览结果:

npx os dev --ui   # → http://localhost:3000/_console/

先看 Agent 产出的 diff,再看控制台。对象、动作和它的可见性谓词、视图——你可以用删除测试把每一行归入核心或投影,而门禁已经拒掉了那些跑不起来的行。如果 Agent 需要引导,如何为 ObjectStack 配置它覆盖了规则文件、技能包和工具服务器。它写出的那份定义就是软件,而且就在你的仓库里。