← 全部文章
AI 与 Agent IT 负责人 · · 作者 ObjectStack Team

AI 模型进了企业还缺什么?一个世界、一双手、一道围栏

缺的不是智力。模型进到一家公司,缺一个可以推理的世界、一双能动手的手、一道围住动作的围栏。一份可执行的业务本体同时供给这三样;文中一张带出处的表,把每一样对到运行时里供给它的机制。

AI 模型进了企业还缺什么?一个世界、一双手、一道围栏 AI 模型进了企业还缺什么?一个世界、一双手、一道围栏
  • AI Agent
  • 可执行业务本体
  • MCP
  • Agent 权限

世界:这家企业里有什么,每样东西是什么类型,彼此怎么连。

双手:Agent 可以对这个世界做哪些操作,以它能调用的工具的形式给出。

围栏:谁可以做什么,每一次调用都判一次而不是只判一次,事后留下记录。

这三样,是 AI 模型进到一家企业时缺的东西;智力不在其中。这句话是 ObjectStack 的,为它首页的「00 · The idea」一节而写:「What the model lacks is not intelligence: a world, hands, and a fence.」(模型缺的不是智力,而是一个世界、一双手和一道围栏。)本文要做的,是把这句话背后那条更窄、可核验的主张讲清楚:一份可执行的业务本体,用同一个工件同时供给这三样,而且每一样都能对到运行时里一个有名有姓的机制,源码里读得到。下面那张表就是这份对照,每个格子都给到文件和行号。表之后依次是:三样东西各自是什么;在自带的 CRM 示例上量出来的一个数字;把最强的替代方案公平地摆出来;以及这一切解决不了什么。

这句话从哪里来

从 2026-10-12 到 2026-10-27,objectstack.ai 的首页用三句话陈述这个想法,页面源码里原样保留(加入这一节的那次提交 712361b7 的 apps/docs/app/[lang]/page.tsx 第 127–131 行;三句话上方的注释写着「verbatim from the maintainer’s ruling」,即逐字取自维护者的裁定):

An enterprise is definition plus execution. When execution is nearly free, it is its definition.

What the model lacks is not intelligence: a world, hands, and a fence.

An ontology written clearly enough is software. Models change; the ontology stays.

三句话的意思依次是:企业等于定义加执行,当执行几乎免费时,企业就是它的定义;模型缺的不是智力,而是一个世界、一双手和一道围栏;一份写得足够清楚的本体就是软件,模型会换,本体留下。

关于出处还有一条说明,因为本文其余的出处都读自同一个带标签的发布版本。把 objectstack.ai 改成开发者站点的那次重写(61c07986,2026-10-27)撤掉了这一节,所以在标签 @objectstack/[email protected] 上,仓库里已经没有这三句话。本文其余部分核验的仍然是这同一个想法,而核验本身并不取决于这句话印在哪里。

把中间那句当作一份诊断来读。几乎每一个企业 AI 试点,都是把一个很能干的模型接进公司,然后一次事故一次事故地发现:模型不知道「客户」在这家公司里到底指什么(没有世界);它其实改不动一个客户记录(没有手);或者它改了一条不该碰的记录(没有围栏)。常见的应对是把缺的三样分别买齐:一层检索供给世界,一层函数调用供给双手,一层策略引擎供给围栏。这是合理的应对,本文后半会给它应有的篇幅。而第一句和第三句说的,是 ObjectStack 为什么走另一条路:如果业务的定义写得精确到可以直接执行,那同一份定义就是世界,就声明了双手,也自带围栏——而且不管这个季度读它的是哪个模型,它都在。

三样供给,一个工件

ObjectStack README 的一行定义是「一份可执行的业务本体。AI 编写它,运行时运行它,Agent 操作它,你拥有它。」概念页把它的核心列为六类东西:对象与字段、关系、动作、权限、流程、Agent 与工具定义。本文的主张是:这六类正好归入三样供给,而运行时把每一样都变成 Agent 直接可用的东西,中间不需要任何人写胶水代码。下面是对照表。所有出处都是 objectstack-ai/objectstack 仓库在标签 @objectstack/[email protected](2026-11-04 读取,即当天 npm create objectstack@latest 安装的那个版本)上的文件,或者从同一仓库构建出来的文档页。

模型缺的本体里供给它的运行时机制,以及写在哪里
一个世界——名词:有哪些实体,每个是什么类型,彼此什么关系对象与字段;关系(六类中的两类)一份类型化定义编译成 objectstack.json;运行时从它推导出数据库结构、REST 与实时 API、Console 界面和一个 MCP 服务器(ontology.mdx:64-70)。Agent 用 list_objects 和 describe_object 读这个世界(connect-mcp.mdx:225-229)。
一双手——动词:可以对这些名词做什么动作、流程,以及记录本身运行中的应用就是一个 MCP 服务器,位于 /api/v1/mcp,默认开启;对象自动暴露,动作要用 ai: { exposed: true } 主动开启(README.md:136-151)。这个开关是一个 schema 字段,默认 false,开启时必须附一段不少于 40 个字符、面向模型的描述(action.zod.ts:815-822)。
一道围栏——谁可以做什么、对哪些行哪些字段,每次调用都查,查完留痕权限、行级与字段级安全、共享、审计安全是注册在 ObjectQL 引擎上的中间件,引擎的每一次操作都要经过它(security-plugin-authz-middleware.ts:4-7)。MCP 工具以调用者的身份运行,走的是「与 REST API 相同的权限/RLS 强制路径」(mcp-http-tools.ts:7-11)。审计行由引擎钩子在每一次插入、更新、删除时写入(audit-writers.ts:2435-2440)。

这些文件在哪里,方便按出处翻开:README.md 在仓库根;ontology.mdx 在 content/docs/concepts/,connect-mcp.mdx 在 content/docs/ai/;action.zod.ts 在 packages/spec/src/ui/;security-plugin-authz-middleware.ts 在 packages/plugins/plugin-security/src/;mcp-http-tools.ts 在 packages/mcp/src/;audit-writers.ts 在 packages/plugins/plugin-audit/src/。行号以该标签为准。

概念页把右边那一列压成一句话,值得整句保留:运行时「在每一次调用上强制执行这份定义。RBAC、行级与字段级安全、共享和审计,对一个 REST 请求、一次 Console 点击和一次 Agent 的工具调用一视同仁。运行时不做任何推理:跑的就是校验过的。」(原文:enforces the definition on every call. RBAC, row-level and field-level security, sharing, and audit apply to a REST request, a Console click and an agent’s tool call alike. Nothing is reasoned over at runtime: what runs is what was validated.)

一份可执行的业务本体,从 objectstack.config.ts 编译成 objectstack.json,供给三样东西:一个世界(对象、字段与关系,运行时从中推导出数据库、API 和 describe_object 工具)、一双手(用 ai exposed true 开启的动作、流程和记录,以 MCP 工具的形式在 /api/v1/mcp 提供)、一道围栏(权限集、行级与字段级安全、共享和审计,由安全中间件在引擎的每一次调用上强制执行,由审计钩子记录)

接下来逐行展开这张表。

世界:一份运行时也在读的定义

模型的世界要同时满足两个读者。模型需要它足够小、足够明确,能一次装进上下文整体把握;运行时需要它足够精确,能直接执行。文档只满足第一个读者:一份检索出来的制度 PDF 能告诉模型「商机」是什么,但没有任何东西检查数据库是否同意。

可执行本体的世界,就是那份类型化定义本身。概念页说,本体「是你的业务里那份由运行时执行的定义:以类型化元数据写在 objectstack.config.ts 里,编译成 objectstack.json 工件」。对象与字段声明实体及其类型化属性;关系声明查找和主从链接。运行时从这份定义推导出表和接口,MCP 服务器的 describe_object 工具也从这同一份定义作答。于是 Agent 读到的世界,在构造上就是正在运行的那个世界。没有第二份描述可以漂移。

由此推出两个结论,这个博客上都已经有专文论证,这里只指向,不重复。第一,世界是有边界的:视图、仪表盘、应用和翻译是本体的投影,不是它的一部分,这正是 Agent 能从核心而不是从每一个屏幕出发推理业务的原因(那篇定义文章里有一条「删除测试」用来区分二者)。第二,世界先校验后运行:os validate 会拒绝那些类型检查通过、运行时却会静默失败的定义,包括缺失的安全姿态;所以交到 Agent 手里的世界,已经为其中每一处悬空引用被拒绝过一次。

双手:由作者武装的工具,不是模型猜出来的工具

双手是大多数企业 AI 技术栈已经有的部分,形态就是函数调用。可执行本体带来的差别,在于由谁决定哪些手存在。

对象默认带手。因为定义是类型化的,运行时把它作为 MCP 服务器提供出来:「对象是工具,动作是工具,权限决定 Agent 可以调用什么,审计记录它做了什么。」(README.md:138-139,原文 Objects are tools, actions are tools, permissions decide what an agent may call, and audit records what it did.)connect-mcp 页面把 Agent 拿到的东西列为「十二个工具,由你的元数据生成」:发现(list_objects、describe_object、validate_expression),读取(query_records、get_record、aggregate_records),写入(create_record、update_record、delete_record),以及动作(list_actions、run_action、resume_run)。没有人为新对象写工具定义;对象就是工具。

动作默认不带手,这是刻意设计的部分。一个动作只有在作者写下 ai: { exposed: true }、并写好模型据以选择它的描述之后,才成为 Agent 可调用的工具。action.zod.ts 里的 schema 注释说明了原因:「这是一道刻意设置的治理闸门:在一个由 AI 编写的世界里,平台的价值就在于人可以精确管住 Agent 群被允许调用哪些能力——一个半成品的、未经评审的动作,绝不能被静默地武装起来。」(原文:This is a deliberate governance gate: in an AI-authoring world the platform’s value is that a human can govern exactly which capabilities the agent fleet is allowed to invoke — a half-finished or unreviewed action must never be silently armed.)最小的自带示例 todo 应用,是这样武装一个动作的(examples/app-todo/src/actions/task.actions.ts,去掉了只与界面相关的键):

export const CompleteTaskAction = defineAction({
  name: 'complete_task',
  label: 'Mark Complete',
  objectName: 'todo_task',
  type: 'script',
  target: 'completeTask',
  ai: {
    exposed: true,
    description: 'Mark a todo task as complete. Use when the user says a task is done or finished.',
  },
});

自带的 CRM 则一个也没有武装。它唯一的动作「Convert Lead」是一个 flow 类型的动作,没有 ai 块,所以连上这个应用的 Agent 拥有六个对象上的全部数据工具,而 list_actions 返回空。这不是示例的缺口,这是规则在起作用:对业务逻辑的手,由定义里一行经过评审的代码授予,绝不因为界面上有个按钮就被默认拥有。

为什么偏偏写操作需要这一份受治理的定义,而不是在界面旁边另开一条工具通路,是本体动作那篇的主题,这里不重复。

围栏:由运行时在每一次调用上裁决

写在提示词里的围栏,是一个请求。架在工具前面的策略服务里的围栏,是一道针对调用的闸。可执行本体自带的围栏在数据路径上,这个区别决定了一次错误能够到达多远。

机制是中间件。plugin-security 把它的强制执行注册在 ObjectQL 引擎上(ql.registerMiddleware(...),security-plugin-authz-middleware.ts:2466,由插件的 start() 调用),而引擎是每一次读写都必经的地方,不管请求来自 REST 路由、Console 点击还是 MCP 工具。MCP 工具模块在自己的文件头里就声明它从不直接触碰数据引擎:「所有读写都经过一个注入的 McpDataBridge,由宿主接到与 REST API 相同的权限/RLS 强制路径上。这个模块只负责工具的形状;桥负责执行与安全。」没有一道专给 Agent 的围栏需要和给人的那道保持同步,因为只有一道围栏。每一处检查到底在哪里运行、为什么事后过滤模型输出已经太晚,Agent 权限在哪里强制执行一文有完整论证。

这道围栏有三个性质值得点名,因为替代方案通常至少缺其中一个。

它裁决的是行和字段,不只是调用。 权限集授予对象级的增、读、改、删;行级安全过滤调用者能看到哪些记录;字段级安全遮住哪些列。安全服务契约把行过滤的失败姿态写得很直白:「收窄访问的回答失败时关闭……解析失败返回一个 DENY 过滤器(零行),绝不返回 undefined」(packages/spec/src/contracts/security-service.ts:28-31)。一个什么都想要的模型,拿到的是它的用户被允许看到的。

它单独把守业务逻辑,因为逻辑背后没有兜底。 对象的增删改查在每一行上都被 RLS 和 FLS 框住。动作体不是:一旦被调用,它「作为受信任的应用代码执行」,所以桥「在调用时把关:ai.exposed(作者明确的 AI 开启,ADR-0011)加上 ADR-0066 D4 的能力闸」(mcp-http-tools.ts:1450-1455)。双手和围栏正是在这里相遇:武装一个动作的那一行,就是围栏检查的那一行。

它留记录。 审计写入器是挂在 afterInsert、afterUpdate、afterDelete 上的引擎钩子(audit-writers.ts:2435-2440),所以 Agent 用 update_record 改过的一行,和在 Console 里改过的一行,以同样的方式、带着操作者身份落进审计日志。README 的第四道闸把后果写成一句:「运行时在每一次调用上强制权限和审计,所以哪怕是一个写错了的应用,也留在围栏之内。」(原文:The runtime enforces permissions and audit on every call, so even a wrong app stays inside the fence.)

在自带的 CRM 上量一个数

一个数字,在标签 @objectstack/[email protected] 的 examples/app-crm 上量得,数法和 README 数这个应用时一样:src/ 下的 TypeScript,不含测试。README 自己写的数字是 1,873 行,标注的是更早的提交(95294be3);此后应用多出的 213 行里,178 行是翻译:

find examples/app-crm/src -name '*.ts' -not -name '*.test.ts' | xargs cat | wc -l
# 2086

30 个文件、2,086 行,是整个应用。把它的目录归入三样供给:

供给目录行数里面是什么
世界src/objects/516六个对象(客户、联系人、线索、商机、商机明细、活动)及其字段和查找关系
双手src/actions/ + src/flows/240一个动作「Convert Lead」,以及它启动的屏幕流程
围栏src/security/99其中 sales-positions.ts 占 89 行:两个权限集、三个岗位,每个岗位点名它分发的权限集
管道工具注册、鉴权中间件、审计代码0在 src/ 下搜索 registerTool、middleware、authoriz、checkPermission 和 audit,只有四处命中,都在注释里

其余是投影(视图、一个仪表盘、一个应用、一个页面、翻译、一个分析数据集)和种子数据,不在本文范围内。

把头条数字叫作 89 行围栏。这是一个六对象 CRM 的全部授权声明,而同样这 89 行管着 REST API、Console 和全部十二个 MCP 工具,因为三者都通过围栏所在的那个引擎触达数据。与之互补的数字是那个零:应用声明了它的世界、它的双手和它的围栏,却不含一行把它们接到模型上的代码。接线是运行时的事,这正是「一个工件」这四个字的全部内容。

关于这次测量,两条诚实的脚注。89 行是应用自己的声明;它们所隐含的行过滤和字段遮蔽,由运行时根据每个对象上的 sharingModel 和这些权限集算出来,所以运行中的系统里,「围栏」比它的声明要大,这也是应该的。还有,如前所述,这个 CRM 没有向 AI 暴露任何动作,所以在作者武装一个动作之前,它在业务逻辑上的手是零。

把替代方案公平地摆出来

供给这三样东西的常见做法是一个技术栈:对文档和 schema 做检索供给世界,函数调用(或者你自己写一个工具服务)供给双手,一个策略引擎或网关架在工具前面供给围栏。它值得被公平对待,因为在好几种情形下它就是正确答案。

它能和你已经在跑的系统一起工作。什么都不用迁移;检索器索引已有的东西,工具包装已有的 API。每一层都是各自领域里最好的、可替换的,换一个更好的检索器或更严的策略引擎,不必动其他层。这些模式人人熟悉,供应商众多,一个已经有工具网关的团队,围栏的大半已经就位。如果 Agent 的工作是横跨十几个遗留系统读取、却不写入任何一个,这个技术栈大概就是你要的——而架在那些系统之上的可执行本体,目前是只读的、早期的。

它解决不了的,正是首页第一句话说的那件事:三样供给是三个工件,三个主人,没有任何东西检查它们是否一致。

检索 + 函数调用 + 策略层一份可执行本体
世界模型读的一个语料库;系统不读它运行时执行的那份定义;describe_object 从它作答
双手手写的工具 schema,一个操作一个,在系统旁边另行维护对象自动成为工具;动作用一行经过评审的代码开启
围栏架在工具调用上的闸,每个工具背后通常是一个服务账号的权限数据路径上的中间件,以调用用户的权限运行,连行和字段一起管
漂移对业务的三份描述,每一份都可能过期一个工件,先校验后运行
审计记下了调用;行的变更是下游系统的事行的变更带着身份被记录,每条路径都一样

出事的时候,围栏那一行最要紧。架在 get_customer 工具前面的策略层,可以裁决 Agent 能不能调用它。它通常裁决不了结果里哪些行是 Agent 的用户被允许看的,因为工具是用集成拿到的那份凭据在跑。数据路径上的行级和字段级安全,对每一个调用者都回答这个问题,而且对人和 Agent 的回答是同一个。

这些解决不了什么

一篇讲围栏的文章,应该说清围栏到哪里为止。

它不供给判断力。 围栏让一次错误调用可以被拒绝、可以被追溯,但不会让这次调用变对。模型仍然可能选错工具、读错字段,或者要求一个它本不该想要的动作。世界和围栏框住损害的范围;而选择,仍然由模型确实带来的那样东西——智力——来做。

开启闸门只把守运行时拥有的那些门。 ai.exposed 这道闸在 MCP 的 run_action 工具所到达的动作门上强制执行。spec 自己的注释说得明白:「REST /actions 不受 ai.exposed 把守,在这道闸之外,所以那条路由前面要不要有个人,仍然由你安排。」(packages/spec/src/ai/tool.zod.ts:95-98)那条路由上权限照常生效;AI 开启不生效。

读审计比写审计窄。 每一次插入、更新、删除都有记录。记录单条记录的详情查看是按对象逐个开启的,列表和搜索结果不审计(packages/plugins/plugin-audit/src/audit-plugin.ts:49-68)。一个大量读取的 Agent,留下的痕迹比一个写入的少。

世界只到运行时所运行的应用为止。 对它不运行的系统,联邦一个外部数据源默认只读,并被标明为早期。三样供给覆盖的是本体所执行的部分;其余的 IT 资产仍然需要上面那个技术栈,或者一次迁移。

暴露出去的动作是受信任的代码。 run_action 工具在它自己的描述里就对模型说了:动作体「作为受信任的应用代码运行,拥有应用的全部数据权限」。所以武装一个动作是一项评审决定,不是一个开关;对那些应该先问人的动作,有 requiresConfirmation: true。

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

上面的一切都是开放栈,Apache-2.0,在同一个仓库里:定义是你手里的一个文件。为了运行它,ObjectStack 仓库提供 ObjectOS Community —— ghcr.io/objectstack-ai/objectstack 镜像、os start 和 Console,免费且自托管 —— 而 ObjectOS Cloud 是托管版本,ObjectOS Enterprise 是自管理、需许可的版本。(这句是标签上 README 那句话的译文,README.md:26-31。)ObjectOS 有三个版本:ObjectOS Community,开源、可免费自托管;ObjectOS Cloud,托管在浏览器中、无需安装;ObjectOS Enterprise,自管部署在你自己的基础设施上。你的本体是普通的 ObjectStack 元数据。从 ObjectOS Cloud 或 ObjectOS Enterprise 导出本体、在开源的 ObjectStack 运行时上运行,目前尚在计划中。三样供给就在这份元数据里,所以无论哪个版本运行它,它们都成立。

让你的 Agent 指向它

上面这张对照表不需要靠信任来接受;运行中的应用会把它展示给任何一个 MCP 客户端。

  1. 用 npm create objectstack@latest my-app 搭一个项目。脚手架会写好 AGENTS.md 并安装技能包,编码 Agent 一开始就带着协议规则;工具服务器怎么接,见文末的 Connect an MCP Client 页面。
  2. 向 Agent 要一个世界:几个带字段的对象,对象之间有一个查找关系。让它跑 npm run validate,先看 diff,再看 Console。
  3. 用 npx os dev --ui 启动应用,把任意 MCP 客户端指向 http://localhost:3000/api/v1/mcp。Agent 以一个用户的身份连进来——走部署自己的 OAuth 服务器,或者一把 API key——describe_object 返回的就是你刚评审过的那个世界。
  4. 武装一只手:给一个动作加上 ai: { exposed: true } 和一段描述,再校验一次,看着 list_actions 从空变成一个。
  5. 用第二个用户测围栏:让他缺一项第一个用户有的权限,读那条拒绝,再读那条审计记录。

Connect an MCP Client 页面有每种客户端的一行命令,Business Ontology 页面是定义本身的权威出处。把两页都交给你的 Agent。世界、双手和围栏,就在它即将写下的同一个文件里。