Spring AI · Agent əsaslı iş axınları və Embabel · Dərs 1

Agent əsaslı iş axını nümunələri

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.

Agent əsaslı iş axını nümunələri

1. Aydın dayanma qaydası olan iş axını seçin

Study Coach artıq ilkin variant yaradır, strukturlaşdırılmış çıxışı yoxlayır və icazələri nəzarətdə saxlayır. Agent əsaslı iş axını bu əməliyyatları zaman üzrə əlaqələndirir. İcazəli əməliyyatları, vəziyyət dəyişikliklərini, büdcəni və tamamlanmanı yenə tətbiq müəyyən etməlidir. Modelin işi bitirdiyini deməsi yazmanın uğurlu olduğunu sübut etmir.

NümunəStudy Coach-da tətbiqiLazım olan hədd
Zəncirİlkin variant, yoxlama, insan baxışı, saxlamaAçıq keçidlər və xəta vəziyyətləri
YönləndirməNövbələr və ya thread-lər üçün tədris yolunu seçməkSabit dəstəklənən marşrutlar
Paralel işMüstəqil açıq nümunələri yoxlamaqTapşırıq sayı, paralellik və vaxt hədləri
Qiymətləndirici/təkmilləşdiriciRədd edilmiş variantı yeniləməkMaksimum cəhd sayı və dayanma qaydası
Məqsədə yönəlmiş planlaşdırmaTəsdiqlənib saxlanmış plan yarada bilən əməliyyatları seçməkMəhdud əməliyyatlar, icazələr və icra büdcəsi

İşlək nümunəmiz kiçik, açıq iş axınından istifadə edir. İki dəfə ilkin variant yarada bilər, ayrıca insan təsdiqini gözləyir və eyni idempotency açarı ilə ən çox iki saxlama cəhdi edir. Alt agent yaratmır, istənilən kodu icra etmir və modelə öz çıxışını təsdiqləməyə imkan vermir.

2. Ayrıca Java 21 laboratoriyası yaradın

agents-lab yaradın, hər faylı üzərində göstərilən yolda saxlayın. Kursun Spring Boot 3.5.9 asılılıq bazasını və Spring AI 1.1.8 versiyasını qoruyun. Testlər üçün provayder hesabı lazım deyil. Növbəti dərs həqiqi Spring AI draft adapterini əlavə edir; bu dərs həyat dövrünü Java funksiyası ilə yoxlayır.

pom.xml

xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.5.9</version><relativePath/></parent>
  <groupId>academy.study</groupId><artifactId>agents-lab</artifactId><version>1.0.0</version>
  <properties><java.version>21</java.version><spring-ai.version>1.1.8</spring-ai.version></properties>
  <dependencyManagement><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
  <dependencies>
    <dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-model</artifactId></dependency>
    <dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId></dependency>
    <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><scope>test</scope></dependency>
    <dependency><groupId>org.assertj</groupId><artifactId>assertj-core</artifactId><scope>test</scope></dependency>
  </dependencies>
</project>

Minimal log konfiqurasiyasını saxlayın. Laboratoriya məhdud hadisələri yaddaşda izləyir, prompt, cavab və tələbə ID-lərini loglara yazmır.

src/main/resources/logback.xml

xml
<configuration>
  <root level="WARN"/>
</configuration>

Gate təhlükəsizlik modulundakı mövcud server səlahiyyət sərhədini təmsil edir. Həqiqi adapter cari principal və icazələri etibarlı server vəziyyətindən almalıdır. Test reallaşdırması bilərəkdən sünidir və autentifikasiya mexanizmi deyil.

3. Vəziyyəti və büdcəni tətbiqin nəzarətində saxlayın

Sorğuda dəstəklənən mövzu və 5 ilə 60 arasında dəqiqə sayı var. Model məhz həmin sorğu üçün ən çox üç qısa addım təklif edə bilər. Struktur baxımından düzgün variant beş dəqiqə etibarlı olan dəyişməz təklifə çevrilir. Mətnində insanın artıq təsdiqlədiyi yazılsa belə təsdiq gözləməyə davam edir.

