某中型企业财务部每月处理大量报销单,团队疲于奔命:
| 痛点 | 具体表现 | 后果 |
|---|---|---|
| 发票录入耗时 | 每张发票手工录入 | 财务时间被重复劳动占满 |
| 合规审核靠人眼 | 真伪、抬头、连号靠抽检 | 漏检风险 |
| 重复报销难查 | 同一张票被报两次偶发出现 | 资金损失 |
| 报销周期长 | 提交到到账周期长 | 员工体验差 |
一、业务方案
1.1 目标
- 拍照后自动识别并填单
- 常见合规规则自动校验
- 重复报销可拦截
- 异常单附上下文转人工
1.2 流程改造
text
改造前:
员工贴发票 → 填写报销单 → 主管审批 → 财务初审 → 财务复审 → 出纳打款
改造后:
员工拍照上传 → Agent 识别+填单+合规检查 → 主管审批 → 通过后打款
↓
异常 → 人工介入(附检查摘要)
二、技术落地(ReActAgent 实现)
说明 :这是典型业务闭环,正确入口是
ReActAgent + 领域工具 + HITL。不要使用虚构的
implements Tool、Harness.call()、AiInterceptor、memory.search()。
2.1 模块结构
text
com.example.finance/
├── tools/
│ ├── OcrTools.java # 发票 OCR
│ ├── ComplianceTools.java # 合规与重复报销检查
│ └── PaymentTools.java # 打款执行
├── hitl/
│ └── FinanceHitlController.java # 超标/异常审批
└── ReimbursementAgentApp.java
2.2 工具定义(真实 API)
java
import org.noear.solon.ai.annotation.ToolMapping;
import org.noear.solon.ai.chat.tool.AbsToolProvider;
import org.noear.solon.annotation.Param;
/** 发票 OCR 工具 */
public class OcrTools extends AbsToolProvider {
@ToolMapping(description = "识别发票图片,返回发票代码、号码、日期、金额、购买方、销售方等字段")
public String ocr_invoice(
@Param(description = "发票图片 base64 或文件 ID") String imageRef) {
// 生产环境调用 OCR 服务
return """
{
"code":"044001900111",
"number":"12345678",
"date":"2026-06-18",
"amount":1200.00,
"tax":72.00,
"total":1272.00,
"buyer":"某某科技有限公司",
"seller":"某某酒店",
"category":"住宿"
}
""";
}
}
/** 合规检查工具 */
public class ComplianceTools extends AbsToolProvider {
@ToolMapping(description = "检查发票真伪、抬头、是否重复报销,返回合规结论")
public String check_invoice(
@Param(description = "发票号码") String invoiceNo,
@Param(description = "购买方名称") String buyer,
@Param(description = "价税合计") double total) {
// 生产环境:税务查验接口 + 已报销库查询 + 公司抬头校验
if ("12345678".equals(invoiceNo) && total > 0) {
return """
{
"invoiceNo":"12345678",
"titleOk":true,
"duplicate":false,
"taxVerified":true,
"result":"PASS",
"message":"合规检查通过"
}
""";
}
return """
{
"result":"REJECT",
"message":"发票校验失败,请转人工"
}
""";
}
@ToolMapping(description = "按费用类别检查是否超过公司报销标准")
public String check_policy(
@Param(description = "费用类别,如住宿/交通/餐饮") String category,
@Param(description = "金额") double amount) {
if ("餐饮".equals(category) && amount > 400) {
return "{\"result\":\"OVER_LIMIT\",\"limit\":400,\"amount\":"
+ amount + ",\"message\":\"餐饮超标准,需主管确认\"}";
}
return "{\"result\":\"OK\",\"message\":\"符合标准\"}";
}
}
/** 打款工具(敏感操作) */
public class PaymentTools extends AbsToolProvider {
@ToolMapping(description = "提交报销打款。仅在合规通过且审批完成后调用")
public String submit_payment(
@Param(description = "报销单号") String reportId,
@Param(description = "收款人") String employee,
@Param(description = "打款金额") double amount) {
return "{\"status\":\"submitted\",\"reportId\":\"" + reportId
+ "\",\"employee\":\"" + employee + "\",\"amount\":" + amount + "}";
}
}
2.3 组装报销 Agent
java
import org.noear.solon.ai.agent.AgentSession;
import org.noear.solon.ai.agent.react.ReActAgent;
import org.noear.solon.ai.agent.react.intercept.HITLInterceptor;
import org.noear.solon.ai.agent.session.InMemoryAgentSession;
import org.noear.solon.ai.chat.ChatModel;
ChatModel chatModel = LlmUtil.getChatModel();
HITLInterceptor hitl = new HITLInterceptor()
.onTool("submit_payment", (trace, args) -> {
// 打款一律人工确认;也可按金额阈值判断
return "报销打款需财务确认";
});
ReActAgent agent = ReActAgent.of(chatModel)
.role("""
你是企业智能报销助手。处理步骤:
1. 先 OCR 识别发票
2. 再做合规检查(真伪/抬头/重复报销)
3. 再检查费用标准
4. 全部通过后,生成报销摘要;需要打款时调用 submit_payment
5. 任一检查失败,明确列出原因并建议转人工
""")
.defaultToolAdd(new OcrTools())
.defaultToolAdd(new ComplianceTools())
.defaultToolAdd(new PaymentTools())
.defaultInterceptorAdd(hitl)
.modelOptions(o -> o.temperature(0.0))
.maxTurns(12)
.autoRethink(true)
.build();
AgentSession session = InMemoryAgentSession.of("expense_report_001");
String prompt = """
员工张三提交差旅报销,部门销售部。
已上传 1 张住宿发票图片,文件 ID=img_001。
请完成识别、合规检查,并在可打款时提交支付。
""";
String result = agent.prompt(prompt)
.session(session)
.call()
.getContent();
2.4 重复报销怎么做?
不要 虚构 Memory memory; memory.search(...)。
正确做法:
- 在
check_invoice工具内部查"已报销发票库"(DB/Redis) - 报销成功后,由支付/归档服务把发票号写入已报销库
- 下次再报同一票号时,工具直接返回
duplicate=true
java
// check_invoice 内部伪代码
if (reimbursedInvoiceRepository.exists(invoiceNo)) {
return "{\"result\":\"REJECT\",\"message\":\"该发票已被报销过\"}";
}
2.5 超标场景
当 check_policy 返回 OVER_LIMIT 时:
- Agent 不应直接打款
- 应生成摘要给主管
- 若你希望"超标也允许打款但必须审批",可在 HITL 策略里根据参数返回审批原因
java
HITLInterceptor hitl = new HITLInterceptor()
.onTool("submit_payment", (trace, args) -> {
double amount = Double.parseDouble(args.get("amount").toString());
if (amount > 2000) {
return "单笔报销超过 2000,需财务负责人审批";
}
return "常规报销打款确认";
});
三、业务效果(定性)
- 录入从手工填单变成 OCR + 自动填字段
- 合规检查可覆盖全量票据,而不是抽检
- 重复报销可在工具层硬拦截
- 财务从"逐张核对"转向"异常复核"
真实处理示例
员工上传:差旅住宿发票
Agent 链路:
ocr_invoice(img_001)→ 金额 1,272,类别住宿check_invoice→ PASScheck_policy(住宿, 1272)→ OKsubmit_payment→ HITL 挂起- 财务批准后续传,完成打款提交
若餐饮票 500 元、标准 400 元:
- OCR 成功
- 合规通过
check_policy返回超标准- Agent 输出"需主管确认超标 100 元",不直接打款
四、同类场景扩展
| 业务场景 | 核心工具 | 关键规则 | 业务价值 |
|---|---|---|---|
| 采购发票校验 | 发票识别 + 订单匹配 | 三单匹配 | 对账效率提升 |
| 备用金核销 | 发票识别 + 预算查询 | 超标预警 | 核销周期缩短 |
| 客户开票 | 开票 API + 商品匹配 | 额度控制 | 降低开票错误 |
| 凭证生成 | 发票识别 + 科目映射 | 借贷平衡 | 制证提速 |
| 进项税认证 | 发票识别 + 税务接口 | 可抵扣判断 | 减少人工认证 |
五、落地建议
- 先做自动填单 + 人工确认,准确率稳定后再放开自动提交
- 优先覆盖高频票种(专票/普票)
- 重复报销检查必须做在工具层,不要只靠 prompt
- 打款必须 HITL,资金动作不能全自动裸奔