Agentning vositalarni bajarish yo‘lini mahalliy vosita chaqiruviga o‘tkazish

Agentning vositalarni bajarish yo‘lini mahalliy vosita chaqiruviga o‘tkazish

“Model JSON formatiga amal qilsa, hammasi yaxshi bo‘ladi”, deb o‘ylagan paytimda haqiqiy muammoni hal qilish boshlandi.

Foydalanuvchilar xizmatlarni tabiiy tilda boshqara olishi uchun ichki platformamizga AI agentini qo‘shish ustida ishladik. Tizim ikkita xizmatga bo‘lingan edi. MCP serveri vositalar ro‘yxati, siyosatlar, tasdiqlashlar va auditni boshqarar, agent hosti esa suhbatni qayta ishlash, LLM chaqiruvlari va vositalarni bajarish sikliga mas’ul edi. Bitta asosiy tamoyil bor edi: fikrlashni umumiy maqsadli LLM’ga topshirish, ammo bajarilish nazoratini platforma qo‘lida saqlash.

Dastlabki yechim ishladi. Ammo “ishlaydi” degani “ishonchli” degani emas edi. Sikl qarorlari promptga kiritilgan vositalar ro‘yxatiga va model matn sifatida yozgan JSON’ni bevosita tahlil qilishga bog‘liq edi. Format bir marta bo‘lsa ham noto‘g‘ri chiqsa, sikl qayta urinib ko‘rar, so‘ng birorta vositani chaqirmasdan to‘xtardi.

Ushbu maqolada promptdagi JSON’ni tahlil qilishga asoslangan vositalar siklini provayderning o‘ziga xos vosita chaqiruvi bilan qanday almashtirganimiz, shuningdek, qo‘lda yozilgan LLM chaqiruvlari va MCP mijozini Spring AI 2.0 bilan qanday almashtirganimiz bayon qilinadi. Muammolar bir nechta edi, ammo ularning ildiz sababi bitta edi. Bajarilish qatlamini to‘liq almashtirgan bo‘lsak-da, nazorat qatlamidagi birorta qator kodni o‘zgartirmadik. Bu ishning muhim jihatlari shular edi.

1. Texnologiyani tanlashga zamin — promptdagi JSON’ni tahlil qilish nega samara bermadi

Almashtirishga qaror qilishdan oldin, allaqachon ishlayotgan siklni batafsil ko‘rib chiqdik. Ko‘rib chiqish davomida quyidagi muammolar aniqlandi.

  • Qarorlar formatga bog‘lanib qolgan edi. Vositalar ro‘yxati promptga matn sifatida qo‘shilar, model matn shaklida yozgan {"action":"call_tool", ...} kabi JSON esa bevosita tahlil qilinardi. Format noto‘g‘ri bo‘lsa, sikl ikki marta qayta urinib ko‘rar, so‘ng vositani chaqirmasdan to‘xtardi.

  • Kuzatuvlar cheksiz yig‘ilib borardi. Har bir bosqichda oldingi barcha kuzatuvlarning to‘liq matni yangi foydalanuvchi xabarini tuzish uchun yana qo‘shilardi. Bosqichlar soni oshgani sari prompt ham mutanosib ravishda kattalashardi.

  • Vosita chaqirmasdan darhol javob berishning oldini oladigan himoya yo‘q edi. Tizim promptida shunchaki “Biznesga oid ma’lumotlarni to‘qib chiqarmang” deyilgan, ammo buni ta’minlaydigan kod yo‘q edi.

  • 40 dan ortiq vosita bor edi va ularning barchasi har bir so‘rov bilan yuborilardi. Faqat vositalar ro‘yxatining o‘zi har bir bosqichda taxminan 4 000 token egallardi.

  • MCP mijozini xom HttpClient va JSON-RPC yordamida o‘zimiz yozdik. initialize’dan keyin notifications/initialized xabarini yubormaganimiz sababli protokolni buzish xavfi bor edi.

Dastlab bu bandlarning har birini alohida takomillashtirish vazifasi sifatida belgiladik. Ammo sabablarini izlaganimizda, dastlabki uchtasining ildiz sababi bitta ekani ma’lum bo‘ldi: LLM chaqiruv porti bo‘lgan AgentAskRequest atigi ikkita satrdan — prompt va systemPrompt’dan iborat edi.

[Muammoning ildizi — atigi ikkita satrdan iborat dvigatel porti]

// Before: the engine port had no notion of a messages array.
public class AgentAskRequest {
    private String prompt;        // question + tool list + observations, all in here
    private String systemPrompt;  // rules go here
}

Tuzilmada xabarlar massivi tushunchasi bo‘lmagani uchun suhbat tarixini joylashtirish yoki vositalar ro‘yxatini kiritish imkoni yo‘q edi. Natijada vositalar ro‘yxati va kuzatuvlarni promptga satrlar sifatida qo‘shishdan boshqa chora qolmadi. Boshqacha aytganda, tuzilma shunchaki mahalliy vosita chaqiruvidan foydalanmagan emas, balki undan foydalanishni imkonsiz qilgan edi.

Xulosa aniq edi. Muammolarni birma-bir tuzatish o‘rniga, dvigatel portini xabarlar + vositalar modeliga yangilashga va bajarilish yo‘lini provayderning o‘ziga xos vosita chaqiruviga o‘tkazishga qaror qildik. Shu bilan birga, provayderga qo‘lda yuboriladigan REST chaqiruvlari va MCP mijozini Spring AI 2.0 bilan almashtirishga kelishdik. spring-ai-bom:2.0.0 allaqachon asosiy build.gradle faylida e’lon qilingan bo‘lsa-da, mijoz tomonidagi modul Spring AI’ning birorta sinfini import qilmas, ayni funksionallikni qo‘lda qayta amalga oshirayotgan edi.

2. Amalga oshirish jarayoni

Ish to‘rt bosqichda olib borildi: mas’uliyat chegaralarini belgilash, dvigatel portini almashtirish, vositalar nomlari va ro‘yxatini boshqarish hamda nazorat qatlami o‘zgarmaganini tekshirish. Har bir bosqichni mustaqil ravishda ortga qaytarish mumkin bo‘ladigan tartibda bajardik.

2.1 Mas’uliyat chegaralarini belgilash — nimani kutubxonaga topshirish, nimani o‘zimizda saqlash kerak

Har qanday kodni ko‘chirishdan oldin chegaralarni belgilab oldik. Vazifa kutubxona bilan almashtirish mumkin bo‘lgan narsalarni o‘z zimmamizda saqlashimiz kerak bo‘lgan narsalardan ajratish edi.

Toifa

Spring AI bajaradi

Bevosita agent bajaradi

Qamrov

LLM chaqiruvlari, xabarlar va tool_calls’ni o‘zgartirish, MCP protokoli/mijozi, vositalar sxemasi, ko‘rsatkichlar va kuzatuv

Siklning borish siyosati, vositalarga ruxsat berish siyosati, xavf va tasdiqlash, chaqiruvlar soni va vaqt cheklovlari, audit, SSE hodisalari

Qoida

Protokollar va transport — standartlari mavjud, o‘zimiz yaxshiroq bajaramiz deb hisoblashga asos yo‘q bo‘lgan sohalar

Nazorat va mas’uliyat — platformamiz siyosatlari boshqaradigan sohalar

Bu jadval keyingi barcha qarorlar uchun asos bo‘ldi. “Buni qayerga joylashtirish kerak?” degan savol tug‘ilganda, gap standartlar haqidami yoki siyosat haqidami, deb so‘rash orqali javobni aniqlardik.

2.2 Dvigatel portini almashtirish — ChatModel’ni bevosita chaqirish, ammo bajarilishni topshirmaslik

Spring AI’ni joriy etishda eng ko‘p e’tibor bergan jihatimiz avtomatik vosita bajarilishi bo‘ldi. Spring AI’da model so‘ragan vositalarni freymvork avtomatik bajaradigan yo‘l mavjud. Bu qulay, ammo shu yo‘ldan foydalanish siyosatlarimizni, tasdiqlashlarni va auditni chetlab o‘tgan bo‘lardi.

Spring AI 2.0’ning ChatModel qatlami faqat ToolCallingManager.resolveToolDefinitions’ni chaqirishini, executeToolCalls’ni esa chaqirmasligini tasdiqladik. Boshqacha aytganda, ChatModel bevosita chaqirilsa, vositalar avtomatik bajarilmaydi. Shu xususiyatdan foydalanib, vosita ta’riflarini faqat sxema sifatida uzatadigan va ularni bajarmasdan bajarish niyatlarini qaytaradigan adapter qo‘shdik.

[Faqat bajarish niyatlarini qabul qiladigan dvigatel porti]

// The engine returns only the model's tool-call intent. It never executes.
public interface NativeToolCallingEngine extends AgentEngine {

    AgentNativeToolResponse askWithTools(AgentNativeToolRequest request);
}

// Calling ChatModel directly: ToolCallback only supplies the schema, it is not run.
ChatResponse chatResponse = chatModel.call(new Prompt(nativeMessages(request), options));
AssistantMessage assistant = chatResponse.getResult().getOutput();
List<AgentNativeToolCall> toolCalls = toolCalls(assistant);   // structured tool_calls

Avvalgi bosqichdagi vosita chaqiruvlari va natijalarini yana satr sifatida qo‘shish o‘rniga, ularni AssistantMessage va ToolResponseMessage ko‘rinishida xabarlar massivida jamladik. Kuzatuvlarning haddan tashqari yig‘ilishi muammosini promptni qayta tuzishdan uning o‘z o‘rniga — suhbatlar massiviga ko‘chirdik.

Bir nechta vosita chaqiruvini qabul qilib, ularni ketma-ket bajardik va har bir tool_call_id javob olishini ta’minladik. Ruxsat berilmagan vositalar va takroriy chaqiruvlar siklni to‘xtatadigan istisnolarni keltirib chiqarmadi; aksincha, ular ERROR: bilan boshlanuvchi kuzatuvlar sifatida qaytarildi, shunda model yangi qaror qabul qilishi mumkin edi.

2.3 Vositalar nomlari va ro‘yxatlari — cheklovlarni faqat chegarada hal qilish

O‘tish jarayonida amaliy bir cheklovga duch keldik. Vositalar nomi navi.<service>.<verb> formatida edi, ammo OpenAI uslubidagi funksiya nomlari spetsifikatsiyasi nuqta (.) ishlatishga ruxsat bermasdi.

Nomlash sxemasini o‘zgartirish mumkin edi, ammo bu MCP, jurnallar, siyosatlar va auditlarda ishlatiladigan nomlarni ham o‘zgartirgan bo‘lardi. Cheklov faqat bitta joyga — LLM’ga uzatiladigan maydonga — tegishli bo‘lgani uchun nomlarni faqat shu chegarada, moslik jadvali yordamida o‘zgartirishga qaror qildik.

[Nomlash cheklovini bitta chegarada hal qilish]

// Convert the name only at the LLM boundary.
// MCP, policy and audit keep the original name.
private String nativeName(String qualifiedName) {
    String sanitized = qualifiedName.replaceAll("[^A-Za-z0-9_-]", "_");
    if (sanitized.length() <= MAX_NATIVE_NAME_LENGTH) return sanitized;
    String hash = String.format("%08x", qualifiedName.hashCode());
    return sanitized.substring(0, MAX_NATIVE_NAME_LENGTH - hash.length() - 1) + "_" + hash;
}

// If two names collapse into one, fail fast instead of silently overwriting.
String previous = nativeNames.putIfAbsent(nativeName, qualifiedName);
if (previous != null && !previous.equals(qualifiedName)) {
    throw new AgentNativeToolException("Native tool name collision: " + previous);
}

Maksimal uzunlik talabiga mos kelish uchun nomlarni ba’zan qisqartirish kerak bo‘lgani sababli, turli vositalarning nomlari bir xil qisqa nomga aylansa, tizim darhol istisno chiqaradigan qildik. Bunday to‘qnashuv jimgina e’tiborsiz qoldirilsa, u ancha keyin, “model noto‘g‘ri vositani chaqirdi” degan holat yuz bergandagina aniqlanishi mumkin.

Vositalar ro‘yxatining o‘zini ham qisqartirdik. Promptdagi sohaga oid atamalar asosida ro‘yxatni toraytiradigan, ammo moslik topilmasa to‘liq ro‘yxatni saqlab qoladigan tanlagich qo‘shdik. Shuningdek, salomlashish yoki yordam so‘rash kabi vositalar kerak bo‘lmagan so‘rovlarga MCP seansini ochishdan oldin javob beramiz va vositalar katalogi uchun qisqa TTL’li keshdan foydalanamiz.

2.4 Nazorat qatlamiga tegmaslik

Bu ishning eng muhim mezoni unumdorlik emas, balki “nima o‘zgarmadi?” degan savol edi. Butun bajarilish qatlamini almashtirayotganimiz sababli, siyosatlar, tasdiqlashlar va auditni boshqaradigan ToolExecutionGateway’ga kiritilgan har qanday o‘zgarish chegaralarni noto‘g‘ri belgilaganimizni anglatgan bo‘lardi.

Ma’lum bo‘lishicha, ToolExecutionGateway’da birorta qator ham o‘zgarmadi. Bajarish usuli promptdagi JSON’ni tahlil qilishdan mahalliy vosita chaqiruviga o‘zgardi, ammo gateway nuqtayi nazaridan unga hanuz “server id + vosita nomi + argumentlar” kelardi.

Shunga qaramay, zaxira yo‘lni saqlab qoldik. Mahalliy vosita chaqiruvi faqat formatni kafolatlaydi; bo‘sh javoblar va modelning sukut saqlashi ehtimoli hamon bor. Biroq sikl rejimi va finishReason’ni SSE yakunlash hodisasi hamda audit jurnalida birga qayd etadigan qildik, shunda yakunlashgacha bosib o‘tilgan yo‘l doimo yozib boriladi.

3. Sinov va xatolar orqali o‘rgangan saboqlar

Birinchidan, alomatlarni sanashdan ko‘ra ildiz sababni topish tezroq. Dastlab kuzatuvlarning ko‘payib ketishi, format xatolari va argumentlarni tekshirishni alohida muammolar sifatida belgiladik. Uchalasi ham bitta cheklovdan — dvigatel interfeysi atigi ikkita satrdan iborat bo‘lganidan — kelib chiqqan edi va ildiz sababni tuzatish uchtasini ham birdan hal qildi. Har birini alohida ko‘rib chiqqanimizda, uchta muammoni tuzatib, to‘rtinchisiga duch kelgan bo‘lardik.

Ikkinchidan, LLM integratsiyasi portida suhbatlar massivi boshidanoq asosiy tushuncha sifatida qaralishi kerak. Hatto dastlab bir martalik savol-javobdan boshlasangiz ham, xabarlarni standart ma’lumotlar tuzilmasi sifatida ishlatish keyinroq vositalar yoki ko‘p bosqichli suhbatlarni qo‘shish uchun interfeysni tubdan qayta ishlash zaruratini yo‘qotadi. Aslida, bu o‘tish ishining yarmi funksionallik qo‘shishga emas, interfeysni tuzatishga ketdi.

Uchinchidan, bog‘liqlikni shunchaki e’lon qilish bilan undan to‘liq foydalanilgan bo‘lmaydi; u amalda ishlatilgandagina joriy etilgan hisoblanadi. BOM allaqachon e’lon qilingan edi, ammo mijoz moduli ayni funksionallikni hanuz qo‘lda amalga oshirardi va bu holat bir muddat davom etdi. Qo‘lda yozilgan JSON-RPC joriy etilishi olib tashlangach, notifications/initialized xabarini yubormaslikdan kelib chiqqan xato ham ildizida bartaraf bo‘ldi.

To‘rtinchidan, qulay avtomatlashtirish yo‘lidan foydalanishdan oldin, u nazorat chegaralaringizni chetlab o‘tishi mumkinmi, tekshiring. Freymvorkning vositalarni avtomatik bajarishi jozibador, ammo bu yo‘ldan foydalanish tasdiqlash yoki auditsiz bajarilishlarga imkon bergan bo‘lardi. Uni qabul qilishga qaror qilishdan oldin, ChatModel qatlami kodda vositalarni bajarmasligini tasdiqladik.

Beshinchidan, zaxira yo‘llari ishlatilmagani sababli asta-sekin eskirib qoladi. Keyinchalik tozalash ishlarida jumlalarni muntazam ifodalar yordamida vositalarga bevosita bog‘laydigan qisqa yo‘lni ham, JSON qaror siklini ham olib tashladik. Har ikkala yo‘l ham «ehtimol kerak bo‘lib qolar» degan maqsadda yaratilgan edi, ammo amalda ularning konfiguratsiyasi bo‘sh bo‘lgani uchun ular hech qachon ishlatilmagan va biz saqlab turishimiz kerak bo‘lgan oqimlar sonini ikki baravar oshirgan, xolos.

4. Natijalar

Migratsiya natijalarini quyidagicha umumlashtirish mumkin.

Band

Oldin

Keyin

Qaror yo‘li

Prompt JSON’ini tahlil qilish + ikki marta qayta urinish

Provayderning o‘z vosita chaqiruvi (tuzilmali tool_calls)

Suhbat holati

Har bir bosqichda kuzatuv transkriptini satr sifatida qayta yig‘ish

Assistant va vosita xabarlarini messages massivida to‘plash

Vositalar ro‘yxati

Har bir bosqichda 40 dan ortiq vositani, taxminan 4 000 tokenni yuborish

Domen bo‘yicha tanlandi va TTL keshi ishlatildi; zarur bo‘lmaganda MCP o‘tkazib yuborildi

MCP mijozi

Raw HttpClient + maxsus JSON-RPC amalga oshirishi

Spring AI 2.0 (protokolni tashlab ketish xatosi tuzatildi)

Boshqaruv qatlami

Siyosat, tasdiqlash va audit shlyuzi

O‘zgarish yo‘q (0 qator)

Endi mahalliy va zaxira yo‘llarning ikkalasi ham transport uchun Spring AI’dan foydalanadi, shu sababli provayder kodi ikkita alohida amalga oshirishga bo‘linmadi. Build va testlar muvaffaqiyatli o‘tdi, shuningdek, birlik testlarining yetti turini qo‘shdik.

Buning yana bir qo‘shimcha foydasi ham bo‘ldi. Mahalliy vosita chaqiruvi talab qiladigan messages massivi suhbat seanslari uchun kerak bo‘lgan ma’lumotlar tuzilmasining aynan o‘zi. Shu bois bu migratsiya suhbat xotirasini joriy etish uchun ham poydevor yaratdi. Keyinchalik bir nechta seans uchun suhbat tarixini qo‘shganimizda server shartnomasini qayta loyihalashga hojat qolmadi.

5. Cheklovlar va kelgusi rejalar

Birinchidan, biz hali ham modelga bog‘liqmiz. Ichkarida ishlatiladigan model bir vaqtning o‘zida bitta vosita chaqiruvini bajaradimi yoki bir nechta chaqiruvni amalga oshiradimi, buni o‘lchashimiz kerak edi. Siyosatga kiritilgan parallel bajarish bayrog‘i esa siklda hanuz ishlatilmayapti. Parallel bajarishni asta-sekin yoqish, avvaliga uni faqat o‘qish uchun mo‘ljallangan vositalar bilan cheklash eng xavfsiz yo‘l, deb hisoblaymiz.

Ikkinchidan, domen lug‘atidan foydalanib vositalar ro‘yxatini toraytirish domenlar soni va ular orasidagi umumiy lug‘at oshgani sari o‘z cheklovlariga ega bo‘ladi. Keyingi ishlarimizda bu yondashuvni barcha vositalarni to‘liq ko‘rsatish o‘rniga vositalarni indekslash va nomi bo‘yicha yuklash tomonga kengaytirdik. Bu haqda alohida yozishni rejalashtirganmiz.

Uchinchidan, modelga vosita natijalarini o‘z so‘zlari bilan ifodalashga ruxsat berar ekanmiz, taqdimot har safar o‘zgarib turadi. Shu sababli server ko‘rsatish kontraktini taqdim etadigan, UI esa uni o‘zgartirmasdan aks ettiradigan yondashuvga o‘tdik.

Bu ish agentni o‘zimiz yaratish inference’ni ham o‘zimiz yaratishimiz kerak degani emasligini yana bir bor tasdiqladi. Qarorlarni doimo LLM qabul qiladi; biz esa siklni, vositalar bilan ulanishlarni, shuningdek, siyosatlar va auditni yaratamiz. Kod tuzilmasida bu chegarani aniq belgilash orqali boshqaruv qatlamining birorta qatoriga tegmasdan butun bajarish qatlamini almashtira oldik.

Manbalar

Junny

Site footer