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 status | Cavab göstərilir? |
|---|---|---|---|
ACCEPTED | Struktur qaydası keçib və qiymətləndirici müsbət nəticə verib | 200 | Bəli |
REJECTED | Struktur qaydası və ya qiymətləndirici cavabı qəbul etməyib | 422 | Xeyr |
UNAVAILABLE | Generasiya və ya qiymətləndirmə runtime exception atıb | 503 | Xeyr |
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:
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:
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:
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:
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:
env -u RUN_LIVE_EVALUATION mvn clean verifyOn 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:
java -jar target/study-coach-0.0.1-SNAPSHOT.jarBaşqa terminaldan sabit nümunəni bir dəfə çağır:
curl -i -X POST http://127.0.0.1:8080/api/study/examples/inheritance-reviewQə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.