Thực hành: dựng agent đối chiếu ba chiều không có quyền chi tiền
Phần khó của đối chiếu hoá đơn không nằm ở phép so sánh mà ở những ngoại lệ, và một agent tốt phải biết khi nào thì dừng tay.
Tóm tắt nhanh
- Viết phần so khớp bằng quy tắc trước; agent chỉ nhận những dòng mà quy tắc không xử lý được
- Hoá đơn đến trước phiếu nhập kho nên được giữ ở trạng thái chặn thanh toán, có thể gỡ, chứ đừng từ chối hẳn
- Tool của agent chỉ được ghi trạng thái; quyết định chi tiền luôn thuộc về con người
Nhà cung cấp giao 46 trên 50 thùng nhưng xuất hoá đơn cho đủ 50. Bốn thùng còn lại có thể đang trên chuyến xe sau, cũng có thể sẽ không bao giờ tới. Đó là ví dụ mà Lleverage, một công ty làm tự động hoá đối chiếu, dùng để mô tả kiểu ngoại lệ làm vỡ các hệ thống so khớp hoá đơn.
Phép so sánh ba con số thì ai cũng viết được trong mười phút. Phần việc thật nằm ở chỗ khác.
Theo số liệu APQC 2025 mà vendor Peakflo trích lại, các tổ chức dùng đối chiếu bằng quy tắc chỉ đạt 52-58% hoá đơn được xử lý thẳng, không cần người chạm tay.
Phần còn lại, gần một nửa số hoá đơn, là phần việc FDE được trả tiền để giải quyết.
Bài này hướng dẫn bạn dựng một agent đối chiếu ba chiều theo đúng thứ tự nên làm ở site khách: quy tắc trước, ngoại lệ sau, agent cuối cùng, và một ranh giới cứng là agent không được chi tiền. Code bên dưới là Python thuần, đã giản lược để minh hoạ logic. Nó không gắn với API của ERP nào.
Bạn sẽ dựng gì, và cần chuẩn bị gì?
Đầu ra là một pipeline nhỏ. Nó nhận ba loại chứng từ (đơn mua hàng, phiếu nhập kho, hoá đơn), so từng dòng, gắn mỗi dòng một trạng thái, và chỉ chuyển cho agent những dòng mà quy tắc không giải quyết được. Agent đề xuất cách gán dòng và giải thích lý do. Con người duyệt.
Bạn cần Python 3.10 trở lên, một bộ dữ liệu mẫu gồm vài đơn mua, phiếu nhận và hoá đơn (tự tạo cũng được), cùng quyền gọi một LLM có hỗ trợ tool use. Quan trọng hơn cả, bạn cần biết khách đang cấu hình dung sai bao nhiêu trong ERP của họ.
Thứ tự này không phải tuỳ hứng. Anthropic, trong hướng dẫn xây agent của mình, khuyên tìm giải pháp đơn giản nhất có thể và chỉ tăng độ phức tạp khi thật sự cần. Với đối chiếu hoá đơn, giải pháp đơn giản nhất là một hàm so sánh.
Bước 1: chuẩn hoá về từng dòng
Mọi thứ diễn ra ở cấp dòng chứ không phải cấp chứng từ. Hãy đưa cả ba loại tài liệu về cùng một cấu trúc:
from dataclasses import dataclass
@dataclass
class Line:
doc_id: str # số PO, số phiếu nhận hoặc số hoá đơn
po_line: str | None # dòng PO mà dòng này trỏ tới (có thể thiếu)
sku: str
qty: float
unit_price: float
Kiểm tra: in ra mọi dòng hoá đơn có po_line rỗng. Đó là ứng viên đầu tiên cho bước 4, vì nhà cung cấp không ghi tham chiếu, hoặc đã gộp nhiều dòng thành một.
Bước 2: so khớp bằng quy tắc, có dung sai
Các ERP lớn làm đúng việc này. Tài liệu của Sage X3 mô tả hai phép so: giá trên hoá đơn so với giá trên đơn mua, số lượng nhận so với số lượng đặt, mỗi phép có ngưỡng dung sai cấu hình được.
Vượt ngưỡng thì hệ thống cảnh báo hoặc chặn. Bạn viết lại logic đó, thêm một cờ cho trường hợp hoá đơn tính ít hơn số đã đặt:
def check_line(po: Line, gr_qty: float, inv: Line,
price_tol=0.02, qty_tol=0.0) -> list[str]:
issues = []
if gr_qty == 0:
issues.append("MISSING_GR")
elif inv.qty > gr_qty * (1 + qty_tol):
issues.append("QTY_OVER_RECEIPT")
elif inv.qty < po.qty:
issues.append("PARTIAL") # tính đúng số đã nhận, ít hơn số đặt
if abs(inv.unit_price - po.unit_price) > po.unit_price * price_tol:
issues.append("PRICE_VARIANCE")
return issues
Thử với một ví dụ giả định. Đơn mua đặt 50 đơn vị, đơn giá 10.00, dung sai giá 2%. Hoá đơn ghi đơn giá 10.15, lệch 1,5%: qua. Nhưng hoá đơn tính 50 trong khi phiếu nhận chỉ có 46: dòng này nhận cờ QTY_OVER_RECEIPT, đúng ca mà Lleverage mô tả.
Kiểm tra: chạy hàm trên toàn bộ dữ liệu mẫu và đếm tỷ lệ dòng không có cờ nào. Con số đó là baseline của bạn. Mọi cải tiến về sau phải được đo so với nó.
Bước 3: ngoại lệ nào thì xử lý ra sao?
Một danh sách cờ chưa phải quyết định. Bạn cần biến cờ thành trạng thái, và mỗi loại ngoại lệ có cách xử lý riêng.
Thiếu phiếu nhập kho là ca phổ biến: theo Lleverage, kho chưa ghi nhận, hoặc ghi nhầm vào dòng đơn khác. SAP S/4HANA Cloud có hẳn khoá dung sai DW, kích hoạt khi hoá đơn được ghi nhận lúc số lượng nhập kho bằng 0.
SAP khuyến nghị cấu hình DW ở mức chặn thanh toán thay vì báo lỗi, để hoá đơn đến trước phiếu nhập kho vẫn gỡ ra được. Bạn nên giữ đúng tinh thần ấy: trạng thái HOLD, rồi tự kiểm tra lại khi có phiếu nhận mới.
Giao một phần cần phân biệt kỹ. Peakflo cho rằng agent nên tách giao một phần hợp lệ khỏi sai lệch thật, và chỉ sai lệch thật mới chuyển cho người duyệt.
Một cách vạch ranh giới hợp lý: hoá đơn chỉ tính đúng số đã nhận và PO còn mở thì là hợp lệ, phần còn lại sẽ đến sau. Nếu PO đã đóng, phần thiếu sẽ không bao giờ được giao, nên cần người xử lý. Còn hoá đơn tính vượt số đã nhận, như ca 46 trên 50, thì luôn là sai lệch.
Giá đổi là ca mà dung sai không cứu được. Lleverage viết thẳng rằng dung sai giúp xử lý chuyện làm tròn, không giúp được chuyện này, vì lỗi nằm ở chính đơn mua đã lỗi thời. Ca này phải có người: ai đó cần sửa PO hoặc từ chối giá mới.
def decide(issues: list[str], po_open: bool) -> str:
if not issues:
return "MATCHED"
if "PRICE_VARIANCE" in issues:
return "ESCALATE" # PO có thể đã lỗi thời, kể cả khi thiếu GR
if "QTY_OVER_RECEIPT" in issues:
return "ESCALATE" # tính tiền vượt số đã nhận
if issues == ["MISSING_GR"]:
return "HOLD" # chặn thanh toán, gỡ được khi có phiếu nhận
if issues == ["PARTIAL"]:
return "MATCHED" if po_open else "ESCALATE"
return "ESCALATE" # mọi tổ hợp lạ: đưa cho người
Thứ tự các nhánh là có chủ ý. Một dòng vừa thiếu phiếu nhận vừa lệch giá sẽ thành ESCALATE chứ không phải HOLD: chờ kho ghi nhận không sửa được một đơn mua sai giá, nên giữ hoá đơn lại chỉ trì hoãn việc người mua hàng phải làm.
Kiểm tra: không trạng thái nào trong hàm này có nghĩa là “trả tiền”. MATCHED chỉ có nghĩa là ba chứng từ khớp nhau.
Bước 4: agent chỉ vào việc ở chỗ quy tắc bó tay
Chỗ quy tắc thật sự bó tay là cấu trúc dòng. Theo Lleverage, quy tắc cấp dòng mặc định một dòng hoá đơn ứng với một dòng đơn mua, nên mọi thứ khác đều rơi ra ngoài.
Hoá đơn gộp ba dòng PO thành một, hoặc tách một dòng thành hai đợt giao, cần ai đó đọc mô tả hàng, SKU, số lượng rồi gán mỗi dòng về đúng PO và đúng phiếu nhận. Đó là việc của agent.
Anthropic khuyên “poka-yoke” tool, tức là chỉnh tham số sao cho khó dùng sai. Áp vào đây, agent chỉ có hai tool, và không tool nào chạm tới thanh toán:
TOOLS = [
{"name": "propose_line_mapping",
"params": {"invoice_line": "str", "po_line": "str",
"gr_id": "str", "reason": "str"}},
{"name": "record_match_result",
"params": {"invoice_line": "str",
"status": ["MATCHED", "HOLD", "ESCALATE"],
"reason": "str"}},
]
Đây là mô tả giản lược, bạn sẽ phải dịch nó sang đúng định dạng tool của LLM mình dùng. Chi tiết đáng chú ý là status là một enum đóng. Agent không thể bịa ra trạng thái “APPROVED_FOR_PAYMENT” vì nó không tồn tại.
Sau khi agent đề xuất cách gán, hãy chạy lại check_line và decide trên kết quả đó. Agent lo phần đọc hiểu, còn phép so vẫn để code quyết định. Đề xuất nào không qua được quy tắc thì tự động thành ESCALATE.
Bước 5: kiểm thử bằng chính các ngoại lệ
Lleverage đặt ranh giới rất rõ: giải phóng một khoản thanh toán không bao giờ là quyền của agent. Hãy biến câu đó thành test. Viết một bộ case nhỏ gồm bốn ngoại lệ ở trên, ca vừa thiếu phiếu nhận vừa lệch giá, cộng một dòng khớp hoàn hảo, rồi khẳng định từng trạng thái đầu ra.
Thêm một test nữa: quét toàn bộ tool mà agent có quyền gọi và khẳng định không tool nào ghi vào bảng thanh toán. Test này nghe hiển nhiên, nhưng nó là thứ bạn đưa ra khi kiểm toán viên của khách hỏi “làm sao chứng minh máy không tự chi tiền?”.
Những lỗi hay gặp
Lỗi đầu tiên là đưa agent vào từ đầu, cho nó xử lý cả những dòng mà một phép trừ đã giải quyết được. Bạn tốn chi phí, mất khả năng giải thích, và không có baseline để đo.
Lỗi thứ hai là đẩy dung sai lên cao để tăng tỷ lệ khớp. Con số đẹp hơn, nhưng các ca giá đổi sẽ lọt qua thay vì đến tay người sửa PO.
Lỗi thứ ba là từ chối hẳn hoá đơn thiếu phiếu nhập kho. Nhà cung cấp phải gửi lại, phòng tài chính phải nhập lại, trong khi kho chỉ đơn giản là ghi nhận chậm một ngày. Chính SAP cũng khuyên dùng trạng thái chặn có thể gỡ.
Kỹ năng này xuất hiện thế nào ở site khách?
Ở site khách, bạn hiếm khi được viết check_line từ số không. Dung sai đã nằm sẵn trong ERP, và việc đầu tiên là đọc cấu hình đó, hỏi người kế toán vì sao họ chọn những con số ấy. Agent của bạn phải tôn trọng cùng một ngưỡng, nếu không hai hệ thống sẽ cãi nhau trên cùng một hoá đơn.
Việc thứ hai là lấy một tuần hoá đơn thật, chạy quy tắc, rồi ngồi với nhân viên kế toán phải trả để phân loại từng ngoại lệ bằng tay. Bảng phân loại đó cho biết agent đáng làm ở đâu, và trở thành bộ eval đầu tiên của bạn.
Nếu bạn đang muốn chuyển sang vai trò FDE, hãy để ý những JD nhắc tới “accounts payable”, “ERP integration” hay “human-in-the-loop”. Trong CV, đừng viết “xây agent AI cho tài chính”.
Hãy viết rằng bạn đã tách ngoại lệ thành từng loại, đo tỷ lệ xử lý thẳng trước và sau, và thiết kế tool để agent không có cách nào chạm vào thanh toán. Người tuyển FDE muốn thấy bạn biết chỗ nào máy được tự quyết, chỗ nào không.
Còn bốn thùng hàng chưa đến kia thì không cần agent thông minh hơn. Thứ cần là một hệ thống chịu giữ hoá đơn lại, nói rõ lý do, và chờ một người có thẩm quyền quyết định.
Bài này có hữu ích không?
Cảm ơn bạn đã góp ý!
6 nguồn
- Three-way match — Procurement Glossary | LightSource
- Matching tolerance (Sage X3 documentation) · 2026-10-14
- Supplier invoice blocked or in error due to missing goods receipt in 3-way match in SAP S/4 HANA Cloud Public Edition
- Three-Way Matching Automation: The Exceptions That Break It · 2026-04-27
- Autonomous PO Matching: Can AI Agents Match Invoices Without Humans? · 2026-04-14
- Building effective agents · 2024-12-19