Kì vừa rồi thì trường mình HCMUS - VNUHCM có mở thêm môn học mới tên là Tư Duy Tính Toán. Môn học này chủ yếu dạy về kĩ năng phân rã, phân tích & làm rõ vấn đề, giới thiệu các nền tảng, sử dụng API và thêm nữa là cách vibecoding.
Đồ án môn học của nhóm mình là xây dựng BMI - Bite Mapping Intelligent, một ứng dụng hỗ trợ người dùng lên lịch trình ăn uống bằng ngôn ngữ tự nhiên. Các bạn có thể tham khảo mã nguồn tại đây
Ý tưởng ban đầu khá đơn giản: thay vì người dùng phải tự tìm từng quán ăn, so sánh giá, vị trí, khoảng cách rồi ghép lại thành một lịch trình, tại sao không để họ chỉ cần nhập một câu như:
"Mình có 500k, muốn ăn cả ngày ở Sài Gòn. Buổi chiều muốn uống cà phê ở quán yên tĩnh, tối muốn ăn nhà hàng Âu."
Sau đó hệ thống tự phân tích và trả về một lịch trình phù hợp.
Nhưng khi bắt tay vào làm thì bài toán không còn đơn giản là: Prompt → LLM → Kết quả
Nếu giao toàn bộ việc cho LLM, model có thể tự nghĩ ra quán không tồn tại, tính sai ngân sách, chọn địa điểm quá xa hoặc bỏ qua những ràng buộc mà người dùng đưa ra.
Vì vậy cách nhóm mình xây dựng hệ thống là kết hợp LLM với các thuật toán thông thường. LLM được đặt vào những bước cần hiểu ngôn ngữ và ra quyết định ngữ nghĩa, còn những việc như lọc dữ liệu, tính khoảng cách hay chấm điểm được Backend xử lý bằng thuật toán xác định.
I. Pipeline tổng quan
Nếu rút gọn hệ thống BMI thì một request tìm kiếm sẽ đi qua pipeline như sau:

