Bạn muốn lên kế hoạch cho một chuyến đi đến Goa. Bạn có năm ngày, ngân sách 25.000 rupee, và sở thích rõ ràng là các bãi biển và hải sản. Thông thường, điều này có nghĩa là bạn phải mở mười tab trình duyệt, đọc các bài đăng cũ trên diễn đàn và tự tay chắp vá một lịch trình. Thay vào đó, hãy tưởng tượng việc gửi một yêu cầu POST duy nhất và nhận lại một kế hoạch chi tiết từng ngày có cấu trúc với các gợi ý bữa ăn, danh sách hoạt động và phân bổ ngân sách chính xác. Đó chính là những gì dự án này mang lại.
Chúng ta sẽ xây dựng một REST API sử dụng Spring Boot và Azure OpenAI. API này chấp nhận điểm đến, ngân sách, thời gian và sở thích. Nó trả về JSON sạch mà một ứng dụng frontend hoặc di động có thể hiển thị ngay lập tức. Không cào dữ liệu (scraping), không lịch trình viết cứng (hardcoded). Chỉ là một mô hình AI được yêu cầu đóng vai trò như một người lập kế hoạch du lịch.
Kết quả trả về của API
Phản hồi không phải là một khối văn bản Markdown mà bạn phải dùng regex để tách ra. Nó là một đối tượng JSON có cấu trúc chứa các hoạt động hàng ngày, đề xuất bữa ăn và phân bổ ngân sách. Đối với một chuyến đi Goa, bạn có thể nhận được một phần cho ngày đầu tiên phân bổ 500 rupee cho bữa sáng tại một quán ăn ven biển, một buổi sáng ở Palolem và một bữa tối hải sản vào buổi tối tại một khu vực cụ thể. Mỗi ngày đều có các khung giờ, chi phí ước tính và các thẻ như "beach" hoặc "food". Cấu trúc này rất quan trọng vì các ứng dụng du lịch hiện đại không muốn phân tích các đoạn văn bản. Họ muốn các đối tượng mà họ có thể ánh xạ vào RecyclerViews hoặc các thành phần React.
Công nghệ sử dụng và lý do lựa chọn
Dự án sử dụng Spring Boot 3.5 với Spring AI. Spring AI là thành phần quan trọng nhất. Nó cung cấp một sự trừu tượng hóa ChatModel thống nhất để bạn không phải viết các HTTP client thô cho Azure OpenAI. Bạn chỉ cần thay đổi các dependency và thuộc tính, chứ không phải thay đổi mã nguồn dịch vụ.
Bạn cần bốn dependency trong tệp build của mình:
spring-boot-starter-webcho lớp REST.spring-ai-starter-model-azure-openaiđể kết nối với LLM thông qua giao diện của Spring AI.springdoc-openapiđể tự động tạo tài liệu Swagger.Lombokđể giảm bớt mã lặp lại (boilerplate) trong các POJO request và response của bạn.
Spring AI nằm giữa logic nghiệp vụ và nhà cung cấp LLM. Vị trí đó là có chủ đích. Nó giữ cho các lớp @Service của bạn sạch sẽ và không phụ thuộc vào nhà cung cấp (provider-agnostic).
Kỹ thuật Prompt Engineering với PromptTemplates
Việc viết cứng (hardcoding) các prompt bên trong các chuỗi Java là một cách nhanh chóng để tạo ra phần mềm khó bảo trì. Nếu đội ngũ sản phẩm quyết định rằng AI nên có giọng điệu thân thiện hơn hoặc từ chối các ước tính ngân sách trên một ngưỡng nhất định, bạn sẽ không phải biên dịch lại dịch vụ của mình.
Spring AI cung cấp PromptTemplate. Bạn lưu trữ khung prompt trong một tệp tài nguyên hoặc một chuỗi template chuyên dụng, để lại các placeholder cho các biến như {destination}, {budget}, {days}, và {interests}. Tại thời điểm chạy (runtime), dịch vụ sẽ tạo một đối tượng Prompt và chèn các giá trị của người dùng vào.
Hãy tách biệt tin nhắn hệ thống (system messages) khỏi tin nhắn người dùng (user messages). Sử dụng tin nhắn hệ thống để định nghĩa persona (vai trò). Ví dụ, bạn bảo mô hình rằng nó là một người lập kế hoạch du lịch chuyên về các điểm đến ở Ấn Độ, chú trọng đến ngân sách và nghiêm ngặt trong việc chỉ trả về JSON mà không có các dấu bao markdown. Sử dụng tin nhắn người dùng để truyền các chi tiết cụ thể của chuyến đi. Sự phân tách này giúp ích khi sau này bạn muốn thực hiện A/B testing cho các persona mà không làm thay đổi hợp đồng API (API contract).
Lớp Service: Giao tiếp với Azure OpenAI
Lớp @Service chỉ có một nhiệm vụ. Nó xây dựng prompt, gọi mô hình, làm sạch phản hồi và phân tích kết quả.
Inject ChatClient hoặc ChatModel của Spring AI. Render PromptTemplate với các giá trị yêu cầu đầu vào, sau đó gọi phương thức chat. Phản hồi trả về dưới dạng một String. Đây là nơi mà nhiều hướng dẫn dừng lại, còn mã nguồn thực tế trong môi trường production thì bắt đầu.
Các LLM đôi khi thêm các lời chào hỏi lịch sự. Bạn có thể nhận được một phản hồi bắt đầu bằng "Here is your itinerary" và sau đó là một khối JSON được bao quanh bởi ba dấu backtick. Nếu bạn cố gắng deserialize trực tiếp bằng Jackson, ứng dụng của bạn sẽ bị crash. Hãy thêm một phương thức hỗ trợ nhỏ để quét chuỗi thô, tìm dấu ngoặc nhọn mở đầu tiên và dấu ngoặc nhọn đóng cuối cùng, sau đó chỉ trích xuất payload JSON. Sau đó, hãy xác thực khối đã trích xuất. Kiểm tra xem các trường bắt buộc có tồn tại không và các giá trị số có hợp lý không trước khi bạn trả đối tượng về cho controller.
Việc phân tích cú pháp phòng thủ (defensive parsing) này không phải là tùy chọn. Nó là ranh giới giữa một bản demo và một API đáng tin cậy.
Xử lý lỗi như một hệ thống chuyên nghiệp
Các API bên ngoài có thể gặp lỗi. Azure OpenAI sẽ trả về lỗi giới hạn tốc độ (rate limit), lỗi xác thực hoặc lỗi 500 tạm thời. Nếu bạn để những lỗi này hiển thị trực tiếp cho người dùng dưới dạng stack trace, bạn sẽ mất uy tín.
Sử dụng @RestControllerAdvice để chặn các ngoại lệ (exceptions) trên phạm vi toàn cục. Ánh xạ các ngoại lệ của Spring AI, HttpClientErrorException và các RuntimeException chung thành các phản hồi lỗi nhất quán. Trả về một thân JSON (JSON body) với thông báo rõ ràng, một mã trạng thái HTTP như 429 cho giới hạn tốc độ (rate limits), và đủ chi tiết để phía client có thể thử lại hoặc ghi lại nhật ký (log) sự cố. Người dùng nên thấy một thông báo kiểu như "Dịch vụ hiện đang bận. Vui lòng thử lại sau 30 giây," thay vì một màn hình đầy rẫy các tên lớp Java.
Không bao giờ mã hóa cứng các thông tin bí mật
Khóa API Azure OpenAI của bạn không nên nằm trong tệp application.properties được đẩy lên Git. Hãy đưa nó ra bên ngoài. Sử dụng các biến môi trường được tham chiếu trong cấu hình Spring của bạn, chẳng hạn như ${AZURE_OPENAI_KEY} và ${AZURE_OPENAI_ENDPOINT}. Hãy giữ một tệp .env cục bộ để phát triển, thêm nó vào .gitignore và tải nó thông qua cơ chế relaxed binding của Spring Boot. Nếu một khóa bị rò rỉ, bạn chỉ cần thay đổi (rotate) nó ở một nơi duy nhất thay vì phải xây dựng lại toàn bộ artifact của mình.
Kiểm thử thông qua Swagger
Dependency springdoc-openapi sẽ cung cấp một endpoint Swagger UI khi ứng dụng chạy. Sau khi ứng dụng của bạn khởi động, hãy mở /swagger-ui.html trong trình duyệt. Bạn có thể điền trực tiếp ví dụ về Goa: điểm đến là "Goa", ngân sách là 25000, số ngày là 5, sở thích là "beaches, food". Nhấn execute và theo dõi lịch trình JSON xuất hiện. Điều này cho phép bạn xác thực các thay đổi về prompt, kiểm tra quá trình serialization và chia sẻ một môi trường thử nghiệm trực tiếp (live playground) với các lập trình viên frontend trước khi bất kỳ bên nào viết unit test.
Thay đổi nhà cung cấp mà không cần viết lại mã nguồn
Các startup thường thay đổi nhà cung cấp. Có thể là do hết hạn tín dụng Azure, hoặc bạn muốn chạy suy luận (inference) trên một instance Ollama cục bộ để cắt giảm chi phí. Vì Spring AI trừu tượng hóa interface ChatModel, việc thay đổi này chỉ mang tính cơ học. Chỉ cần thay đổi Maven dependency từ spring-ai-starter-model-azure-openai sang một starter khác, cập nhật tệp properties với endpoint và khóa mới, và giữ nguyên service class của bạn. Hợp đồng API (API contract) mà ứng dụng di động của bạn nhìn thấy vẫn giữ nguyên không đổi.
Khả năng linh hoạt đó làm cho kiến trúc này đặc biệt hữu ích cho các sản phẩm thực tế. Bạn không "kết hôn" với Azure. Bạn chỉ đang sử dụng nó như một động cơ được cắm vào một đường ống (pipeline) Spring sạch sẽ.
Bài học cốt lõi
Một mô hình AI không phải là ứng dụng của bạn. Nó là một dịch vụ bên ngoài trả về văn bản không thể dự đoán trước. Hãy đối xử với nó bằng sự nghiêm ngặt tương tự như cách bạn đối xử với một cổng thanh toán hoặc một API thời tiết của bên thứ ba. Hãy đưa các thông tin xác thực (credentials) ra bên ngoài. Xác thực mọi phản hồi. Làm sạch payload trước khi phân tích (parsing). Xử lý lỗi trên phạm vi toàn cục để người dùng của bạn không bao giờ nhìn thấy stack trace.
Hãy để AI đảm nhận công việc sáng tạo là xây dựng lịch trình đi Goa với ngân sách 25.000 rupee. Bạn hãy đảm nhận phần hạ tầng (plumbing). Khi hai phần này tách biệt, bạn sẽ có một hệ thống thực sự có thể đưa vào vận hành (ship).
Bài hướng dẫn gốc truyền cảm hứng cho bài viết này có thể được tìm thấy tại đây.
Bạn quan tâm đến việc thảo luận về Spring AI và các dự án tương tự? Hãy tham gia cộng đồng học tập GyaanSetu.