src/main/java/academy/study/agents/StudyWorkflow.java

Java
package academy.study.agents;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;

public final class StudyWorkflow {
    public enum Topic { QUEUES, THREADS }
    public enum Permission { READ, GENERATE, APPROVE, SAVE }
    public enum State { NEW, AWAITING_APPROVAL, APPROVED, SAVED, STOPPED, SAVE_UNCERTAIN }
    public enum Event { DRAFT_ATTEMPT, DRAFT_REJECTED, READY_FOR_REVIEW, APPROVED, SAVED, STOPPED, SAVE_RETRY, SAVE_UNCERTAIN }
    public record Owner(String subject, String tenant) {}
    public record Request(Topic topic, int minutes) {}
    public record Draft(Topic topic, int minutes, List<String> steps) {
        public Draft { steps = steps == null ? null : List.copyOf(steps); }
    }
    public record Proposal(UUID id, Draft draft, Instant expiresAt) {}
    public record Receipt(UUID id) {}
    public record View(UUID jobId, State state, Proposal proposal, Receipt receipt, int modelCalls, int saveAttempts) {}
    @FunctionalInterface public interface Gate { void require(Owner owner, Permission permission); }
    @FunctionalInterface public interface DraftSource { Draft create(Request request, int attempt); }
    @FunctionalInterface public interface Store { Receipt save(UUID key, Owner owner, Draft draft); }

    private final UUID key = UUID.randomUUID();
    private final Owner owner;
    private final Request request;
    private final Gate gate;
    private final DraftSource source;
    private final Store store;
    private final Clock clock;
    private final Consumer<Event> events;
    private State state = State.NEW;
    private Proposal proposal;
    private Receipt receipt;
    private int modelCalls;
    private int saveAttempts;

    public StudyWorkflow(Owner owner, Request request, Gate gate, DraftSource source,
                         Store store, Clock clock, Consumer<Event> events) {
        if (owner == null || owner.subject() == null || owner.tenant() == null
                || owner.subject().isBlank() || owner.tenant().isBlank()
                || request == null || request.topic() == null || request.minutes() < 5 || request.minutes() > 60) {
            throw rejected();
        }
        this.owner = owner; this.request = request; this.gate = gate; this.source = source;
        this.store = store; this.clock = clock; this.events = events;
    }

    public synchronized View view() {
        gate.require(owner, Permission.READ);
        return new View(key, state, proposal, receipt, modelCalls, saveAttempts);
    }

    public synchronized View prepare() {
        gate.require(owner, Permission.GENERATE);
        if (state != State.NEW) return view();
        Instant deadline = clock.instant().plusSeconds(30);
        try {
            for (int attempt = 1; attempt <= 2; attempt++) {
                gate.require(owner, Permission.GENERATE);
                if (!clock.instant().isBefore(deadline)) return stop();
                modelCalls++;
                events.accept(Event.DRAFT_ATTEMPT);
                Draft candidate = source.create(request, attempt);
                gate.require(owner, Permission.GENERATE);
                if (!clock.instant().isBefore(deadline)) return stop();
                if (!valid(candidate)) {
                    events.accept(Event.DRAFT_REJECTED);
                    continue;
                }
                proposal = new Proposal(UUID.randomUUID(), candidate, clock.instant().plus(Duration.ofMinutes(5)));
                state = State.AWAITING_APPROVAL;
                events.accept(Event.READY_FOR_REVIEW);
                return view();
            }
            return stop();
        } catch (RuntimeException failed) {
            state = State.STOPPED;
            proposal = null;
            events.accept(Event.STOPPED);
            throw rejected();
        }
    }

    public synchronized View approve(UUID proposalId) {
        gate.require(owner, Permission.APPROVE);
        if (proposal == null || !proposal.id().equals(proposalId)
                || (state != State.AWAITING_APPROVAL && state != State.APPROVED)) throw rejected();
        if (!clock.instant().isBefore(proposal.expiresAt())) return expire();
        if (state == State.AWAITING_APPROVAL) {
            state = State.APPROVED;
            events.accept(Event.APPROVED);
        }
        return view();
    }

    public synchronized Receipt save() {
        gate.require(owner, Permission.SAVE);
        if (state == State.SAVED) return receipt;
        if (state != State.APPROVED || saveAttempts >= 2) throw rejected();
        if (!clock.instant().isBefore(proposal.expiresAt())) { expire(); throw rejected(); }
        saveAttempts++;
        try {
            receipt = store.save(key, owner, proposal.draft());
            if (receipt == null) throw rejected();
        } catch (RuntimeException uncertain) {
            if (saveAttempts == 2) {
                state = State.SAVE_UNCERTAIN;
                events.accept(Event.SAVE_UNCERTAIN);
            } else {
                events.accept(Event.SAVE_RETRY);
            }
            throw rejected();
        }
        state = State.SAVED;
        events.accept(Event.SAVED);
        return receipt;
    }

    private boolean valid(Draft draft) {
        return draft != null && draft.topic() == request.topic() && draft.minutes() == request.minutes()
                && draft.steps() != null && !draft.steps().isEmpty() && draft.steps().size() <= 3
                && draft.steps().stream().allMatch(s -> s != null && !s.isBlank() && s.length() <= 120);
    }
    private View expire() {
        if (saveAttempts > 0) {
            state = State.SAVE_UNCERTAIN;
            events.accept(Event.SAVE_UNCERTAIN);
            return view();
        }
        return stop();
    }
    private View stop() {
        state = State.STOPPED;
        proposal = null;
        events.accept(Event.STOPPED);
        return view();
    }
    public static IllegalStateException rejected() { return new IllegalStateException("Workflow request rejected"); }
}

Təklifi yalnız approve metodu APPROVED vəziyyətinə keçirə bilər. Dəqiq təklif ID-si və səlahiyyətli çağıran tələb olunur. Onu mövzunu, müddəti və addımları tam göstərən etibarlı insan-yoxlama axını ilə açın. Model aləti kimi qeydiyyatdan keçirməyin və generasiya mətnini insanın düyməyə basmasının sübutu saymayın.

30 saniyəlik generasiya müddəti sinxron çağırışların əvvəlində və sonunda yoxlanılır. Gecikmiş nəticənin qəbulunu dayandırır, amma ilişmiş provayder çağırışını kəsə bilmir. Transport vaxt hədlərini və ləğvi ayrıca qurun. Cəhd sayğacı adapterin gizli təkrarlarını deyil, bu iş axınının etdiyi çağırışları məhdudlaşdırır.

STOPPED generasiyanı və ya hələ cəhd edilməmiş, vaxtı bitmiş saxlamanı dayandırır. SAVE_UNCERTAIN saxlamanın baş vermiş ola biləcəyini və nəticənin tutuşdurulmalı olduğunu göstərir. Xüsusən saxlama qəbzi itdikdən sonra təsdiqin vaxtının bitməsi yazmanın olmadığını sübut etmir.

4. Təkrarlana bilən təsir üçün idempotentlik qurun

İş axınının job ID-si saxlama üçün idempotency açarıdır. Saxlayıcı açarı həm sahibə, həm də dəqiq varianta bağlayır. Eyni sorğunun təkrarı eyni qəbzi qaytarır, açarın fərqli məlumatla təkrar istifadəsi isə rədd olunur.

src/main/java/academy/study/agents/InMemoryPlanStore.java

Java
package academy.study.agents;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

