Spring AI · Çatdan kənar AI tətbiqləri · Dərs 2

Mətnlərin tərcüməsi

Spring AI ilə mətnlərin xülasəsini çıxar, tərcümə et və mətnin ifadə etdiyi emosional münasibəti təhlil et.

Mətnlərin tərcüməsi

1. Texniki müqaviləni qoruyaraq mətni tərcümə edin

Study Coach eyni işlək nümunələrin ətrafında ingilis və Azərbaycan dillərində izahlar təqdim etməlidir. planId adını, əmri və ya kod nümunəsini dəyişən axıcı tərcümə dərsi poza bilər. Modeldən texniki materialı qüsursuz təkrarlamasını istəmək əvəzinə onu server kodunda qoruyun.

Xülasə dərsinin use-cases-lab layihəsini davam etdirin. Giriş tam blokların sıralanmış siyahısıdır. PROSE blokları tərcümə oluna bilər. VERBATIM blokları dəqiq köçürülür və modelə göndərilmir. Kod, əmrlər, URL-lər, identifikatorlar və dəyişməməli başqa material üçün verbatim bloklardan istifadə edin.

Bu, avtomatik Markdown parser-i deyil, açıq blok müqaviləsidir. Etibarlı sənəd hazırlığı mərhələsi qorunan materialı, o cümlədən mətnin içindəki identifikatorları düzgün ayırmalıdır. Qarışıq inline mətn üçün ayrıca yoxlanmış placeholder və ya sintaksisi anlayan axın lazımdır. Bütöv abzası prose kimi işarələmək içindəki kod identifikatorunu qorumur.

2. Dili və blok kimliyini açıq göstərin

Servis yalnız EN və AZ qəbul edir, mənbə və hədəf dili tətbiqdən alır. Tələbənin kimliyinə əsasən dili təxmin etmir və səssiz ehtiyat dil seçmir. Model cavabı hər iki dil etiketini və tərcümə edilən bütün blokların ID/sırasını saxlamalıdır.

src/main/java/academy/study/usecases/ProtectedTranslation.java

Java
package academy.study.usecases;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

public final class ProtectedTranslation {
    public enum Locale { EN, AZ }
    public enum Kind { PROSE, VERBATIM }
    public record Part(String id, Kind kind, String text) {}
    public record TextPart(String id, String text) {}
    public record Request(Locale sourceLocale, Locale targetLocale, List<TextPart> parts) {}
    public record Draft(Locale sourceLocale, Locale targetLocale, List<TextPart> parts) {}
    private final StructuredWork work;
    public ProtectedTranslation(StructuredWork work) { this.work = work; }

    public List<Part> translate(Locale source, Locale target, List<Part> parts) {
        if (source == null || target == null || parts == null || parts.isEmpty() || parts.size() > 8) {
            throw StructuredWork.invalid();
        }
        var ids = new HashSet<String>();
        int length = 0;
        for (var part : parts) {
            if (part == null || part.id() == null || !part.id().matches("p[1-8]") || !ids.add(part.id())
                    || part.kind() == null || part.text() == null || part.text().isBlank()
                    || part.text().length() > (part.kind() == Kind.PROSE ? 600 : 1200)) {
                throw StructuredWork.invalid();
            }
            length += part.text().length();
        }
        if (length > 3000) throw StructuredWork.invalid();
        parts = List.copyOf(parts);
        var prose = parts.stream().filter(p -> p.kind() == Kind.PROSE)
                .map(p -> new TextPart(p.id(), p.text())).toList();
        if (source == target || prose.isEmpty()) return parts;
        var draft = work.run("""
                Translate every prose block into targetLocale, preserving its meaning and qualifications.
                Return sourceLocale, targetLocale and translated parts with the same IDs and order.
                EN means English; AZ means Azerbaijani. Do not invent or add blocks.
                """, new Request(source, target, prose), Draft.class);
        if (draft.sourceLocale() != source || draft.targetLocale() != target || draft.parts() == null
                || draft.parts().size() != prose.size()) throw StructuredWork.invalid();
        for (int i = 0; i < prose.size(); i++) {
            var translated = draft.parts().get(i);
            if (translated == null || !prose.get(i).id().equals(translated.id()) || translated.text() == null
                    || translated.text().isBlank() || translated.text().length() > 1200) {
                throw StructuredWork.invalid();
            }
        }
        var result = new ArrayList<Part>();
        int next = 0;
        for (var part : parts) {
            result.add(part.kind() == Kind.VERBATIM ? part
                    : new Part(part.id(), part.kind(), draft.parts().get(next++).text()));
        }
        return List.copyOf(result);
    }
}

Girişdə ən çox səkkiz blok və ümumilikdə 3,000 simvol var. Hər prose bloku 600, qorunan blok isə 1,200 simvolla məhdudlaşır. Tərcümədə genişlənməyə yer vermək üçün prose çıxışı 1,200 simvola qədər ola bilər. Bunlar tətbiq hədləridir, provayderin token istifadəsi proqnozu deyil.

Model yalnız prose bloklarını görür. Java hər qorunan bloku orijinal girişdən bərpa edir. Mənbə və hədəf dil eynidirsə və ya bütün bloklar qorunursa, servis model çağırmadan orijinal ardıcıllığı qaytarır.

3. Qorunmanı tərcümə keyfiyyətindən ayırın

Kod blok kimliyinin, sıranın və qorunan mətnlərin saxlandığını sübut edir. Mətnin Azərbaycan dilində olduğunu, inkarın qorunduğunu və ya terminin uyğunluğunu sübut edə bilməz. Cavab targetLocale=AZ yazdığı halda ingilis mətni daşıya bilər.

Redaktə zamanı yoxlanmış terminologiya lüğətindən və sabit texniki adlardan istifadə edin. Başlıqları, inkarı, vahidləri, rəqəmləri, nümunələri və təlimatları mənbə ilə müqayisə edin. Mənbə dəyişəndə köhnə tərcümə səssiz istifadə edilməsin deyə yoxlanmış tərcümə ilə mənbə versiyasını saxlayın. Həmin nəşr axını bu kiçik servisdən kənardadır.

Qorunan bloklar model girişində yoxdur. Bu, açıqlanma və dəyişdirilmə riskini azaldır, amma tərcüməyə kömək edə biləcək konteksti də çıxarır. Yığılmış sənəddə həmin bloklara verilən mətn istinadlarını yoxlayın. Servis bilərəkdən icra olunan materialı tərcümə çağırışına vermək əvəzinə onun dəqiq saxlanmasını seçir.

4. Dəqiq qorunmanı yoxlayın, struktur dəyişikliyini rədd edin

Bu beş testi saxlayın. Mövcud ScriptedModel və ortaq icraçıdan istifadə edirlər. İlk test sərhədin hər iki tərəfini yoxlayır: kod prompt-a daxil olmur və orijinal kod bloku dəyişmədən bərpa edilir.

src/test/java/academy/study/usecases/TranslationTest.java

Java
package academy.study.usecases;

import java.util.List;
import org.junit.jupiter.api.Test;
import static academy.study.usecases.ProtectedTranslation.*;
import static org.assertj.core.api.Assertions.*;

