Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 3 — Đối soát giao dịch (phần 1): khớp tất định và khớp mờ

Đã đọc 0/8 mục · còn khoảng 3 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Đối soát (reconciliation) là trái tim của agent tài chính: khớp mỗi dòng sao kê ngân hàng với một dòng trong sổ nội bộ. Bài này xây tầng khớp tất định trước, rồi bổ sung khớp mờ.

Chiến lược phân tầng (matching cascade)

Đừng để LLM khớp tất cả. Hãy dùng nhiều tầng, từ chắc chắn nhất đến mờ nhất:

Tầng 1: Khớp chính xác  -> cùng số tiền + cùng ngày + cùng mã tham chiếu
Tầng 2: Khớp gần đúng   -> cùng số tiền + ngày lệch <= 3 + tên đối tác giống
Tầng 3: Khớp mờ (LLM)   -> số tiền khớp nhưng mô tả khó hiểu, cần suy luận
Còn lại: Ngoại lệ       -> đẩy cho con người xem

Mỗi tầng chỉ xử lý phần chưa khớp từ tầng trước. Càng lên tầng cao càng ít dòng, nên LLM (đắt) chỉ chạy trên phần nhỏ.

Tầng 1 & 2: code tất định

def reconcile_deterministic(bank, ledger, date_window=3):
    matches, used = [], set()
    for b in bank:
        for l in ledger:
            if l["id"] in used:
                continue
            same_amt = b["amount"] == l["amount"]
            days = abs(date_diff(b["date"], l["date"]))
            if same_amt and days == 0:
                matches.append((b, l, 1.0)); used.add(l["id"]); break
            if same_amt and days <= date_window \
               and fuzzy_name(b["counterparty"], l["counterparty"]) >= 0.85:
                matches.append((b, l, 0.9)); used.add(l["id"]); break
    unmatched_bank = [b for b in bank if b not in [m[0] for m in matches]]
    return matches, unmatched_bank

Lưu ý dùng used để một dòng sổ không bị khớp hai lần.

Khớp mờ tên đối tác

Tên đối tác thường viết tắt, thiếu dấu, gõ hoa. fuzzy_name nên chuẩn hóa trước khi so:

def normalize(name):
    name = name.lower().strip()
    name = remove_accents(name)                 # bỏ dấu tiếng Việt
    for w in ["cong ty","ct","tnhh","cp","co.","ltd"]:
        name = name.replace(w, "")
    return " ".join(name.split())

Ví dụ: "CT TNHH VP MIEN NAM" và "Văn phòng phẩm Miền Nam" sau chuẩn hóa còn "vp mien nam" vs "van phong pham mien nam" — độ tương đồng đủ cao để tầng 2 khớp.

Ví dụ chạy thực tế

Với 500 dòng sao kê: Tầng 1 khớp 410 dòng (82%), Tầng 2 khớp thêm 60 (12%), còn 30 dòng chuyển sang Bài 4 để LLM và con người xử lý. Tỷ lệ tự động đạt 94% mà không dùng token LLM nào — tiết kiệm chi phí lớn.

Khung tư duy: một-một, một-nhiều

  • 1–1: một chuyển khoản = một hóa đơn (dễ nhất).
  • 1–nhiều: một lần chi gộp trả nhiều hóa đơn (cần khớp tổng).
  • nhiều–1: khách trả góp nhiều lần cho một hợp đồng.
Tầng tất định xử tốt 1–1. Các quan hệ gộp/tách để dành cho LLM ở bài sau.

Các bước

  • Chuẩn hóa cả hai danh sách về schema chung.
  • Chạy Tầng 1 (khớp tuyệt đối), đánh dấu đã dùng.
  • Chạy Tầng 2 (ngày lệch + fuzzy tên) trên phần còn lại.
  • Ghi độ tin cậy cho mỗi cặp khớp.
  • Xuất danh sách chưa khớp cho tầng LLM.
  • Viết unit test với ca một-nhiều để chắc không khớp sai.

Sai lầm thường gặp

  • Khớp một dòng sổ cho nhiều dòng bank: gây khớp trùng, lệch sổ. Luôn đánh dấu used.
  • So tên thô không chuẩn hóa: hoa/thường, có dấu/không dấu làm trượt khớp. Chuẩn hóa trước.
  • Đặt cửa sổ ngày quá rộng: khớp nhầm hai giao dịch cùng số tiền khác bản chất. Giữ 2–3 ngày.
  • Bỏ qua độ tin cậy: không phân biệt được khớp chắc và khớp đoán. Luôn gắn score.
Bài 4 sẽ dùng LLM để xử lý phần khó và thiết kế quy trình duyệt ngoại lệ.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi