Schema 进去,整套全栈 CRUD 出来:一个面向 AI 的确定性生成层

作者:

在

🇬🇧 English

适用范围:模板覆盖范围内的 CRUD 应用;更复杂的业务逻辑(跨模块关联态、超出运行时能力的需求等)仍需扩展模板或手写代码。

流行做法

一个开发者坐在屏幕前,打开一个对话窗口,输入「帮我做一个商品管理后台」,等待 AI 把整个应用生成出来。这个流程简洁、直观,几乎所有刚接触 AI 编程的人都会自然地走上这条路。描述需求、拿到代码、跑起来——如果一切正常,问题到此为止;如果不正常,再追问、再修改,直到满意为止。这套方式之所以流行,是因为它把"写代码"这件事变成了"提要求",把原本需要数周的工作压缩到了几轮对话之内。对原型验证、一次性脚本、小型个人项目来说,这几乎是最优解:系统规模小,验证负担也小,生成的每一行代码都还在注意力可覆盖的范围内,人工审一遍就能基本确认质量。

但这条路的隐性成本在规模扩大时开始显现。一个完整的业务应用往往有成百上千行代码,每一行都可能是 AI 的幻觉产物——接口签名错误、字段类型不匹配、边界条件遗漏。更关键的是,一旦把上一次生成的结果当成不可信基线,任何改动都可能牵动一大片需要重新验证的代码。结果往往是:开发越快,验证越累;改动越多,风险越不可控。这种张力在很多团队的长期维护阶段会被反复感知——最初生成的代码能用,但半年后想加一个字段、改一个页面逻辑,发现改动本身带来的不确定性比从头写还高。

于是另一种做法开始被更多人采纳:不再让 AI 直接生成完整应用,而是让它只做一件事——产出结构化的、可验证的配置或 Schema,然后交给一个确定性的生成层去执行剩余工作。Schema 的范围被严格限制,输出的内容也是受约束的——JSON 字段定义、类型白名单、命名规范——这些比一整个应用容易检查得多。Schema 一次通过,生成器便按固定规则产出代码;AI 后续只做增量编辑,工作在更小的语言表面和更少的上下文负担下完成。

这种分治策略听起来反直觉——多了一道间接层,为什么不一步到位?但它瞄准的是 AI 编码时代的一个结构性问题:不是生成能力不够,而是当生成代码的规模和变更范围扩大时,验证负担可能随之上升。把生成约束集中到 Schema、复用经过验证的模板,就能减少重复审查通用逻辑的需要——把"决策"和"执行"拆开,让 AI 做它擅长的事(决策),让模板做它可靠的事(执行),两者之间的接口就是一个小而确定的 Schema。

把它画成一条管线:AI 产出受约束的 Schema → validate 校验 → 确定性生成器 → 生成完整应用。AI 只碰最左边的输入,剩下都是可复现的机械执行。

替代方案

另一个方向是把代码全手写,靠人类记忆和审查习惯维持质量,但这在快速迭代的场景下同样不可持续。

lume-nsgm 选的第三条路,是把"决策"和"执行"拆开。

AI 只做一件事:产出结构化的 JSON Schema。模块名、字段名、类型——全部落在受约束的小范围里。lib/validate.lume 在一开始就把所有问题一次性报出来:

$ lume-core main.lume bad.json
schema validation failed (2 error(s)) in "bad.json":
  2. modules[0].fields[1]: "name" must match [a-z][a-z0-9_]* (got "2bad")
fix the config and re-run.

类型白名单切死了非法输入,命名规范同时收窄了标识符注入面——"evil\"; DROP TABLE x--" 这类曾从未经校验的生成器里滑出来的 payload,在名字被插进 SQL DDL 和路由之前就被挡回去了。一梭子验证返回所有错误并附带精确路径,便于 AI 据此集中修正 Schema,减少逐个发现问题的迭代。字段级 default 属于同一类注入面——它原样拼进 DDL,因此被收窄为单引号字面量(内部仅限字母、数字、下划线、空格与连字符):'pending' 合法,而 x'); DROP TABLE o-- 这类片段会在校验阶段被直接拒绝。

