Làm chủ Context Engineering: Vượt xa những câu Prompt đơn giản cho AI thực tế

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

Sự chuyển dịch từ Prompting sang Context Engineering

Khi lần đầu xây dựng các tính năng dựa trên LLM, tôi từng nghĩ “bí kíp” chỉ đơn giản là viết một câu prompt thật khéo. Tôi đã dành hàng giờ để ám ảnh về những tính từ như “chuyên gia” hay “ngắn gọn”, cầu nguyện rằng mô hình sẽ ngừng hoạt động sai lệch. Mọi thứ đã thay đổi khi chúng tôi chuyển từ các script chạy cục bộ sang môi trường production với 5.000 người dùng hoạt động. Đột nhiên, mô hình mất dấu các luồng hội thoại dài, gặp hiện tượng ảo giác (hallucination) về các tính năng không tồn tại, và khiến hóa đơn API của chúng tôi tăng vọt 400% chỉ trong một tuần.

Thất bại đó đã dạy tôi một bài học xương máu: prompt engineering chỉ là phần nổi của tảng băng chìm. Để xây dựng phần mềm đáng tin cậy, bạn cần Context Engineering (Kỹ thuật Ngữ cảnh). Đây là việc quản lý chiến lược từng bit dữ liệu đi vào bộ nhớ hạn chế của mô hình. Theo kinh nghiệm của tôi, việc làm chủ kỹ năng này là thứ phân biệt giữa một bản demo mong manh và một ứng dụng AI cấp doanh nghiệp mạnh mẽ.

So sánh các phương pháp tiếp cận: Naive Prompting và Context Engineering

Trong thực tế, bạn thường thấy hai cách để xử lý luồng dữ liệu. Một cách dễ thiết lập nhưng tốn kém để duy trì; cách còn lại yêu cầu nhiều mã nguồn hơn nhưng mang lại kết quả nhất quán.

1. Phương pháp tiếp cận ngây thơ (Append-and-Pray)

Hầu hết các nhà phát triển bắt đầu từ đây. Bạn lấy đầu vào của người dùng, gắn thêm mười tin nhắn chat gần nhất và gửi nó tới API. Không có bộ lọc hay sự ưu tiên nào. Bạn hoàn toàn dựa dẫm vào mô hình để nó tự hiểu điều gì là quan trọng. Điều này thường dẫn đến tình trạng “nhồi nhét ngữ cảnh”, nơi mô hình bị bối rối bởi các chi tiết không liên quan từ ba ngày trước.

2. Phương pháp tiếp cận Context Engineering

Phương pháp này coi cửa sổ ngữ cảnh (context window) là một tài nguyên quý giá, giống như việc quản lý RAM trong lập trình cấp thấp. Bạn sử dụng các template có cấu trúc, cắt tỉa (pruning) động và các chỉ dẫn đa lớp. Thay vì một mớ văn bản hỗn độn, bạn cung cấp cho mô hình một bản đồ hội thoại sạch sẽ và được ưu tiên.

Tính năng Naive Approach Context Engineering
Tổ chức Các khối văn bản không cấu trúc Thẻ XML hoặc Markdown có cấu trúc
Bộ nhớ N tin nhắn cuối cùng (cố định) Lịch sử được tóm tắt và sắp xếp theo thứ tự ưu tiên
Ảo giác Cao (tỷ lệ lỗi 10-15%) Thấp (dưới 2% với kỹ thuật grounding)
Chi phí Lãng phí và khó dự đoán Được tối ưu hóa qua ngân sách token

Những sự đánh đổi trong thực tế

Kỹ thuật ít khi xoay quanh việc tìm ra giải pháp “hoàn hảo”; nó là về việc lựa chọn sự đánh đổi phù hợp. Context engineering cũng không ngoại lệ.

Lợi ích

  • Độ tin cậy cao: Sử dụng các thẻ như <context> đảm bảo mô hình không nhầm lẫn chỉ dẫn với dữ liệu người dùng.
  • Ít ảo giác hơn: Việc định nghĩa rõ ràng những gì mô hình không biết buộc nó phải thừa nhận sự thiếu sót thay vì bịa đặt sự thật.
  • Hóa đơn thấp hơn: Bằng cách loại bỏ dữ liệu dư thừa, bạn thường có thể giảm mức sử dụng token từ 30% trở lên cho mỗi yêu cầu.

Thách thức

  • Độ phức tạp của mã nguồn: Backend của bạn cần logic để cắt tỉa, tóm tắt và định dạng dữ liệu trước mỗi lần gọi API.
  • Độ trễ tăng thêm: Nếu bạn sử dụng bước “tiền xử lý”—như dùng một mô hình nhỏ hơn để tóm tắt lịch sử—bạn có thể cộng thêm 200ms đến 500ms vào thời gian phản hồi.

Một cấu trúc chuẩn cho môi trường Production

