Ví dụ: “Có bao nhiêu thanh toán bị lỗi trong tuần qua?” “Liệt kê người dùng từ Ấn Độ?” “Chỉ số này có giảm sau bản phát hành mới không?”
Vấn đề không phải là SQL mà là truy cập dữ liệu.
Quản lý sản phẩm, QA, nhà phân tích và lãnh đạo thường cần câu trả lời nhanh, nhưng:
- họ không viết được SQL;
- họ không nên có quyền truy cập trực tiếp vào DB;
- lập trình viên trở thành nút thắt.
Tôi đặt câu hỏi đơn giản: Có thể cho người không kỹ thuật truy vấn dữ liệu bằng tiếng Anh thuần túy – một cách an toàn không?

Bài viết này mô tả cách tôi xây dựng một agent đọc‑chỉ chuyển ngôn ngữ tự nhiên sang SQL, thực hiện đúng yêu cầu trên.
Yêu cầu những ràng buộc nghiêm ngặt
- Nhận đầu vào bằng tiếng Anh thuần túy.
- Chỉ cho phép câu lệnh SQL đọc‑chỉ (SELECT / WITH).
- Không cho phép INSERT, UPDATE, DELETE, DROP – dưới mọi trường hợp.
- Xử lý câu hỏi mơ hồ bằng các câu hỏi phụ.
- Hiển thị SQL rõ ràng, không “hộp đen”.
- Đủ an toàn để quản lý và QA sử dụng.
Đây không phải là demo chatbot. Đó là lớp truy cập dữ liệu được kiểm soát.
Kiến trúc tổng quan (đơn giản mà mạnh mẽ)
Luồng dữ liệu:
- Người dùng nhập câu hỏi (tiếng Anh).
- Phát hiện ý định + Chuẩn hoá.
- Yêu cầu làm rõ (nếu cần).
- Định tuyến quyết định hoặc tạo SQL bằng LLM.
- Kiểm tra rào cản SQL.
- Thực thi chỉ‑đọc (SQLite).
- Trả về kết quả + SQL sinh ra.
Ý tưởng chính: LLM không được tin tưởng tuyệt đối; chúng được bao bọc bằng quy tắc, định tuyến và xác thực.
Bước 1: Phát hiện ý định (trước khi tạo SQL)
Nhiều demo NL→SQL để LLM quyết định mọi thứ, gây rủi ro.
Tôi thêm bộ phát hiện ý định dựa trên quy tắc nhẹ:
def detect_intent(question: str) -> dict: q = question.lower() return { "is_filter": bool(re.search(r"\b(from|where|in)\b", q)), "is_group_by": bool(re.search(r"\b(by|per|grouped)\b", q)), "is_aggregate": bool(re.search(r"\b(count|how many|total|sum)\b", q)), "has_time_range": bool( re.search(r"\b(last|past|\d+\s*days|\d+\s*weeks)\b", q) ), } Điều này cho phép hệ thống lý luận trước khi tạo SQL.
Ví dụ: “Show failed payments last 7 days” → filter; “Show failed payments count last 7 days” → aggregate.
- “Hiển thị các khoản thanh toán không thành công trong 7 ngày qua” → danh sách
- “Hiển thị số lượng thanh toán không thành công trong 7 ngày qua” → tổng hợp
Phân biệt này quan trọng cho các bước tiếp theo.
Bước 2: Câu hỏi phụ để làm rõ (thân thiện)
Câu hỏi mơ hồ luôn xuất hiện. Thay vì đoán, agent sẽ hỏi lại.
Ví dụ:
Người dùng: Show users from India Hiển thị người dùng từ Ấn Độ
Agent: Do you want the list or just the count? Bạn muốn danh sách hay chỉ đếm?
Quy tắc làm rõ đơn giản:
def needs_clarification(intent, question): if intent["is_filter"] and not intent["is_aggregate"]: if "list" not in question and "count" not in question: return "Do you want the list or just the count?" return None Hệ thống ghi nhớ câu hỏi đang chờ, nên các phản hồi như “list” hoặc “count” hoạt động tự nhiên.
Bước 3: Định tuyến quyết định (bí quyết)
Không phải truy vấn nào cũng cần LLM.
Đối với các truy vấn đơn giản, có rủi ro cao (như người dùng hoặc thanh toán), tôi định tuyến theo quy tắc.
Ví dụ xây dựng truy vấn người dùng:
def build_users_query(country=None, aggregate=False): if aggregate: sql = "SELECT COUNT(*) AS user_count FROM users" else: sql = "SELECT * FROM users" if country: sql += " WHERE country = ?" sql += " LIMIT 50" return sql Lợi ích:
- Không có nguy cơ hallucination ảo giác.
- Hiệu năng dự đoán được.
- Thích hợp cho demo và môi trường sản xuất.
LLM chỉ được dùng cho các truy vấn phức tạp.
Bước 4: Rào cản SQL (đảm bảo an toàn)
Mọi câu lệnh sinh ra đều qua bộ kiểm tra:
FORBIDDEN_KEYWORDS = ( "INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "TRUNCATE", "CREATE" ) def validate_sql(sql: str): upper = sql.upper() if not upper.startswith(("SELECT", "WITH")): return {"status": "blocked", "reason": "Only SELECT allowed"} for kw in FORBIDDEN_KEYWORDS: if kw in upper: return {"status": "blocked", "reason": f"Forbidden keyword: {kw}"} return {"status": "ok", "sql": sql} Ngay cả khi LLM tạo câu lệnh nguy hiểm, nó sẽ không được thực thi. Đây là điều làm cho hệ thống an toàn cho những người không phải là nhà phát triển.
Bước 5: Thực thi minh bạch (transparent)
Thực thi qua helper chỉ‑đọc SQLite:
def execute_read_only_query(sql: str): with sqlite3.connect(DB_PATH) as conn: conn.row_factory = sqlite3.Row cursor = conn.execute(sql) rows = cursor.fetchall() return [dict(row) for row in rows] Phản hồi luôn bao gồm: SQL sinh ra, dữ liệu trả về, và giải thích. Không có “ma thuật” ẩn.
Nhược điểm & giới hạn
- Ưu tiên an toàn và dự đoán hơn tốc độ.
- Chạy hoàn toàn trên CPU bằng mô hình ngôn ngữ mở, nên chậm hơn API LLM đám mây.
- Tuy nhiên chi phí thấp, dễ kiểm toán, an toàn hơn.
- Trong môi trường sản xuất, có thể thay mô hình nhanh hơn mà không thay đổi bảo mật.
Demo trực tiếp (có thể thử)
Đã triển khai demo công khai:
Có thể thử:
- Show failed payments last 7 days
- Show failed payments count last 7 days
- List users from India
Mã nguồn
Mọi thành phần đều mở: phát hiện ý định, bộ nhớ làm rõ, định tuyến, kiểm tra SQL, thực thi.
Lợi ích thực tiễn cho đội ngũ
- Không thay thế lập trình viên, chỉ giảm ma sát.
- PM tự kiểm chứng giả thuyết.
- QA kiểm tra dữ liệu nhanh mà không chờ dev.
- Dev tập trung vào công việc chính.
- An toàn DB được đảm bảo.
Phù hợp cho:
- công cụ nội bộ
- cổng phân tích
- dashboard quản trị
- hệ thống báo cáo chỉ‑đọc
Kết luận
Chuyển ngôn ngữ tự nhiên sang SQL dễ demo.
Thách thức thực sự là làm cho nó an toàn.
Chiến thắng không nằm ở mô hình, mà ở các rào cản, kiểm soát ý định và định tuyến xung quanh nó.
Tham khảo: medium.com

Bài viết liên quan: