Tôi phát hiện ra một điều khá xấu hổ tuần trước.
Pipeline podcast AI của tôi có một system prompt công phu — một "dialogue guide" — mà tôi đã dành nhiều ngày để viết. Nó hướng dẫn hai host (hai bình luận viên bóng đá do AI tạo ra) cách nói chuyện với nhau. Đừng lặp lại cách mở đầu. Thay đổi cấu trúc giữa các phân đoạn. Dùng catchphrase một cách tiết chế. Tuyệt đối không bịa số liệu.
LLM đọc từng chữ. Rồi phớt lờ gần như tất cả.
Host tiếng Việt nói "Trời ơi" 13 lần trong một tập. Host tiếng Nhật mở đầu mọi phân đoạn bằng "いやー、健一さん!" (Này, Kenichi-san!). Host tiếng Anh tự tin trích dẫn xG 0.35 và tỷ lệ kiểm soát bóng 65% — những con số không hề tồn tại trong database của tôi.
System prompt được lịch sự ghi nhận. Rồi bị phớt lờ.
Đây là điều hữu ích nhất tôi học được về prompt engineering: LLM có hai chế độ đọc, và chúng xử lý hai chế độ này hoàn toàn khác nhau.
Vấn Đề Hai Chế Độ Đọc
Đây là dialogue guide gốc của tôi. Tôi dịch bản tiếng Việt sang tiếng Anh vì cấu trúc giống hệt:
STYLE GUIDELINES:
HOST INTERACTION PATTERN: Emotional Host + Analytical Host.
STRICT RULES FOR EVERY SEGMENT:
0. NEVER REPEAT THE SAME OPENING. Vary which host initiates,
which match moment they react to first.
1. EMOTIONAL START (mandatory): Every segment begins with
the emotional host's pure reaction.
2. DATA-GROUNDING RULE (mandatory): Every claim must cite
specific numbers. Not "they dominated" but "61% possession,
3.17 xG to 0.65."
3. CATCHPHRASES:
Emotional host: "You see that?!" "Wait wait wait..."
Analytical host: "There's a deeper reason..." "Let me tell you."
Trông kỹ lưỡng đấy chứ? Tôi thực sự nghĩ mình đã làm đủ rồi.
Nhưng khi đọc kịch bản thực tế được tạo ra, đây là những gì xảy ra:
| Quy tắc | Guide nói gì | LLM làm gì |
|---|---|---|
| Quy tắc 0 (không lặp) | Thay đổi cách mở đầu | Mọi phân đoạn đều mở đầu bằng catchphrase của emotional host |
| Quy tắc 2 (dữ liệu) | Trích dẫn số liệu cụ thể | Trích dẫn số liệu bịa đặt với sự tự tin tuyệt đối |
| Quy tắc 3 (catchphrase) | Danh sách câu cửa miệng | Dùng mỗi câu 5–13 lần mỗi tập |
Quy tắc có đó. LLM đọc rồi. Nhưng chúng có sức nặng của một gợi ý, không phải một yêu cầu.
Đây là lúc tôi nhận ra: LLM đang xử lý quy tắc của tôi như "style context" — chứ không phải "ràng buộc có thể thực thi."
Mẫu Hình: Context vs. Command
Hãy nghĩ về cách bạn đọc những loại văn bản khác nhau. Nếu tôi đưa bạn một thực đơn nhà hàng, bạn đọc nó như thông tin để hành động. Nếu tôi đưa bạn một bài Wikipedia về lịch sử mì pasta, bạn đọc nó như kiến thức nền.
LLM làm điều tương tự với prompt. Khi quy tắc xuất hiện trong một mục có tiêu đề "STYLE GUIDELINES" hay "HOST INTERACTION PATTERN" hay "RULES FOR SEGMENTS," model coi chúng là thông tin nền — ngữ cảnh tô màu cho phản hồi nhưng không ràng buộc nó.
Khi quy tắc xuất hiện trong một mục có tiêu đề "CRITICAL IMPROVEMENTS REQUIRED" hay "SPECIFIC PHRASE LIMITS," model coi chúng là lệnh có thể thực thi — những thứ nó phải kiểm tra và sửa.
Insight không nằm ở từ "critical" hay "required." Nó nằm ở vị trí của quy tắc trong pipeline.
Cách Sửa: Để Bước Review Thực Thi Những Gì Bước Generation Phớt Lờ
Pipeline của tôi có hai lần gọi LLM:
- Bước generation — viết kịch bản từng phân đoạn, dùng dialogue guide làm ngữ cảnh
- Bước review — đọc toàn bộ kịch bản đã ghép và cải thiện nó
Tất cả quy tắc của tôi đều nằm ở bước generation. Bước review có những quy tắc chống lặp chung chung ("đừng nói 'That's fascinating' hai lần") hoàn toàn chỉ tập trung vào tiếng Anh và không biết gì về catchphrase của host hay yêu cầu cấu trúc.
Cách sửa mang tính cấu trúc, không phải từ vựng. Tôi chuyển việc thực thi sang bước review, nơi toàn bộ kịch bản được nhìn thấy cùng một lúc.
Đây là những gì đã thay đổi:
Trước: Quy tắc trong prompt generation (bị phớt lờ)
STYLE GUIDELINES:
0. NEVER REPEAT THE SAME OPENING...
1. EMOTIONAL START...
2. DATA-GROUNDING...
3. CATCHPHRASES: "You see that?!" ...
Sau: Quy tắc được trích xuất và đưa vào prompt review (được thực thi)
CRITICAL IMPROVEMENTS REQUIRED ACROSS THE ENTIRE SCRIPT:
2a. ENFORCE STYLE GUIDELINES' STRUCTURAL RULES:
The STYLE GUIDELINES above contain specific requirements
about segment structure. These are REQUIREMENTS, not suggestions.
- If the guidelines say "X of Y segments must vary," CHANGE
segments that violate this.
- Check how EACH segment opens. If 4+ out of 5 open identically,
VARY at least 2.
3b. LOCALE-SPECIFIC PHRASE LIMITS (extracted from host profiles —
MAXIMUM 2 TIMES each across entire script):
- "Trời ơi" / "Trời má" — max 4x combined
- "Có một lý do sâu hơn" — max 2x
- "Mày thấy chưa?!" — max 2x
...
Hai điều đã xảy ra ở đây:
-
Các quy tắc cấu trúc được nâng cấp từ context lên command — được diễn đạt lại thành "ENFORCE this" và "CHANGE segments that violate this" thay vì "đây là mẫu ưu tiên"
-
Các catchphrase được trích xuất từ host profile và hiển thị dưới dạng giới hạn rõ ràng, không phải danh sách gợi ý. "Catchphrases: đây là vài câu" trở thành "Những câu sau: TỐI ĐA 2 LẦN mỗi câu."
Cả hai thay đổi đều nằm trong prompt review, không phải prompt generation. Đó là quyết định kiến trúc then chốt. Bước generation nhận style guide làm ngữ cảnh — nó cần hương vị cá tính. Bước review nhận quy tắc làm công cụ thực thi — nó cần nhiệm vụ tuân thủ.
Chi Tiết Trích Xuất (Tại Sao Cách Này Hoạt Động Trên 7 Ngôn Ngữ)
Tôi không muốn duy trì danh sách câu cửa miệng cho từng ngôn ngữ bằng tay. Mỗi lần tôi chỉnh sửa host profile, tôi lại phải cập nhật prompt review. Đó là kiểu drift giết chết sản phẩm của một solo developer.
Thay vào đó, tôi viết một hàm trích xuất nhỏ phân tích host profile tại thời điểm review. Profile của mỗi locale có một mục catchphrase với nhãn chuẩn:
- Tiếng Việt:
Câu cửa miệng: - Tiếng Nhật:
口癖: - Tiếng Tây Ban Nha:
Latiguillos: - Tiếng Pháp:
Phrases fétiches: - Tiếng Hàn:
입버릇: - Tiếng Trung:
口头禅:
Hàm tìm nhãn đó, trích xuất bất kỳ chuỗi nào trong dấu ngoặc kép theo sau, và định dạng chúng thành giới hạn có thể thực thi:
@staticmethod
def _extract_catchphrase_limits(host_profiles: str) -> str:
patterns = [
(r'Câu cửa miệng:?', r'"([^"]+)"'), # Vietnamese
(r'口癖:?', r'「([^」]+)」'), # Japanese
(r'Latiguillos:?', r'"([^"]+)"'), # Spanish
# ... etc for all 7 languages
]
limits = []
for marker_pattern, quote_pattern in patterns:
match = re.search(marker_pattern, host_profiles)
if match:
phrases = re.findall(quote_pattern, host_profiles[match.end():])
for phrase in phrases:
limits.append(f' - "{phrase}" — max 2x')
return "\n".join(limits)
Giờ mỗi khi tôi chỉnh sửa host profile và thay đổi catchphrase, prompt review tự động cập nhật. Không drift. Không ngôn ngữ nào bị bỏ quên.
Những Gì Cách Này Thực Sự Sửa Được (Có Số Liệu)
Đây là những gì thay đổi giữa tập được tạo trước các bản sửa này và tập được tạo sau đó:
| Chỉ số | Trước | Sau |
|---|---|---|
| Số lần "Trời ơi" (hạn mức: tối đa 4) | 13 | Chưa test bằng tiếng Việt, nhưng với tiếng Nhật: cả 4 catchphrase đều trong hạn mức |
| "Có một lý do sâu hơn" (hạn mức: tối đa 2) | 5 | Trong hạn mức ở bài test tiếng Nhật tương đương |
| Số liệu bịa đặt (xG, tỷ lệ kiểm soát bóng, độ rộng đội hình) | 4 con số bịa | Không còn số nào bịa |
| Phân đoạn mở đầu với cùng một mẫu | 6/6 giống hệt | Vẫn 5/5 với tiếng Nhật — xem bên dưới |
| Kết nối Việt Nam/Nhật Bản được giữ lại | 1/4 sống sót (review đã xóa 3) | 11/11 sống sót |
| Host voice bị lẫn vào nhau | Cả hai dùng câu của nhau | Phân tách rõ ràng |
Hạn mức catchphrase, quy tắc chống bịa đặt, và bảo toàn bản địa hóa đều hoạt động. Host profile giờ quy định cá tính của từng host đủ rõ ràng để LLM không còn nhầm lẫn ai nói gì.
Những Gì Vẫn Hỏng
Một quy tắc vẫn thất bại: đa dạng cấu trúc. Dialogue guide nói "ít nhất 2 trong 5 phân đoạn phải dùng cách mở đầu khác nhau." Sau các bản sửa, tập tiếng Nhật vẫn mở đầu mọi phân đoạn giống hệt: "いやー、健一さん!" — năm lần trong một tập.
Đây không phải vấn đề prompt enforcement. Đây là vấn đề kiến trúc.
Bước generation tạo ra từng phân đoạn độc lập. Phân đoạn 3 không hề biết rằng phân đoạn 1 và 2 đã mở đầu bằng catchphrase của emotional host. Mỗi phân đoạn được tạo ra riêng lẻ với cùng một dialogue guide, nên mỗi phân đoạn đều đi đến cùng một quyết định "mở đầu bằng emotional host."
Bước review đáng lẽ phải bắt được điều này — nó thấy toàn bộ kịch bản cùng lúc. Và tôi đã thêm ngôn ngữ thực thi rõ ràng bảo nó thay đổi cách mở đầu. Nhưng Gemini 3.5 Flash, model đang chạy bước review, dường như thấy việc cấu trúc lại cách mở đầu phân đoạn khó hơn việc cắt catchphrase hay làm mềm các tuyên bố bịa đặt. Đa dạng cách mở đầu đòi hỏi phải tạo lại một phần nội dung, chứ không chỉ thắt chặt hay xóa bỏ. Đó là một nhiệm vụ nặng hơn với model.
Tôi vẫn đang làm việc với vấn đề này — có lẽ là một model review mạnh hơn hoặc tái cấu trúc pipeline để các phân đoạn chia sẻ trạng thái mở đầu. Điểm chính: prompt enforcement không thể sửa vấn đề kiến trúc. Nếu cấu trúc pipeline của bạn khiến một quy tắc không thể tuân theo, thì không lượng "CRITICAL" hay "MANDATORY" nào trong prompt cứu được bạn.
Framework: Ba Câu Hỏi Cho Mỗi Quy Tắc
Sau trải nghiệm này, giờ tôi kiểm tra mọi system prompt mình viết bằng ba câu hỏi:
1. Quy tắc này nằm ở đâu — generation hay review? Quy tắc về cá tính và giọng điệu thuộc về generation. Quy tắc về tuân thủ, nhất quán, và cấu trúc thuộc về review. Nếu một quy tắc chạm vào cả hai (như catchphrase — chúng là cá tính nhưng cần giới hạn), hãy đặt ở cả hai.
2. Quy tắc này được diễn đạt như context hay command? "Đây là các catchphrase" là context. "Tối đa 2 lần mỗi câu" là command. Cả hai đều cần cho cùng một quy tắc — bước generation cần biết catchphrase LÀ GÌ, và bước review cần thực thi tần suất chúng XUẤT HIỆN.
3. Pipeline có thực sự tuân theo được quy tắc này không? Generation theo từng phân đoạn không thể thực thi đa dạng xuyên suốt các phân đoạn. Không prompt nào sửa được điều này. Bạn cần hoặc chuyển quy tắc sang bước review (nơi thấy toàn bộ kịch bản) hoặc tái cấu trúc pipeline để các phân đoạn chia sẻ trạng thái.
Framework này không chỉ dành cho việc tạo podcast. Nó áp dụng cho bất kỳ pipeline LLM nhiều bước nào — tạo content, review code, tóm tắt tài liệu, bất cứ thứ gì có các bước generation và quality-control riêng biệt.
Tôi vẫn đang tiếp tục cải thiện, nhưng nếu bạn đang xây dựng pipeline LLM nhiều bước đầu tiên của mình: hãy tách nó thành ít nhất hai bước. Bước đầu tạo ra với cá tính và tự do — cho nó context, ví dụ, hướng dẫn giọng điệu. Bước thứ hai review với độ chính xác và thực thi — cho nó giới hạn rõ ràng, kiểm tra tuân thủ, và quyền thay đổi mọi thứ.
Bước generation nên giống như bạn đang brief cho một cộng sự sáng tạo. Bước review nên giống như bạn đưa checklist cho một editor.
Và khi quy tắc của bạn vẫn bị phớt lờ sau khi bạn đã làm cả hai, hãy hỏi câu hỏi kiến trúc trước. Có phải do prompt, hay do pipeline?
Câu Hỏi Thường Gặp
Cách này có áp dụng được cho setup single-prompt (không có pipeline) không?
Có, nhưng ở mức hạn chế. Trong một prompt duy nhất, bạn đang yêu cầu cùng một model vừa sáng tạo vừa tuân thủ cùng lúc — nó sẽ luôn ưu tiên sáng tạo hơn tuân thủ khi hai thứ xung đột. Ngay cả trong một prompt, việc tách hướng dẫn của bạn thành phần "context" và phần "constraints" rõ ràng cũng có ích. Nhưng bạn sẽ đụng trần. Hai lần gọi riêng biệt với hướng dẫn khác nhau hầu như luôn vượt trội hơn một lần.
Sao không đơn giản viết quy tắc trong prompt generation mạnh mẽ hơn?
Tôi đã thử. Tôi thêm "MANDATORY," "CRITICAL," "NEVER," và viết hoa toàn bộ mọi thứ. Bước generation có quá nhiều yêu cầu cạnh tranh — nó đang cố vừa hấp dẫn, vừa tự nhiên, vừa đa dạng, vừa chân thực về mặt địa phương, vừa tuân thủ cùng một lúc. Khi model phải chọn giữa "nói như một fan bóng đá Việt Nam phấn khích" và "chỉ nói 'Trời ơi' hai lần," cá tính luôn thắng. Bước review không có xung đột như vậy — công việc duy nhất của nó là thực thi.
Bạn dùng model gì cho bước review?
Gemini 3.5 Flash (vì chi phí — bước review chạy trên mọi tập của 7 ngôn ngữ). Nó xử lý tốt việc thực thi đơn giản (cắt catchphrase, làm mềm số liệu bịa đặt). Nó gặp khó khăn với những nhiệm vụ đòi hỏi tạo lại nội dung, như cấu trúc lại cách mở đầu phân đoạn. Một model mạnh hơn sẽ giúp ích ở đó, nhưng tôi chưa thấy xứng đáng với chi phí.
Tôi xây dựng DIALOGUE, một nền tảng podcast AI, một mình vào buổi tối và cuối tuần. Code cho hàm trích xuất catchphrase và các thay đổi prompt review là open-source — bạn có thể đọc toàn bộ system prompt và logic thực thi trong podcast-engine repository của tôi. Tôi viết về những gì mình học được trên đường đi.
Nếu bạn từng gặp vấn đề tương tự — quy tắc được đọc nhưng không được thực thi — tôi tò mò: quy tắc đó là gì, và nó nằm ở đâu trong pipeline của bạn?
Vậy thôi, hết rồi.
Thân mến, Chandler