Schema 通过之后,模板接管剩余工作。这些细节不需要 AI 每次重新推断——只要输入落在模板覆盖的范围内,输出就是确定且可复现的。

这个拆分的直觉很简单:AI 擅长在约束空间内做决策,模板擅长把确定性规则应用到任意输入上。两者之间的接口就是一个小而稳定的 Schema。

拆分的代价也随之而来。模板覆盖不到的部分,需要往底层运行时补。lume-core 早期没有 SHA256/HMAC 内建,scaffold/lib/pay.lume 里的微信支付 v3 适配因此被整体删除——不是电商能力不够,而是底层缺了那个符号。

外部依赖同样要处理。支付链路没法直接对着沙箱跑,于是 scripts/mock_pay_gateway.py 用 Python stdlib 实现了一个完整的 PayPal Express Checkout mock——OAuth2 token 发放、建单、捕获、退款,无第三方依赖:

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        if self.path == "/v1/oauth2/token":
            self._oauth(raw)
        elif self.path == "/v2/checkout/orders":
            self._orders(raw)
        elif self.path.startswith("/v2/checkout/orders/") and self.path.endswith("/capture"):
            oid = self.path[len("/v2/checkout/orders/"):-len("/capture")]
            self._capture(raw, oid)

mock 网关把外部依赖变成可预测的状态机,make test-e2e 才能在每次变更时跑完整链路——建单、跳转 approve、return 捕获、状态落库。不依赖真实网关,也不依赖人工干预。

这条路的适用范围是清晰的:CRUD 密集型项目、模板能覆盖大部分页面的项目。它的确定性和可验证性在范围内是无价的;超出范围,代价是核心运行时的持续修补、模板边界的不断外扩、以及配置层的认知负担。

证据

validation 不是一次报一个错误,而是一梭子跑完。lib/validate.lume 在 generation 之前做一次全量检查,JSON 格式、模块非空、命名规范、类型白名单、重名检测、空字段、default 字面量——每一项都带精确路径,出错时一次性全部返回,附带修复提示。两条报错对应两个不同层次的问题——类型不在白名单内,名字不以字母开头。校验器一次性返回全部错误和精确路径,便于 AI 据此集中修正,减少逐轮试错的迭代。tests/t6.lume 里有 19 条断言覆盖这个流程,失败时 exit code 为 1,generation 被整体跳过,不会吐出半残的项目。

命名约束的设计意图不止于风格统一。字段名会被直接拼进 SQL DDL 和动态路由,因此这里要区分两类风险:标识符注入——字段名、表名被拼进 DDL/路由——靠 "[a-z][a-z0-9_]*" 这样严格的白名单来限制;而值注入——查询里来自用户的取值——仍应交给参数化查询等机制,不能靠字段名校验兜底。历史上 "evil\"; DROP TABLE x--" 这种 payload 曾被未经校验的生成器放过,lume-nsgm 在 validate 阶段先把这类非法标识符挡在门外。Schema 越受约束,生成器需要处理的非法输入就越少。

电商场景的 Schema 长这样(example-paypal.json):

{
  "db": "shop_paypal_demo",
  "out": "./gen",
  "modules": [
    {
      "name": "product",
      "fields": [
        {"name": "name", "type": "varchar"},
        {"name": "price_cents", "type": "integer"},
        {"name": "stock", "type": "integer"}
      ]
    },
    {
      "name": "order",
      "fields": [
        {"name": "order_no", "type": "varchar"},
        {"name": "amount_cents", "type": "integer"},
        {"name": "product_id", "type": "integer"},
        {"name": "product_name", "type": "varchar"},
        {"name": "paypal_order_id", "type": "varchar"},
        {"name": "capture_id", "type": "varchar"},
        {"name": "status", "type": "varchar", "default": "'pending'"}
      ]
    }
  ],
  "pay": {
    "provider": "paypal",
    "secret_env": "PAYPAL_KEY",
    "mode": "sandbox",
    "currency": "USD",
    "order_module": "order",
    "order_no_field": "order_no",
    "amount_field": "amount_cents",
    "status_field": "status"
  },
  "shop": {
    "product_module": "product",
    "order_module": "order",
    "name_field": "name",
    "price_field": "price_cents",
    "stock_field": "stock",
    "product_field": "product_id",
    "product_name_field": "product_name"
  }
}

