把 AI 放进审批流:先教它说"我不知道"

SailTrack
2026-09-20
点 赞
0
热 度
6
评 论
0
  1. 首页
  2. 后端开发
  3. 把 AI 放进审批流:先教它说"我不知道"

写在前面

审批流里最费人的环节不是"判断该不该批",是初审:材料齐不齐、事由合不合规、格式对不对——这些机械判断消耗了审批人大量的注意力。AI 预审想接的就是这一段:申请提交后先由模型过一遍,给出建议,人只处理值得处理的部分。

但把模型放进审批流,第一件事不是写提示词,是回答一个更根本的问题:**模型说"我觉得可以",这个"觉得"要被信任到什么程度?**这篇记录我的答案:三值决策、输出重建、超时驳回——三道设计,每一道都在教模型说"我不知道"。

一、预审在流程里的位置

系列之前写过审批接入层,提过提交时的分叉:配置了预审的模板,申请先进入 PENDING_REVIEW 状态,AI 任务预审材料;没配置就直进人工审批。

触发逻辑的关键在前两个判断:

WorkflowAgentResultCallback callback = workflowCallbackProvider.getIfAvailable();
AgentRow agent = agentRepository.findActivePreReviewAgent();
if (callback == null || agent == null || agent.publishedVersionId() == null) {
    return null;   // 没接入回调、没有启用的预审助手 → 跳过预审
}

预审是增强,不是依赖。 没有配置 AI 的部署形态下,审批流完整可用;AI 缺席时系统优雅降级,而不是报错。这个 return null 是整篇里我最看重的一行——AI 增强的功能,必须能在 AI 关闭时原样工作,否则 AI 就从"功能"变成了"单点"。

任务创建后照例走 Agent 模块的老三样:落库 PENDING、写审计、事务提交后异步执行。执行细节(状态护栏、版本快照)在 Agent 链路那篇讲过,不重复。

二、三值决策:给模型一个"逃跑出口"

预审的输出协议是三个值:

decision: APPROVED、REJECTED、TO_HUMAN 三选一。

而系统提示词里最重要的一句是:

只有明显缺少核心信息或明显违规时才能 REJECTED;不确定时输出 TO_HUMAN。

TO_HUMAN 这个第三值,是整个设计的支点。没有它,模型面对"拿不准的申请"只有两个出口——通过或驳回,而模型对"不确定"的默认行为是编一个理由选一个。有了 TO_HUMAN,"我不确定"成了合法且被鼓励的输出,模型的压力小了,错误率反而降了。

对应的规则设计是:APPROVED 的申请进入人工审批(AI 建议作为参考卡片展示);REJECTED 直接驳回(仅限"明显缺信息或明显违规");TO_HUMAN 正常进入人工。三个值最终都汇入人工审批流——只有 REJECTED 是例外,而它的门槛被提示词抬到最高。预审系统的第一版本从不直接驳回任何东西,运行数据积累出信心后,才逐步放宽。

三、不信任模型输出:重建一份干净的决策

模型返回的 JSON 不能直接用,要过三道处理:

第一道:容忍格式噪音。 提示词明明说了"不要输出 Markdown",模型还是时不时包一层 ```json。解析器按最坏情况处理:

private String extractJsonObject(String text) {
    String trimmed = text.trim();
    if (trimmed.startsWith("```")) {   // 剥掉 Markdown 围栏
        ...
    }
    int start = trimmed.indexOf('{');
    int end = trimmed.lastIndexOf('}');
    if (start < 0 || end <= start) {
        throw new BizException(502, "AI预审返回不是JSON对象");
    }
    return trimmed.substring(start, end + 1);
}

第二道:字段级重建。 拿到 JSON 后不是原样存库,而是逐字段取出、补默认值、重新构造:

String decisionValue = normalizeDecision(node.path("decision").asText());
decision.put("decision", decisionValue);
decision.put("conclusion", textOrDefault(node, "conclusion", defaultConclusion(decisionValue)));
decision.put("reason", textOrDefault(node, "reason", "模型已完成预审"));
decision.put("suggestion", textOrDefault(node, "suggestion", "建议直属上级结合业务安排复核后审批"));
decision.put("riskLevel", textOrDefault(node, "riskLevel", "中"));

模型漏了 conclusion?按决策值生成兜底文案。漏了 suggestion?用通用的审批建议补上。下游永远收到结构完整、语义齐全的决策——缺字段的概率被摊销到了一个构造函数里,而不是散落在每个展示它的页面上。

第三道:决策白名单归一。

private String normalizeDecision(String value) {
    String normalized = value == null ? "" : value.trim().toUpperCase();
    if ("APPROVED".equals(normalized) || "REJECTED".equals(normalized) || "TO_HUMAN".equals(normalized)) {
        return normalized;
    }
    return "TO_HUMAN";   // 任何不认识的值 → 转人工
}

模型输出 “Approve”、"approved "、“PASS”、甚至一段感慨——统统归一成 TO_HUMAN。决策的失效方向被钉死在"转人工"上:模型表达不清时,系统的行为是多花一个人工,而不是错误地通过或驳回一个申请。这个细节和插件体系里的"默认拒绝"是同一条原则:安全机制的失败,必须落到代价最小的那个分支。

四、超时收口:等不到的预审按驳回处理

预审卡住(模型挂了、任务异常、超时收口扫描命中)怎么办?看这段回调:

public void onPreReviewTimeout(Long requestId, Long agentTaskId, String decisionJson) {
    terminateAgentPreReview(requestId, agentTaskId, decisionJson);
}

private void terminateAgentPreReview(Long requestId, Long agentTaskId, String decisionJson) {
    WorkflowRequestRow workflowRequest = workflowRepository.getRequestByIdForUpdateForThrow(requestId);
    ensurePendingAgentPreReview(workflowRequest, agentTaskId);
    releaseFrozenLeaveIfNeeded(workflowRequest, STATUS_REJECTED, agentTaskId);
    workflowRepository.finishRequest(requestId, STATUS_REJECTED, agentTaskId);
    workflowRepository.closeAllPendingTodos(requestId);
}

超时的预审按驳回处理,且完整走驳回的全部账务:释放冻结的假期余额、主表置 REJECTED、关闭所有待办。

为什么不是"超时转人工"?推演一下:请假单冻结着余额等预审,预审卡住三天,审批人收到了一个"等待中"的单子——期间员工的新申请全被冻结额度挡住。超时转人工把系统故障的代价转嫁给了申请人的资金占用,超时驳回则把状态立即收口、资金立即释放,申请人重新提交的成本是一次点击。两害相权,驳回的代价更小、更确定。

FOR UPDATE 读取和状态校验(ensurePendingAgentPreReview)依然是老搭档:超时回调可能和预审完成回调并发到达,行锁加状态检查保证只有一方生效。

五、决策要能被看见

AI 的预审结果最终以卡片形式出现在审批单详情里:结论、依据、风险等级、检查项列表,连同"知识库模型预审"的模式标识。审批人看到的不只是"AI 说可以",是AI 为什么说可以——每一项 checks(如"请假事由是否完整:通过")都对应提示词里要求的结构化检查。

这个可视化的意义超出体验:它是信任的校准器。审批人用得多了,自然知道哪类申请的 AI 建议可信、哪类要警惕;运营方也能基于这些卡片评估"要不要把 REJECTED 的门槛再放宽一点"。黑盒的 AI 进不了严肃流程,可解释的那部分必须留下。

六、小结

三点体会:

  1. 先给模型一个逃跑出口,再谈准确性。 TO_HUMAN 的存在让"不确定"有了合法表达,预审的错误率下降靠的是这个出口,不是更长的提示词。
  2. 模型输出当作不可信输入处理。 格式要容错、字段要补全、决策要归一,三个环节共同保证:无论模型说什么,流入下游的都是一份结构完整、语义保守的决策。
  3. 超时要有确定性的善后。 预审超时按驳回处理并立即释放冻结——宁可让申请人多点一次提交,不让资金在故障里无期限冻结。

预审上线后,初审通过的人工处理量下降了不少——但更重要的是没有出现过一次"AI 错误驳回"的投诉。三道设计保住的不只是准确率,是审批人对这个系统的基本信任。


让我们忠于理想,让我们面对显示

SailTrack

entp 辩论家

站长

不具版权性
不具时效性

文章内容不具时效性。若文章内容有错误之处,请您批评指正。

目录

欢迎来到SailTrack的站点,为您导航全站动态

65 文章数
11 分类数
5 评论数
60标签数