能交给 APUS-OpenJev-v1 的是“建议做什么”,必须由业务代码把控的是“能不能做、做完出问题怎么办”。按这个标准切,端侧决策模型只占链路里的一段:它读输入、产出建议动作与参数;执行动作的发起、结果校验、异常兜底都留在业务侧。适用场景是动作可枚举、参数可写规则校验、执行有副作用的链路;如果动作本身列不出白名单、也写不出判定条件,即使模型给出看起来合理的输出,也不适合让它进入执行路径。是否切对了有两个可验证的点:执行入口是否只接受内部命令对象,审计记录里是否同时留下模型原始输出和校验结论。
把 APUS-OpenJev-v1 限定在决策段,它的产出只是建议,不是可执行指令。业务侧在决策后加一层校验:字段缺失、取值越界、超时未返回等条件逐条判定,白名单外默认拒绝;校验不通过时走预定义兜底动作,并把原始输出、命中的规则、最终动作写进同一条记录。执行段只接收通过校验后转换出的内部命令对象,模型异常不会卡死链路,复盘时也能区分是模型输出问题还是业务校验与兜底配置问题。
把业务链路拆成输入、决策、执行、校验四段
先把链路拆成四段,责任才有落点。输入段负责把上下文整理成模型可读结构;决策段由模型产出建议;执行段调用真实写接口或下发指令;校验段判断决策输出是否可接受。下表给出一份建议的职责划分,最后一列写的是出问题后可以从哪里查证据。
| 段落 | 这一段做什么 | 当前责任方 | 出问题由谁承担 | 可验证的载体 |
|---|---|---|---|---|
| 输入 | 把上下文整理成模型可读结构,控制字段范围 | 业务侧 | 业务侧 | 输入构造日志、字段清单 |
| 决策 | APUS-OpenJev-v1 输出建议动作与参数 | 端侧模型 | 模型结果由业务侧承担,按模型版本与输入回放 | 模型原始输出日志 |
| 执行 | 调用写接口、下发指令、变更状态 | 业务侧 | 业务侧 | 执行入口日志、内部命令对象 |
| 校验 | 判断决策输出是否可接受,决定接受、修正或兜底 | 业务侧 | 业务侧 | 规则命中日志、兜底动作记录 |
这张表不是文档装饰。一次故障复盘应当能指出问题落在哪一段:是输入构造漏了字段,是模型建议与场景不符,是执行段处理了没通过校验的输出,还是校验规则本身缺了一条。落不到具体段落,讨论就会变成“模型不好用”和“业务代码兜一下”的互相推诿。
标出模型只允许出现在哪一段
模型只允许出现在决策段。执行段不接收模型原文,不消费模型输出里的命令字符串,也不根据模型返回的字段名动态决定调用哪个接口。理由有三层:模型输出是概率性的,字段名和结构可能漂移;执行动作需要权限、幂等和审计,这些约束写在业务代码里才可测;如果执行器直接读模型原文,模型一升级执行行为就跟着变,回归测试很难覆盖。
体现方式是在决策段和执行段之间放一个转换层:只有通过校验的字段才被映射成业务侧定义的内部命令对象。
decision = model.decide(input) # 决策段:只产出建议,不产生副作用
verdict = validate(decision, rules) # 校验段:业务侧逐条判定
if not verdict.ok:
cmd = fallback(verdict.reason) # 兜底:不执行或保守执行
else:
cmd = to_command(decision) # 转换:只取白名单字段
execute(cmd) # 执行段:只接受内部命令对象
这段骨架里,execute 的入参是业务侧定义的命令对象,不是模型输出。需要替换的是模型调用方式和规则表内容;放置位置在决策调用返回之后、任何执行调用之前。验证方式是检查执行入口的调用点:如果还能找到把模型返回直接传给执行函数的代码,说明边界没有守住。
为每条输出写一条可判定的校验规则
校验规则要写成能算出真或假的条件,而不是“看起来是否合理”。建议每条决策输出至少过一遍下面几类判定,规则可以放配置,也可以直接写代码,关键是拒绝动作要指向明确的兜底项。
| 规则 | 判定条件 | 命中后的动作 | 怎么验证 |
|---|---|---|---|
| 字段完整性 | action 或 params 缺失 | 拒绝,走默认兜底 | 构造缺字段输入,看是否被拒 |
| 取值白名单 | action 不在允许枚举内 | 拒绝,走默认兜底 | 传一个白名单外的动作名 |
| 数值范围 | 参数超出配置区间 | 拒绝,走默认兜底 | 传区间外的边界值 |
| 决策超时 | 调用超过设定的 deadline | 视为未返回,走超时兜底 | 调小 deadline 复现超时分支 |
| 结构可解析 | 输出不是可解析结构 | 拒绝,走默认兜底 | 注入不可解析输出 |
下面是一份可替换的规则配置骨架,字段名按项目实际情况调整,重点是条件可判定、动作指向具体兜底函数。
rules:
- id: required_fields
when: missing(action) or missing(params)
action: fallback_default_deny
- id: action_allowlist
when: action not in ['noop', 'set_profile', 'request_confirm']
action: fallback_default_deny
- id: range_check
when: params.timeout_ms < 1000 or params.timeout_ms > 30000
action: fallback_default_deny
- id: decision_timeout
when: elapsed_ms > 1500
action: fallback_timeout_retry_once
- id: schema_parse
when: parse_failed == true
action: fallback_default_deny
验证方式:为每条规则构造一个必然命中的输入,观察它是否被拒绝并进入对应兜底动作;同时确认没有规则覆盖的输出不会默认放行。默认拒绝通常比默认放行更安全,代价是多几次确认,而不是多几次无法解释的执行。
定义校验不通过时的兜底动作
兜底动作要提前列清楚并绑定触发条件,否则校验失败时容易临时决定,行为不可复现。下面是一份偏保守的清单,按风险从低到高排列。
- 默认拒绝:任何校验不通过先走这里,动作是不执行、返回可解释结果并记录。触发条件是规则命中且没有更合适的兜底项。
- 保守等价动作:用业务侧默认值代替,例如维持现状、不改变参数。触发条件是该动作无副作用且规则允许。
- 一次幂等重试:仅当决策调用超时且重试无副作用时使用;重试仍失败就回到默认拒绝。
- 转确认:动作影响面不可判定时,把选择交回用户或人工流程。触发条件是参数缺失但意图可识别,或某个白名单外动作被频繁请求。
- 降级规则引擎:端侧模型不可用或连续校验失败时,用固定规则产出建议,行为可预测。
回写记录要能回答两个问题:模型原本说了什么,业务侧最后做了什么。建议在业务请求的同一条审计记录里补齐这些字段。
request_id: req-xxxx
model_version: APUS-OpenJev-v1
raw_output: 截断后的模型原文
rule_id: action_allowlist
fallback_action: fallback_default_deny
final_action: request_confirm
elapsed_ms: 数值
如果这些字段分散在多张表,用 request_id 关联。只记最终动作会导致复盘时无法区分是模型输出有问题,还是校验与兜底配置本身有问题。
用一次真实请求走查边界表
职责表写完不代表能执行,建议用一条固定的回归请求走查一遍,例如“调整一个可枚举参数”。走查时不改动模型本身,只看链路各段实际发生了什么,把越界点记下来,改业务侧。
| 走查点 | 预期 | 实际观察 | 越界点 | 修订 |
|---|---|---|---|---|
| 输入段 | 业务侧构造上下文 | 用户原文与设备状态一起进入 | 无 | 在输入日志记录字段清单 |
| 决策段 | 只输出建议动作与参数 | 输出里夹带了可执行命令字符串 | 无,只要不消费即可 | 不改模型 |
| 执行段 | 只接收内部命令对象 | 执行器读取了模型原文里的命令字段 | 模型指令直接进入执行段 | 执行入口改为只接受命令对象,解析放到转换层 |
| 校验段 | 逐条判定,白名单外拒绝 | 某参数没有对应规则,走了放行 | 缺省放行 | 改为默认拒绝 |
| 记录 | 原始输出与结论同条 | 只记录了最终动作 | 无法回溯 | 补 raw_output、rule_id、fallback_action |
走查的验证方式很直接:重放同一条请求,看执行入口日志里是否还出现模型原文,看审计记录是否带上了 rule_id 和 fallback_action。修订原则是优先改业务侧的消费方式和规则配置,而不是去调模型提示词——越界通常不是模型造成的,而是业务代码让模型原文流到了不该到的地方。边界表改完后,模型升级、校验规则变更、兜底动作调整,都应能对应到表中某一格。