product 有三列:名称、价格(以分为单位)、库存;order 除系统自增 id 外为订单号、amount_cents、商品 ID、商品名快照、paypal_order_id、capture_id,以及默认 'pending' 的 status。pay 段把订单模块接到 PayPal(secret_env 只写环境变量名,密钥值不进配置),shop 段声明商品模块与下单要写入的列。字段数量、类型、命名都落在白名单内,validate 一次通过,后续所有生成步骤基于这份输入。

Schema 通过之后,生成器接管剩余工作。参数转义、分页边界、UI 约定——这些细节固化在模板里,任何落在覆盖范围内的合法 Schema 输入都得到一致的输出。lib/model.lume 负责类型映射和索引过滤,lib/sql.lume 负责 DDL 生成,lib/modlume.lume 负责模块生成,lib/reactgen.lume 负责前端,lib/scaffold.lume 负责把整棵目录树写到 gen/。四个模块分工明确,每个都有对应的回归测试:t1.lume 验证 model 和 sql 生成器(13 条断言),t2.lume 验证 modlume 和 reactgen(模块与前端),t3.lume 验证模板引擎的变量替换和条件渲染,t4.lume 验证 sql_default() 的回归行为。

测试套件本身也是一份证据。make check && make test && make test-e2e 全绿意味着什么——意味着从 Schema 到 DDL、从路由注册到 React 页面、从 SSR 渲染到 API 接口,整条链路都是可复现的。这不是"生成器写出来了",而是"生成器写出来的东西被验证过"。

模板解决不了所有问题,有些业务逻辑需要运行时干预。库存扣减就是一个典型——正常购买、售罄拒绝、退款回补三种边界,结果全符合预期。扣减逻辑本身只有一条 SQL:

UPDATE product SET stock = stock - 1 WHERE id = ? AND stock >= 1

单条语句,原子执行。sql_write 返回受影响行数,0 表示已售罄或并发被抢,此时走补偿路径:DELETE 掉刚建的那条 pending 订单,展示「已售罄」页。库存扣减通过单条带条件的 UPDATE 原子完成,避免了「先读库存、再单独写回」所产生的典型 read-modify-write 竞态;但订单创建与库存扣减之间的失败补偿(扣减失败后的回滚、或进程中途崩溃),属于跨操作的一致性,需要单独验证其异常恢复行为。

订单状态机在应用层用 varchar + 路由内白名单校验实现:pending → paid(支付回调幂等),paid → refunded(仅 paid 可退、重复退幂等)。状态流转不走数据库约束,完全由路由逻辑控制,便于观察和调试。

支付链路的实现细节不是本文重点,下面只把它当作"生成层可扩展性"的一个案例来看。管理端电商跑通之后,下一步是真实收款。crypto 内建补齐之后,PayPal Express Checkout 适配器得以重新落地。scaffold/lib/pay.lume 新增 paypal_base_url、paypal_basic_auth、paypal_fmt_cents、paypal_token、paypal_create、paypal_capture、paypal_verify_webhook——OAuth2 client_credentials 取 Bearer,HTTP Basic 凭证用 base64_encode 内建编码,client_secret 走 secret_env 注入不进配置和镜像。入口路由生成 PAYPAL_CFG,前端拿 approve_url 后 window.location 跳转,return 回调捕获并改状态,幂等保证重复 return 不重复落库。

支付链路的 e2e 验证同样走前面那个 mock:_oauth 校验 Authorization: Basic base64(client_id:client_secret),下发 Bearer token;_orders 接收建单请求,记录 return_url,返回 approve_url;_capture 完成资金捕获;_pp_refund 处理退款,重复退同一 capture 返回 422。外部依赖被收敛成一个可预测的状态机,链路才能在每次变更时自动跑通。需要说明的是,这里的 e2e 指的是应用与本地模拟网关之间的端到端验证,并不等于已在真实 PayPal 沙箱中完成全部支付、退款及异常场景的验证。

