⚙️ 单元层(Unit Layer)· 一次调用的完整流转

任何一次业务动作 —— 无论是 AI 编排自动调用、用户表单/确认、还是 REST 直调 —— 都从同一个入口 unit.Registry.Execute 进入, 强制经过 来源证明 / 确认凭证 / 幂等 / 事务 / 审计 五道硬校验。本图以「教师请假」为示例走完全流程。
📨 一次单元调用到达 统一入口
①
单元契约定义 & 注册期静态防线 backend/go/internal/unit AI 只认这个接口,不认业务
Unit 契约(最小接口)
// 一个单元 = 一个原子业务事务
type Unit interface {
    Name() string
    Describe() Descriptor   // 给 AI 的说明书 + 硬约束声明
    Execute(ctx, req) (*ExecuteResult, error)
}
type TxAwareUnit interface {
    Unit
    ExecuteTx(ctx, tx pgx.Tx, req) (*ExecuteResult, error)
    // Atomic=true 的单元必须实现,否则注册即报错
}
🚨 注册时校验(服务启动即验证,不是文档承诺):声明 Atomic=true 却未实现 ExecuteTx → Register() 直接报错;单元名重复同样拒绝。
Descriptor —— 安全机制从哪里来
字段作用(示例)
InputSchema参数校验依据;关键 ID 字段内嵌 x-source 标记(来源证明)
RequiresUserConfirm资金/权属承诺点=true(建报销单、入库、付款登记…)
IdempotentAI 超时重试安全:同键命中直接返回首次结果
Atomictrue = 单数据库事务(业务表 + 引擎表同事务提交)
Domain业务域(请假/报销/…),编排候选集过滤依据
请求载荷 ExecuteRequest 携带:TenantID / ActorType(user·ai·system) / TraceID / IdempotencyKey / Args / PriorResults / ConfirmationToken —— PriorResults(前序单元真实输出)与 ConfirmationToken(用户确认凭证)就是第 3 / 3.5 步校验的对象。
②
统一执行入口 —— Execute 八步流水线 Registry.Execute(ctx, unitName, req, opts…) 任何调用者都绕不过去
1
查注册表取单元实例
按 unitName 从 79 个已注册单元中查找。
✗ 未注册 → ErrUnitNotFound,编排层据此修正候选集
2
JSON Schema 校验参数
按 InputSchema 校验必填字段与类型(string/number/integer/boolean/array)。
✗ 缺参/类型错 → ErrSchemaValidation("缺少必填参数: reason")→ 对话侧广播 form_request 补表单
3 🔒
来源证明校验(防 AI 编造 ID)
Schema 中声明 x-source: prior_unit_result 的字段(request_id / employee_id / …),取值必须精确等于 PriorResults 中对应单元输出的对应路径值。
✗ 编造 ID → ErrSourceProof("request_id 未命中 create_leave_request 的真实输出,请先调用该单元")
✓ 当前登录用户等上下文由系统注入(prior[lookup_employee].employee_id = userID),不给 AI 编造机会
3.5 🔒
确认凭证校验(资金/权属承诺点)
仅 RequiresUserConfirm=true 的单元执行到此步:必须携带 ConfirmationToken;服务端按 租户+用户+单元+参数哈希 校验,成功后原子消费(issued→consumed)。
✗ 无凭证 → ErrConfirmRequired → 挂起 needs_confirm 等确认卡;✗ 无效/过期/改参 → ErrConfirmRejected
4·5
前置条件 + 权限校验
由单元自定义执行(如"单据状态必须为 draft");租户归属与操作者权限沿用系统约定,关键写操作 SQL 层补租户条件(纵深防御,如 submit CAS 带 tenant_id AND user_id)。
6 🔒
幂等校验(先于事务)
Idempotent=true 的单元必须携带 IdempotencyKey;查 unit_audit_logs 唯一索引 (tenant_id, unit_name, idempotency_key),命中历史成功记录 → 直接返回首次结果快照,不再执行。
✗ 幂等单元缺键 → ErrIdempotencyKeyMiss
✓ AI 超时必然重试 —— 这是重试安全的根基
7 🔒
事务边界(Atomic 单元)
开启数据库事务 → 经 context 注入 → 单元内部只能通过 unit.Tx(ctx) 取连接(不暴露连接池,结构上杜绝"忘记用事务连接")。
✓ 业务写入 + 流程引擎 StartTx + 审计落库 = 同一事务,失败整体回滚
8
执行 → 审计 → 事件
成功:提交事务 + 写 unit_audit_logs(脱敏 args / 结果快照)+ 返回 ExecuteResult{Data, Event},Data 并入 PriorResults 供下一步引用(含 wf instance_id)。失败:回滚 + 落 error 审计,返回带明确原因的错误(供 AI 重新规划)。
🔑 架构核心:"自由组合在技术上安全" 由这五步强制,而不是靠提示词约束模型。校验失败是"可预期拒绝"(业务规则/参数问题),与系统错误(5xx)区分,错误文案直接回喂编排层重新规划。
③
两种执行形态 —— 事务怎么切 Atomic 单事务 vs 非原子 事务与业务一一对应
🟢 Atomic = 单数据库事务(提交类单元)
submit_leave_request 示例执行序列:
tx := db.Begin()
txCtx := unit.WithTx(ctx, tx)      // 事务注入 context
u.ExecuteTx(txCtx, tx, req) {      // 单元内只经 unit.Tx(ctx)
  ① CAS: 业务表 draft→pending
     (UPDATE ... WHERE id=$1
       AND status='draft'
       AND tenant_id=$2 AND user_id=$3)
  ② Engine.StartTx 同一事务建 wf 实例
     + 自动推进 ai_auto 节点
}
writeAuditTx(同事务)                // 审计与业务同生共死
tx.Commit()  /  失败 → Rollback
⚪ 非原子 = 直接执行 + 独立审计(查询/轻动作)
lookup_org / query_receipt_stats 等只读或单写单元:
u.Execute(ctx, req)          // 直接执行
writeAudit(...)             // 成功后独立写审计
// 出错 → 写 error 审计后返回错误
硬约束:任何外部调用(AI/通知/第三方)不得在持有 DB 事务时进行 —— AI 审批决策拆两段(见引擎层图 ⑤),事务内只做规则校验与落库。
④
示例走查:帮我请两天病假(真实单元全链路) V3 编排 runV3 · 两步单元 · 每步过 8 步流水线 idempotency = planID:v3:step
① create_leave_request 建草稿→ ② submit_leave_request 提交进审批流→ (wf 引擎 ai_auto 自动通过)→ done · 汇报
STEP 1 · create_leave_request(Atomic)
Args(AI 给出 + 系统注入 employee_id)
{ "employee_id": "<当前登录用户ID·系统注入>",
  "leave_type": "sick",
  "start_at":  "2026-09-09T08:30:00",
  "end_at":    "2026-09-10T17:30:00",
  "reason":    "感冒发烧,需要休息两天" }
过流水线
  • 2 ✓ Schema:employee_id(注入) / leave_type enum / 日期 format 全通过
  • 3 ✓ 来源证明:employee_id = 系统注入的 prior.lookup_employee.employee_id(精确命中)
  • 3.5 — 非承诺点(纯草稿),跳过
  • 6 ✓ 幂等:plan:v3:1 首次执行
  • 7 ✓ 单事务写 oa_teacher_leaves(status=draft)
Output → 写入 PriorResults
{ "request_id": "<真实UUID>", "status": "draft" }
STEP 2 · submit_leave_request(Atomic + wf 引擎)
Args(resolveRefArgs 自动从前序输出取真实 ID)
{ "request_id": "<= create_leave_request 输出 ✓>" }
过流水线
  • 3 ✓ 来源证明:request_id 精确等于 STEP1 真实输出 —— 模型若编一个别的值,这里直接 ErrSourceProof
  • 6 ✓ 幂等:plan:v3:2;7 ✓ 单事务内:业务 draft→pending(CAS 防并发)+ Engine.StartTx 启动审批实例,8 ✓ 审计
Output(含 wf 实例信息 → 编排据此决定挂起与否)
{ "instance_id": "<wf实例ID>", "status": "pending" }
编排读到 instance_id 后查 wf 待办:全部 ai_auto 已自动通过 → 继续下一步;存在 manual 校长待办 → 挂起 waiting_approval,审批通过事件到达后自动续跑(引擎层图 ⑥ 有完整走查)。
⑤
防线实测 —— 四个"坏行为"分别被哪一步拦下 审计表可回放每一次拦截 不是提示词约束,是入口硬校验
攻击 / 误操作拦截点系统行为
AI 编造 request_id="12345" 提交请假第 3 步 来源证明ErrSourceProof:值未命中 create_leave_request 的真实输出,疑似编造 ID → 编排层据错误重规划(先调 create)
AI 直接调 create_expense_request(资金承诺点)无凭证第 3.5 步 确认凭证ErrConfirmRequired → 对话挂起 needs_confirm,前端渲染确认卡,用户点击后 POST /oa/confirm/issue 签发一次性 15 分钟凭证再续跑 —— 绝不静默执行/跳过
AI 从历史会话抄来一个 token 但改了金额第 3.5 步 args 哈希凭证绑定 CanonicalHash(args)(键排序规范化 JSON 的 SHA-256),改任一处 → ErrConfirmRejected
AI 调用超时 → 自动重试同一单元第 6 步 幂等同 IdempotencyKey 命中 unit_audit_logs 唯一索引 → 直接返回首次成功结果,不会重复建单
📌 审计兜底:每一次执行(含被拦下的调用)都写 unit_audit_logs(trace_id / actor_type / unit_name / 脱敏 args / result),既是"来源证明拦截次数"等指标的取数源,也可回放 AI 的每一步决策与动作。
⚙️ 单元层 · 一次调用 = 查注册表 → Schema 校验 → 来源证明 → 确认凭证 → 前置/权限 → 幂等 → 事务 → 审计 | 全部调用同入口,AI/用户/系统无差别
代码:backend/go/internal/unit/(unit.go 契约 · registry.go Execute 流水线 · validate.go 来源证明 · canonical.go 参数哈希)| store/unit_audit_store.go(幂等存储)| service/confirm_issuer.go(凭证签发)