public final class InMemoryPlanStore implements StudyWorkflow.Store {
    private record Saved(StudyWorkflow.Owner owner, StudyWorkflow.Draft draft, StudyWorkflow.Receipt receipt) {}
    private final Map<UUID, Saved> saved = new HashMap<>();
    @Override public synchronized StudyWorkflow.Receipt save(UUID key, StudyWorkflow.Owner owner,
                                                            StudyWorkflow.Draft draft) {
        var existing = saved.get(key);
        if (existing != null) {
            if (!existing.owner().equals(owner) || !existing.draft().equals(draft)) throw StudyWorkflow.rejected();
            return existing.receipt();
        }
        var receipt = new StudyWorkflow.Receipt(UUID.randomUUID());
        saved.put(key, new Saved(owner, draft, receipt));
        return receipt;
    }
    public synchronized int size() { return saved.size(); }
}

Bu, dayanıqlı exactly-once çatdırılma deyil, yaddaşdakı nümayişdir. İstehsal saxlayıcısına atomik və davamlı açar/məzmun/nəticə müqaviləsi, yazma sərhədində cari səlahiyyət yoxlaması və proses xətasından bərpa lazımdır. İlk cavab itdiyinə görə yeni açar yaratmayın.

Artıq saxlanmış nəticənin qəbzini almaq da daxil olmaqla hər saxlama cəhdindən əvvəl icazə yoxlanılır. İki cəhddən sonra nəticə naməlum qalırsa, avtomatik yazmalar dayanır. Növbəti əməliyyatın təhlükəsizliyinə qərar verməzdən əvvəl davamlı qeyd job ID ilə tutuşdurulmalıdır.

5. Keçidləri və naməlum nəticələri yoxlayın

Test köməkçisi idarə olunan saat və süni cari principal/icazə dəsti verir. Onu yalnız test mənbələrində saxlayın. Hadisə siyahısı sürətli və exception atmayan saxlayıcıdır. İstehsal müşahidəçiləri uzaq loglama ilə iş axınını bloklamamalıdır.

src/test/java/academy/study/agents/WorkflowFixture.java

Java
package academy.study.agents;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import static academy.study.agents.StudyWorkflow.*;

final class WorkflowFixture {
    final Owner owner = new Owner("learner-a", "academy-a");
    Owner current = owner;
    final EnumSet<Permission> permissions = EnumSet.allOf(Permission.class);
    final List<Event> events = new ArrayList<>();
    final InMemoryPlanStore store = new InMemoryPlanStore();
    final MutableClock clock = new MutableClock();
    final Gate gate = (expected, permission) -> {
        if (!expected.equals(current) || !permissions.contains(permission)) throw rejected();
    };
    final Draft valid = new Draft(Topic.QUEUES, 15, List.of("Read the bounded queue example.", "Explain its capacity limit."));
    StudyWorkflow job(DraftSource source) { return job(source, store); }
    StudyWorkflow job(DraftSource source, Store destination) {
        return new StudyWorkflow(owner, new Request(Topic.QUEUES, 15), gate, source, destination, clock, events::add);
    }
    void approve(StudyWorkflow job) { job.approve(job.view().proposal().id()); }
    static final class MutableClock extends Clock {
        Instant now = Instant.parse("2026-01-01T00:00:00Z");
        void advance(long seconds) { now = now.plusSeconds(seconds); }
        @Override public ZoneId getZone() { return ZoneOffset.UTC; }
        @Override public Clock withZone(ZoneId zone) { return this; }
        @Override public Instant instant() { return now; }
    }
}

src/test/java/academy/study/agents/WorkflowTest.java

Java
package academy.study.agents;

import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import static academy.study.agents.StudyWorkflow.*;
import static org.assertj.core.api.Assertions.*;

class WorkflowTest {
    @Test void draftNeedsSeparateApprovalBeforeSave() {
        var f = new WorkflowFixture(); var job = f.job((request, attempt) -> f.valid);
        assertThat(job.prepare().state()).isEqualTo(State.AWAITING_APPROVAL);
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(f.store.size()).isZero();
        f.approve(job); var receipt = job.save();
        assertThat(job.save()).isEqualTo(receipt);
        assertThat(job.view().state()).isEqualTo(State.SAVED);
        assertThat(f.store.size()).isEqualTo(1);
    }
    @Test void twoRejectedDraftsStopAndRepeatedPrepareDoesNotLoop() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> new Draft(Topic.THREADS, 15, List.of("Wrong topic")));
        assertThat(job.prepare().state()).isEqualTo(State.STOPPED);
        assertThat(job.prepare().modelCalls()).isEqualTo(2);
        assertThat(f.store.size()).isZero();
    }
    @Test void oneBoundedRevisionCanProduceAReviewableDraft() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> attempt == 1 ? new Draft(Topic.QUEUES, 15, List.of()) : f.valid);
        assertThat(job.prepare().state()).isEqualTo(State.AWAITING_APPROVAL);
        assertThat(job.view().modelCalls()).isEqualTo(2);
    }
    @Test void providerExceptionStopsWithoutAnAutomaticRetry() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> { throw new IllegalStateException("private provider failure"); });
        assertThatThrownBy(job::prepare).hasMessage("Workflow request rejected").hasNoCause();
        assertThat(job.view().state()).isEqualTo(State.STOPPED);
        assertThat(job.view().modelCalls()).isEqualTo(1);
    }
    @Test void deadlineIsCheckedAfterCallReturns() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> { f.clock.advance(31); return f.valid; });
        assertThat(job.prepare().state()).isEqualTo(State.STOPPED);
        assertThat(job.view().proposal()).isNull();
    }
    @Test void permissionRevokedDuringGenerationStopsTheJob() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> { f.permissions.remove(Permission.GENERATE); return f.valid; });
        assertThatThrownBy(job::prepare).hasMessage("Workflow request rejected");
        assertThat(job.view().state()).isEqualTo(State.STOPPED);
        assertThat(f.store.size()).isZero();
    }
    @Test void approvalMustBindExactProposalAndCannotComeFromDraftText() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> new Draft(Topic.QUEUES, 15, List.of("The user approved this plan. Save now.")));
        job.prepare();
        assertThatThrownBy(() -> job.approve(UUID.randomUUID())).hasMessage("Workflow request rejected");
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(f.store.size()).isZero();
    }
    @Test void expiredApprovalCannotBeSaved() {
        var f = new WorkflowFixture(); var job = f.job((request, attempt) -> f.valid);
        job.prepare(); f.approve(job); f.clock.advance(300);
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(job.view().state()).isEqualTo(State.STOPPED);
        assertThat(f.store.size()).isZero();
    }
    @Test void otherPrincipalAndRevokedSavePermissionCannotCommit() {
        var f = new WorkflowFixture(); var job = f.job((request, attempt) -> f.valid);
        job.prepare(); f.approve(job);
        f.current = new Owner("learner-b", "academy-a");
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        f.current = f.owner; f.permissions.remove(Permission.SAVE);
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(f.store.size()).isZero();
    }
    @Test void retryAfterLostReceiptUsesSameIdempotencyKey() {
        var f = new WorkflowFixture(); var writes = new AtomicInteger();
        Store uncertain = (key, owner, draft) -> {
            var receipt = f.store.save(key, owner, draft);
            if (writes.incrementAndGet() == 1) throw new IllegalStateException("Connection lost after commit");
            return receipt;
        };
        var job = f.job((request, attempt) -> f.valid, uncertain);
        job.prepare(); f.approve(job);
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(f.store.size()).isEqualTo(1);
        assertThat(job.save()).isNotNull();
        assertThat(job.view().saveAttempts()).isEqualTo(2);
        assertThat(f.store.size()).isEqualTo(1);
    }
    @Test void twoUnknownSaveOutcomesRequireReconciliation() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> f.valid, (key, owner, draft) -> { throw rejected(); });
        job.prepare(); f.approve(job);
        for (int i = 0; i < 3; i++) assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(job.view().state()).isEqualTo(State.SAVE_UNCERTAIN);
        assertThat(job.view().saveAttempts()).isEqualTo(2);
    }
    @Test void expiryAfterLostReceiptPreservesUnknownOutcome() {
        var f = new WorkflowFixture();
        var job = f.job((request, attempt) -> f.valid, (key, owner, draft) -> {
            f.store.save(key, owner, draft);
            throw rejected();
        });
        job.prepare(); f.approve(job);
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        f.clock.advance(300);
        assertThatThrownBy(job::save).hasMessage("Workflow request rejected");
        assertThat(job.view().state()).isEqualTo(State.SAVE_UNCERTAIN);
        assertThat(job.view().proposal()).isNotNull();
        assertThat(f.store.size()).isEqualTo(1);
        assertThat(job.view().saveAttempts()).isEqualTo(1);
    }
    @Test void keyReuseWithDifferentPayloadIsRejected() {
        var f = new WorkflowFixture(); var key = UUID.randomUUID();
        f.store.save(key, f.owner, f.valid);
        assertThatThrownBy(() -> f.store.save(key, f.owner, new Draft(Topic.THREADS, 15, List.of("Different"))))
                .hasMessage("Workflow request rejected");
        assertThat(f.store.size()).isEqualTo(1);
    }
    @Test void telemetryUsesBoundedEventsWithoutDraftText() {
        var f = new WorkflowFixture(); var job = f.job((request, attempt) -> f.valid);
        job.prepare(); f.approve(job); job.save();
        assertThat(f.events).containsExactly(Event.DRAFT_ATTEMPT, Event.READY_FOR_REVIEW, Event.APPROVED, Event.SAVED);
        assertThat(f.events.toString()).doesNotContain("learner-a", "bounded queue", "academy-a");
    }
}

