شما میخواهید برای یک سفر به گوا برنامهریزی کنید. پنج روز زمان دارید، بودجه شما ۲۵,۰۰۰ روپیه است و تمایل واضحی به سواحل و غذاهای دریایی دارید. در حالت عادی، این یعنی باز کردن ده تب در مرورگر، خواندن پستهای قدیمی در انجمنها و چیدن دستی یک برنامه سفر. در عوض، تصور کنید تنها با ارسال یک درخواست POST، یک برنامه روزانه ساختاریافته شامل پیشنهادهای غذایی، لیست فعالیتها و تقسیمبندی دقیق بودجه دریافت میکنید. این دقیقاً همان چیزی است که این پروژه ارائه میدهد.
ما یک REST API با استفاده از Spring Boot و Azure OpenAI خواهیم ساخت. این API مقصد، بودجه، مدت زمان و علایق را دریافت میکند و یک JSON تمیز برمیگرداند که یک اپلیکیشن فرانتاند یا موبایل میتواند بلافاصله آن را رندر کند. بدون Scraping، بدون برنامههای سفر از پیش نوشته شده. فقط یک مدل هوش مصنوعی که با پرامپت (Prompt) هدایت شده تا به عنوان یک برنامهریز سفر عمل کند.
آنچه API برمیگرداند
پاسخ، یک بلوک متن Markdown نیست که مجبور باشید با regex آن را تجزیه کنید. بلکه یک شیء JSON ساختاریافته است که شامل فعالیتهای روزانه، توصیههای غذایی و جزئیات بودجه میباشد. برای یک سفر به گوا، ممکن است بخشی مربوط به روز اول دریافت کنید که ۵۰۰ روپیه را برای صبحانه در یک کافه ساحلی، یک صبح در Palolem و یک شام دریایی در منطقهای خاص اختصاص داده است. هر روز شامل بازههای زمانی، هزینههای تخمینی و برچسبهایی مانند "beach" یا "food" است. این ساختار اهمیت زیادی دارد، زیرا اپلیکیشنهای مدرن سفر نمیخواهند پاراگرافها را تجزیه کنند؛ آنها اشیایی میخواهند که بتوانند آنها را به RecyclerViewها یا کامپوننتهای React نگاشت کنند.
تکنولوژیهای مورد استفاده و دلیل انتخاب آنها
این پروژه از Spring Boot 3.5 به همراه Spring AI استفاده میکند. Spring AI بخش حیاتی این پروژه است. این ابزار یک انتزاع (abstraction) واحد از ChatModel ارائه میدهد تا مجبور نباشید برای Azure OpenAI کلاینتهای HTTP خام بنویسید. شما وابستگیها و ویژگیها را تغییر میدهید، نه کد سرویس را.
شما به چهار وابستگی در فایل build خود نیاز دارید:
spring-boot-starter-webبرای لایه REST.spring-ai-starter-model-azure-openaiبرای اتصال به LLM از طریق رابط Spring AI.springdoc-openapiبرای مستندات خودکار Swagger.Lombokبرای کاهش کدهای تکراری (boilerplate) در POJOهای درخواست و پاسخ.
Spring AI بین منطق تجاری (business logic) شما و ارائهدهنده LLM قرار میگیرد. این جایگذاری عامدانه است؛ زیرا باعث میشود کلاسهای @Service شما تمیز و مستقل از ارائهدهنده (provider-agnostic) باقی بمانند.
مهندسی پرامپت با استفاده از PromptTemplates
قرار دادن مستقیم (Hardcoding) پرامپتها در رشتههای جاوا، راه سریعی برای ساخت نرمافزاری است که نگهداری آن دشوار باشد. اگر تیم محصول تصمیم بگیرد که هوش مصنوعی باید لحن دوستانهتری داشته باشد یا از ارائه برآورد بودجه بالاتر از یک حد مشخص خودداری کند، شما نباید مجبور باشید سرویس خود را دوباره کامپایل کنید.
Spring AI کلاس PromptTemplate را ارائه میدهد. شما اسکلت پرامپت را در یک فایل resource یا یک رشته قالب (template string) اختصاصی ذخیره میکنید و جایگاههایی (placeholders) برای متغیرهایی مانند {destination}، {budget}، {days} و {interests} باقی میگذارید. در زمان اجرا، سرویس یک شیء Prompt ایجاد کرده و مقادیر کاربر را در آن تزریق میکند.
پیامهای سیستم (system messages) را از پیامهای کاربر (user messages) جدا کنید. از پیام سیستم برای تعریف پرسونا (persona) استفاده کنید. به عنوان مثال، به مدل میگویید که یک برنامهریز سفر متخصص در مقاصد هند، حساس به بودجه و دقیق در بازگرداندن فقط JSON بدون استفاده از markdown fences است. از پیام کاربر برای ارسال جزئیات دقیق سفر استفاده کنید. این تفکیک زمانی که بخواهید پرسوناها را بدون تغییر در قرارداد API تست کنید (A/B test)، بسیار کمککننده خواهد بود.
لایه سرویس: گفتگو با Azure OpenAI
کلاس @Service تنها یک وظیفه دارد: ساخت پرامپت، فراخوانی مدل، پاکسازی پاسخ و تجزیه نتیجه.
ChatClient یا ChatModel مربوط به Spring AI را تزریق کنید. PromptTemplate را با مقادیر درخواست ورودی رندر کنید و سپس متد chat را فراخوانی کنید. پاسخ به صورت یک String میرسد. اینجاست که بسیاری از آموزشها متوقف میشوند و کد واقعیِ محیط عملیاتی (production) شروع میشود.
مدلهای زبانی بزرگ (LLMs) گاهی اوقات مقدمههای مؤدبانهای اضافه میکنند. ممکن است پاسخی دریافت کنید که با "Here is your itinerary" شروع شده و سپس یک JSON را که در میان سه بکتیک (triple backticks) محصور شده، رها کند. اگر سعی کنید آن را مستقیماً با Jackson تجزیه (deserialize) کنید، اپلیکیشن شما کرش میکند. یک متد کمکی کوچک اضافه کنید که رشته خام را اسکن کرده، اولین آکولاد باز و آخرین آکولاد بسته را پیدا کند و فقط محتوای JSON را استخراج نماید. سپس بلوک استخراج شده را اعتبارسنجی کنید. قبل از بازگرداندن شیء به کنترلر، بررسی کنید که فیلدهای مورد نیاز وجود داشته باشند و مقادیر عددی منطقی باشند.
این تجزیه دفاعی (defensive parsing) اختیاری نیست؛ بلکه مرز بین یک نسخه دموی ساده و یک API قابل اعتماد است.
مدیریت خطاها مانند یک سیستم بالغ
رابطهای برنامهنویسی (API) خارجی ممکن است با خطا مواجه شوند. Azure OpenAI ممکن است خطاهای محدودیت نرخ (rate limit)، خطاهای احراز هویت یا خطاهای گذرا (transient 500s) را برگرداند. اگر اجازه دهید این خطاها به صورت stack trace به کاربر نمایش داده شوند، اعتبار خود را از دست میدهید.
از @RestControllerAdvice برای مدیریت سراسری استثناها (exceptions) استفاده کنید. استثناهای Spring AI، HttpClientErrorException و RuntimeExceptionهای عمومی را به پاسخهای خطای یکپارچه نگاشت کنید. یک بدنه JSON شامل پیامی واضح، یک وضعیت HTTP مانند 429 برای محدودیت نرخ (rate limits) و جزئیات کافی برای اینکه کلاینت بتواند دوباره تلاش کند یا مشکل را ثبت (log) کند، بازگردانید. کاربر باید چیزی شبیه به "سرویس موقتاً مشغول است. لطفاً ۳۰ ثانیه دیگر دوباره تلاش کنید" را ببیند، نه صفحهای پر از نام کلاسهای جاوا.
هرگز اطلاعات حساس (Secrets) را به صورت Hardcode وارد نکنید
کلید API مربوط به Azure OpenAI نباید در فایل application.properties که در Git ذخیره میشود، قرار بگیرد. آن را خارجیسازی (Externalize) کنید. از متغیرهای محیطی (environment variables) که در تنظیمات Spring به آنها ارجاع داده شده است، مانند ${AZURE_OPENAI_KEY} و ${AZURE_OPENAI_ENDPOINT} استفاده کنید. برای توسعه، یک فایل .env محلی نگه دارید، آن را به .gitignore اضافه کنید و از طریق relaxed binding در Spring Boot بارگذاری کنید. اگر کلیدی لو رفت، به جای بازسازی کل محصول (artifact)، فقط آن را در یک نقطه تغییر (rotate) میدهید.
تست از طریق Swagger
وابستگی (dependency) springdoc-openapi یک نقطه اتصال (endpoint) Swagger UI را در زمان اجرا فراهم میکند. پس از شروع برنامه، /swagger-ui.html را در مرورگر باز کنید. میتوانید مستقیماً مثال Goa را پر کنید: مقصد "Goa"، بودجه 25000، تعداد روزها 5 و علایق "beaches, food". روی execute کلیک کنید و مشاهده کنید که برنامه سفر (itinerary) به صورت JSON ظاهر میشود. این کار به شما اجازه میدهد تغییرات پرامپت (prompt) را اعتبارسنجی کنید، سریالسازی (serialization) را بررسی کنید و یک محیط تست زنده (live playground) را با توسعهدهندگان فرانتاند به اشتراک بگذارید، پیش از آنکه هر دو طرف تست واحد (unit test) بنویسند.
تعویض ارائهدهندگان بدون بازنویسی کد
استارتاپها ارائهدهندگان خود را تغییر میدهند. شاید اعتبار Azure شما تمام شود، یا بخواهید برای کاهش هزینهها، استنتاج (inference) را روی یک نمونه محلی Ollama اجرا کنید. از آنجایی که Spring AI رابط ChatModel را انتزاع (abstract) میکند، این تعویض صرفاً یک تغییر مکانیکی است. وابستگی Maven را از spring-ai-starter-model-azure-openai به یک starter دیگر تغییر دهید، فایل properties خود را با endpoint و کلید جدید بهروزرسانی کنید و به کلاس سرویس خود کاری نداشته باشید. قرارداد API که اپلیکیشن موبایل شما میبیند، بدون تغییر باقی میماند.
این قابلیت جابهجایی (portability)، این معماری را بهویژه برای محصولات واقعی مفید میکند. شما با Azure ازدواج نکردهاید؛ بلکه از آن به عنوان یک موتور که به یک خط لوله (pipeline) تمیز Spring متصل شده است، استفاده میکنید.
نتیجهگیری اصلی
یک مدل هوش مصنوعی، اپلیکیشن شما نیست. آن یک سرویس خارجی است که متنی غیرقابل پیشبینی برمیگرداند. با آن با همان دقتی برخورد کنید که با یک درگاه پرداخت یا یک API هواشناسی شخص ثالث برخورد میکنید. اطلاعات احراز هویت (credentials) خود را خارجیسازی کنید. هر پاسخ را اعتبارسنجی کنید. قبل از تجزیه (parsing)، دادهها (payload) را پاکسازی کنید. خطاها را به صورت سراسری مدیریت کنید تا کاربران شما هرگز با stack trace مواجه نشوند.
اجازه دهید هوش مصنوعی کار خلاقانه ساختن برنامه سفر Goa با بودجه ۲۵,۰۰۰ روپیه را انجام دهد. شما زیرساختها (plumbing) را مدیریت کنید. وقتی این دو بخش مجزا باقی بمانند، سیستمی خواهید داشت که واقعاً قابل عرضه (ship) باشد.
راهنمای اصلی که الهامبخش این مقاله بود را میتوانید در اینجا پیدا کنید.
علاقهمند به بحث درباره Spring AI و پروژههای مشابه هستید؟ به جامعه یادگیری GyaanSetu بپیوندید.
