Bài 4 — Trích xuất & chuẩn hóa dữ liệu có cấu trúc
Sau bài 3 bạn có nhiều đoạn text thô kèm URL nguồn. Muốn so sánh đối thủ thì phải biến text thành bảng: mỗi đối thủ một hàng, mỗi tiêu chí một cột, và mỗi ô kèm nguồn. Đây là bước biến "đọc hiểu" thành "dữ liệu".
Nguyên tắc vàng: định nghĩa schema TRƯỚC
Đừng để LLM tự do trả về. Hãy ép nó điền vào một cấu trúc bạn định nghĩa sẵn (structured output). Điều này chống lan man và bắt buộc gắn nguồn.
from pydantic import BaseModel
from typing import Optional
class Fact(BaseModel):
value: str # ví dụ "0đ cho 3 lần đầu/tháng"
source_url: str # BẮT BUỘC
confidence: float # 0-1, agent tự đánh giá
class CompetitorRow(BaseModel):
name: str
withdrawal_fee: Optional[Fact] = None
cashback: Optional[Fact] = None
recent_hiring: Optional[Fact] = None
Mỗi dữ kiện là một Fact luôn đi kèm source_url. Không nguồn thì để trống, không được bịa.
Các bước trích xuất
- Ghép text + URL thành một khối context cho LLM, giữ rõ đoạn nào từ URL nào.
- Gọi LLM với chỉ dẫn nghiêm ngặt và schema:
prompt = f"""Trích thông tin về đối thủ {name} từ các nguồn dưới đây.
QUY TẮC:
- Chỉ dùng thông tin CÓ trong nguồn. Không suy đoán.
- Mỗi trường phải kèm source_url là URL đã chứa thông tin đó.
- Nếu không tìm thấy, để null. TUYỆT ĐỐI không bịa.
NGUỒN:
{sources_block}
"""
# gọi model với response_format = CompetitorRow
- Validate bằng Pydantic. Nếu model trả sai schema, bắt lỗi và thử lại 1 lần.
- Kiểm tra chéo source_url có thật nằm trong danh sách nguồn không — chống model tự chế URL.
def verify_sources(row, allowed_urls):
for f in [row.withdrawal_fee, row.cashback, row.recent_hiring]:
if f and f.source_url not in allowed_urls:
f.confidence = 0.0 # cờ đỏ: nguồn không khớp
return row
Chuẩn hóa (normalize) để so sánh được
Dữ liệu thô mỗi nơi một kiểu: "miễn phí", "0 đồng", "Free". Muốn so sánh phải quy về chuẩn:
- Tiền tệ → cùng đơn vị (VND).
- "miễn phí"/"free"/"0đ" →
0. - Ngày tháng → định dạng
YYYY-MM-DD. - Đơn vị % giữ số, bỏ ký hiệu.
def normalize_fee(s):
s = s.lower()
if any(k in s for k in ["miễn phí", "free", "0đ", "0 đồng"]):
return 0
digits = "".join(ch for ch in s if ch.isdigit())
return int(digits) if digits else None
Khung tư duy: mỗi ô = Giá trị + Nguồn + Độ tin
Một ô dữ liệu tốt luôn trả lời được 3 câu: Con số là gì? Lấy ở đâu? Tin được bao nhiêu? Thiếu bất kỳ vế nào thì ô đó chưa dùng được cho báo cáo ra quyết định.
Sai lầm thường gặp
- Để LLM trả text tự do rồi tự parse — cực dễ vỡ và mất nguồn.
- Không ép source_url — báo cáo đẹp nhưng không kiểm chứng được.
- Tin URL do model tự sinh — model có thể chế ra URL nghe hợp lý mà không tồn tại.
- Quên normalize — không thể so sánh "Free" với "1.000đ".
- Không xử lý null — ép model điền bằng mọi giá dẫn tới bịa số.
- Gộp nhiều đối thủ trong 1 lần gọi lớn — dễ lẫn nguồn giữa các đối thủ.
Checklist khâu trích xuất
- [ ] Schema định nghĩa trước, mỗi Fact có source_url
- [ ] Prompt cấm bịa, cho phép null
- [ ] Validate schema + retry 1 lần
- [ ] Kiểm tra source_url nằm trong nguồn thật
- [ ] Dữ liệu đã normalize về đơn vị chung
Ví dụ: kết quả một hàng dữ liệu đạt chuẩn
{
"name": "MoMo",
"withdrawal_fee": {
"value": "0đ cho 3 lần đầu/tháng, sau đó 0,5%",
"source_url": "https://momo.vn/phi",
"confidence": 0.9
},
"recent_hiring": {
"value": "đang tuyển 2 vị trí Data Scientist",
"source_url": "https://tuyendung.momo.vn",
"confidence": 0.7
}
}
Hàng dữ liệu này dùng được vì mỗi ô có value, source_url thật và confidence để bài 5-7 biết ô nào đủ tin cho khuyến nghị. Ô cashback để null vì không tìm thấy nguồn — đúng tinh thần "thà trống còn hơn bịa". Khi bạn nhìn thấy một hàng như thế này, khâu trích xuất coi như đạt.
Bài 5: tổng hợp các hàng dữ liệu này thành báo cáo so sánh mạch lạc.