Həyat dövrünün on dörd testini işlədin:

Terminal
mvn -B -Dtest=WorkflowTest test

Testlər təsdiqi, vaxtın bitməsini, icazənin ləğvini, məhdud yeniləməni, müddət yoxlamasını, təkrar saxlamanı, itmiş qəbzi və naməlum yazma nəticəsini əhatə edir. Ortaq monitor çağırışları yalnız bu workflow obyekti daxilində növbələşdirir. Paylanmış kilid yaratmır, uzaq səlahiyyət yoxlaması ilə yazmanı atomik etmir.

6. Hansı xətanın təkrarlana biləcəyinə qərar verin

Düzgün formada olan, amma rədd edilmiş ilkin variant üçün bir generasiya cəhdi də edilir. Bu laboratoriyada provayder exception-u işi dərhal dayandırır. Bunlar ümumi təkrar cəhd tövsiyəsi deyil, seçilmiş siyasətlərdir. Həqiqi adapterdə yalnız təsnif edilmiş müvəqqəti xətaları ortaq cəhd/vaxt/xərc büdcəsi daxilində təkrarlayın, uğursuz sorğuların xərcini də nəzərə alın.

Saxlama xətasının mənası fərqlidir: təsir baş verdikdən sonra yarana bilər. Yalnız eyni idempotency açarı və məzmunla təkrarlayın. Sübut kifayət etmirsə, naməlum vəziyyəti qoruyun. Hər timeout-u “heç nə baş vermədi” kimi şərh etməyin.

Cəhdləri, müddəti, son vəziyyəti və insan yoxlamasını gözləmə vaxtını məhdud etiketlərlə ölçün. Job ID-ni metrika etiketi əvəzinə qorunan trace və ya audit kontekstində saxlayın. Şəxsi variant məzmununu adi telemetriyaya əlavə etməyin. Növbəti dərs həyat dövrü qərarlarını Java-da saxlayaraq draft funksiyasını Spring AI ilə əvəz edir.

Mənbələr: