Appearance
个人报销智能审批:Java 本地计算引擎与大模型 Agentic 混合架构指南
归类:Java / 智能审计 状态:✅ 已验证
一、 架构背景与演进动机
在原有架构 (V1) 中,大模型需要同时处理非结构化的语义分析(如“判断是否为市内交通”、“是否存在敏感词”)与高精度的确定性计算(如“核对行程金额与发票总额是否一致”、“判断行程时间是否属于非工作日或超窗时间段”、“判断行程日期是否超过3个月时效限制”)。
由于大模型在处理数字运算和日期推算时存在天然的不确定性(LLM Hallucination),V1 架构面临以下痛点:
- 逻辑幻觉频繁:大模型难以精确计算非工作日重叠,特别是在面对逗号分隔的多日期多行程单时,经常算错月份或金额。
- 性能低下(响应迟钝):为防算错,必须迫使大模型开启“思考模式(enable_thinking)”,导致 API 延迟攀升了 3至5倍,极大影响了前端审批流转时效。
升级后的 Agentic 架构 (V2) 将数学计算/时间比对等确定性规则下沉到 Java 本地计算引擎(LocalAuditEngine) 预先执行,并将产出的结构化事实(Fact)注入大模型上下文,大模型仅需要做单纯的语义判断与润色输出,不仅保证了 100% 正确性,还能关闭思考模式使响应速度提升 90% 左右。
二、 服务端核心改动及结构设计
本次升级在 erp_wf_expenseExternal 服务端工程中新增了以下三个核心文件,无任何对现有生产业务代码的破坏性修改。
text
erp_wf_expenseExternal/
├── src/main/java/com/bj58/expenseExternal/vo/chatling/LocalAuditResult.java # 预计算结果传输 VO
├── src/main/java/com/bj58/expenseExternal/utils/chatling/LocalAuditEngine.java # 本地核心计算引擎
└── src/test/java/com/bj58/expenseExternal/utils/LocalAuditEngineTest.java # 规则边界 JUnit 单元测试1. 数据传输模型:LocalAuditResult.java
承载 Java 本地计算产出的预核对结论:
java
package com.bj58.expenseExternal.vo.chatling;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
@Data
public class LocalAuditResult implements Serializable {
private static final long serialVersionUID = 1L;
/** 是否命中工作日超窗违规(工作日非09:00~18:00时间段出行) */
private boolean timeWindowViolation;
/** 违规的具体时间列表,方便大模型在输出意见时引用叙述 */
private List<String> violatingTimes;
/** 是否命中休息日违规(出行日期在restDays中) */
private boolean restDayViolation;
/** 违规的具体休息日出行日期列表 */
private List<String> violatingDates;
/** 是否超期违规(行程出行时间距离报销申请月超过3个月) */
private boolean dateExpiredViolation;
/** 违规的超期行程日期列表 */
private List<String> expiredDates;
/** 发票总金额与行程单总金额是否一致(求和后进行完全等值匹配) */
private boolean amountMatched;
}2. 工具引擎类:LocalAuditEngine.java
这是计算引擎的主体,使用了纯函数的设计思想。输入包含行程单、发票、休息日和提单时间,输出封装好的核对结论。
- Rule 1 (金额撮合):对
invoices与itineraries的总额进行BigDecimal累加比较。 - Rule 2 (非工作日/工作日超窗):优先校验休息日,若是休息日则计入违规日期且不再进行超窗校验(防重复报告);否则校验出行小时/分钟是否超出
[09:00, 18:00]窗口。 - Rule 4 (超期校验):计算行程月度距提单申请月度偏离度,偏移量超
[0, 3]视为超期。
三、 面向全栈/新同学的 Java 基础与工程规范解读
为了帮助前端或转全栈的同学快速上手后端工程代码,这里对其常见的 Java 头部定义及工程习惯进行拆解:
1. 包声明与导入(package & import)
java
package com.bj58.expenseExternal.utils.chatling;package:相当于 Node.js 中的目录结构。它是 Java 的命名空间定义,指明当前文件在项目的逻辑层级。Java 的包名通常与文件物理路径完全一致。
java
import com.bj58.expenseExternal.vo.ItineraryInfoVo;
import com.bj58.expenseExternal.vo.chatling.LocalAuditResult;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.*;import:类似于 JS 的import或require。Java 用它导入当前文件需要引用的其他类或第三方库。CollectionUtils:来自 Spring 框架,用于防空指针的集合操作。BigDecimal:Java 用于处理金融、金钱等绝对不能丢失精度的浮点数类型。
2. 什么是 VO?
在 import com.bj58.expenseExternal.vo 包中,VO(Value Object - 值对象) 指的是无业务逻辑、纯粹用于结构化承载和在层级间传递数据的实体对象。 通常只包含私有变量(如 private String boardingDate)和自动生成的读取/写入方法(Getter/Setter,通过类头部的 @Data 自动提供)。
3. 函数还是静态方法?
Java 不存在孤立的全局函数。所有的执行逻辑必须嵌套在类中。 为实现类似 JS 工具函数的效果,Java 设计了静态方法(public static):
java
// 本地引擎的主入口方法,使用 public static 修饰
public static LocalAuditResult performCalculations(
List<ItineraryInfoVo> itineraries,
List<TicketInfoVo> invoices,
List<String> restDays,
String applyDate) {
...
}调用时直接用 LocalAuditEngine.performCalculations(...),无需先 new LocalAuditEngine(),属于典型的线程安全纯函数设计。
四、 本地与大模型调用关系拓扑
1. 业务全链路调用拓扑图
2. 为什么该混合架构可成倍降低大模型延迟?
- 计算下沉:LLM 擅长阅读理解和语言表达,但不擅长复杂的逻辑分支判断和数据计算。在计算下沉到 Java 后,Prompt(提示词资产)的大小可减小约 80%。
- 参数裁剪:大模型无需开启
enable_thinking进行冗长推理,可以直接根据localAuditResult中的事实字段生成直接结论。ChatLing 接口端到端耗时可从 ~15s 降低至 ~1.5s,极大地保护了核心 API 吞吐量。