class TranslationTest {
    static final String CODE = "var planId = \"p-17\";\nservice.savePlan(planId);";
    static final List<Part> SOURCE = List.of(new Part("p1", Kind.PROSE, "Save only after confirmation."),
            new Part("p2", Kind.VERBATIM, CODE), new Part("p3", Kind.PROSE, "The example uses a stable identifier."));
    static final String VALID = """
            {"sourceLocale":"EN","targetLocale":"AZ","parts":[
            {"id":"p1","text":"Yalnız təsdiqdən sonra saxlayın."},
            {"id":"p3","text":"Nümunə sabit identifikatordan istifadə edir."}]}
            """;
    ProtectedTranslation service(ScriptedModel model) { return new ProtectedTranslation(new StructuredWork(model)); }
    @Test void protectedCodeNeverEntersTheModelAndIsRestoredUnchanged() {
        var model = new ScriptedModel(VALID);
        var result = service(model).translate(Locale.EN, Locale.AZ, SOURCE);
        assertThat(result.get(1)).isEqualTo(SOURCE.get(1));
        assertThat(result.get(0).text()).isEqualTo("Yalnız təsdiqdən sonra saxlayın.");
        assertThat(model.prompts.getFirst().getContents()).doesNotContain("p-17", "savePlan", "planId");
    }
    @Test void wrongLocaleMissingBlockAndInventedProtectedBlockAreRejected() {
        for (String output : List.of(VALID.replace("\"targetLocale\":\"AZ\"", "\"targetLocale\":\"EN\""),
                VALID.replace("\"id\":\"p3\"", "\"id\":\"p2\""),
                "{\"sourceLocale\":\"EN\",\"targetLocale\":\"AZ\",\"parts\":[]}")) {
            assertThatThrownBy(() -> service(new ScriptedModel(output)).translate(Locale.EN, Locale.AZ, SOURCE))
                    .isInstanceOf(IllegalArgumentException.class);
        }
    }
    @Test void sameLocaleAndCodeOnlyInputsDoNotCallModel() {
        var model = new ScriptedModel(VALID);
        assertThat(service(model).translate(Locale.EN, Locale.EN, SOURCE)).isEqualTo(SOURCE);
        assertThat(service(model).translate(Locale.EN, Locale.AZ, List.of(SOURCE.get(1))))
                .containsExactly(SOURCE.get(1));
        assertThat(model.prompts).isEmpty();
    }
    @Test void duplicateInputIdsAreRejectedBeforeModelCall() {
        var model = new ScriptedModel(VALID);
        assertThatThrownBy(() -> service(model).translate(Locale.EN, Locale.AZ,
                List.of(SOURCE.get(0), SOURCE.get(0)))).isInstanceOf(IllegalArgumentException.class);
        assertThat(model.prompts).isEmpty();
    }
    @Test void localeLabelAloneDoesNotProveTranslationQuality() {
        var model = new ScriptedModel(VALID.replace("Yalnız təsdiqdən sonra saxlayın.", "Save only after confirmation."));
        var result = service(model).translate(Locale.EN, Locale.AZ, SOURCE);
        assertThat(result.getFirst().text()).isEqualTo("Save only after confirmation.");
    }
}

Yanlış dil testi Azərbaycan dili etiketi ilə gələn ingilis mətnini bilərəkdən qəbul edir. Bu, struktur yoxlamasının sərhədini qeyd edir ki, sonradan UI parse olunmuş record-u redaktor təsdiqi saymasın.

5. Provayderi qiymətləndirməzdən əvvəl müqaviləni yoxlayın

Layihə qovluğunda işlədin:

Terminal
mvn -B -Dtest=TranslationTest test

Bu beş test əl ilə yazılmış çıxışdan istifadə edir və şəbəkə çağırışı etmir. Həqiqi modeli qiymətləndirmək üçün texniki mətn, inkar, rəqəmlər, qısa qeyri-müəyyən ifadələr və tətbiqin işlədiyi mənbə versiyalarından ibarət yoxlanmış ikidilli dəst hazırlayın. Axıcı mətni avtomatik düzgün saymaq əvəzinə model və prompt versiyalarını həmin dəstdə müqayisə edin.

Seçilmiş model dəstəkləyirsə, provayderin native JSON və ya schema funksiyası format uyğunluğunu yaxşılaşdıra bilər. Bərpa kodunu və ikidilli yoxlamanı əvəz etmir. Nəticə yoxlamadan keçməyəndə sonsuz düzəltmə sorğuları göndərməyin. Hər cəhd gecikməni, xərci və şəxsi mətnin ötürülməsini artıra bilər.

6. Yalnız yoxlanmış nəticəni nəşr edin

Yığılmış tərcüməni mənbə və qorunan bloklarla yanaşı göstərin. Əhəmiyyətli təlimat və ya kurs materialı nəşrindən əvvəl yoxlama tələb edin. Tərcümə variantının təsdiqləndiyini model müəyyən etməməlidir. Açıq redaktə siyasəti olmadan yenidən generasiya zamanı insanın dəyişdirdiyi tərcümənin üzərinə yazmayın.

Prose mətnini etibar edilməyən məlumat sayın. O, çıxış dilini dəyişmək və ya gizli məzmunu açmaq təlimatı daşıya bilər. Dil yoxlamasını, qorunan blokların dəqiq bərpasını və çıxış yoxlamasını modeldən kənarda saxlayın. Şəxsi mətnin provayderə göndərilməsinə icazə olmalıdır. Ortaq converter-in xəta yolunda nə mənbəni, nə də tərcüməni loglayın.

Tapşırıq: iki prose bloku arasında əmr olan Azərbaycan dilindən ingilis dilinə nümunə əlavə edin. Əmrin və blok sırasının dəqiq qaldığını yoxlayın, sonra mətni ikidilli redaktora qiymətləndirin. Bir yoxlamanın keçməsi digərinin də keçdiyini göstərmir.

Mənbələr: