第 24 章 从零搭一个完整系统

前面二十三章是拆开讲的:契约、上下文、证据、状态、闸门、分工、验证、评测、治理。真正做一个系统的时候,它们是同时出现的。

这一章回答那个最实际的问题:现在开始做一个新项目,第一步该干什么?

适用边界 本章的停靠点 C 走查,是把贯穿案例中的真实问题结构压缩成一条可复现的构造演示。字段名、对话和执行结果用于说明机制怎样衔接,不应被当作新的生产统计或效果证明;可核验的生产事实仍以第 1、22、25 章和附录 E 的边界说明为准。

图 24-1 标出第六篇的位置:最后两章把前面的机制合起来,分别处理从零搭建与旧系统迁移。

全书六篇路线,第六篇被突出显示

图 24-1 全书六篇路线:第六篇


一、第一步不是写提示词,也不是画架构

这个问题最常见的两种答案都不太对。

一种是“先写个提示词跑起来看看”。它不算错——第 5 章那条最小复杂度原则就是这个意思,能用提示词解决的不要上架构。但“先跑起来”和“先想清楚”并不冲突,而且有些决定一旦做错,后面每一层机制都会跟着长歪。

另一种是“先把架构画出来”。这更糟。第 5 章那棵诊断树讲得很清楚:没有证据支撑的复杂度,是纯成本。

实际的第一步是第三件事:先把这个系统必须回答的问题列清楚,然后按证据决定做到哪一层。

这一章给三样东西:

设计画布    动手之前必须回答的八个问题
十五步      一条可执行的施工顺序
停靠点 C    一个完整系统的贯通走查

二、设计画布:动手之前的八个问题

这八个问题,每一个都指向后面某一章。答不出来的那一题,就是这个项目最大的风险。

一、这个系统要产生什么结果? 只是对话(回答、澄清、安抚),还是会改变外部世界(建单、发消息、扣款)?有外部副作用,第 9 章和第 18 章就是必需的,而不是可选的。

二、它什么时候允许推进,什么结果才算真正完成? 这是两个问题,必须分开答。前者决定闸门的许可条件(第 13 章),后者决定什么样的提交结果或成功观测结果才算数(第 9、18 章)。“允许执行”和“已经完成”混成一个问题,系统迟早会说出没有依据的完成语。

三、跨轮要记住什么? 哪些字段的正确性依赖前几轮说过的话?如果答案是“没有”,你可能停在停靠点 A 或 B,不需要第三篇那一整套东西。

四、这些字段的取值空间是什么? 每个字段可能处于哪些状态——包括未知、被拒绝、有冲突、已过期。这一题对应第 10 章那个七态枚举和第 13 章的定义闭合。

五、用户可能怎么改口? 纠正、新增、撤回、换个说法再说一遍。这一题决定第 11、12 章要做到什么程度。

六、有哪些事绝不能做? 不可逆的动作、已被拒绝的索取、越出授权范围的操作。这一题对应第 15 章的不变量。

七、信息从哪里来,它有什么权力? 不要用“可信/不可信”一个维度压平,要分三问(第 7、9、23 章):

它有指令权吗?        材料里的文字不因为出现在上下文里就获得执行权
它能证明什么事实?    工具回执没有指令权,却是外部结果最强的证据
它属于当前作用域吗?  这条内容说的是不是当前这个对象、这个用户、这次任务

八、怎么判断它做对了? 可验收的标准从哪里来,谁来定(第 19 章)。

第四题和第六题最容易被跳过,而它们决定了系统的下限。

第四题被跳过的表现是:字段只有“有值”和“没值”两种状态,于是“没问过”和“问了被拒”变成同一件事——第 10 章那次事故就是这么来的。第六题被跳过的表现是:所有约束都写成“应该”,没有一条写成“绝不”,于是系统在最坏情况下没有底。

画布的用法很简单:把八个问题写下来,逐条填。填不满的那几条,就是你要先去问业务、而不是先去写提示词的地方。

三、十五步:一条可执行的顺序

分四组。

第一组:定义(1–4)

1. 写下三到五个对话样例。 优先用真实历史对话;全新业务没有数据时,用业务方确认过的代表性场景代替,并明确标注为构造样例

样例不要只写顺利的那种。按系统实际具备的能力,从下面这些类型里挑适用的:

正常路径   一切顺利地走完                      ← 通常都需要
边界       一句话塞进三个字段、超长输入、格式怪异  ← 通常都需要
改口       中途纠正、新增需求、撤回              ← 有跨轮状态时
拒绝       明确拒绝某个字段或某种联系方式         ← 需要索取字段时
工具失败   外部调用超时或返回失败                ← 有外部副作用时

后三类不必强求——一个单轮分类器没有跨轮改口,也没有工具可失败,硬写这些种子等于提前设计不存在的复杂度

2. 从样例反推字段清单与取值空间。 每个字段列出所有可能的状态,包括未知与拒绝。

3. 写出推进条件。 什么情况下允许进入下一步——这是闸门真值表的雏形(第 13 章)。如果这个任务有外部副作用,还要单独写一条:什么样的结果才能证明它真正完成了(第 9、18 章)。

4. 写出禁止清单。 哪些状态与动作的组合绝不允许发生(第 15 章)。

这一组的产出全是文档,一行代码和提示词都还没写。但它决定了后面所有东西的形状。

第二组:最小可用(5–8)

5. 写第一版提示词。 任务、输入、约束、输出契约(第 3 章)。

6. 补三到五条示例。 覆盖第 1 步那些你担心的情况(第 4 章)。

7. 建回归集。 把第 1 步的样例变成可验收的用例:先写当前架构能观察到的那些断言——require、forbid、格式与输出结构;等第三组把状态和动作层建起来之后,再补状态断言和动作断言(第 19 章)。

这里有一条值得留意的规律:评测面是跟着架构一起变丰富的。 只有一整块提示词的时候,你能断言的只有输出的那段文字;有了槽位、闸门和状态机,才能断言“这一轮之后某个字段应当是什么状态”。

8. 跑一遍,记录基线。

第 8 步不能跳。 没有基线,之后所有“变好了”都只是感觉。而且越晚补,越可能因为模型、数据、配置和环境都已经变过,再也拿不到真正同口径的第一版基线——第 1 章那次重构就是这么丢掉对比数据的。

第三组:加状态(9–12),只在诊断需要时

9. 槽位与状态空间(第 10 章) 10. 证据、事件与归并器(第 11、12 章) 11. 唯一闸门与策略(第 13 章) 12. 流程阶段、状态机、不变量与安全降级(第 14、15 章)

这一组每一步都要有证据才做。第 5 章那条判据在这里仍然有效:能指出一个具体的、可复现的失败,或者一项已经确定的业务与合规要求,并确认当前这一层处理不了它。

这是诊断顺序,不是强制施工顺序。很多系统永远不需要走到第三组,那不是缺陷。

第四组:运行时落地与生产准备(13–15)

13. 先分工,再拆调用:把已经能写成规则、状态、权限和业务策略的东西外置给确定性执行层——它可以是普通代码、工作流节点、规则引擎或数据库约束——然后才建立抽取、计划动作、渲染三份契约(第 16、17 章)。

如果系统跑在可视化工作流平台上,模型调用、检索、条件路由和工具调用都可以映射成节点;但持久状态、权限、不变量、原子提交和自动回归是否留在平台内,要按平台能力和业务风险单独判断——这几项通常是决定能不能上生产的部分。

顺序不能反——先决定什么归代码、什么留给模型,才谈得上模型调用怎么拆。

14. 工具与提交:工具动作与观测结果、发出前验证、状态与消息分层提交(第 9、18 章) 15. 追踪、探针与健康检查,以及上线前的关键评测(第 19、20 章)

第 15 步经常被推迟到“以后有空再说”。但它有个特点:越晚做,补起来越贵——因为追踪要嵌在每一层的中间产物上,而那时候每一层都已经写完了。

还要说明:走完这十五步,意味着运行时主体已经具备进入生产验证的条件,不等于可以直接全量上线。正式发布前还要做完安全审查(第 23 章)和回滚预案,并通过灰度逐步放量;上线之后则继续做健康检查、版本治理和周期性的安全复查(第 20、22、23 章)。

四、停靠点 C:一个完整的多轮客服

停靠点 这是全书第三个、也是最后一个停靠点。停靠点 A 是一个提示词就够的分类器,停靠点 B 是一个能查资料、能说不知道的问答助手。C 是一个完整体:有跨轮状态、有唯一闸门、有持续生效的拒绝约束、有工具调用、有验证与提交、有追踪和回归集。

它做什么

在产品页面上回答用户的问题;当用户明确要求后续处理时,收集三样东西——单位、联系人、联系方式;收齐之后输出一份需求摘要,并向后续业务系统提交服务请求。

字段、业务状态与执行账本

表 24-1 端到端案例的状态对象

状态对象 状态空间或关键字段 说明
unit UNKNOWN / VALID / DECLINED 单位名称
contact UNKNOWN / VALID / DECLINED 联系人称谓
phone UNKNOWN / VALID / INVALID / DECLINED 手机号
alt_channel UNKNOWN / VALID / DECLINED 非电话渠道及其值
channel_permission 按渠道分别取 UNKNOWN / GRANTED / DENIED 每种联系方式是否获准使用
service_request.status NOT_STARTED / PENDING_CONFIRMATION / SUBMITTED / FAILED 这次服务请求的业务执行状态
execution_record action_id / idempotency_key / 尝试状态 / 最近一次观测 某个逻辑动作的持久化执行账本

前三类对象不要混在一起。前五行是业务字段;service_request.status 描述这次服务请求整体走到了哪一步;execution_record 则记录某个逻辑动作怎样被执行和观测。业务字段状态、业务执行状态、执行尝试状态是三套不同的枚举。 一次提交服务请求可以包含多次调用尝试,所以幂等键和最近一次观测属于执行账本,不塞进槽位,也不塞进 service_request.status

channel_permission 是这个系统里最容易做错的地方,它落实的是第 15 章那条冻结区分:

字段状态说的是“我给不给你这个值”;渠道许可说的是“你能不能用这种方式联系我”。拒绝什么,就约束什么。

而且许可要按渠道分别记录:拒绝电话不等于拒绝其他渠道,给了某个即时消息账号也不自动等于允许用它联系——许可必须来自用户的表达,不能由“他给了账号”推断出来。

为了让例子保持简单,这里只留了一个非电话渠道。实际系统同时存在即时消息、邮箱、站内信等多种渠道时,渠道值和渠道许可都要按渠道分别建模,不要照抄单个 alt_channel

两道闸门

FOLLOWUP_GATE      该不该开始收集联系方式
  条件:出现明确后续处理意向 ∧ 不在纯知识问答场景
  阻塞码:NO_FOLLOWUP_INTENT / PURE_KNOWLEDGE_QUERY

SUBMISSION_GATE   能不能执行"提交服务请求"这个动作
  条件:unit=VALID ∧ contact=VALID
        ∧ 存在渠道 c,其值有效且 channel_permission[c] = GRANTED
        ∧ service_request.status = NOT_STARTED
  阻塞码:MISSING_UNIT / MISSING_CONTACT / NO_GRANTED_CHANNEL
        / ALREADY_SUBMITTED / PENDING_CONFIRMATION / RETRY_NOT_AUTHORIZED

第二道闸门的名字值得留意:它判定的是能不能提交服务请求,而不是“能不能输出摘要”。提交服务请求是一次外部动作,输出摘要是一次对话动作,两者不是同一件事——把闸门命名成“摘要闸门”,读者很容易把它们当成一个动作,而那正是这一节要拆开的东西。

条件里的 service_request.status 也不是修辞。闸门条件里出现的每一个概念,都必须在状态里有对应的承载,否则这条件就是一句没人执行的话(第 13 章定义闭合)。它区分了三种默认不能提交的情况:已经成功过(SUBMITTED);上次结果还没确认(PENDING_CONFIRMATION);上次已明确失败、但还没有重试授权(FAILED)。前两种分别进入去重和对账流程;第三种交给独立的重试策略与预算判断。失败不等于自动再试。 如果获准重试同一个逻辑动作,仍复用原来的稳定幂等键。

一轮完整走查

这是本章的核心,也是全书唯一一次把第三、四篇的机制放在同一轮里展开。

图 24-2 先给出全景,并直接沿用下文 ①—⑪ 的编号。图中的回环是关键:外部结果不能直接变成一句完成语,它要先回到证据与状态链,重新折叠和决策之后,才会产生第二个对话动作。

停靠点 C 在四个责任层之间形成一轮端到端闭环

图 24-2 停靠点 C 的一轮端到端闭环

当前状态:unit=UNKNOWNcontact=UNKNOWNphone=UNKNOWNservice_request.status=NOT_STARTEDphase=COLLECTING、已确认后续处理意向。用户这一轮说:

“我想进一步了解这款产品,我们是A公司,联系人是刘先生。不方便接电话,用即时消息联系就行,账号im_liu”

① 证据(第 11 章) —— 只记录观察,不做判断。这里统一采用从 0 开始、左闭右开的字符区间 [start, end);计数对象是去掉外层引号后的原始用户文本:

E1  quote="A公司"               span=[15,18)  source=user  ts=T
E2  quote="刘先生"              span=[23,26)  source=user  ts=T
E3  quote="不方便接电话"        span=[27,33)  source=user  ts=T
E4  quote="用即时消息联系就行"  span=[34,43)  source=user  ts=T
E5  quote="im_liu"              span=[46,52)  source=user  ts=T

② 事件(第 11 章) —— 对观察的解释,只引用证据编号:

ev1  set(unit, "A公司")                         evidence_refs=[E1]
ev2  set(contact, "刘先生")                     evidence_refs=[E2]
ev3  deny(channel_permission.PHONE_CALL)        evidence_refs=[E3]
ev4  grant(channel_permission.IM)               evidence_refs=[E4]
ev5  set(alt_channel, "即时消息:im_liu")        evidence_refs=[E5]

这里有两处最容易做错的地方。

ev3 是拒绝电话联系,不是拒绝提供手机号。 用户说的是不方便接电话,他甚至没提过号码这件事——写成 decline(phone) 就是把两条不同的约束合并了。

ev4ev5 必须分开。 授权来自“用即时消息联系就行”这句话(E4),账号值来自“im_liu”(E5)。如果用户只丢了个账号却没说可以用它联系,你拿到的是一个值,不是一份许可。

③ 归并(第 12 章) —— 纯函数折叠,不重新理解语言:

候选状态 = 折叠(上一份已提交状态, [ev1..ev5])

unit                = VALID    "A公司"
contact             = VALID    "刘先生"
phone               = UNKNOWN                       ← 没有任何事件动过它
alt_channel         = VALID    "即时消息:im_liu"
channel_permission  = { PHONE_CALL: DENIED, IM: GRANTED }
service_request.status = NOT_STARTED
base_version        = 7

④ 状态不变量、状态机与闸门(第 13、14、15 章)

状态不变量   没有字段同时处于 VALID 与 DECLINED ✓
             已拒绝的渠道没有被标成可用 ✓

状态机       phase = COLLECTING
             本轮没有待应用的 phase 转换 ✓
             SUBMIT_REQUEST 是当前阶段获准执行的计划动作,
             它本身不是一次 phase 转换 ✓

SUBMISSION_GATE → PASS
  unit=VALID ✓  contact=VALID ✓
  渠道 IM:值有效 ✓ 且许可为 GRANTED ✓
  service_request.status = NOT_STARTED ✓
allowed_actions = [SUBMIT_REQUEST]

注意 phone=UNKNOWN 完全不影响放行——已经有一个获授权的渠道就够了。如果当初把条件写成“必须有手机号”,这一轮就会卡死,然后系统会去问一个用户刚刚表达过不便的方向。

