Spring AI · Model cavablarının qiymətləndirilməsi · Dərs 3

Tətbiq işləyərkən cavabların qiymətləndirilməsi

Spring AI qiymətləndiriciləri ilə böyük dil modelinin (LLM) cavablarının keyfiyyətini, faktlara uyğunluğunu və verilən kontekstə əsaslanmasını yoxla.

Tətbiq işləyərkən cavabların qiymətləndirilməsi

Əvvəlki iki qiymətləndirmə dərsindən davam et. Sabit irsilik sualı üçün ayrıca lokal tədris endpoint-i əlavə edəcəyik. Serverin saxladığı istinad onun əhatəsini aydın edir. Mövcud /api/study/questions endpoint-i dəyişmir; o, cavabları hələ bu yoxlama axını olmadan qaytarır.

1. Hər nəticənin mənasını müəyyən et

Yeni route üçün tətbiqdə üç nəticə var:

NəticəMənasıHTTP statusCavab göstərilir?
ACCEPTEDStruktur qaydası keçib və qiymətləndirici müsbət nəticə verib200Bəli
REJECTEDStruktur qaydası və ya qiymətləndirici cavabı qəbul etməyib422Xeyr
UNAVAILABLEGenerasiya və ya qiymətləndirmə runtime exception atıb503Xeyr

ACCEPTED iş axınının nəticə adıdır, düzgünlük sertifikatı deyil. REJECTED gözlənilməz qiymətləndirici formatını da əhatə edə bilər və namizəd cavabın yanlışlığını sübut etmir. UNAVAILABLE keyfiyyət qərarı deyil. Status seçimi bu endpoint-in siyasətidir, Spring AI-nin avtomatik davranışı deyil.

2. Hər sorğunun işini məhdudlaşdır

src/main/java/academy/study/ReviewedStudyService.java faylını saxla:

Java
package academy.study;

import java.util.List;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.evaluation.RelevancyEvaluator;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.evaluation.EvaluationRequest;
import org.springframework.ai.evaluation.Evaluator;
import org.springframework.stereotype.Service;

@Service
public class ReviewedStudyService {
    private final StudyService studyService;
    private final Evaluator evaluator;

    public ReviewedStudyService(StudyService studyService, ChatModel judgeModel) {
        this.studyService = studyService;
        this.evaluator = new RelevancyEvaluator(ChatClient.builder(judgeModel));
    }

    public ReviewResult reviewInheritance() {
        String answer;
        try {
            answer = studyService.answer(InheritanceReference.QUESTION);
        } catch (RuntimeException failure) {
            return new ReviewResult(ReviewStatus.UNAVAILABLE, null);
        }
        if (AnswerChecks.check(answer) != AnswerChecks.Result.PASS) {
            return new ReviewResult(ReviewStatus.REJECTED, null);
        }
        var request = new EvaluationRequest(
                InheritanceReference.QUESTION,
                List.of(new Document(InheritanceReference.CONTEXT)), answer);
        try {
            boolean pass = evaluator.evaluate(request).isPass();
            return pass
                    ? new ReviewResult(ReviewStatus.ACCEPTED, answer)
                    : new ReviewResult(ReviewStatus.REJECTED, null);
        } catch (RuntimeException failure) {
            return new ReviewResult(ReviewStatus.UNAVAILABLE, null);
        }
    }

    public enum ReviewStatus { ACCEPTED, REJECTED, UNAVAILABLE }
    public record ReviewResult(ReviewStatus status, String answer) {}
}

Ardıcıllıq belədir: generasiya, deterministik mətn yoxlaması, sonra bir qiymətləndirmə çağırışı. Struktur yoxlaması uğursuz olarsa, qiymətləndirici çağırılmır və rədd edilən cavab qaytarılmır. Giriş modulundakı spring.ai.retry.max-attempts: 1 parametrini saxla: bu axın hər sorğuda ən çox iki model çağırışı başladır və qiymətləndirici razılaşana qədər yenidən cavab yaratmır. Generator və qiymətləndirici eyni konfiqurasiya edilmiş modeli, amma ayrı client-ləri istifadə edir; tədris köməkçisinin system prompt-u qiymətləndiricinin təlimatı kimi təkrar istifadə edilmir.

Model inteqrasiyası uğursuz olanda catch blokları mətni qaytarmır. Onlar runtime exception-ları qəsdən UNAVAILABLE nəticəsində birləşdirir; bura proqramlaşdırma və ya parse xətaları da daxildir. Ona görə production diaqnostikası prompt və məxfi məlumatları açıqlamadan bu səbəbləri ayırmalıdır. Bu kiçik nümunədə həmin müşahidə mexanizmi qurulmur.

Bu yanaşma çağırış sayını məhdudlaşdırır, vaxtı, ümumi token sayını, paralel işi və ya bütün sorğular üzrə xərci deyil. Çıxış limiti generasiyadan sonra tətbiq olunur. Yayımlamadan əvvəl provayderə uyğun çıxış büdcəsi, HTTP vaxt hədləri və tətbiq istifadə limitlərini konfiqurasiya et; Java timeout assertion-unu uzaqdakı işin ləğvi ilə eyniləşdirmə.

3. Məhdud iş axınını endpoint kimi təqdim et

src/main/java/academy/study/ReviewController.java faylını saxla:

Java
package academy.study;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ReviewController {
    private final ReviewedStudyService service;

    public ReviewController(ReviewedStudyService service) {
        this.service = service;
    }

    @PostMapping("/api/study/examples/inheritance-review")
    public ResponseEntity<ReviewedStudyService.ReviewResult> review() {
        var result = service.reviewInheritance();
        HttpStatus status = switch (result.status()) {
            case ACCEPTED -> HttpStatus.OK;
            case REJECTED -> HttpStatus.UNPROCESSABLE_ENTITY;
            case UNAVAILABLE -> HttpStatus.SERVICE_UNAVAILABLE;
        };
        return ResponseEntity.status(status).body(result);
    }
}

Bu POST route sual parametri qəbul etmir. Həmişə InheritanceReference.QUESTION və serverdəki istinaddan istifadə edir. POST bu açıq, xərc yarada bilən əməliyyatı status oxumaqdan ayırır. Loopback ünvanına bağlanmaq yenə tələb olunur; dərs autentifikasiya və ya sorğu tezliyi limiti əlavə etmir.

Bu istinadı ümumi qiymətləndirmə yoluna köçürüb bütün mövzuları onunla yoxlama. Daha geniş istifadə üçün tətbiq uyğun və istifadəsinə icazə verilən kontekst seçməlidir. Bu seçim sonrakı RAG və təhlükəsizlik işinin hissəsidir. Kontekstin uzunluğu və məxfiliyi qiymətləndirmə sorğusuna da təsir edir.

4. Rədd və xəta yollarını provaydersiz test et

src/test/java/academy/study/ReviewedStudyServiceTest.java faylını saxla:

Java
package academy.study;

import java.util.List;
import org.junit.jupiter.api.BeforeEach;
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 static academy.study.ReviewedStudyService.ReviewStatus.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class ReviewedStudyServiceTest {
    private StudyService generator;
    private ChatModel judge;
    private ReviewedStudyService service;

    @BeforeEach
    void setUp() {
        generator = mock(StudyService.class);
        judge = mock(ChatModel.class);
        service = new ReviewedStudyService(generator, judge);
    }

    private void judgeReturns(String text) {
        when(judge.call(any(Prompt.class))).thenReturn(new ChatResponse(
                List.of(new Generation(new AssistantMessage(text)))));
    }

    @Test
    void releasesOnlyAcceptedAnswer() {
        when(generator.answer(InheritanceReference.QUESTION)).thenReturn("One superclass.");
        judgeReturns("YES");
        var result = service.reviewInheritance();
        assertEquals(ACCEPTED, result.status());
        assertEquals("One superclass.", result.answer());
        verify(generator).answer(InheritanceReference.QUESTION);
        verify(judge).call(any(Prompt.class));
    }

    @Test
    void skipsJudgeForStructurallyRejectedAnswer() {
        when(generator.answer(InheritanceReference.QUESTION))
                .thenReturn(null, "   ", "x".repeat(4001));
        for (int i = 0; i < 3; i++) {
            var result = service.reviewInheritance();
            assertEquals(REJECTED, result.status());
            assertNull(result.answer());
        }
        verify(judge, never()).call(any(Prompt.class));
    }

    @Test
    void withholdsAnswerWhenJudgeRejects() {
        when(generator.answer(InheritanceReference.QUESTION)).thenReturn("Two superclasses.");
        judgeReturns("NO");
        var result = service.reviewInheritance();
        assertEquals(REJECTED, result.status());
        assertNull(result.answer());
        verify(judge).call(any(Prompt.class));
    }

    @Test
    void withholdsAnswerWhenJudgeFails() {
        when(generator.answer(InheritanceReference.QUESTION)).thenReturn("One superclass.");
        when(judge.call(any(Prompt.class))).thenThrow(new IllegalStateException("offline failure"));
        var result = service.reviewInheritance();
        assertEquals(UNAVAILABLE, result.status());
        assertNull(result.answer());
        verify(judge).call(any(Prompt.class));
    }

    @Test
    void skipsJudgeWhenGenerationFails() {
        when(generator.answer(InheritanceReference.QUESTION))
                .thenThrow(new IllegalStateException("offline failure"));
        var result = service.reviewInheritance();
        assertEquals(UNAVAILABLE, result.status());
        assertNull(result.answer());
        verify(judge, never()).call(any(Prompt.class));
    }
}

Bu beş test şaxələnməni, mətnin qaytarılmamasını, buraxılan çağırışları və tək çağırış gözləntilərini yoxlayır. Qiymətləndiricinin faktlar üzrə mühakiməsini sübut etmir. src/test/java/academy/study/ReviewControllerTest.java faylını saxla:

Java
package academy.study;

import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static academy.study.ReviewedStudyService.ReviewStatus.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class ReviewControllerTest {
    @Test
    void mapsReviewOutcomesToHttpWithoutLeakingRejectedText() throws Exception {
        var service = mock(ReviewedStudyService.class);
        when(service.reviewInheritance()).thenReturn(
                new ReviewedStudyService.ReviewResult(ACCEPTED, "One superclass."),
                new ReviewedStudyService.ReviewResult(REJECTED, null),
                new ReviewedStudyService.ReviewResult(UNAVAILABLE, null));
        var mvc = MockMvcBuilders.standaloneSetup(new ReviewController(service)).build();
        String path = "/api/study/examples/inheritance-review";
        mvc.perform(post(path)).andExpect(status().isOk())
                .andExpect(jsonPath("$.status").value("ACCEPTED"))
                .andExpect(jsonPath("$.answer").value("One superclass."));
        mvc.perform(post(path)).andExpect(status().isUnprocessableEntity())
                .andExpect(jsonPath("$.status").value("REJECTED"))
                .andExpect(jsonPath("$.answer").isEmpty());
        mvc.perform(post(path)).andExpect(status().isServiceUnavailable())
                .andExpect(jsonPath("$.status").value("UNAVAILABLE"))
                .andExpect(jsonPath("$.answer").isEmpty());
    }
}

Controller testi hər üç HTTP nəticəsini yoxlayır və rədd/əlçatmaz cavablarında cavab mətninin olmadığını təsdiqləyir. Servis testləri bu nəticələrin niyə yarandığını yoxlayır. Əvvəlki testləri saxla; yeni route ilkin endpoint-in müqaviləsini xəbərsiz dəyişməməlidir.

5. Yeni route-u işə sal və yoxla

Real qiymətləndirməni söndürərək build et:

sh
env -u RUN_LIVE_EVALUATION mvn clean verify

On doqquz offline test metodu keçməli, bir real test metodu isə buraxılmalıdır. Giriş modulundakı eyni icazəli provayder mühitini verdikdən sonra paketlənmiş tətbiqi başlat:

sh
java -jar target/study-coach-0.0.1-SNAPSHOT.jar

Başqa terminaldan sabit nümunəni bir dəfə çağır:

sh
curl -i -X POST http://127.0.0.1:8080/api/study/examples/inheritance-review

Qəbul edilmiş cavabda status: "ACCEPTED" və cavab mətni olur. 422 və ya 503 cavabında uyğun status və null cavab olur. Real model üçün konkret nəticəyə zəmanət yoxdur. Əməliyyat sabit sualı generatora göndərir; struktur yoxlaması keçərsə, namizəd cavabı və istinadı qiymətləndiriciyə ötürür. İki model çağırışına görə xərc yarana bilər. İşin bitəndə tətbiqi Ctrl+C ilə dayandır.

6. Genişləndirmədən əvvəl seçimin nəticəsini qiymətləndir

Tapşırıq: offline testlərlə qiymətləndiricinin əlçatmazlığını canlandır. Namizəd cavabın qaytarılmadığını və avtomatik yenidən generasiya olmadığını təsdiqlə. Sonra real tədris məhsulunda belə cavabı bloklayacağını, təxirə salacağını və ya insan yoxlamasına göndərəcəyini izah et. Səbəbi və istifadəçiyə görünən davranışı qeyd et.

Cavabın yoxlanması: bu implementasiya yoxlama uğursuz olanda cavabı buraxmır; HTTP 503 ilə UNAVAILABLE qaytarır və cavab mətnini vermir. Bu seçim əlçatanlığı azalda bilər və düşünülmüş məhsul qərarı olmalıdır. Yanlışlıqla YES qaytaran qiymətləndirici pis cavabı yenə buraxır, düzgün cavab isə rədd edilə bilər. Namizəd mətn qiymətləndiriciyə təsir etməyə də çalışa bilər. Qiymətləndirmə giriş hüququ yoxlamasını, alətlərin təhlükəsiz icrasını və yüksək məsuliyyətli hallarda insan yoxlamasını əvəz etmir. Spring AI qiymətləndiricisinin əhatəsi

Offline testlər və lokal provayder əvəzedicisi real modelin keyfiyyətini qiymətləndirmədən bu axını yoxlaya bilər. Xarici provayder davranışı, gecikmə, istifadə xərci və qiymətləndiricinin etibarlılığı ayrıca ölçülməlidir. Kiçik istinadı və qeydə alınmış nəticələri sonrakı prompt və RAG dərsləri üçün saxla.