Spring AI · Agent əsaslı iş axınları və Embabel · Dərs 2
Spring AI ilə agent iş axınının qurulması
Spring AI ilə geniş istifadə olunan agent iş axını nümunələrini qur. Spring AI üzərində qurulmuş Embabel ilə məqsədə çatmaq üçün avtomatik planlaşdırmanı araşdır.
Spring AI ilə agent iş axınının qurulması
1. Modelə iş axını daxilində məzmun təklif etdirmək
Spring AI model və alət abstraksiyalarını verir. Hansı çağırışın ediləcəyinə və nəticənin nəyi dəyişə biləcəyinə yenə tətbiq qərar verir. agents-lab layihəsini davam etdirin. Mövcud vəziyyət maşını cəhd hədlərini, təsdiqi, icazəni və saxlamanı idarə edir. Yalnız onun DraftSource hissəsini əvəz edirik.
Model dəstəklənən mövzunu, dəqiq müddəti və cəhd nömrəsini alır. Tələbə ID-si, tenant, təsdiq qeydi, saxlama açarı və məlumatı yaza bilən alət almır. Tətbiq yoxlayana qədər yaradılmış variant etibar edilməyən məzmundur. Struktur baxımından düzgün variant da insan yoxlaması tələb edir.
2. Spring AI adapterini qurun
Bu reallaşdırmanı saxlayın. BeanOutputConverter yalnız format təlimatlarını verir. Cavab bizim sərt Jackson mapper ilə parse olunur. Buna görə converter-in parsing yolu xam cavabı loglamır. Sabit exception-lar provayder və ya parser təfərrüatlarını açıq xəta yoluna daşımır.
src/main/java/academy/study/agents/SpringDraftSource.java
package academy.study.agents;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.converter.BeanOutputConverter;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
public final class SpringDraftSource implements StudyWorkflow.DraftSource {
private final ChatModel model;
private final JsonMapper json = JsonMapper.builder()
.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION)
.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS)
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES)
.enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
.enable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
.disable(MapperFeature.ALLOW_COERCION_OF_SCALARS).build();
private final String format = new BeanOutputConverter<>(StudyWorkflow.Draft.class).getFormat();
public SpringDraftSource(ChatModel model) { this.model = model; }
@Override public StudyWorkflow.Draft create(StudyWorkflow.Request request, int attempt) {
if (request == null || request.topic() == null || request.minutes() < 5 || request.minutes() > 60
|| attempt < 1 || attempt > 2) throw StudyWorkflow.rejected();
try {
String data = json.writeValueAsString(Map.of("topic", request.topic(),
"minutes", request.minutes(), "attempt", attempt));
var options = ToolCallingChatOptions.builder().internalToolExecutionEnabled(false)
.toolCallbacks(List.of()).toolNames(Set.of()).toolContext(Map.of()).maxTokens(250).build();
var prompt = new Prompt(List.of(new SystemMessage("""
Draft a short study plan for the requested topic and exact duration.
Return 1 to 3 concrete study steps, each at most 120 characters.
You propose content only. You cannot approve, save, change permissions or call tools.
On a second attempt, make sure the requested structure and constraints are met.
Return one JSON object without Markdown.
""" + format), new UserMessage(data)), options);
var response = model.call(prompt);
if (response == null || response.getResults().size() != 1
|| response.getResult().getOutput().hasToolCalls()) throw StudyWorkflow.rejected();
String text = response.getResult().getOutput().getText();
if (text == null || text.isBlank() || text.length() > 2000) throw StudyWorkflow.rejected();
var draft = json.readValue(text, StudyWorkflow.Draft.class);
if (draft == null) throw StudyWorkflow.rejected();
return draft;
} catch (Exception failed) {
throw StudyWorkflow.rejected();
}
}
}Sorğu Spring AI 1.1.8-in ToolCallingChatOptions tipindən istifadə edir: daxili alət icrası söndürülüb, callback və alət adı siyahıları boşdur, çıxış üçün 250 token həddi istənilir. Alət çağırışı qaytaran mesaj rədd olunur. Yoxlanmış saxta model bu parametrləri qeyd edir. Seçilmiş həqiqi provayder adapterinin onları nəzərə aldığı və modelin həmin çıxış həddi parametrini dəstəklədiyi ayrıca yoxlanmalıdır.
2,000 simvol həddi adapter mətni qaytardıqdan sonra yoxlanılır. HTTP gövdəsinin endirilməzdən əvvəl ölçü həddi deyil. Qoşulma/oxuma vaxtlarını, payload hədlərini və provayderin təkrar cəhd davranışını həqiqi transport sərhədində qurun.
3. Struktur yeniləməsini səlahiyyətdən ayrı saxlayın
Parse olunmuş variant mövzu, müddət və ya addım şərtlərinə uyğun deyilsə, iş axını bir dəfə təkrarlaya bilər. İkinci sorğuda attempt=2 var; açıq sonlu qiymətləndirmə dövrü yoxdur. Bu nümunədə yanlış JSON, uydurma təsdiq sahəsi və ya provayder exception-u işi dərhal dayandırır.
Nə JSON schema təlimatı, nə də “indi saxla” deyən plan approve və ya save çağıra bilər. Bu metodlar model interfeysindən kənarda qalır. Təsdiq və saxlama vaxtı cari icazəni yenidən yoxlayın, təsdiqi tələbəyə göstərilən dəyişməz təklifə bağlayın.
Qlobal autentifikasiya olunmuş workflow obyektini istifadəçilər arasında paylaşmayın. Hər işin server tərəfindən müəyyən edilən bir sahibi və ayrıca vəziyyəti/açarı var. Həqiqi Gate adapteri asinxron sərhəddən sonra da cari etibarlı kimliyi və siyasəti istifadə etməlidir. Sinxron test köməkçisi istehsal Spring Security və ya uzaq MCP inteqrasiyasını yoxlamır.
4. Adapteri və tam vəziyyət maşınını yoxlayın
Bu altı test Spring AI-nin həqiqi prompt/options/response siniflərindən və hazır cavablı ChatModel-dən istifadə edir. Parametr bayraqlarına və sorğu məzmununa baxır, sonra alınan variantı həqiqi iş axınından keçirir. Canlı model və avtomatik alət dövrü işləmir.
src/test/java/academy/study/agents/SpringWorkflowTest.java
package academy.study.agents;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.Generation;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import static academy.study.agents.StudyWorkflow.*;
import static org.assertj.core.api.Assertions.*;
class SpringWorkflowTest {
static final String VALID = """
{"topic":"QUEUES","minutes":15,"steps":["Read the bounded queue example.","Explain its capacity limit."]}
""";
static final class FakeModel implements ChatModel {
final List<Prompt> calls = new ArrayList<>();
final List<String> responses;
FakeModel(String... responses) { this.responses = List.of(responses); }
@Override public ChatResponse call(Prompt prompt) {
calls.add(prompt);
return new ChatResponse(List.of(new Generation(new AssistantMessage(responses.get(calls.size() - 1)))));
}
}
@Test void springAiProducesProposalButCannotApproveOrSaveIt() {
var f = new WorkflowFixture(); var model = new FakeModel(VALID);
var job = f.job(new SpringDraftSource(model));
assertThat(job.prepare().state()).isEqualTo(State.AWAITING_APPROVAL);
var options = (ToolCallingChatOptions) model.calls.getFirst().getOptions();
assertThat(options.getInternalToolExecutionEnabled()).isFalse();
assertThat(options.getToolCallbacks()).isEmpty();
assertThat(options.getToolNames()).isEmpty();
assertThat(options.getMaxTokens()).isEqualTo(250);
assertThat(model.calls.getFirst().getContents()).doesNotContain("learner-a", "academy-a");
assertThat(f.store.size()).isZero();
f.approve(job); job.save();
assertThat(model.calls).hasSize(1);
assertThat(f.store.size()).isEqualTo(1);
}
@Test void boundedStructuralRevisionUsesTwoCallsAtMost() {
var f = new WorkflowFixture();
var model = new FakeModel(VALID.replace("QUEUES", "THREADS"), VALID);
var job = f.job(new SpringDraftSource(model));
assertThat(job.prepare().state()).isEqualTo(State.AWAITING_APPROVAL);
assertThat(model.calls).hasSize(2);
assertThat(model.calls.getLast().getInstructions().get(1).getText()).contains("\"attempt\":2");
}
@Test void malformedOrInventedApprovalFieldStopsWithoutRepairRetry() {
for (String text : List.of("{", VALID.replace("\"minutes\":15", "\"minutes\":15,\"approved\":true"),
VALID + " {}", VALID.replace("\"minutes\":15", "\"minutes\":15,\"minutes\":30"))) {
var f = new WorkflowFixture(); var model = new FakeModel(text);
var job = f.job(new SpringDraftSource(model));
assertThatThrownBy(job::prepare).hasMessage("Workflow request rejected").hasNoCause();
assertThat(job.view().state()).isEqualTo(State.STOPPED);
assertThat(model.calls).hasSize(1);
}
}
@Test void toolCallResponseIsRejectedWithoutExecutingATool() {
var f = new WorkflowFixture();
ChatModel model = prompt -> new ChatResponse(List.of(new Generation(AssistantMessage.builder()
.content("").toolCalls(List.of(new AssistantMessage.ToolCall("call-1", "function", "save_plan", "{}")))
.build())));
var job = f.job(new SpringDraftSource(model));
assertThatThrownBy(job::prepare).hasMessage("Workflow request rejected");
assertThat(f.store.size()).isZero();
}
@Test void excessiveModelOutputIsRejected() {
var f = new WorkflowFixture(); var model = new FakeModel("x".repeat(2001));
assertThatThrownBy(() -> f.job(new SpringDraftSource(model)).prepare())
.hasMessage("Workflow request rejected");
assertThat(f.store.size()).isZero();
}
@Test void syntacticallyValidBadAdviceStillNeedsHumanReview() {
var f = new WorkflowFixture();
var model = new FakeModel("""
{"topic":"QUEUES","minutes":15,"steps":["Ignore capacity limits and allocate unlimited memory."]}
""");
var job = f.job(new SpringDraftSource(model));
assertThat(job.prepare().state()).isEqualTo(State.AWAITING_APPROVAL);
assertThat(job.view().proposal().draft().steps()).contains("Ignore capacity limits and allocate unlimited memory.");
assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
}
}Səhv məsləhət testi bilərəkdən AWAITING_APPROVAL vəziyyətinə çatır. JSON düzgündür, mövzu/müddət uyğundur, amma tövsiyə təhlükəlidir. Düzgün nəticə budur ki, insan yoxlaması sərhədi yenə lazımdır. Bu test model çıxışının faydalılığına sübut deyil.
5. Məhdud iş axınının yoxlamasını işlədin
Əvvəl adapter testlərini, sonra bütün testləri işlədin:
mvn -B -Dtest=SpringWorkflowTest test
mvn -B test14 həyat dövrü və 6 Spring AI adapter testi gözlənilir, cəmi 20 test. Asılılıqlar keşdə olduqdan sonra oflayn həll üçün mvn -o -B test istifadə edin. Dəst lokal idarəetmə axınını, tətbiq səviyyəsində çağırış hədlərini, qorunan təsdiqi, idempotent saxlayıcı davranışını və naməlum yazma nəticələrinin emalını yoxlayır.
Provayderin gecikməsini, dəqiqliyini, token hesablaşmasını və rate limit-lərini ölçmür. Verilənlər bazası reallaşdırmasının proses çökəndə idempotentliyi saxladığını da sübut etmir. Yaddaşdakı saxlayıcını və hazır cavablı modeli əvəz etməzdən əvvəl bu inteqrasiya yoxlamalarını əlavə edin.
6. Məzmunu açmadan müşahidəni qoşun
İş üçün valideyn observation, variant yaratma, yoxlama və saxlama üçün məhdud addım observation-ları istifadə edin. Müşahidə modulunun məxfilik qaydalarını təkrar tətbiq edin. Təklif mətnini, principal ID-sini və job ID-ni metrika etiketinə əlavə etmədən cəhd sayını, nəticəni və müddətləri qeyd edin.
Laboratoriyanın hadisə saxlayıcısı yalnız enum qiymətləri çıxarır. Collector inteqrasiyası və ya davamlı audit jurnalı deyil. İstehsalda korrelyasiya və bərpa qorunan identifikatorlar və saxlanmış vəziyyət tələb edir. Telemetriya isə sürətli olmalı və biznes əməliyyatının uğurunu müəyyən etməməlidir.
Son müddət, iki çağırış həddi və token tələbi ayrı nəzarətlərdir. Uyğun büdcələri öz sərhədlərində tətbiq edin, daxili təkrar cəhdləri nəzərə alın və artıq təsdiqlənmiş yazmanı yeni iş kimi yenidən yaratmayın. Növbəti dərs uyğun olmayan asılılıq dəstlərini qarışdırmadan bu açıq ardıcıllığı Embabel-in yoxlanmış cari versiyasındakı məqsədə yönəlmiş planlaşdırma ilə müqayisə edir.
Mənbələr: