אתם רוצים לתכנן טיול לגואה. יש לכם חמישה ימים, תקציב של 25,000 רופי, והעדפה ברורה לחופים ולאוכל ים (seafood). בדרך כלל, זה אומר לפתוח עשרה טאבים בדפדפן, לקרוא פוסטים מיושנים בפורומים, ולהרכיב באופן ידני מסלול טיול. במקום זאת, דמיינו שליחת בקשת POST בודדת וקבלת תוכנית מובנית יום-יומית עם הצעות לארוחות, רשימות פעילויות וחלוקת תקציב מדויקת. זה מה שהפרויקט הזה מספק.
אנחנו נבנה REST API באמצעות Spring Boot ו-Azure OpenAI. ה-API מקבל יעד, תקציב, משך זמן ותחומי עניין. הוא מחזיר JSON נקי שניתן להציג באופן מיידי באפליקציית פרונטנד או אפליקציה לנייד. ללא scraping. ללא מסלולי טיול קשיחים (hardcoded). רק מודל AI שניתן לו Prompt לפעול כמתכנן טיולים.
מה ה-API מחזיר
התגובה אינה בלוק של טקסט Markdown שעליכם לנתח באמצעות regex. זהו אובייקט JSON מובנה המכיל פעילויות יומיות, המלצות לארוחות ופירוט תקציב. עבור טיול לגואה, אתם עשויים לקבל מקטע עבור היום הראשון שמקצה 500 רופי לארוחת בוקר בדוכן חוף (beach shack), בוקר ב-Palolem, וארוחת ערב של פירות ים באזור ספציפי. כל יום כולל טווחי שעות, עלויות משוערות ותגיות כמו "beach" או "food". המבנה הזה חשוב מכיוון שאפליקציות טיולים מודרניות אינן רוצות לנתח פסקאות. הן רוצות אובייקטים שהן יכולות למפות ל-RecyclerViews או לרכיבי React.
ה-Stack ולמה הוא מתאים
הפרויקט משתמש ב-Spring Boot 3.5 עם Spring AI. Spring AI הוא החלק הקריטי. הוא מספק הפשטה מאוחדת של ChatModel כך שלא תצטרכו לכתוב לקוחות HTTP גולמיים מול Azure OpenAI. אתם מחליפים dependencies ו-properties, ולא את קוד השירות.
אתם זקוקים לארבעה dependencies בקובץ ה-build שלכם:
spring-boot-starter-webעבור שכבת ה-REST.spring-ai-starter-model-azure-openaiכדי להתחבר ל-LLM דרך הממשק של Spring AI.springdoc-openapiעבור תיעוד Swagger אוטומטי.Lombokכדי לצמצם את ה-boilerplate ב-POJOs של הבקשות והתגובות שלכם.
Spring AI יושב בין הלוגיקה העסקית שלכם לבין ספק ה-LLM. המיקום הזה הוא מכוון. הוא שומר על מחלקות ה-@Service שלכם נקיות ובלתי תלויות בספק (provider-agnostic).
Prompt Engineering עם PromptTemplates
Hardcoding של prompts בתוך מחרוזות Java הוא דרך מהירה ליצור תוכנה שקשה לתחזק. אם צוות המוצר יחליט שה-AI צריך להישמע קליל יותר או לסרב להערכות תקציב מעל סף מסוים, אתם לא אמורים להצטרך לקמפל מחדש את השירות שלכם.
Spring AI מספק PromptTemplate. אתם שומרים את שלד ה-prompt בקובץ משאבים או במחרוזת תבנית ייעודית, ומשאירים מקומות (placeholders) למשתנים כמו {destination}, {budget}, {days}, ו-{interests}. בזמן ריצה, השירות יוצר אובייקט Prompt ומזריק את ערכי המשתמש.
הפרידו בין הודעות מערכת (system messages) להודעות משתמש (user messages). השתמשו בהודעת המערכת כדי להגדיר את ה-persona. לדוגמה, תגידו למודל שהוא מתכנן טיולים המתמחה ביעדים בהודו, מודע לתקציב, ומקפיד להחזיר רק JSON ללא markdown fences. השתמשו בהודעת המשתמש כדי להעביר את פרטי הטיול הספציפיים. הפיצול הזה עוזר כאשר תרצו מאוחר יותר לבצע A/B testing ל-personas מבלי לשנות את חוזה ה-API.
שכבת השירות (Service Layer): תקשורת עם Azure OpenAI
למחלקת ה-@Service יש עבודה אחת. היא בונה את ה-prompt, קוראת למודל, מנקה את התגובה ומנתחת (parses) את התוצאה.
הזריקו את ה-ChatClient או ה-ChatModel של Spring AI. הריצו את ה-PromptTemplate עם ערכי הבקשה הנכנסים, ואז קראו למתודת הצ'אט. התגובה מגיעה כ-String. כאן הרבה מדריכים עוצרים ומתחיל הקוד של סביבת Production אמיתית.
LLMs לפעמים מוסיפים פתיח מנומס. אתם עשויים לקבל תגובה שנפתחת ב-"Here is your itinerary" ואז מציפה JSON עטוף ב-triple backticks. אם תנסו לבצע deserialization לזה ישירות עם Jackson, האפליקציה שלכם תקרוס. הוסיפו מתודת עזר קטנה שסורקת את המחרוזת הגולמית, מוצאת את הסוגריים המסולסלים הפותחים הראשונים ואת הסוגריים המסולסלים הסוגרים האחרונים, ומחלצת רק את ה-JSON payload. לאחר מכן, ודאו את הבלוק שחולץ. בדקו ששדות חובה קיימים וערכים מספריים הגיוניים לפני שאתם מחזירים את האובייקט ל-controller.
הניתוח ההגנתי הזה אינו אופציונלי. הוא הגבול בין דמו לבין API אמין.
טיפול בשגיאות כמו מערכת בשלה
API חיצוניים נכשלים. Azure OpenAI יחזיר שגיאות של מגבלת קצב (rate limit), כשלי אימות, או שגיאות 500 זמניות. אם תתנו לשגיאות הללו לעלות למשתמש כ-stack traces, אתם תאבדו אמינות.
Use @RestControllerAdvice to intercept exceptions globally. Map Spring AI exceptions, HttpClientErrorException, and generic RuntimeExceptions to consistent error responses. Return a JSON body with a clear message, an HTTP status like 429 for rate limits, and enough detail for the client to retry or log the issue. The user should see something like "Service temporarily busy. Please retry in 30 seconds," not a screen full of Java class names.
Never Hardcode Secrets
Your Azure OpenAI API key does not belong in application.properties checked into Git. Externalize it. Use environment variables referenced in your Spring configuration, such as ${AZURE_OPENAI_KEY} and ${AZURE_OPENAI_ENDPOINT}. Keep a local .env file for development, add it to .gitignore, and load it through Spring Boot’s relaxed binding. If a key leaks, you rotate it in one place rather than rebuilding your artifact.
Testing Through Swagger
The springdoc-openapi dependency exposes a Swagger UI endpoint at runtime. Once your application starts, open /swagger-ui.html in a browser. You can fill in the Goa example directly: destination as "Goa," budget as 25000, days as 5, interests as "beaches, food." Hit execute and watch the JSON itinerary appear. This lets you validate prompt changes, verify serialization, and share a live playground with frontend developers before either side writes a unit test.
Swapping Providers Without Rewriting Code
Startups change providers. Maybe Azure credits expire, or you want to run inference against a local Ollama instance to cut costs. Because Spring AI abstracts the ChatModel interface, the swap is mechanical. Change the Maven dependency from spring-ai-starter-model-azure-openai to another starter, update your properties file with the new endpoint and key, and leave your service class alone. The API contract seen by your mobile app stays identical.
That portability makes this architecture particularly useful for real products. You are not marrying Azure. You are using it as one engine plugged into a clean Spring pipeline.
The Real Takeaway
An AI model is not your application. It is an external service that returns unpredictable text. Treat it with the same rigor you would give a payment gateway or a third-party weather API. Externalize your credentials. Validate every response. Clean the payload before parsing. Handle errors globally so your users never see a stack trace.
Let the AI handle the creative work of building a Goa itinerary on a 25,000-rupee budget. You handle the plumbing. When the two stay separate, you get a system that actually ships.
The original walkthrough that inspired this article can be found here.
Interested in discussing Spring AI and similar projects? Join the GyaanSetu learning community.