Docker 部署层也做了配套调整:Dockerfile 新增 ARG NSGM_CONFIG(example-paypal.json 即支付变体构建),docker-compose.yml 把 PAYPAL_KEY、PAYPAL_RETURN_URL 从宿主 env 注入(PAY_GATEWAY 留空时默认指向代码内建的沙箱地址 https://api-m.sandbox.paypal.com,需要改指向时在 env 里显式覆盖),都不进镜像层;healthcheck 改为探 GET /,两种构建配置都有该路由。

把四个层面的证据串起来看,lume-nsgm 的"确定性生成"不是一句口号,而是一条可 grep 的证据链:

  • 验证层:lib/validate.lume 在 Schema 阶段关掉非法输入,t6.lume 的 19 条断言锁定行为。
  • 运行时层:带库存条件的原子 UPDATE 避免了典型的库存 read-modify-write 竞态;跨操作失败补偿仍需独立验证。
  • 测试层:mock_pay_gateway.py 的状态机关掉外部依赖的不确定性。
  • 回归层:断言数字锁死质量基线——test-crypto.lume 31 条(sha256/hmac 17 + base64 14)、t6.lume 19 条、t1.lume 13 条。

四层证据对应四个维度的可控性:输入可控、执行可控、依赖可控、结果可控。每一条都可以 grep 到源码里的具体实现,没有一处依赖"希望它能跑"。这也解释了为什么这条路能成立——不是因为它比直接生成更聪明,而是因为它在每个环节都留下了可验证的痕迹。

不过要区分三个层次——它们各自解决不同的问题,也都不能单独保证"业务结果正确"(这些断言数量只表示对应行为已被测试覆盖,不代表未覆盖的场景也正确):

层次 能解决的问题 不能单独保证
Schema 校验 输入格式、字段类型、命名约束 业务需求是否完整
确定性生成 统一代码结构、减少重复实现 模板本身没有缺陷
自动化测试 检查已覆盖的预期行为 未覆盖的场景也正确

所以这套方法的真正主张不是"让 AI 不犯错",而是"让错误更容易被发现、定位和修复":把开放式代码生成收敛到一个更小的输入空间,从而压低验证成本。

边界

一个团队用 example-paypal.json 里的 product 和 order 两个模块搭出了一套能跑的电商管理后台。库存扣减走了守卫 UPDATE,支付链路配了 mock 网关,make test-e2e 全绿,交付物整齐干净。这是这套方法的高光时刻——也是它的边界开始浮现的地方。不是它不够好,而是它太"确定"了。


当 CRUD 不再是 CRUD

这套方法的底层假设是:业务可以被拆成模块和字段,模块之间通过 ref 类型关联,状态以字符串流转。这个假设对后台管理系统、内容管理平台、内部工具很准。对前端交互密集、业务模型超出表格-表单范式的场景,假设就不成立了。

一个典型的撞墙时刻发生在购物车上。模板层目前没有表达跨模块关联态的能力:购物车要落成 cart / cart_item 两张表,session 里只放 cart_id,而模板还生成不出这套结构。ref 类型能把订单关联到商品,但购物车是一个有生命周期的中间态:加购、修改数量、过期清理、合并多端数据。这些行为无法用几行 JSON 字段描述,只能在模板之外手写。

enum 类型同样暴露了边界。要让 enum 在校验期收窄取值、DDL 仍用 varchar、表单/列表按 values 渲染,就得把渲染逻辑写进模板——模板引擎 {{#each}} 和 {{#if}} 能做条件分支,但每次新增枚举值都要改模板,而不是改配置。配置和逻辑的耦合开始变重,"Schema 越精确 AI 决策越容易"的说法在这里打了折扣。


当运行时缺口需要核心级修补

lume-nsgm 的生成层是确定性的,但它的确定性建立在 lume-core 提供的内建函数之上。缺什么,就要补什么——而补核心的工程量和补一份配置不在同一个量级。

微信支付 v3 适配被删除就是最好的例子。不是电商能力不够,是 SHA256-RSA 这个符号不在 core 里。

这种"补核心"不是偶发事件,而是结构性代价。当业务需求超出模板能表达的范围时,要么扩展模板(增加复杂性),要么补齐运行时(扩大 scope)。两种方式都没有错,但都需要意识到:确定性生成层不是一个自包含的系统,它依赖底层运行时持续提供支撑。把核心运行时改到能满足需求,这个过程本身也是工程投入。


当 Schema 本身成为瓶颈

lib/validate.lume 的设计哲学是一梭子验证——JSON 格式、模块非空、命名规范、类型白名单、重名检测、空字段、default 字面量,所有错误一次性返回,带精确路径。这对小规模 Schema 非常高效:错误一次性返回、带精确路径,便于 AI 集中修正;tests/t6.lume 的 19 条断言覆盖这个流程。

但当 Schema 膨胀到几十个模块、上百个字段时,一梭子验证的回执会变得很长。每一条错误都带修复提示,AI 能处理,但人类阅读和定位问题的成本在上升。更关键的是,Schema 承担了原来由开发者直接在代码里表达的业务意图——字段含义、关联关系语义、状态流转规则——现在全部分解成 JSON 结构描述。Schema 越精确,AI 决策越容易;Schema 越复杂,理解它的认知负担也越高。

这种情况下,一个曾经清晰的接口(AI 出 Schema,模板出代码)开始变得模糊:Schema 本身需要被人理解和维护,而它已经不是"小的、可控的、可验证的"了。


什么时候回到流行做法

不是所有项目都值得建一套生成层。

如果项目只需要一次性的原型验证、或者功能集很小且变更不频繁,直接让 AI 生成整个应用依然是最省力的路。验证负担小,工具链简单,不需要维护模板和 Schema 之间的约定。

如果项目需要深度的自定义交互、频繁的业务模型迭代、或者强依赖某些运行时能力而补这些能力的成本高于重新生成,手写代码或者直接用 AI 生成整块逻辑可能是更好的选择。确定性生成的价值在于"一次验证、反复复用",但如果复用的基础本身不稳定,这个价值就不成立。


这套方法针对的是"生成规模扩大后验证负担上升"的问题,把"决策"和"执行"拆开,让 AI 做决策、让模板做执行。但它没有消除成本,只是把成本从代码生成阶段转移到了架构设计和底层补齐的阶段。

在 CRUD 密集、模板能覆盖大部分页面、运行时缺口可控的范围内,这条路的确定性和可验证性是无价的。超出这个范围,代价是核心运行时的持续修补、模板边界的不断外扩、以及 Schema 本身的认知负担。

把反常识的做法当成绝对真理,和把流行做法当成唯一正途一样危险。工具的选择取决于问题的形状,而不是工具的先进性。

把两种路径的取舍并排放在一起,会更清楚它们各自适合什么:

维度 直接 AI 生成 lume-nsgm
主要生成输入 自然语言需求 结构化 Schema
通用 CRUD 实现 每次生成或修改 复用经校验的模板
输入错误检查 依赖提示与测试 Schema 前置校验
自定义业务逻辑 灵活度较高 需要扩展模板或手写

(这张表是设计取舍的对照,不是量化性能结论;两种路径的实际验证成本都需要针对具体项目和团队单独实测。)

快速体验:从一份 Schema 到可运行应用

不必只信这套论证,可以自己跑一遍最小流程(前置:装好 lume-core,见仓库 README)。

① 输入 —— 一份三字段的 Schema:

{
  "db": "shop_demo",
  "modules": [
    {
      "name": "product",
      "fields": [
        { "name": "id", "type": "integer", "primary": true, "auto": true, "system": true },
        { "name": "name", "type": "varchar", "required": true, "searchable": true },
        { "name": "price_cents", "type": "integer", "required": true }
      ]
    }
  ]
}

② 生成 —— 一条命令产出整套工程:

$ lume-core main.lume quick.json out=/tmp/nsgm-demo
generated: product -> /tmp/nsgm-demo
done: 1 modules
/tmp/nsgm-demo/
├── app.lume              # 入口:server + 注册路由 + run()
├── modules/product.lume  # 数据驱动模块:DDL + 路由 + SSR 列表/表单
├── sql/product.sql       # 建表 DDL
├── lib/                  # db / ui / pay 运行时辅助
├── web/                  # Vite + React + TS 前端(每模块一个页面)
└── www/                  # SSR 静态样式与回退 JS

③ 验证 —— 生成器断言套件与端到端探针:

$ make test        # t1–t8:生成器 / 模板 / 校验 / 支付 断言
$ make test-e2e    # 起生成工程 + curl 打 SSR/CRUD/搜索/分页/动态路由

④ 查看 —— 起服务后打开浏览器:

$ make serve       # → http://127.0.0.1:8081(SSR 列表/表单 + REST API + React 页面)

如果 Schema 里出现非法字段名(如 2bad)或未知类型(如 uuid),生成会在校验阶段一次性全部拒绝并给出精确路径,不产出半成品——即上文的报错示例。

源码导航

源码已开源,默认分支 main:https://github.com/erishen/lume-nsgm

lume-nsgm 为什么选择把 AI 生成和代码生成拆开,而不是让 AI 直接写出整个项目?

直接生成时,随着生成代码的规模和变更范围扩大,验证负担可能随之上升——重生成往往要重新审查大量代码。拆分后 AI 只产出一个受限的 JSON Schema,模板做确定性执行。Schema 一次通过,生成器便按固定规则产出代码;AI 后续只做增量编辑,工作在更小的语言表面下完成。

Schema 通过什么机制保证输入安全,防止注入攻击?

lib/validate.lume 在 generation 之前做全量检查,包含类型白名单、命名规范、重名检测、空字段检查等。要区分两类风险:字段名、表名等标识符会被直接拼进 SQL DDL 和动态路由,靠 [a-z][a-z0-9_]* 白名单限制——"evil\"; DROP TABLE x--" 这类 payload 在名字被插入之前就被挡回;而查询里来自用户的取值,仍应交给参数化查询等机制,不能靠字段名校验兜底。字段级 default 同属标识符侧的注入面,被限制为单引号字面量。19 条断言在 tests/t6.lume 中覆盖校验流程,失败时 exit code 为 1,generation 整体跳过。

这套方案适合什么样的项目,不适合什么样的场景?

适合 CRUD 密集型、模板能覆盖大部分页面的项目,在其范围内确定性和可验证性是无价的。不适合强依赖自定义运行时能力(如早期缺 SHA256/HMAC 导致微信支付 v3 适配被删除)或业务模型频繁超出预定义类型(如自定义 JOIN 查询、嵌套枚举渲染、跨模块关联态购物车)的场景。

e2e 测试中的支付链路如何验证,是否需要真实网关?

不需要。scripts/mock_pay_gateway.py 用 Python stdlib 实现了一个完整的 PayPal Express Checkout mock,包含 OAuth2 token 发放、建单、捕获、退款,无第三方依赖。它将外部依赖变成可预测的状态机,使 make test-e2e 能在每次变更时跑通链路——建单、跳转 approve、return 捕获、状态落库。需要说明的是,这里的 e2e 指应用与本地模拟网关之间的端到端验证,并不等于已在真实 PayPal 沙箱中完成全部支付、退款及异常场景的验证。

当 Schema 膨胀到几十个模块、上百个字段时,会有什么问题?

验证本身的维护负担会上升——t6.lume 的 19 条断言每次加新类型或新约束都要跟着改写。同时 Schema 承担了原来由开发者直接在代码里表达的业务意图(字段含义、关联关系语义、状态流转规则),Schema 越精确 AI 决策越容易,但人理解它的成本也越高,认知负担从代码层转移到了配置层。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

商店 Web Chat Nsbp 关于 隐私政策

@ 2026 ESN
沪ICP备2024079226号-1   沪公网安备31010502007082号