发票报销看似只是“识别图片并填写表单”,真正落地时却同时涉及 OCR、真伪校验、抬头核对、重复报销检测、审批规则和人工复核。某中型企业财务团队面对大量月度报销单,手工录入占用大量时间,抽检又难以稳定发现连号票、重复票等风险。
ReActAgent 适合承担这里的流程编排工作:模型负责理解票面信息和决定下一步动作,确定性的业务工具负责查询、校验和落库。这样既能利用大模型处理非结构化输入,也不会把财务结论完全交给模型猜测。
Agent 不是 OCR 的替代品
一条可靠的智能报销链路可以拆成五步:
- OCR 服务从图片或 PDF 中提取发票号码、代码、金额、税额、日期、购买方名称等字段。
- Agent 检查字段是否齐全,并对模糊字段提出补充识别或人工确认要求。
- 真伪校验工具调用企业已有的税务核验服务或合规数据源。
- 重复检测工具使用发票代码、号码、金额、开票日期等字段查询历史记录。
- 规则引擎决定自动通过、进入普通审批,还是转入财务复核队列。
这里需要明确责任边界:OCR 输出的是识别结果,模型输出的是推理和动作建议,最终合规结论必须来自可追溯的数据查询与业务规则。对于无法接入权威核验源的场景,系统只能标记风险,不能宣称已经验证发票真伪。
Agent 可以使用类似下面的任务提示。字段名称和工具名称可按实际项目调整:
你是企业报销审核助手。你的任务是处理一张已完成 OCR 的发票。
必须遵守:
1. 不得自行补造发票号码、金额、日期或购买方名称。
2. 必须调用 checkDuplicate 检查历史报销记录。
3. 只有 verifyInvoice 返回 VERIFIED,才能标记为“已核验”。
4. 购买方名称与公司主体不一致时,转人工复核。
5. 任一关键字段缺失、工具超时或结果冲突时,返回 MANUAL_REVIEW。
6. 输出必须包含 decision、reason、evidence 和 toolTraceId。
可用工具:
- verifyInvoice:核验发票状态
- checkDuplicate:检查重复报销
- matchCompanyTitle:核对购买方抬头
- saveDraft:保存报销单草稿
这类约束不能只写在提示词里。服务端仍要检查工具调用结果和最终状态,避免模型绕过关键步骤。
工具接口要保持窄、稳、可审计
ReAct 模式允许 Agent 在“观察结果、思考、调用工具”之间循环,但财务工具不应暴露宽泛的数据库访问能力。每个工具最好只完成一个动作,并返回结构化结果。
下面是一个可以改造进 Solon 项目的 Java 示例。由于摘要没有给出具体的 Solon ReActAgent 版本及 API,示例将 Agent SDK 的绑定部分抽象为接口,重点展示工具边界和确定性审核逻辑;接入时应替换为项目所用版本的实际注解或注册方法。
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Map;
import java.util.Set;
public class InvoiceReviewDemo {
record Invoice(
String code,
String number,
LocalDate issueDate,
BigDecimal total,
String buyerName
) {}
record ToolResult(String status, String evidenceId, String message) {}
record ReviewResult(String decision, String reason, Map<String, String> evidence) {}
interface InvoiceTools {
ToolResult verifyInvoice(Invoice invoice);
ToolResult checkDuplicate(Invoice invoice);
ToolResult matchCompanyTitle(String buyerName);
}
static final class DemoTools implements InvoiceTools {
private final Set<String> reimbursedKeys = Set.of("044001900111:87654321");
@Override
public ToolResult verifyInvoice(Invoice invoice) {
// 生产环境应替换为企业获准使用的权威核验服务。
return new ToolResult("VERIFIED", "verify-demo-001", "演示核验通过");
}
@Override
public ToolResult checkDuplicate(Invoice invoice) {
String key = invoice.code() + ":" + invoice.number();
boolean duplicated = reimbursedKeys.contains(key);
return new ToolResult(
duplicated ? "DUPLICATE" : "NOT_FOUND",
"duplicate-demo-001",
duplicated ? "历史报销记录中已存在" : "未发现历史记录"
);
}
@Override
public ToolResult matchCompanyTitle(String buyerName) {
boolean matched = "示例科技有限公司".equals(buyerName);
return new ToolResult(
matched ? "MATCHED" : "MISMATCH",
"title-demo-001",
matched ? "抬头一致" : "购买方名称与公司主体不一致"
);
}
}
static ReviewResult review(Invoice invoice, InvoiceTools tools) {
ToolResult duplicate = tools.checkDuplicate(invoice);
ToolResult title = tools.matchCompanyTitle(invoice.buyerName());
ToolResult verification = tools.verifyInvoice(invoice);
Map<String, String> evidence = Map.of(
"duplicate", duplicate.evidenceId(),
"title", title.evidenceId(),
"verification", verification.evidenceId()
);
if ("DUPLICATE".equals(duplicate.status())) {
return new ReviewResult("REJECT", "疑似重复报销", evidence);
}
if (!"MATCHED".equals(title.status())) {
return new ReviewResult("MANUAL_REVIEW", "发票抬头不一致", evidence);
}
if (!"VERIFIED".equals(verification.status())) {
return new ReviewResult("MANUAL_REVIEW", "发票未完成权威核验", evidence);
}
return new ReviewResult("APPROVE", "规则检查通过", evidence);
}
public static void main(String[] args) {
Invoice invoice = new Invoice(
"044001900111",
"12345678",
LocalDate.of(2025, 3, 10),
new BigDecimal("368.00"),
"示例科技有限公司"
);
System.out.println(review(invoice, new DemoTools()));
}
}
将代码保存为 InvoiceReviewDemo.java 后,可以直接验证这段确定性规则:
javac InvoiceReviewDemo.java
java InvoiceReviewDemo
在实际 Solon 服务中,可以这样实践:把 verifyInvoice、checkDuplicate 和 matchCompanyTitle 注册成 ReActAgent 可调用的工具;让 Agent 负责选择调用顺序、解释异常和整理审核摘要;由 Java 服务根据工具返回值生成最终状态。尤其不要允许 Agent 直接写入“已支付”或“审核通过”等终态。
重复检测不能只比较文件哈希
同一张发票可能被重新拍照、压缩、裁剪或转换成 PDF,文件哈希会随之变化。因此,重复检测至少应组合以下字段:
- 发票代码与发票号码;
- 开票日期;
- 含税金额;
- 销售方税号;
- 购买方名称;
- 权威核验服务返回的唯一标识(如果可用)。
数据库层还应增加幂等约束。下面是一种可改造的表结构,具体字段需要适配企业使用的发票类型:
CREATE TABLE reimbursement_invoice (
id BIGINT PRIMARY KEY,
invoice_code VARCHAR(32) NOT NULL,
invoice_number VARCHAR(32) NOT NULL,
issue_date DATE NOT NULL,
total_amount DECIMAL(18, 2) NOT NULL,
seller_tax_id VARCHAR(32),
verification_id VARCHAR(128),
review_status VARCHAR(32) NOT NULL,
tool_trace_id VARCHAR(128) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (invoice_code, invoice_number)
);
唯一索引是最后一道防线,但不能代替业务检测。不同发票类型可能没有相同的代码规则,红字发票、冲销、拆分报销也需要单独建模,不能简单地把所有重复键都当成欺诈。
让每一次自动决策都能被复盘
财务自动化最怕“系统说通过了,但没人知道为什么”。建议为每次任务保存以下信息:
- 原始文件的对象存储地址和内容摘要;
- OCR 原始响应与标准化字段;
- Agent 使用的模型、提示词版本和执行时间;
- 每次工具调用的输入、输出、耗时和追踪编号;
- 最终规则命中项、审批状态和人工修改记录。
日志中应对银行卡号、身份证号、税号等字段进行脱敏,并设置明确的访问权限和保留期限。模型供应商、OCR 服务和对象存储也必须纳入企业的数据合规评估。
Agent 还需要明确的失败策略。OCR 置信度过低、外部核验超时、模型输出无法解析、工具结果互相矛盾时,都应进入 MANUAL_REVIEW,而不是为了提高自动通过率而猜一个结论。
上线时从“自动填单”开始
较稳妥的采用顺序是:先自动提取字段并生成草稿,再上线重复检测和抬头检查,随后接入权威真伪核验,最后才对低风险票据开放自动审批。每个阶段都应抽样比较系统结论与财务人员结论,并统计字段准确率、人工复核率、误拒率、漏检率和平均处理时长。
上线前可以按这份清单检查:
- 关键结论是否全部来自确定性工具或规则;
- Agent 是否无法绕过重复检测和真伪核验;
- 工具超时是否自动转人工;
- 数据库是否具有幂等约束;
- 每个决定是否关联证据与追踪编号;
- 敏感字段是否脱敏并限制访问;
- 是否保留人工纠正入口和完整审计记录。
Solon ReActAgent 在这个方案中的价值,不是替财务人员“凭感觉审批”,而是把 OCR、业务查询、规则判断和异常解释串成一条可观察的工作流。只有把模型限制在合适的职责范围内,智能报销才能同时缩短处理周期,并守住合规与资金安全的底线。