⑤ 策略与动作不变量(第 13、15 章) —— 这一轮的计划动作是一次工具动作:

planned_action_1 = SUBMIT_REQUEST              ← 有外部副作用
  action_id       = a-demo-0001
  scope           = 本次需求
  base_version    = 7
  gate_ref        = SUBMISSION_GATE:PASS
  fact_refs       = { unit, contact, alt_channel }
  idempotency_key = request:{session}:{action_id}

动作不变量  planned_action ∈ allowed_actions ✓
            PHONE_CALL=DENIED,动作里不含任何电话联系或索取 ✓

这里有一个很容易犯的错误:直接把动作定成“输出摘要”,然后在提交阶段顺手把服务请求同步出去。 那样一来外部副作用就没有对应的计划动作,也不会经过闸门授权——第 9、17、18 章那条链就断了。凡是会改变外部世界的事,都必须先成为一个被授权的计划动作。

幂等键也要留神:它挂在 action_id 上,在这个动作第一次产生时就固定并持久化。同一次逻辑动作重试时必须复用原键——如果按最新的 base_version 重算,重试就会拿到一个新键,幂等保护随即失效(第 9 章)。

⑥ 先记账,再执行(第 18 章)

持久化待执行记录   action_id、idempotency_key、status=PENDING
      ↓
执行               提交服务请求到业务系统
      ↓
观测结果           SUCCESS / FAILURE / UNKNOWN

调用之前先落一条待执行记录,而不是直接发请求——这样进程在调用前后崩掉,都还有一本执行账可以恢复。这也是第 18 章那条“调用前只写待执行,不写已完成”的落地。

⑦ 观测结果回流(第 11、12 章) —— 它先成为证据,再成为事件:

O1   观测结果      外部执行的原始回执
     action_id=a-demo-0001   result=SUCCESS   receipt=...
      ↓
E6   证据          source=tool   ts=T'   scope=本次需求
                   observation_ref=O1
      ↓
ev6  observe_request(SUCCESS)    evidence_refs=[E6]
      ↓
重新折叠 → 业务执行状态变为 SUBMITTED → 候选状态'
      ↓
重新进入确定性控制层

这三层不能压成一层。 观测结果是外部世界返回的回执,证据是系统对这次观察的记录,事件才是对它的业务解释——否则第 11 章那条“每个状态都能追溯到证据”的链就在这里断了。三种回执分别解释为 SUBMITTED / PENDING_CONFIRMATION / FAILED

⑧ 第二个计划动作(第 13、17 章) —— 重新折叠之后,控制层给出这一轮真正要对用户说的话:

SUCCESS  → planned_action_2 = REPORT_SUBMITTED_AND_SUMMARY
UNKNOWN  → planned_action_2 = REPORT_UNCONFIRMED_AND_SUMMARY
FAILURE  → planned_action_2 = REPORT_FAILURE_SAFE(安全降级)

这一步同样不能省。渲染必须有一个明确的、属于对话类的已批准动作可对应,否则后面验证器那条“文本与已批准动作一致”就没有比对对象——SUBMIT_REQUEST 是一次外部执行,它本身不是一句要说给用户听的话。

⑨ 渲染(第 17 章) —— 三种结果对应三种可说的话:

SUCCESS  "服务请求已经提交到业务系统。信息如下——单位:A公司;
          联系人:刘先生;联系方式:即时消息 im_liu。"

UNKNOWN  "我先把需求整理如下……目前还无法确认是否已经同步成功。"

FAILURE  只整理信息,不作任何同步或联系承诺。

两处措辞要特别小心。

第一,SUCCESS 那句说的是“服务请求已经提交到业务系统”,不是“已经有人工人员接单”。 如果回执只证明业务系统收单成功,它就支撑不了“已经进入某位处理人员的队列”这个更强的主张。观测结果能证明到哪一步,完成语就只能说到哪一步(第 18 章)。

第二,UNKNOWN 那句没有加“确认后会再跟您说明”。 那是一个未来承诺——只有当系统确实有对账、回调或轮询机制时才能说;没有的话,它就是又一句没有执行机制支撑的空头承诺。

完成语需要结果证据,未来承诺同样需要可兑现的行动基础。

⑩ 验证与提交(第 18 章)

动作一致    文本对应 planned_action_2 ✓
事实白名单  出现的具体值都在 fact_refs 里 ✓
完成语门槛  "服务请求已经提交到业务系统"有 SUCCESS 观测结果支撑 ✓
未来承诺    未作任何缺乏机制支撑的承诺 ✓
拒绝约束    文本与动作均未违反 PHONE_CALL:DENIED ✓

提交        state_version 7 → 8;写入消息记录
阶段转换    SUCCESS 且摘要已发出 → phase: COLLECTING → SUMMARIZED

如果草稿里出现“我们会电话联系您”,验证器整条否决——而不是把那半句删掉。

⑪ 追踪(第 20 章) —— 这一轮留下的完整链条:

trace_id / turn_id
E1..E5 → ev1..ev5 → base_version=7
→ 不变量 ✓ → 状态机 ✓ → SUBMISSION_GATE=PASS
→ planned_action_1=SUBMIT_REQUEST(action_id, 幂等键)
→ 待执行记录 → 执行 → 观测结果 O1 → 证据 E6 → ev6 → 重新折叠
→ planned_action_2 → 渲染草稿 → 验证通过
→ state_version=8 → phase=SUMMARIZED

顺着它可以回答第 18 章那个问题:这句话,凭什么说得出口。

对应的回归用例

同一轮,写成可判分的用例(第 19 章):

case_id  TC-CH-01
origin   由生产问题抽象:已有获授权的非电话渠道后,系统仍继续索取手机号
输入      "我想进一步了解这款产品,我们是A公司,联系人是刘先生。
          不方便接电话,用即时消息联系就行,账号im_liu"
require_text  "A公司", "刘先生", "im_liu"
forbid_text   "方便留个手机号吗", "我们会电话联系您", "请保持电话畅通"
forbid_action ASK_PHONE, PHONE_CALL
state    channel_permission.PHONE_CALL=DENIED
         channel_permission.IM=GRANTED
         alt_channel=VALID, phone=UNKNOWN
action   本轮工具动作 = SUBMIT_REQUEST
         本轮对话动作 = REPORT_SUBMITTED_AND_SUMMARY
k        3(安全关键:涉及联系许可)

这里不把裸词“手机号”列进 forbid_text。系统可以在透明解释里说“本轮不需要手机号”;真正要禁止的是再次索取的动作和具体询问句。两个禁止理由仍要分开:

PHONE_CALL 与电话承诺      → 违反 channel_permission.PHONE_CALL = DENIED
ASK_PHONE 与索取手机号     → 不是因为用户拒绝提供号码(他没说过),
                            而是闸门条件已经满足、继续收集在业务上
                            没有必要——这属于数据最小化

第 15 章那条区分在这里必须守住:用户说“别打电话”,不等于说“你不能问我的手机号”。 某些业务确实合法需要手机号发短信。这一轮不该再问,理由是没必要,不是被禁止

七态补充走查

主走查触发了 UNKNOWNVALID,但第 10 章定义的其他状态也必须在回归集中有落点。下面三个小用例补齐最容易被遗漏的分支;它们是构造演示,不是新增的生产统计。

表 24-2 七态槽位的补充走查

场景 事件与候选状态 控制层结果
用户先说“A 公司”,随后又说“其实是 B 公司”,且系统无法判断哪一个是更正 两条同作用域证据互相冲突,unit=CONFLICT SUBMISSION_GATE 阻塞,只询问单位名称这一项
用户把目标从产品 A 改成产品 B 依赖产品 A 记录的 product_option=STALE 旧值不再进入事实白名单;需要时重新确认
用户说“这个即时消息账号先别用了” clear(alt_channel),归并后 alt_channel=CLEARED,对应渠道许可同步撤回或失效 重新计算闸门;没有其他获授权渠道时,不提交服务请求

CONFLICT 不是让模型猜一个更像真的值,STALE 不是继续沿用旧值,CLEARED 也不是回到“从未问过”的 UNKNOWN。三者都会改变后续动作,因此必须进入状态、闸门和回归断言,而不能只留在术语表里。

五、模板集:四份最小骨架

下面四段契约骨架与具体语言无关,可以直接照着改。它们对应第 17 章那三份契约,加上第 18 章的验证器。配套仓库的运行方法、阅读顺序和参考实现边界见附录 F

一、抽取契约 —— 只解释语义,不做业务决策:

输出两组,分开放:
  evidence_candidates[]  quote(逐字原文)、span、source
  event_candidates[]     type、field、value、evidence_refs[]
另加 unresolved[]        识别不了的部分,明确列出而不是猜
禁止:给出下一步动作、修改状态、直接输出用户可见文本

二、计划动作契约 —— 控制层的输出,也是渲染层的输入:

action_type       对话动作还是工具动作
scope             作用于哪个对象或哪次需求
base_version      基于哪个版本的状态算出来的
gate_ref          由哪次闸门判定授权(不是重新算一遍许可)
reason_code       为什么是这个动作
fact_refs         允许在文本里引用哪些事实
allowed_claims    允许作出哪些主张(完成语、承诺各自的前提)
idempotency_key   工具类写动作必须有

三、渲染契约 —— 只读已批准的决定:

输入:planned_action、fact_refs 指向的事实快照、base_version
四不许:不改动作、不添事实、不翻案、不直译原因码
自由度:措辞、语气、承接方式

四、验证器检查表 —— 只能否决,不能改写:

□ 文本对应的动作 == planned_action
□ 文本中的每个具体值 ∈ fact_refs
□ 内部状态类完成主张,有成功提交支撑
□ 外部动作类完成主张,有匹配的 SUCCESS 观测结果支撑
□ 未来承诺有可兑现的行动基础
□ 文本与动作均未违反任何 DECLINED 字段状态或 DENIED 权限
否决之后:重渲染(有次数上限,且沿用同一份事实快照与 base_version)
          → 模板化安全表达 → 不发出并转人工

第三、四条要分开,因为它们的证据来源不同:内部状态写成功了,只证明你的库里有这条记录,证明不了外部系统真的收到了(第 18 章)。

六、三个停靠点:不是等级,是完整形态

到这里,全书三个停靠点都出现过了:

表 24-3 三个停靠点的能力与代价

能做什么 需要什么 代价
A(第 5 章) 单轮任务:分类、抽取、改写 契约 + 示例 + 回归集 最低
B(第 7 章) 有据可查的问答,能说不知道 + 检索、引用核验、无命中出口
C(本章) 多轮业务闭环,能改口、能拒绝、能提交 + 证据与事件、归并、状态与作用域、闸门与策略、状态机与不变量、工具与观测结果、验证与提交、追踪

表格里只列了主要增量,C 的完整运行时仍以参考架构那张母图为准——上一节那次走查才是它真正的样子。

有一件事值得说清楚:

停靠点不是等级,是完整形态。停在 A 不丢人;用 C 那一整套去做一个单轮分类器,才是问题。

第 5 章那句话在这里再说一次:用能可靠解决问题的最小复杂度,而复杂度的增加必须有证据。

七、从零开始是幸运的

从零搭建时,先记住三件事:

动手之前先答八个问题;答不出来的那一题,就是这个项目最大的风险。

第一版就要留下基线;越晚补,越可能失去真正同口径比较的机会。

停靠点不是等级,是完整形态——停在能解决问题的那一层就好。

但从零开始其实是幸运的情况。

更多人面对的是另一种局面:手上已经有一份跑在生产上的提示词,一千九百二十五行,谁也不敢动。 这是下一章采用的较早审计快照;第 1 章的两千一百九十六行属于同一演化线上的较晚快照。它每天都在服务真实用户,出过几次事故,每次事故留下一条新规则。你知道它有问题,但不知道从哪儿下刀——删任何一条都可能出事,重写又停不下业务。