1. Bối cảnh: engine bồi thường bảo hiểm và vì sao khó kiểm thử
Một engine bồi thường bảo hiểm quyết định mỗi hồ sơ được duyệt, bị từ chối hay cần con người xem lại, và trả bao nhiêu tiền. Đây là logic nghiệp vụ dày đặc điều kiện: phạm vi bảo hiểm (coverage), điều khoản loại trừ (exclusion), hạn mức (limit), mức miễn thường (deductible), và cờ nghi ngờ gian lận (fraud flag). Sự phức tạp không nằm ở giao diện mà ở tổ hợp điều kiện: hàng chục quy tắc giao nhau, và một sai sót nhỏ có thể khiến công ty trả nhầm hàng tỉ hoặc từ chối oan một khách hàng hợp lệ. Bài này trình bày cách dùng bảng quyết định làm oracle và cho AI mở rộng case biên mà không đánh mất nguồn sự thật.
Về quy mô và tuân thủ, engine này xử lý lượng lớn hồ sơ mỗi ngày với yêu cầu công bằng và minh bạch: mỗi quyết định phải giải thích được, phải lưu vết kiểm toán, và phải nhất quán giữa các hồ sơ tương tự để tránh phân biệt đối xử. Cơ quan quản lý bảo hiểm có thể yêu cầu công ty chứng minh vì sao một hồ sơ bị từ chối. Vì thế test không chỉ kiểm 'kết quả đúng' mà còn kiểm 'lý do đúng' và 'nhất quán'. Đây là bối cảnh lý tưởng cho oracle dạng bảng quyết định, nơi mỗi kết quả gắn với một quy tắc rõ ràng, giải thích được.
2. Trạng thái hồ sơ và các quy tắc nghiệp vụ
Trước khi lập bảng quyết định, phải hiểu vòng đời hồ sơ. Một hồ sơ đi qua các trạng thái: SUBMITTED khi khách nộp, TRIAGED khi được phân loại và thu thập dữ kiện, DECISION khi engine áp bảng quyết định, rồi rẽ thành APPROVED, DENIED hoặc REVIEW nếu cần con người. Hồ sơ được duyệt sẽ tính payout rồi chuyển PAID. Mỗi bước chuyển trạng thái đều có điều kiện, và test phải bao phủ cả các chuyển hợp lệ lẫn các chuyển bị cấm, ví dụ không được nhảy thẳng từ SUBMITTED sang PAID mà bỏ qua bước áp bảng quyết định.
// Kiểu dữ liệu hồ sơ bồi thường & các quy tắc điều kiện
type ClaimState = 'SUBMITTED' | 'TRIAGED' | 'DECISION'
| 'APPROVED' | 'DENIED' | 'REVIEW' | 'PAID';
interface Claim {
id: string;
policyActive: boolean; // hợp đồng còn hiệu lực?
claimType: 'collision' | 'flood' | 'theft' | 'fire';
amount: number; // số tiền yêu cầu (cents)
coverageLimit: number; // hạn mức cho loại này
deductible: number; // mức miễn thường
excludedPerils: string[]; // hiểm hoạ bị loại trừ theo hợp đồng
fraudScore: number; // 0..100 — >70 gắn cờ REVIEW
}
// Chuyển trạng thái hợp lệ (mọi chuyển khác đều bị cấm)
const ALLOWED: Record<ClaimState, ClaimState[]> = {
SUBMITTED: ['TRIAGED'], TRIAGED: ['DECISION'],
DECISION: ['APPROVED', 'DENIED', 'REVIEW'],
APPROVED: ['PAID'], DENIED: [], REVIEW: ['APPROVED', 'DENIED'], PAID: [],
};Các quy tắc nghiệp vụ chính gồm năm nhóm. Coverage: loại tổn thất có nằm trong phạm vi bảo hiểm của hợp đồng không. Exclusion: có rơi vào điều khoản loại trừ như thiên tai không được bảo hiểm không. Limit: số tiền yêu cầu có vượt hạn mức không. Deductible: sau khi duyệt, khách phải tự chịu phần miễn thường, nên payout bằng số tiền được duyệt trừ đi mức miễn thường và không bao giờ âm. Fraud flag: nếu điểm nghi ngờ gian lận cao, hồ sơ phải chuyển REVIEW cho con người dù các điều kiện khác đều đạt. Năm nhóm này chính là các cột điều kiện của bảng quyết định.
3. Bảng quyết định làm oracle: nguồn sự thật duy nhất
Bảng quyết định (decision table) là một bảng liệt kê mọi tổ hợp điều kiện đầu vào và kết quả tương ứng. Mỗi cột là một quy tắc; mỗi hàng là một điều kiện; ô giao nhau cho biết điều kiện đó cần đúng, sai hay không quan trọng cho quy tắc đó; và hàng cuối là kết quả. Sức mạnh của nó với kiểm thử là biến logic mờ trong đầu người thành một tạo tác rõ ràng, kiểm tra được. Khi ta biến bảng thành oracle, mỗi hồ sơ đầu vào chỉ cần tra bảng để biết kết quả đúng, và test so kết quả engine với kết quả bảng.
// Bảng quyết định = nguồn sự thật. Engine PHẢI khớp bảng này.
type Outcome = { decision: 'APPROVE' | 'DENY' | 'REVIEW'; reason: string };
export function decisionOracle(c: Claim): Outcome {
if (c.fraudScore > 70) return { decision: 'REVIEW', reason: 'FRAUD_FLAG' };
if (!c.policyActive) return { decision: 'DENY', reason: 'POLICY_INACTIVE' };
if (c.excludedPerils.includes(c.claimType))
return { decision: 'DENY', reason: 'EXCLUSION' };
if (c.amount > c.coverageLimit)
return { decision: 'DENY', reason: 'OVER_LIMIT' };
return { decision: 'APPROVE', reason: 'COVERED' };
}
// Payout áp SAU khi APPROVE: min(claim, limit) − deductible, không âm
export function payout(c: Claim): number {
if (decisionOracle(c).decision !== 'APPROVE') return 0;
return Math.max(0, Math.min(c.amount, c.coverageLimit) - c.deductible);
}Chú ý thứ tự các điều kiện trong oracle: cờ gian lận được kiểm trước tiên, vì dù mọi điều kiện khác đạt, một hồ sơ nghi gian lận vẫn phải chuyển con người xem lại, không được auto-approve. Thứ tự này chính là một phần của quy tắc nghiệp vụ và phải được ghi rõ trong bảng, không để engine tự quyết ngầm. Bảng quyết định vừa là đặc tả vừa là oracle: nó nói cho lập trình viên biết engine phải làm gì, và nói cho người kiểm thử biết kết quả đúng là gì. Một nguồn sự thật duy nhất cho cả hai vai trò.
❓ Vì sao dùng bảng quyết định làm oracle thay vì viết assertion rời cho từng case?
Vì bảng quyết định buộc ta nghĩ đầy đủ và không mâu thuẫn. Khi liệt kê mọi tổ hợp điều kiện, ta phát hiện ngay các case chưa có luật hoặc hai luật cho kết quả khác nhau trên cùng đầu vào. Assertion rời rạc dễ bỏ sót tổ hợp. Ngoài ra bảng là một tạo tác chung mà cả dev, QA và nghiệp vụ cùng đọc và duyệt, nên nó vừa là đặc tả vừa là oracle — giảm hiểu lầm giữa các bên.
💬 Bình luận (0)
Chưa có bình luận nào. Hãy là người đầu tiên!