Tôi đề xuất một “System Prompt phân lớp” cho hầu hết các trường hợp sử dụng. Thay vì một bức tường văn bản khổng lồ, hãy chia chỉ dẫn của bạn thành bốn vùng riêng biệt:

  1. Persona (Vai trò): Xác định danh tính (ví dụ: “Kiến trúc sư DevOps cao cấp”).
  2. Constraints (Ràng buộc): Thiết lập các rào chắn (ví dụ: “Không bao giờ nhắc đến đối thủ cạnh tranh hoặc đưa ra lời khuyên pháp lý”).
  3. Dynamic Data (Dữ liệu động): Chèn kiến thức thực tế được truy xuất từ cơ sở dữ liệu của bạn, được bao bọc trong các thẻ XML.
  4. Output Format (Định dạng đầu ra): Xác định chính xác schema, chẳng hạn như “Chỉ trả về một đối tượng JSON hợp lệ“.

Mẹo nhỏ: Sử dụng các thẻ XML như <docs> hoặc <history>. Các mô hình như Claude 3.5 và GPT-4o được huấn luyện trên lượng lớn dữ liệu web có cấu trúc. Chúng cực kỳ giỏi trong việc phân biệt các chỉ dẫn cốt lõi của bạn với dữ liệu thô bên trong các thẻ đó.

Triển khai: Xây dựng trình quản lý ngữ cảnh thông minh

Hãy xem xét một bản triển khai bằng Python để xử lý giới hạn token. Script này giữ cho system prompt cố định trong khi cắt tỉa các tin nhắn cũ nhất khi cuộc hội thoại quá dài.

Bước 1: Định nghĩa cấu trúc

import tiktoken

class ContextManager:
    def __init__(self, model_name="gpt-4o", max_tokens=4000):
        self.encoder = tiktoken.encoding_for_model(model_name)
        self.max_tokens = max_tokens
        self.system_prompt = """
        <role>Hỗ trợ kỹ thuật cho itfromzero.com</role>
        <rules>
        - Chỉ trả lời bằng cách sử dụng khối <context>.
        - Nếu thiếu câu trả lời, hãy nói 'Tôi không có thông tin đó'.
        - Duy trì giọng điệu chuyên nghiệp.
        </rules>
        """
        self.history = []

    def _count_tokens(self, text):
        return len(self.encoder.encode(text))

    def add_message(self, role, content):
        self.history.append({"role": role, "content": content})

Bước 2: Logic cắt tỉa

Để tránh vượt quá giới hạn ngữ cảnh, chúng ta giữ lại system prompt và truy vấn gần nhất của người dùng. Sau đó, chúng ta lấp đầy không gian còn lại bằng lịch sử từ mới nhất đến cũ nhất.

    def get_optimized_context(self, dynamic_data):
        full_context = f"{self.system_prompt}\n<context>{dynamic_data}</context>"
        current_tokens = self._count_tokens(full_context)
        optimized_history = []
        
        # Lấp đầy ngân sách còn lại bằng các tin nhắn gần đây
        for msg in reversed(self.history):
            msg_text = f"{msg['role']}: {msg['content']}"
            msg_tokens = self._count_tokens(msg_text)
            
            if current_tokens + msg_tokens > self.max_tokens:
                break
            
            optimized_history.insert(0, msg)
            current_tokens += msg_tokens
            
        return {"system": full_context, "messages": optimized_history}

Bước 3: Grounding mô hình

Grounding buộc mô hình phải nằm trong phạm vi dữ liệu của bạn. Đây là cách bạn gọi trình quản lý với thông tin sản phẩm cụ thể:

# Dữ liệu mẫu từ cơ sở dữ liệu
product_info = "Zero-Cloud VPS: $5/tháng, 1GB RAM, 20GB SSD."

manager = ContextManager(max_tokens=2000)
manager.add_message("user", "Gói VPS rẻ nhất là bao nhiêu?")

payload = manager.get_optimized_context(dynamic_data=product_info)
# Prompt có cấu trúc đảm bảo mô hình sẽ không đoán mò mức giá thấp hơn.

Những bài học từ thực tế

Sau khi quản lý các mô hình này ở quy mô lớn, tôi nhận thấy hai hành vi quan trọng. Đầu tiên, các mô hình thường bỏ qua phần giữa của cửa sổ ngữ cảnh. Các nhà nghiên cứu gọi đây là hiện tượng “Lost-in-the-Middle”. Nếu bạn có dữ liệu quan trọng, hãy đặt nó ở ngay đầu system prompt hoặc ở cuối cuộc hội thoại.

Thứ hai, hãy ngừng việc xóa các tin nhắn cũ và bắt đầu tóm tắt chúng. Nếu một cuộc chat đạt tới 20 tin nhắn, hãy lấy 10 tin nhắn ở giữa và chạy chúng qua một mô hình rẻ hơn như GPT-4o-mini. Thay thế 10 tin nhắn đó bằng một bản tóm tắt ba câu. Điều này giúp bảo tồn “ngữ cảnh” và các sự kiện của cuộc hội thoại mà không lãng phí hàng nghìn token vào những lời chào hỏi cũ kỹ.

Hãy coi ngữ cảnh của bạn như một cấu trúc dữ liệu được quản lý. Khi bạn ngừng đổ văn bản thô vào API, AI của bạn sẽ trở nên ổn định hơn, chính xác hơn và vận hành rẻ hơn nhiều.

Share: