Trích xuất dữ liệu có cấu trúc với BAML và Python: Tăng tốc đầu ra LLM và kiểm thử prompt trực quan

AI tutorial - IT technology blog
AI tutorial - IT technology blog

Vấn đề thực tế: Khi LLM làm gãy data pipeline trên production

Không có gì làm sập một microservice trên production nhanh hơn một phản hồi LLM không thể parse được. Trong môi trường staging, mọi thứ trông thật hoàn hảo. Bạn cấu hình một endpoint yêu cầu JSON chuẩn, chạy thử ba mươi payload kiểm thử và thấy schema validate mượt mà không một lỗi nhỏ.

Nhưng khi lưu lượng thực tế ập đến production, các edge case xuất hiện ngay lập tức. Model bọc phản hồi trong các khối markdown bất ngờ như ```json ... ```. Model khác lại chèn thêm câu giao tiếp thừa thãi như “Chắc chắn rồi! Dưới đây là JSON bạn yêu cầu:” ngay trước dấu ngoặc nhọn mở đầu. Tệ hơn nữa, dấu ngoặc kép không được escape bên trong chuỗi lồng nhau của người dùng sẽ kích hoạt ngoại lệ chí mạng json.decoder.JSONDecodeError, làm mất hoàn toàn các trường bắt buộc.

Việc khắc phục những lỗi này thường rơi vào vòng lặp thử-sai tẻ nhạt. Các kỹ sư sao chép các payload bị lỗi từ log Datadog trên production vào web playground. Họ tuyệt vọng chắp vá thêm các chỉ dẫn prompt như “CHỈ trả về raw JSON hợp lệ, không dùng markdown”. Tiếp theo, họ sửa tạm các f-string trong Python rồi đẩy hotfix. Chu kỳ phản hồi này rất chậm chạp. Nó làm tiêu tốn thời gian của lập trình viên, tốn kém chi phí và hoàn toàn không mang lại bất kỳ đảm bảo nào ở cấp độ compile-time.

Phân tích nguyên nhân gốc rễ: Tại sao pipeline LLM tiêu chuẩn thất bại khi validate schema

Để khắc phục sự thiếu ổn định trong việc trích xuất, chúng ta cần nhận diện những hạn chế của các công cụ Python truyền thống:

1. Prompt dạng chuỗi phá vỡ tính an toàn kiểu dữ liệu (Type Safety)

Hầu hết các framework đều xử lý prompt dưới dạng f-string thô hoặc template Jinja. Bạn có thể duy trì một model Pydantic chỉn chu trong schemas.py, nhưng template prompt của bạn lại không có liên kết tĩnh nào với nó. Khi bạn đổi tên một trường trong Python, prompt của bạn vẫn giữ nguyên cho đến khi bước validation lúc runtime phát nổ trên production.

2. Độ trễ cao trong chế độ JSON Schema gốc

Các nhà cung cấp LLM cung cấp các chế độ JSON gốc và structured output thông qua giải mã ràng buộc theo ngữ pháp (grammar-constrained decoding). Dù rất hiệu quả trong việc thực thi schema, việc mask token có ràng buộc lại làm tăng đáng kể chi phí tính toán. Trong các workload có độ đồng thời cao, thời gian nhận token đầu tiên (TTFT) có thể tăng thêm từ 200ms đến 400ms. Đối với các mảng lồng nhau phức tạp, model có thể bị đơ, sinh ra các giá trị mặc định ảo (hallucinate) hoặc bị cắt cụt giữa chừng khi chạm giới hạn token nghiêm ngặt.

3. Rào cản giữa IDE và Web Playground

Quá trình tinh chỉnh prompt thường diễn ra trên các web console như OpenAI Playground, Anthropic Console hoặc Google AI Studio. Trong khi đó, logic ứng dụng và test fixture của bạn lại nằm trong VS Code. Sự phân tách này buộc bạn phải chuyển đổi ngữ cảnh liên tục. Việc kiểm thử các edge case đồng nghĩa với việc di chuyển JSON giả lập qua lại thủ công giữa các tab trình duyệt và test runner của Python.

So sánh giải pháp: Pydantic vs. Instructor vs. BAML

Các lập trình viên đã giải quyết bài toán sinh dữ liệu có cấu trúc bằng ba mô hình chính:

  • Raw Prompt + json.loads(): Đơn giản, dễ triển khai mà không cần thư viện bên thứ ba. Tuy nhiên, nó hoàn toàn không có bất kỳ đảm bảo validation nào. Bất kỳ dấu phẩy thừa hay lời mở đầu lạc lõng nào cũng sẽ làm parser bị crash.
  • Pydantic Output Parsers (ví dụ: LangChain): Chèn định nghĩa schema vào nội dung prompt và thử lại (retry) khi parse thất bại. Điểm trừ là gì? Mỗi lần retry sẽ kích hoạt một lệnh gọi API mới. Một request 700ms có thể nhanh chóng biến thành một đợt tăng vọt độ trễ lên tới 2.1 giây, làm tăng gấp ba lần hóa đơn token của bạn.
  • Instructor: Vá trực tiếp vào SDK chính thức của OpenAI hoặc Anthropic để liên kết model Pydantic thẳng vào function calling. Thư viện này hoạt động ổn định và giữ nguyên typing của Python. Tuy nhiên, template prompt vẫn là các chuỗi thô trong code của bạn, và việc chạy thử nghiệm prompt tương tác vẫn đòi hỏi các script test tùy chỉnh.
  • BAML (Basically A Made-up Language): Một ngôn ngữ cấu hình chuyên biệt (DSL) được hỗ trợ bởi Rust compiler nguyên bản và trình xem trước tích hợp sẵn trong VS Code. BAML biên dịch prompt và schema trực tiếp thành các client có định kiểu cho Python, TypeScript hoặc Ruby. Bạn nhận được khả năng validation tại thời điểm biên dịch, tốc độ parse dưới 1 mili giây và một test playground ngay trong trình soạn thảo.

Hướng dẫn triển khai từng bước: Xây dựng bộ trích xuất CV với BAML và Python

BAML tách các template prompt và schema mục tiêu ra khỏi chuỗi Python để đưa vào các file cấu hình có cấu trúc. Trình biên dịch sẽ xử lý validation và sinh code từ trước.

Bước 1: Cài đặt BAML CLI và Extension

Cài đặt BAML runtime trong môi trường ảo Python của bạn:

pip install baml-py

Tiếp theo, cài đặt extension BAML từ VS Code Marketplace. Khởi tạo workspace tại thư mục gốc của dự án:

baml-cli init

Lệnh này sẽ khởi tạo một thư mục baml_src/ chứa các định nghĩa client, prompt mẫu và các file cấu hình.

Bước 2: Khai báo Model và Prompt trong BAML

Tạo một schema và hàm trích xuất trong baml_src/extract_resume.baml:

client<llm> FastModel {
  provider openai
  options {
    model "gpt-4o-mini"
    api_key env.OPENAI_API_KEY
  }
}

class WorkExperience {
  company string
  title string
  years_active int
  responsibilities string[]
}

class CandidateProfile {
  name string
  email string
  skills string[]
  experience WorkExperience[]
}

function ExtractCandidate(raw_text: string) -> CandidateProfile {
  client FastModel
  prompt #"
    Trích xuất thông tin chi tiết của ứng viên từ nội dung CV sau:
    ---
    {{ raw_text }}
    ---

    {{ ctx.output_format }}
  "#
}

Hãy chú ý đến macro {{ ctx.output_format }}. BAML sẽ tự động chuyển đổi schema CandidateProfile của bạn thành các chỉ dẫn ngắn gọn, tối ưu riêng cho từng model. Bạn không bao giờ phải giải thích thủ công về định dạng schema hay các quy tắc JSON trong nội dung prompt nữa.

Bước 3: Kiểm thử Prompt trực quan trong VS Code

Mở baml_src/extract_resume.baml trong VS Code. Nhìn ngay phía trên phần khai báo function ExtractCandidate, bấm vào code lens Open Playground.

Một bảng kiểm thử tương tác sẽ mở ra ngay cạnh code của bạn. Hãy dán nội dung CV thô vào ô input test và nhấn Run. Playground sẽ gửi request trực tiếp đến model. Bạn có thể kiểm tra số lượng token, xem phản hồi thô từ nhà cung cấp và xác minh dữ liệu đã parse so với schema của bạn — tất cả đều không cần viết test harness tạm thời hay phải chuyển qua lại trình duyệt.

Bước 4: Sinh Python Binding an toàn kiểu dữ liệu (Type-Safe)

Sinh code client Python bằng CLI:

baml-cli generate

Lệnh này sẽ tự động tạo package baml_client trong repository của bạn. Module được sinh ra chứa đầy đủ type signature, định nghĩa autocomplete cho IDE và bộ giải tuần tự hóa (deserialization) lúc runtime được vận hành bởi engine Rust đã biên dịch.

Bước 5: Gọi hàm BAML từ Python

Import client đã sinh vào luồng xử lý ứng dụng của bạn:

import os
from baml_client import b
from baml_client.types import CandidateProfile

os.environ["OPENAI_API_KEY"] = "khoa-api-cua-ban-o-day"

resume_data = """
Alex Chen - [email protected]
Kỹ sư Backend Senior với 6 năm kinh nghiệm về Python, Rust và Go.
Từng dẫn dắt quá trình di chuyển microservice tại FinTech Corp (2020-2023), nâng cao thông lượng lên 42%.
Triển khai distributed tracing với OpenTelemetry và duy trì cụm EKS đa vùng (multi-region).
"""

def process_resume(text: str) -> CandidateProfile:
    # Lệnh gọi được định kiểu đầy đủ với đồng bộ (b.ExtractCandidate) hoặc bất đồng bộ (b.ExtractCandidate.async_run)
    profile: CandidateProfile = b.ExtractCandidate(raw_text=text)
    return profile

result = process_resume(resume_data)

print(f"Đã trích xuất: {result.name} ({result.email})")
print(f"Kỹ năng: {', '.join(result.skills)}")
for job in result.experience:
    print(f"- {job.title} tại {job.company} ({job.years_active} năm)")

Những điểm mấu chốt cho kiến trúc Production

  • Coi Prompt là Tài nguyên được Biên dịch (Compiled Assets): Quản lý prompt và schema bằng một trình biên dịch chuyên dụng thay vì các chuỗi ghép nối rải rác khắp codebase của bạn.
  • Đưa việc kiểm thử Prompt về sớm (Shift Left): Phát hiện tình trạng lệch schema (schema drift) và các key bị hallucinate ngay trên playground của IDE trong quá trình phát triển, chứ không phải qua các cảnh báo APM trên production.
  • Loại bỏ các vòng lặp Retry tốn kém: Bộ parser viết bằng Rust của BAML có thể tự sửa các đầu ra bị thiếu một phần, thừa dấu ngoặc kép hoặc sai định dạng nhẹ trong thời gian dưới 2ms. Bạn sẽ nhận được dữ liệu chuẩn kiểu ngay từ lần gọi đầu tiên mà không lãng phí token vào các lần retry dự phòng.
Share: