DEV Community

EME GUG
EME GUG

Posted on

Make Small LLMs Say "Check This Line": Grounded Data Extraction That Refuses to Bluff

Tuần này trên Dev.to có một bài khá đáng chú ý: một model nhỏ, không thông minh lắm, tăng độ chính xác khi đọc giấy tờ nhà đất từ 10% lên 99.6%. Tổng chi phí chỉ khoảng $1.15. Điều đáng nói nhất là khi không chắc, model đó trả lời kiểu "hãy kiểm tra dòng này" chứ không bịa ra một câu trả lời nghe có vẻ hợp lý. Trên HN cũng có bài về một CNN 10 năm tuổi chẩn đoán ung thư da tốt hơn các frontier LLM. Hai câu chuyện cùng nói một điều: thiết kế hệ thống quan trọng hơn độ to của model.

Mình đã làm vài dự án trích xuất dữ liệu từ hợp đồng, hoá đơn và CMND/CCCD scan cho khách hàng. Lỗi tốn kém nhất mình gặp không phải là model trả lời sai. Đó là model trả lời sai một cách tự tin, và không ai phát hiện cho đến khi dữ liệu đã vào production. Bài này chia sẻ pattern mình đang dùng: bắt model phải trích dẫn số dòng, sau đó dùng code thường để verify, và cái gì không verify được thì chuyển cho người xem.

1. Vấn đề: LLM không biết nói "tôi không chắc"

Khi bạn hỏi một LLM "số thửa đất trong văn bản này là gì?", nó gần như luôn trả về một con số. Nếu văn bản bị OCR lỗi, hoặc có hai số thửa (thửa cũ và thửa mới sau khi tách), model sẽ chọn đại một số. Trường confidence: 0.95 mà model tự chấm cũng gần như vô nghĩa, vì LLM được train để nghe tự tin chứ không được calibrate xác suất.

Vì vậy giải pháp không nằm ở chỗ hỏi model nó chắc đến đâu. Giải pháp là bắt model đưa ra bằng chứng có thể kiểm tra bằng máy. Bằng chứng đơn giản nhất là: giá trị này nằm ở dòng số mấy, và nguyên văn đoạn text trên dòng đó là gì.

flowchart LR
    A[Document / OCR text] --> B[Đánh số từng dòng]
    B --> C[Small LLM + JSON schema]
    C --> D{Verifier: quote có trong dòng đó?}
    D -- Có --> E[Auto accept]
    D -- Không --> F[Human review queue]
    C -- value = null --> F

Điểm mấu chốt: bước verify là code deterministic, không phải LLM thứ hai. Một hàm Python 20 dòng đáng tin hơn bất kỳ "LLM-as-judge" nào.

2. Bắt model trích dẫn: schema + đánh số dòng

Mình dùng Ollama 0.5+ (hỗ trợ structured output qua JSON schema) với qwen2.5:7b-instruct. Model này chạy ổn trên MacBook M1 16GB hoặc một con GPU 8GB. Cài đặt:

ollama pull qwen2.5:7b-instruct
pip install ollama==0.4.7 pydantic==2.9.2
Enter fullscreen mode Exit fullscreen mode

Schema bắt buộc mỗi field phải đi kèm line và quote. Ngoài ra, phải cho phép value = null. Đây là lối thoát để model không phải bịa:

from pydantic import BaseModel, Field
from typing import Optional
import ollama

class Cited(BaseModel):
    value: Optional[str] = Field(description='Giá trị trích xuất, null nếu không tìm thấy hoặc mơ hồ')
    line: Optional[int] = Field(description='Số dòng chứa giá trị')
    quote: Optional[str] = Field(description='Copy NGUYÊN VĂN đoạn text chứa giá trị')

class Deed(BaseModel):
    owner_name: Cited
    parcel_number: Cited
    area_m2: Cited
    address: Cited

SYSTEM = '''Bạn trích xuất dữ liệu từ giấy chứng nhận quyền sử dụng đất.
Quy tắc:
- Mỗi giá trị PHẢI kèm số dòng và quote nguyên văn từ dòng đó.
- Nếu không tìm thấy, hoặc có nhiều giá trị mâu thuẫn, trả value = null.
- KHÔNG suy luận, KHÔNG sửa lỗi chính tả trong quote.'''

def number_lines(text: str) -> str:
    return '\n'.join(f'[{i}] {line}' for i, line in enumerate(text.splitlines(), 1))

def extract(text: str) -> Deed:
    resp = ollama.chat(
        model='qwen2.5:7b-instruct',
        messages=[
            {'role': 'system', 'content': SYSTEM},
            {'role': 'user', 'content': number_lines(text)},
        ],
        format=Deed.model_json_schema(),
        options={'temperature': 0},
    )
    return Deed.model_validate_json(resp.message.content)
Enter fullscreen mode Exit fullscreen mode

Vài lưu ý từ thực tế:

  • temperature: 0 là bắt buộc. Bạn cần kết quả lặp lại được để debug.
  • Đánh số dòng ở input giúp model 7B định vị tốt hơn hẳn so với bắt nó đếm offset ký tự. Mình đã thử dùng character offset và tỉ lệ sai lên tới khoảng 40%.
  • Cho phép null một cách rõ ràng trong prompt. Nếu không có câu này, model nhỏ gần như không bao giờ trả về null.

3. Verifier: phần quan trọng nhất lại không dùng AI

Đây là chỗ biến một model "not-so-smart" thành hệ thống đáng tin. Mỗi field phải qua ba bước kiểm tra: dòng được trích có tồn tại không, quote có thật sự nằm trên dòng đó không, và value có nằm trong quote không.

import unicodedata, re

def norm(s: str) -> str:
    # Chuẩn hoá Unicode tiếng Việt (NFC vs NFD là bug kinh điển) + khoảng trắng
    s = unicodedata.normalize('NFC', s).lower()
    return re.sub(r'\s+', ' ', s).strip()

def verify(field: Cited, lines: list[str]) -> tuple[str, str]:
    if field.value is None:
        return 'review', 'model abstained'
    if not field.line or not (1 <= field.line <= len(lines)):
        return 'review', f'line {field.line} không tồn tại'
    src = norm(lines[field.line - 1])
    if not field.quote or norm(field.quote) not in src:
        return 'review', f'quote không khớp dòng {field.line}'
    if norm(field.value) not in norm(field.quote):
        return 'review', 'value không nằm trong quote'
    return 'accept', 'ok'

def process(text: str) -> dict:
    lines = text.splitlines()
    deed = extract(text)
    result = {}
    for name, field in deed:
        status, reason = verify(field, lines)
        result[name] = {'value': field.value, 'line': field.line,
                        'status': status, 'reason': reason}
    return result
Enter fullscreen mode Exit fullscreen mode

Một lỗi mình từng mất nửa ngày để tìm ra: text từ OCR (Tesseract 5) trả về tiếng Việt dạng NFD (dấu tách riêng khỏi chữ), trong khi model output dạng NFC. Hai chuỗi trông giống hệt nhau trên màn hình nhưng phép in trả về False, nên verifier reject 100% kết quả. Vì vậy hãy luôn chạy unicodedata.normalize('NFC', ...) trước khi so sánh.

Với field số như diện tích, bạn có thể thêm bước parse: area_m2 phải parse được thành float, nằm trong khoảng hợp lý (ví dụ 10 đến 100000 m²). Rule nghiệp vụ kiểu này bắt được rất nhiều lỗi mà LLM không tự nhận ra.

4. Đo lường: accuracy thôi là chưa đủ

Con số 99.6% trong bài gốc chỉ có ý nghĩa khi đi kèm câu hỏi: trong số kết quả được auto-accept, bao nhiêu phần trăm đúng? Bạn cần theo dõi hai metric:

  • Precision of accepted: trong các field hệ thống tự accept, tỉ lệ đúng là bao nhiêu. Metric này phải rất cao, trên 99%.
  • Review rate: bao nhiêu phần trăm field bị đẩy sang người xem. Metric này quyết định chi phí vận hành.
sequenceDiagram
    participant P as Pipeline
    participant L as LLM
    participant V as Verifier
    participant H as Human
    P->>L: numbered text
    L-->>P: value + line + quote
    P->>V: check quote vs source
    V-->>P: accept / review
    P->>H: chỉ gửi field bị review
    H-->>P: giá trị đúng → thêm vào eval set

Mình dựng một eval set khoảng 200 văn bản có nhãn, sau đó chạy lại mỗi khi đổi prompt hoặc đổi model. Thao tác này giống regression test. Mỗi field được người sửa lại sẽ được đưa vào eval set, nên hệ thống tốt dần theo thời gian. Cũng như bài "Three Times My Test Failed and the LLM Was Right" trên Dev.to, đôi khi chính nhãn của bạn mới là cái sai. Hãy xem lại các case mà model và nhãn lệch nhau, đừng mặc định model sai.

Một điều thú vị là khi mình đổi từ model 7B lên model 32B, precision of accepted gần như không thay đổi (verifier đã chặn lỗi rồi). Thứ thay đổi là review rate giảm từ khoảng 12% xuống 5%. Nói cách khác, model to hơn giúp tiết kiệm công người, còn độ tin cậy đến từ verifier. Điều này giúp bạn ra quyết định chi phí rõ ràng hơn nhiều, nhất là khi giá API đang giảm mạnh như hiện nay.

Kết luận

Những việc bạn có thể áp dụng ngay tuần này:

  1. Đừng tin vào confidence score do LLM tự chấm. Thay vào đó, bắt model trích dẫn số dòng và quote nguyên văn.
  2. Đánh số dòng ở input và cho phép null một cách rõ ràng trong schema lẫn prompt.
  3. Viết verifier bằng code thường: kiểm tra quote có trong source, value có trong quote, cộng thêm rule nghiệp vụ. Nhớ normalize Unicode NFC cho tiếng Việt.
  4. Đo hai metric: precision of accepted và review rate. Precision là trách nhiệm của verifier, còn review rate phụ thuộc vào model.
  5. Bắt đầu với model nhỏ chạy local (Qwen2.5 7B, Llama 3.1 8B). Chỉ nâng cấp khi review rate làm tốn quá nhiều công người.

Một hệ thống biết nói "hãy kiểm tra dòng 14" đáng giá hơn nhiều so với một hệ thống luôn trả lời chắc nịch. Với dữ liệu pháp lý, tài chính hay y tế, đây gần như là cách duy nhất để đưa LLM vào production mà vẫn ngủ ngon.

Top comments (0)