用 Solon ReActAgent 构建可审计的发票识别与智能报销流程

2026-07-15 36 预计阅读时间: 1 分钟
来源: oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:12 分钟

发票报销看似只是“识别图片并填写表单”,真正落地时却同时涉及 OCR、真伪校验、抬头核对、重复报销检测、审批规则和人工复核。某中型企业财务团队面对大量月度报销单,手工录入占用大量时间,抽检又难以稳定发现连号票、重复票等风险。

ReActAgent 适合承担这里的流程编排工作:模型负责理解票面信息和决定下一步动作,确定性的业务工具负责查询、校验和落库。这样既能利用大模型处理非结构化输入,也不会把财务结论完全交给模型猜测。

Agent 不是 OCR 的替代品

一条可靠的智能报销链路可以拆成五步:

  1. OCR 服务从图片或 PDF 中提取发票号码、代码、金额、税额、日期、购买方名称等字段。
  2. Agent 检查字段是否齐全,并对模糊字段提出补充识别或人工确认要求。
  3. 真伪校验工具调用企业已有的税务核验服务或合规数据源。
  4. 重复检测工具使用发票代码、号码、金额、开票日期等字段查询历史记录。
  5. 规则引擎决定自动通过、进入普通审批,还是转入财务复核队列。

这里需要明确责任边界: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 服务中,可以这样实践:把 verifyInvoicecheckDuplicatematchCompanyTitle 注册成 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、业务查询、规则判断和异常解释串成一条可观察的工作流。只有把模型限制在合适的职责范围内,智能报销才能同时缩短处理周期,并守住合规与资金安全的底线。


相关推荐