系列前四篇搭好了整套架构(出码难题 → 问题地图 → Harness → 协议族)。这一篇深挖其中最会流血的两个单点:协议里的 P12(PRD → 交互规格的 intake agent) 和 P23(schema ↔ 代码的 round-trip)。挖到最后你会发现——它们不是两个问题,是同一种病。
一句话主线
这两处流血,本质是同一件事:一份信息在两种表示之间翻译时,边界上必然有损耗或歧义。intake 是「人的意图(散文)→ 机器规格」,病症是 AI 会静默编造缺失的部分;round-trip 是「机器规格 ↔ 代码」,病症是两边会静默漂移。负责任的解法也是同一句——把有损和歧义的部分显式化,而不是替它糊过去。
记住这个统一视角:凡是同一份真相有两个表示,你就有一个「谁是源头 + 怎么对账」的问题。 下面两个点,都是它的实例。
上半场:Intake Agent —— 从 PRD 里抽逻辑
为什么这是全系统唯一「模型变强也救不了」的点
回顾系列的一个核心区分:难题分两类——「信息在输入里,只是难提取」(分块、匹配、布局,模型变强会缓解)和「信息根本不在输入里」(业务逻辑、边界态,模型再强也变不出来)。
intake 是第二类的极致。PRD 是人写的散文,它有三种天生的病:
- 歧义:「提交后跳转」——跳去哪?只在成功时跳,还是失败也跳?
- 遗漏:PRD 几乎从不写「请求失败怎么办」「列表空了显示什么」「没权限时怎样」。那些 unhappy path,占真实工作量的一半,却几乎从不被写下来。
- 隐性知识:「按常规处理」——「常规」是作者脑子里的共识,不在纸上。
关键判断:这些信息不是「藏得深」,是「压根不存在于 PRD 里」。 所以让模型「更聪明地理解 PRD」是缘木求鱼——PRD 里没有的东西,理解得再透也读不出来。模型面对空缺,只会做一件最危险的事:自信地编一套看起来合理的逻辑填进去。
致命误区:把 intake agent 设计成「逻辑生成器」
直觉的做法是:把 PRD 喂给模型,让它输出一份完整的 InteractionSpec。这是错的,且是危险的错——因为对于 PRD 没覆盖的部分,模型会幻觉补全,而且补得像模像样,让人 review 时根本发现不了「这条逻辑其实是编的」。一个自信的错误,比一个明显的空缺危险得多。
正确的重构:它是「抽取器 + 缺口探测器」,不是生成器
把 intake agent 的目标彻底翻转:
它最有价值的产出,不是「逻辑」,而是「问题清单」。
也就是说,它的一等输出是它不确定、PRD 没说清的所有地方,整理成结构化的待决问题交给人;它只对PRD 明确写了的部分输出高置信度规格。这一下就把问题从「AI 悄悄猜逻辑」(危险)变成了「AI 精确地列出还有哪些没定」(安全且有用)。
具体设计四步:
① 接地(Grounding):先把 PRD 里的名词锚到组合树节点
抽逻辑之前,得先解决「PRD 说的『那个提交按钮』到底是页面里哪个节点」。没有这一步,抽出来的规则就是悬空的。
{ "kind":"intake.grounding", "payload": {
"mentions": [
{ "phrase":"提交按钮", "prdRef":"prd#3.2", "node":"n_submitBtn", "confidence":0.9 },
{ "phrase":"列表", "prdRef":"prd#2.1", "node":"n_grid", "confidence":0.7 }
],
"unresolved": [ { "phrase":"操作成功后的提示", "reason":"页面里找不到对应反馈组件" } ] }}
unresolved 本身就是一条要交给人的问题——接不上的名词,往往意味着物料缺失或 PRD 指向了不存在的东西。
② 完备性矩阵:用模板逼出「没写的部分」
这是 intake 最关键的设计。不要让模型自由发挥,而是给它一张必须逐格填的矩阵:对每一个动作 / 每一个数据依赖区域,强制回答一组固定问题——触发、守卫、成功、各错误码、空、加载中、无权限、并发/重复提交。PRD 里没答案的格子,不许留空,必须标成「gap」并生成一个待决问题。
{ "kind":"intake.coverageMatrix", "payload": {
"action":"submitOrder", "node":"n_submitBtn",
"cells": {
"trigger": { "value":"click", "provenance":"prd#3.2", "confidence":0.95 },
"guard": { "value":"表单校验通过", "provenance":"prd#3.2", "confidence":0.8 },
"success": { "value":"跳转订单详情", "provenance":"prd#3.2", "confidence":0.7 },
"error.500":{ "gap":true, "proposal":"toast 报错并保留草稿", "needsHuman":true },
"error.401":{ "gap":true, "proposal":"跳登录", "needsHuman":true },
"empty": { "notApplicable":true },
"loading": { "gap":true, "proposal":"按钮 loading + 禁用防重复", "needsHuman":true },
"concurrent":{ "gap":true, "proposal":"提交中禁用", "needsHuman":true }
}}}
矩阵把「这个 PRD 覆盖了错误处理吗」从一个靠人记性去发现的漏,变成了一个被结构强制检查的格子。模型可以给 proposal(一个合理默认建议),但每个 gap 都 needsHuman=true——建议归建议,决策权交回人。
③ 歧义作为一等输出:把「问题」结构化
模型不确定的地方,不要猜,输出结构化的澄清请求:
{ "kind":"intake.openQuestion", "payload": {
"id":"q_07", "about":"submitOrder.success",
"question":"提交成功后是跳订单详情,还是留在原页并刷新列表?",
"options":["跳订单详情(/order/:id)","留在原页刷新列表"],
"prdRef":"prd#3.2", "blocking":true }}
blocking 标记这条不解决就不能继续——把「必须澄清」和「可用默认兜着」分开,避免每个小疑问都卡住流程。
④ 确认回路 + 溯源:让抽出来的逻辑可审计
每条最终生效的规则,都带 provenance(追到 PRD 第几节)和 confidence,并记录是「模型抽的」还是「人确认/修改的」。人的每次修正回灌 few-shot(协议 P21)。能追溯到 PRD 出处的逻辑,才敢在生产里用;追不到出处的,就是定时炸弹。
上半场小结
intake 的病是静默编造,药是把缺口显式成问题。一句话:你无法抽取不存在的东西——intake agent 的职责不是替空缺填上合理的逻辑,而是精确地描出空缺的形状,交给唯一知道答案的人。
下半场:Round-trip —— schema 与代码的双向同步
为什么低代码总在这里流血
协议 P23 要解决的是:搭建器产出代码后,有人手改了代码,改动能不能同步回 schema?这个「双向」二字,是几乎所有低代码平台的伤口。
先看清楚不对称性:
- 正向(schema → 代码)很容易:它是一个确定性的纯函数,给定 schema 必然生成同样的代码。
- 反向(代码 → schema)是杀手:生成是有损且膨胀的——一个 schema 节点会展开成几十行代码,中间还夹着人手写的胶水。想把任意手改过的代码解析回 schema,一般情况下是不可能的。
用一句话点破本质:代码的表达空间远大于 schema 的表达空间。 人能写出无数种 schema 根本表达不了的代码。所以「完全的双向同步」在数学上就不成立——不是工程没做好,是这条路本身有天花板。
它其实是一个分布式一致性问题
换个视角就通透了:同一份真相(这个页面)有两个可编辑的副本(schema 和代码),两边都能改。 这不就是 Git 面对的同一个问题吗——两个分支各自演进,怎么合并。这在编程语言理论里有个名字叫双向变换(bidirectional transformation / lens),而它的困难是公认的。
具体的流血点有五个:
- 生成不是双射:代码空间比 schema 空间大,手写代码可能没有任何 schema 对应物。
- 身份锚定丢失:要把一处代码改动映射回某个 schema 节点,需要稳定的 id 把「代码区间 ↔ IR 节点」锚起来(就是 P23 的 source map)。一旦重新生成时代码顺序被打乱,锚点就断了。
- 三方合并:如果 schema 和代码同时被改,你面对的是一个跨表示的三方 merge,比 Git 的文本 merge 更难,因为两边根本不是同一种语言。
- 逃生舱失控:真实项目一定需要「这块让我直接写代码」。一旦某块被手写代码接管,它就脱离了 schema 的管辖。
- 重新生成毁掉手改:最朴素的「从 schema 重新生成代码」会无脑覆盖掉所有手工改动。
四种策略,没有银弹,只有取舍
面对这个天花板,成熟系统的做法其实就四种。把取舍摊开:
| 策略 | 做法 | 换来什么 | 代价 | 适合 |
|---|---|---|---|---|
| A. 一次性弹出(eject) | schema→代码生成一次,之后代码归你,不再回改 | 最简单、最诚实,代码完全自由 | 失去「继续可视化编辑」 | 生成脚手架、一次性交付 |
| B. 运行时 schema(不生成代码) | 运行时直接解释 schema,根本没有代码产物 | 双向同步问题消失(只有一个源头) | 表达力有天花板,逃生舱是受限插件槽 | 中后台、表单、配置化页面 |
| C. 保护区(protected regions) | 生成代码里显式划「勿动区(重生成)」和「你的代码区(保留)」 | 兼顾可视化 + 手写,务实的中间态 | 边界维护成本高,仍可能冲突 | 大多数需要长期共存的场景 |
| D. 真·双向变换(lens) | 形式化定义反向变换 | 理论最强,任意小改可回流 | 极难,只对受限改动(如仅改 props)可行 | 局部、约束严格的编辑 |
这就是为什么协议 P23 里那个 roundTrip 字段要取值 structural / props-only / none——它本质是在逐块声明这里用的是 C 里的哪种保护级别、或退化到 A。绝大多数成功的系统选 B 或 C,而不是 D。追求 D(万物皆可双向回改)的团队,基本都在这里流干了血。
决定性的一步:不是「实现双向」,而是「画清并诚实标注边界」
负责任的 round-trip 设计,核心动作不是去实现那个不可能的完全双向,而是:
明确地画出「哪些改动可同步、哪些不可」的边界,并把这条边界对用户显式化。
比如:props 值的修改可以双向(D 在小范围可行);结构增删走保护区(C);一旦进了逃生舱写了自定义代码,就明确标记「此块已脱管(none)」,UI 上直接告诉用户「这里改了就回不来了」。把「哪里能改回、哪里不能」明明白白告诉用户,比假装「随便改都能同步」然后在某次重新生成时悄悄吃掉他两小时的手改,负责任一万倍。
下半场小结
round-trip 的病是静默漂移,药是显式声明可同步边界 + 单一源头。而「选 A/B/C/D 哪种」根本不是技术选型,是产品哲学选型——它等价于系列第三、四篇里那个分叉:你的页面产物,究竟是「代码」还是「运行时 schema」。
合流:两个点是同一种病
现在把两半场并起来看,那句主线就立住了:
| Intake(P12) | Round-trip(P23) | |
|---|---|---|
| 边界 | 人的意图(散文)→ 机器规格 | 机器规格 ↔ 代码 |
| 方向 | 单向摄入 | 双向同步 |
| 病症 | 静默编造缺失的逻辑 | 静默漂移 / 覆盖手改 |
| 根因 | 信息不在输入里(欠约束) | 表达空间不对等(不可双射) |
| 药 | 把缺口显式成问题 | 把可同步边界显式标注 |
它们共享同一个母题:每当同一份信息要跨越一个表示边界,边界上一定有信息损耗或歧义;不负责任的系统假装它不存在(猜、或悄悄覆盖),负责任的系统把它显式地摆到台面上(问、或标注)。 这也正是整个系列反复出现的那条线——confidence、provenance、单一指标会骗你、自动 vs 人工的边界——全都是同一件事的不同侧面:显式化你不知道的东西。
预设反对:「等模型更强,这俩不都自动了?」
会缓解一部分,但根都拔不掉:
- intake:模型再强也读不出 PRD 里没写的东西。它只会更擅长编造——反而更危险,因为编得更像真的。完备性矩阵和「缺口即问题」的结构,任何模型强度下都省不掉。
- round-trip:代码表达空间大于 schema 这个数学事实,不随模型变强而改变。反向变换的不可能性是结构性的,不是能力性的。
能力提升能移动边界的位置,但消不掉边界本身。 承认这一点,才不会在错误的方向上投入。
三条可迁移的设计律
跳出搭建系统,这两处流血教给我们的,能用在任何跨表示的系统上:
- 在信息会丢失的边界上,让「丢了什么」成为一等产出。 intake 输出问题清单,round-trip 输出
roundTrip标注——都是把损耗显式化,而不是掩盖。 - 面对「同一真相的两个可编辑副本」,先定源头,再谈同步。 要么单一源头(运行时 schema,B),要么接受部分/最终一致并显式 merge(保护区,C)。假装两边都是源头且永远一致,是一切同步噩梦的开端。
- 区分「能力问题」和「结构问题」。 能力问题(信息难提取)会随模型变强消失;结构问题(信息不存在、表达空间不对等)不会。把判断力和工程投入押在结构问题上——那才是护城河所在。
写在最后:五篇走到这里,越挖越会发现——从设计稿到生产页面的全部难度,几乎都能归到一句话:把隐性的、跨边界会丢失的信息,显式化成可校验、可追溯、可演进的结构。 intake 显式化「PRD 没说的」,round-trip 显式化「代码与 schema 对不上的」。工具会一直变强,但「诚实地面对信息在哪里丢失」这件事,永远是工程师而不是模型的责任。