Còn nếu người dùng chỉ đặt câu hỏi, Intent Classification sẽ điều hướng sang một pipeline Q&A khác.
Như vậy trong hệ thống, LLM không chỉ có nhiệm vụ chat với người dùng mà được sử dụng cho nhiều vai trò khác nhau.
II. Vai trò đầu tiên: Phân loại ý định người dùng
Khi người dùng gửi một prompt, việc đầu tiên hệ thống cần biết là:
Người dùng thực sự muốn làm gì?
Ví dụ ba câu:
"Tìm cho tôi quán Nhật ở Quận 1"
"Làm sao thêm quán vào lịch trình?"
"Người bị Gout nên hạn chế ăn gì?"
đều là ngôn ngữ tự nhiên, nhưng rõ ràng không thể đưa vào cùng một pipeline.
Vì vậy bước đầu tiên của hệ thống sử dụng Llama-3.3-70B-Versatile để phân loại yêu cầu.
Các nhánh chính gồm: Search, System_QA, Knowledge_QA
Ngoài ra hệ thống cũng kiểm tra những trường hợp người dùng cung cấp quá ít thông tin để có thể yêu cầu bổ sung.
Bước này khá giống một Router.
LLM không trực tiếp giải quyết bài toán mà quyết định request tiếp theo phải được gửi đến đâu.
Điều mình thấy hay ở cách này là thay vì viết hàng loạt if, Regex hoặc từ khóa để cố đoán ý định, LLM có thể hiểu những cách diễn đạt rất khác nhau của người dùng.
III. Vai trò thứ hai: Biến ngôn ngữ tự nhiên thành dữ liệu có cấu trúc
Giả sử người dùng nhập:
"Tối nay tôi muốn ăn đồ Nhật ở Quận 1, khoảng 300k, quán yên tĩnh một chút."
Đây là câu mà con người đọc rất dễ hiểu.
Nhưng những module phía sau như bộ lọc hay thuật toán chấm điểm lại không thể làm việc trực tiếp với câu này.
Backend cần một cấu trúc kiểu:
{
"budget": 300000,
"num_meals": 1,
"location_pref": "Quận 1",
"meals_detail": [
{
"meal": "tối",
"type": ["Quán Nhật"],
"semantic_query": "yên tĩnh",
"dish": ""
}
]
}Đây là nhiệm vụ thứ hai của LLM trong hệ thống: Intent/Entity Extraction.
Nhóm mình truyền vào model:user_prompt system_context
Trong đó system_context chứa ngữ cảnh của cuộc hội thoại và những kết quả trước đó.
Sau đó sử dụng Prompt Engineering để ép model trả kết quả về JSON theo một schema định trước.
Một số thông tin được trích xuất gồm:
budget: ngân sách.num_meals: số bữa cần tìm.location_pref: khu vực.shu: mức độ cay.meals_detail: yêu cầu riêng cho từng bữa.semantic_query: những yêu cầu mang tính ngữ nghĩa như "lãng mạn", "yên tĩnh", "view đẹp", "có máy lạnh".dish: món ăn cụ thể.wants_alternative: người dùng có đang muốn đổi phương án không.feedback_reason: lý do muốn đổi.target_shop_id: quán cụ thể mà người dùng đang nhắc tới.
Ví dụ
"Ăn trưa xong tìm cho tôi quán nước ngồi chill."
LLM có thể tách thành hai nhu cầu khác nhau:
{
"num_meals": 2,
"meals_detail": [
{
"meal": "trưa"
},
{
"meal": "xế",
"type": ["Quán nước"],
"semantic_query": "chill"
}
]
}Đây là phần mà nếu chỉ dùng Regex thì sẽ nhanh chóng trở nên rất khó quản lý.
Tuy nhiên nhóm mình cũng không tin hoàn toàn output của LLM.
Sau Parsing vẫn còn một lớp chuẩn hóa ở Backend để:
- whitelist các loại nhà hàng hợp lệ;
- sửa những trường hợp
mealbị trả về mơ hồ; - merge các item trùng;
- cập nhật lại
num_meals; - loại các giá trị sai schema;
- kế thừa điều kiện từ truy vấn trước nếu người dùng chỉ nói "đổi lịch trình khác".
IV. Sau LLM là thuật toán, không phải một LLM khác
Sau khi đã hiểu được người dùng muốn gì, hệ thống bắt đầu xử lý dữ liệu nhà hàng.
Đây là đoạn mà nhóm mình cố tình không giao cho LLM.
Giả sử database có hơn 1000 nhà hàng.
Thay vì gửi toàn bộ 1000 quán cho Gemini rồi hỏi:
"Quán nào phù hợp nhất?"
Backend tiến hành thu hẹp tập dữ liệu trước.
Hard Filtering
Các nhà hàng có thể được lọc dựa trên: Sức khoẻ, Vị trí, Khoảng cách, Bữa ăn, Ngân sách,... Ví dụ người dùng ở Quận 1 thì không cần gửi cho LLM một nhà hàng cách đó 20 km. gười dùng có 200k thì cũng không cần giữ những quán có mức giá vượt quá ngân sách.
Nếu yêu cầu một món cụ thể, hệ thống trước tiên tìm trực tiếp trong menu. Trong trường hợp không match được chuỗi, ChromaDB được sử dụng để Semantic Search những món có ý nghĩa gần với truy vấn.
Sau bước này ập dữ liệu đã nhỏ đi rất nhiều.
V. Phản hồi của người dùng cũng được LLM biến thành tín hiệu cho thuật toán
Đây là một phần mình thấy khá thú vị khi xây dựng hệ thống.
Giả sử BMI vừa đề xuất một quán và người dùng nói:
"Quán này xa quá, đổi quán khác đi."
Ở bước Intent Extraction, LLM có thể trả:
{
"wants_alternative": true,
"feedback_reason": "far",
"target_shop_id": "..."
}
Nếu người dùng nói:
"Mắc quá."
thì:
{
"wants_alternative": true,
"feedback_reason": "expensive"
}
Các giá trị feedback_reason được giới hạn trong: expensive, far, unhealthy, not_style, low_rating
Sau đó Dynamic Weighting sử dụng thông tin này để thay đổi trọng số của thuật toán.
Ngoài phản hồi, hệ thống còn kết hợp thời tiết và thời gian trong ngày. Ví dụ trời mưa thì ưu tiên khoảng cách hơn, buổi tối hoặc cuối tuần có thể ưu tiên trải nghiệm và độ phù hợp ngữ nghĩa nhiều hơn.LLM không trực tiếp tính trọng số. Nó chỉ chuyển một câu nói tự nhiên thành tín hiệu mà thuật toán hiểu được.
VI. Chấm điểm và lấy Top-K nhà hàng
Sau bước Filtering, các nhà hàng còn lại được chấm điểm theo bốn tiêu chí chính: Semantic Score, Price Score, Distance Score và Rating Score. Ngoài ra hệ thống còn có Health Penalty để giảm điểm những quán có rủi ro liên quan đến sức khỏe.
Trọng số cơ sở nhóm mình sử dụng là:
Semantic: 0.35
Price: 0.25
Distance: 0.25
Rating: 0.15
Các trọng số này không hoàn toàn cố định mà có thể được cộng thêm buff weight từ bước Dynamic Weighting, dựa trên thời tiết, thời gian hoặc phản hồi trước đó của người dùng. Sau khi tính total_score, hệ thống sắp xếp và giữ lại Top-K nhà hàng tốt nhất cho từng bữa.
VII. Vai trò thứ ba của LLM: Chọn phương án cuối cùng
Sau Ranking, nhóm mình không lấy cứng nhà hàng có score cao nhất để trả về. Thay vào đó, Top-K ứng viên cùng prompt gốc, Parsed Intent và ngữ cảnh hội thoại được gửi cho LLM để thực hiện bước Final Selection.
LLM lúc này đóng vai trò Finalizer, xem lại toàn bộ yêu cầu của người dùng và lựa chọn phương án phù hợp nhất trong tập ứng viên đã được thuật toán chuẩn bị.
Điểm quan trọng là LLM không được phép tự tạo thêm nhà hàng. Model chỉ được chọn những restaurant_id đã tồn tại trong Top-K.
Ví dụ Backend cung cấp:
{
"tối": [
{"id": "101", "name": "Restaurant A", "score": 0.87},
{"id": "245", "name": "Restaurant B", "score": 0.84},
{"id": "310", "name": "Restaurant C", "score": 0.82}
]
}
LLM chỉ được chọn 101, 245 hoặc 310, không thể tự sinh thêm một Restaurant D ngoài dữ liệu.
Cách làm này giúp kết hợp hai phía: thuật toán đảm bảo quán tồn tại, đúng ngân sách, khoảng cách và các ràng buộc, còn LLM xem xét ngữ cảnh tổng thể để đưa ra lựa chọn cuối cùng tự nhiên hơn.
VIII. Backend vẫn kiểm soát kết quả sau Final Selection
Ngay cả sau khi LLM lựa chọn, Backend vẫn tiếp tục kiểm tra một số ràng buộc. Ví dụ hệ thống duy trì tập Used để hạn chế một nhà hàng xuất hiện ở nhiều bữa trong cùng lịch trình.
Nếu người dùng chỉ tìm một bữa, hệ thống có thể trả tối đa 3 quán để lựa chọn. Với yêu cầu nhiều bữa, hệ thống giữ 1 quán cho mỗi bữa. Khi người dùng yêu cầu đổi quán, những restaurant_id vừa được đề xuất sẽ được ưu tiên loại khỏi kết quả mới.
Nếu LLM gặp lỗi, hệ thống vẫn có thể sử dụng thứ tự Ranking ban đầu để tiếp tục trả kết quả. Vì vậy Final LLM Selection không có toàn quyền quyết định mà vẫn nằm trong các constraint của Backend.
IX. Vai trò thứ tư: AI hỏi đáp
Không phải prompt nào cũng là yêu cầu tìm nhà hàng. BMI còn có phần Q&A.
Ban đầu nhóm mình chỉ chia thành Search và QA, nhưng cách này chưa đủ. Hai câu:
"Làm sao để thêm quán vào lịch trình?"
và
"Người bị tiểu đường nên hạn chế ăn gì?"
đều có thể bị xếp vào QA, trong khi bản chất hoàn toàn khác nhau. Vì vậy nhóm tiếp tục chia thành System_QA, Knowledge_QA và Out_Scope.
System_QA
System_QA xử lý những câu hỏi về chính BMI như:
"Làm sao lưu lịch trình?"
"BMI có gửi được ảnh không?"
"Làm sao thay đổi vị trí?"
Vì LLM không biết ứng dụng do nhóm mình tự xây dựng hoạt động như thế nào, nhóm tạo file system_docs.md chứa tài liệu hướng dẫn hệ thống. Khi có câu hỏi System_QA, nội dung tài liệu, câu hỏi của người dùng và System Prompt được đưa vào LLM.
Prompt yêu cầu model chỉ trả lời dựa trên tài liệu được cung cấp và không tự suy đoán khi tài liệu không đề cập. Mục tiêu chính là hạn chế hallucination khi AI trả lời về các tính năng của BMI.
Knowledge_QA
Knowledge_QA dành cho các câu hỏi liên quan đến ẩm thực, dinh dưỡng, sức khỏe và nhà hàng. Khác với System_QA, model được phép sử dụng kiến thức có sẵn để trả lời.
Đối với câu hỏi sức khỏe, hệ thống bổ sung cảnh báo rằng thông tin chỉ mang tính tham khảo và người dùng nên trao đổi với bác sĩ hoặc chuyên gia trước khi thay đổi chế độ ăn uống.
Out_Scope
Nếu câu hỏi không liên quan đến phạm vi của BMI, chẳng hạn:
"Viết cho tôi code Quick Sort."
hệ thống trả một response cố định mà không gọi thêm LLM. Cách này vừa tiết kiệm token vừa giúp chatbot giữ đúng phạm vi ban đầu.
X. Vai trò thứ năm: Multimodal
Ngoài văn bản, BMI còn hỗ trợ người dùng gửi ảnh menu hoặc món ăn.
Frontend upload ảnh lên Cloudinary, sau đó Backend gửi URL ảnh cùng prompt tới model vision meta-llama/llama-4-scout-17b-16e-instruct.
Model vision không trực tiếp tìm nhà hàng. Nhiệm vụ của nó là đọc nội dung ảnh và tạo ra transformed_prompt.
Ví dụ người dùng gửi ảnh một món ăn kèm câu:
"Tôi muốn ăn món này."
Model có thể chuyển thành:
"Tôi muốn ăn phở bò."
transformed_prompt sau đó được đưa vào pipeline xử lý văn bản bình thường gồm Intent Classification, Parsing, Filtering, Scoring và Final Selection.
Cách thiết kế này giúp nhóm không phải xây dựng một pipeline hoàn toàn riêng cho hình ảnh. Multimodal chỉ đóng vai trò như một lớp tiền xử lý đầu vào.
XI. Dual-LLM và Fallback
Hệ thống sử dụng hai nhà cung cấp chính là Google AI và Groq. Các model text chính gồm Gemini 2.5 Flash Lite và Llama 3.3 70B Versatile, còn phần hình ảnh sử dụng Llama 4 Scout 17B.
Gemini được sử dụng nhiều cho Entity Extraction và Final Selection, trong khi Llama 3.3 70B đảm nhiệm Routing, QA và đóng vai trò fallback ở một số bước.
Nếu Gemini gặp lỗi như rate limit hoặc lỗi API, Backend có thể chuyển request sang Groq thay vì để toàn bộ pipeline thất bại. Với một đồ án môn học thì cơ chế này hơi "overkill", nhưng qua đó nhóm mình học được cách giảm sự phụ thuộc vào một API duy nhất khi xây dựng ứng dụng sử dụng LLM.
XII. Nhìn lại toàn bộ vai trò của LLM
Tổng hợp lại, LLM trong BMI có 5 vai trò chính:
- Routing: Hiểu người dùng muốn làm gì.
- Intent / Entity Extraction: Biến ngôn ngữ tự nhiên thành JSON để các thuật toán phía sau xử lý.
- Final Selection: Chọn phương án cuối cùng từ Top-K ứng viên đã được lọc và chấm điểm.
- Question Answering: Trả lời câu hỏi về hệ thống, ẩm thực và sức khỏe.
- Multimodal Understanding: Chuyển ảnh menu hoặc món ăn thành đầu vào văn bản.
Điều quan trọng là LLM không hoạt động một mình. Hệ thống còn kết hợp Rule-based, Pandas Filtering, Haversine Distance, Vector Search, Weighted Scoring, Health Constraints, Weather API và Backend Validation.
Trước đây khi nghĩ đến một "ứng dụng AI", mình thường hình dung đơn giản là người dùng gửi prompt và LLM trả lời. Nhưng sau khi làm đồ án, mình nhận ra LLM phù hợp nhất với những phần cần hiểu ngôn ngữ, suy luận trên ngữ cảnh và lựa chọn, còn các thông tin cần tính chính xác như khoảng cách, ngân sách, điểm số, dữ liệu nhà hàng hay constraint vẫn nên được kiểm soát bằng code và thuật toán.
XIII. Kết luận
BMI ban đầu chỉ xuất phát từ một ý tưởng khá đơn giản: người dùng nhập một câu và AI tự lên lịch trình ăn uống. Nhưng để biến ý tưởng đó thành một hệ thống hoạt động được, nhóm mình phải phân rã bài toán thành nhiều module khác nhau.
LLM chịu trách nhiệm hiểu người dùng muốn gì, trích xuất các điều kiện trong prompt, phân tích phản hồi và lựa chọn phương án phù hợp từ Top-K. Trong khi đó Backend và các thuật toán chịu trách nhiệm kiểm tra dữ liệu, ngân sách, khoảng cách, constraint và tính điểm.
Điều lớn nhất mình học được từ project này là: xây dựng một ứng dụng sử dụng LLM không có nghĩa là giao toàn bộ bài toán cho AI. Quan trọng hơn là xác định phần nào nên để LLM xử lý, phần nào nên giải quyết bằng thuật toán và cách kết hợp chúng thành một pipeline hoàn chỉnh.

Thảo luận bài viết
Câu hỏi, ghi chú và góc nhìn của bạn về nội dung này.