
Xây dựng Prompt thế hệ RAG tối ưu: Kết hợp Base Prompt và các quy tắc theo từng câu hỏi
Khám phá kỹ thuật xây dựng hệ thống Dispatcher trong RAG để tối ưu hóa prompt. Thay vì sử dụng 'mega-prompt' cồng kềnh, bài viết hướng dẫn cách kết hợp một Base Prompt cố định với các đoạn fragment linh hoạt dựa trên yêu cầu của từng câu hỏi, giúp tăng độ chính xác, khả năng kiểm soát và tính minh bạch cho hệ thống LLM doanh nghiệp.
Xây dựng Prompt thế hệ RAG tối ưu: Kết hợp Base Prompt và các quy tắc theo từng câu hỏi
Đây là phần thứ hai trong chuỗi bài viết về "Generation Brick" (Khối thế hệ) của hệ thống Enterprise Document Intelligence. Trong chuỗi bài này, chúng ta xây dựng một hệ thống RAG doanh nghiệp dựa trên bốn trụ cột: phân tích tài liệu (document parsing), phân tích câu hỏi (question parsing), truy xuất (retrieval) và thế hệ (generation).

1. Từ yêu cầu đến Prompt: Vai trò của Dispatcher
Thay vì sử dụng một "mega-prompt" cồng kềnh chứa mọi điều kiện cho mọi loại câu hỏi, chúng ta sẽ xây dựng một Dispatcher.
Cơ chế hoạt động:
- Đầu vào: Một
ParsedQuestion(Câu hỏi đã được phân tích). - Đầu ra:
- Schema (được chọn từ
ANSWER_REGISTRYdựa trên hình thái câu trả lời). - System Prompt (kết hợp từ một
BASEcố định và các đoạn fragment tùy chỉnh). - User Prompt (chứa câu hỏi, từ khóa và các dòng văn bản được gắn nhãn).
- Schema (được chọn từ
Cách tiếp cận này giúp việc thêm một loại hình câu hỏi mới chỉ đơn giản là thêm một fragment, thay vì làm phức tạp hóa logic điều kiện trong prompt tổng.
1.1 Cấu trúc ParsedQuestion
Dispatcher đọc đối tượng ParsedQuestion được tạo ra từ bước phân tích câu hỏi. Dưới đây là cấu trúc dữ liệu cơ bản:
class Keyword(BaseModel):
text: str
weight: float = 1.0
source: Literal['direct','llm_expansion','expert_dictionary'] = 'direct'
is_regex: bool = False
class GenerationBrief(BaseModel):
original_question: str
format_constraint: dict[str, str] = {}
disambiguation: str | None = None
must_distinguish: list[Distinction] = []
class ParsedQuestion(BaseModel):
original_question: str
keywords: list[Keyword]
expected_answer_shape: Literal['text','amount','date','boolean','list','table']
generation: GenerationBrief
# ... các trường khác
1.2 Gợi ý cấu trúc từ câu hỏi
Người dùng có thể chỉ định phạm vi tìm kiếm ngay trong câu hỏi (ví dụ: "trên trang 1", "từ trang 5 đến 7"). Thông tin này được lưu vào StructuralHints. Retrieval sẽ sử dụng thông tin này để lọc không gian tìm kiếm thay vì dùng các flag pipeline phức tạp.
1.3 System Prompt: BASE + Fragments
BASE là phần cố định, đảm bảo tính nhất quán về hợp đồng (trích dẫn, định dạng, trung thực). Các fragments là các phần bổ sung cụ thể cho từng loại câu hỏi.
BASE = """Bạn trả lời câu hỏi nghiêm ngặt từ các đoạn văn bản được cung cấp.
Quy tắc:
- Sử dụng thông tin từ các đoạn văn bản.
- Mỗi mục trong `items` phải có ít nhất một Span trích dẫn số dòng nguồn.
- Span.line_start và Span.line_end PHẢI là giá trị GLOBAL_LINE (cột đầu tiên).
- Nếu không tìm thấy câu trả lời, trả về items=[], answer_found=False.
"""
SHAPE_FRAGMENTS = {
"text": "Sử dụng `text=...` cho mỗi mục.",
"amount": "Điền `amount = Amount(value, currency, unit)`. ISO 4217.",
"list": "Mỗi mục phải có Spans riêng."
}
2. Thực thi và Truy vết (Tracing)
2.1 Gọi Model và Lưu vết
Luôn sử dụng temperature=0.0 cho các tác vụ RAG vì đây là bài toán trích xuất, không phải sáng tạo. Quan trọng nhất, hãy lưu toàn bộ payload phản hồi từ API vào cơ sở dữ liệu để phục vụ việc kiểm tra (audit) sau này.
def generate(parsed_q: ParsedQuestion, filtered_line_df, client) -> GenerationResult:
# ... logic xây dựng prompt ...
resp = client.responses.parse(
model="gpt-4",
input=[{"role": "system", "content": system}, {"role": "user", "content": user}],
text_format=Answer,
temperature=0.0
)
return GenerationResult(answer=Answer.model_validate_json(resp.output_text), meta={...})
2.2 Trích xuất theo trường (Field-level Evidence)
Khi cần trích xuất nhiều trường dữ liệu phức tạp, hãy sử dụng wrapper FieldExtraction[T] cho từng trường thay vì một khối evidence tổng quát. Điều này cho phép mỗi trường dữ liệu có citation riêng, giúp việc kiểm chứng (verify) trở nên chính xác tuyệt đối.
class FieldExtraction(BaseModel, Generic[T]):
value: T | None
quote: str
page: int | None
line_start: int | None
line_end: int | None
3. Dynamic Few-shot: Áp dụng truy xuất vào Prompt
Một kỹ thuật nâng cao là sử dụng chính cơ chế truy xuất để tìm các ví dụ (examples) đã được kiểm chứng từ một "ngân hàng ví dụ" (example bank) và chèn chúng vào prompt trước khi gọi LLM. Điều này giúp model học được cách định dạng hoặc cách xử lý các trường hợp khó mà không cần fine-tuning.
4. Kết luận
Bằng cách tách biệt logic xây dựng prompt thành các mảnh (fragments) và sử dụng Dispatcher, hệ thống RAG của bạn sẽ trở nên:
- Dễ bảo trì: Thay đổi định dạng chỉ cần sửa fragment tương ứng.
- Minh bạch: Mọi quyết định đều được lưu vết trong
pipeline_trace.json. - Đáng tin cậy: Kiểm chứng citation ở cấp độ từng trường dữ liệu giúp loại bỏ hiện tượng ảo giác (hallucination).
Việc kết hợp giữa cấu trúc dữ liệu chặt chẽ (Pydantic) và prompt engineering có cấu trúc là chìa khóa để đưa ứng dụng RAG từ thử nghiệm lên môi trường sản xuất doanh nghiệp.
Do you like this post?
Upvote to push this post higher on the community feed
