SDK
8. fejezet

LLM hívások pluginből

A platform háromféle módon teszi lehetővé az LLM elérését plugin fejlesztők számára: egyedi system prompttal, beépített domain tudású asszisztensekkel, és Spark-alapú párhuzamos batch feldolgozással.

8.1   Áttekintés

Réteg Osztály / interfész Mire való
Tool paraméter-kinyerés ILlmAssistant Felhasználói kérdésből kinyeri a tool request bean mezőit; JSON schema-kényszeres
Domain asszisztens MedicalAIAssistant, WebSearchAssistant, BaseSummaryAssistant, EntityAnalysisAssistant Beépített szaktudású prompt, @Autowired injektálható
PDF generálás DocumentGenerationAssistant (abstract) Leszármaztatással testreszabható PDF tool asszisztens
Batch / párhuzamos BaseLLMTransformer, SummaryToolTransformer, BatchedSummaryTransformer Spark-alapú, sok sort párhuzamosan dolgoz fel; ingestion pipeline-okhoz
ℹ️
A plugin nem példányosít LLM klienst és nem kezeli a HTTP kommunikációt maga – az összes LLM elérési út a platform által biztosított osztályokon és interfészeken keresztül történik, amelyek az SDK-ban elérhetők és @Autowired-del injektálhatók.

8.2   ILlmAssistant – tool paraméter-kinyerés

Az ILlmAssistant szerepe specifikus: a felhasználó természetes nyelvű kérdéséből kinyerni azokat a strukturált paramétereket, amelyekre a tool-nak szüksége van a futáshoz. Nem általános célú LLM hívó – arra a közvetlen kliens injektálás (8.5) való.

A platform a tool getRequestAsJsonNode() által leírt JSON sémát kényszerként alkalmazza az LLM válaszára (constrained decoding), így a kimenet mindig a request bean struktúrájának megfelelő – nem kell JSON parsing hibákat kezelni.

Beépített paraméter-kinyerő – exractToolParam()

Az IChatbotTool interfész egy beépített default metódust biztosít, amely elvégzi a paraméter-kinyerést anélkül, hogy saját system promptot kellene írni. Elég implementálni a következő kettőt:

MetódusMit kell visszaadni
getInstruction() Magyar nyelvű leírás: melyik request mezőbe mit kell kinyerni. Érdemes belerakni a mai dátumot, hogy az LLM relatív időreferenciákat (pl. "ezen a héten") abszolút értékre hozza.
getRequestClass() A request bean osztálya: return DutySearchRequest.class;
💡
getRequestAsJsonNode()-ot nem kell felülírni – az interfész default implementációja (JsonSchemaGenerator.generateSchema(getRequestClass())) a fenti kettő alapján automatikusan a helyes sémát adja vissza.
⚠️
Mivel a séma kényszer, a getInstruction() szövegében megadott típusoknak egyezniük kell a request bean mezőinek típusával. Ha az instrukció pl. (type: integer)-t ír egy String mezőhöz, a modell a számot nem írhatja le, és a dekódolás csak whitespace-t enged neki - a kimenet üres szóközsorozatba fut. A platform az ilyen, illetve az ismétlésbe ragadt kimenetet levágja, és a csonka strukturált választ a lezáratlan JSON-szerkezetek lezárásával helyreállítja, de a hiányzó mezők értéke ilyenkor elvész.

Az executeWithResult()-ban egyetlen sor az egész paraméter-kinyerés:

@Autowired private ILlmAssistant llmAssistant;
@Autowired private ObjectMapper objectMapper;

@Override
public ToolExecutionResult executeWithResult(...) {
    try {
        MyRequest request = exractToolParam(llmAssistant, objectMapper, userPrompt);
        if (request == null) {
            request = new MyRequest(); // fallback: mezők default értékekkel
        }
        return executeWithRequest(request);
    } catch (Exception e) {
        return null;
    }
}

@Override
public String getInstruction() {
    return """
        Mai dátum: %s
        - year: (yyyy) Ha a felhasználó megadta, melyik évre kíváncsi
        - month: (MM) Ha a felhasználó megadta, melyik hónapra kíváncsi
        """.formatted(GeneralUtil.getValueAsLocalDateInfo(LocalDate.now()));
}

// getRequestAsJsonNode()-ot nem írjuk felül - a default interfész-implementáció elegendő

@Override
public Class<? extends MyRequest> getRequestClass() {
    return MyRequest.class;
}
ℹ️
exractToolParam() null-t adhat vissza, ha az LLM nem tudta kinyerni a paramétereket (pl. túl kevés info a kérdésben). Érdemes fallback-et definiálni – vagy üres default request-tel folytatni, vagy null visszaadásával jelezni, hogy a tool nem tudja kiszolgálni a kérést.

Eseti instrukció – exractToolParam(..., instruction)

Ha a rendszerprompt és a request-séma maradhat a beépített default, csak az instrukció szövegét kell a feldolgozandó szöveg tartalma alapján, futásidőben kiválasztani (pl. két, egymástól eltérő forgatókönyvre két külön, dedikált instrukció-szöveg tartozik), nem kell a lenti, teljesen manuális hívásra váltani – az exractToolParam()-nak van egy negyedik paraméteres változata is, amely a getInstruction() helyett egy explicit módon átadott instrukciót használ:

private static final String INSTRUCTION_A = "...";
private static final String INSTRUCTION_B = "...";

@Override
public ToolExecutionResult executeWithResult(...) {
    try {
        String instruction = someCondition ? INSTRUCTION_A : INSTRUCTION_B;
        MyRequest request = exractToolParam(llmAssistant, objectMapper, userPrompt, instruction);
        ...
    } catch (Exception e) {
        return null;
    }
}
💡
A getInstruction()-t ilyenkor is implementálni kell – az interfész elvárja, hogy sose adjon vissza null-t. Érdemes belőle az egyik forgatókönyv szövegét visszaadni, hogy a metódus önmagában is értelmes maradjon.

Ha ugyanez a döntés (a fenti példában someCondition) egy request-mezőt is meghatároz, amit emiatt nem az LLM-nek kell kitöltenie, hanem a hívó kódnak a kinyerés után, azt a mezőt érdemes @ExcludeFromSchema-val kivenni a sémából (→ 3. fejezet), és közvetlenül, kódban beállítani:

MyRequest request = exractToolParam(llmAssistant, objectMapper, userPrompt, instruction);
if (request != null) {
    request.setSomeConditionUsed(someCondition);
}

Manuális hívás – egyedi system prompttal

Ha az alapértelmezett system prompt nem elegendő (pl. összetett instrukció, több lépéses kinyerés), az ILlmAssistant közvetlenül is hívható:

Injektálás

@Autowired
private ILlmAssistant llmAssistant;

@Autowired
private ObjectMapper objectMapper;

Hívás

HashMap<String, Object> extracted = llmAssistant.generateToolRequest(
    userPrompt,    // a felhasználói kérdés
    systemPrompt,  // az LLM instrukciója – mit kell kinyerni
    tasks,         // opcionális kiegészítés (null ha nincs)
    this           // a hívó tool – névhez és sémához
);

MyRequest request = objectMapper.convertValue(
    extracted.get(getName()), MyRequest.class);

A %s helyőrző minta

Ha a systemPrompt %s helyőrzőket tartalmaz, a platform automatikusan behelyettesíti:

private static final String SYSTEM_PROMPT = """
        Feladatod: %s
        Azonosítsd a következő mezőket:
        %s
        """;

// Híváskor:
llmAssistant.generateToolRequest(
    userPrompt,
    SYSTEM_PROMPT,
    "Keress szakrendelést a felhasználó panaszai alapján.",  // → első %s
    this  // getInstruction() → második %s
);

Ha a prompt nem tartalmaz %s-t, a tasks és a getInstruction() értéke figyelmen kívül marad.

8.3   Platform asszisztensek – beépített domain tudás

A platform több specializált asszisztenst biztosít, amelyek tartalmazzák a szaktérülethez szükséges system promptot. Ezek @Service Spring bean-ek, @Autowired-del injektálhatók.

A hívás szignaturája eltér az ILlmAssistant-tól: nincs systemPrompt paraméter – az a beépített getSystemPrompt()-ból jön. A tasks (első %s) és a tool.getInstruction() (második %s) ugyanúgy behelyettesítődik.

// BaseAIAssistant.generateToolRequest szignatúra – 3 paraméter:
HashMap<String, Object> result = medicalAIAssistant.generateToolRequest(
    userPrompt,  // felhasználói kérdés
    tasks,       // opcionális kiegészítő instrukció (null ha nincs)
    this         // a hívó tool
);

MedicalAIAssistant

Orvosi szakismerettel rendelkező asszisztens. Beépített logikája:

@Autowired
private MedicalAIAssistant medicalAIAssistant;

// Használat executeWithResult()-ben:
HashMap<String, Object> resp = medicalAIAssistant.generateToolRequest(
    userPrompt,
    "Keress szakrendelést a felhasználó panaszai alapján.",
    this
);

WebSearchAssistant

Web keresési paraméterek kinyeréséhez optimalizált asszisztens. Az AbstractWebSearchTool-ban használják.

@Autowired
private WebSearchAssistant webSearchAssistant;

BaseSummaryAssistant

Összefoglaló generáláshoz optimalizált asszisztens. Az összefoglaló tool-ok belső LLM hívásaihoz használják.

EntityAnalysisAssistant

Entitás elemzéshez és kinyeréshez – az AbstractEntityAnalysisTool és az AbstractContextSearchTool használja. Strukturált JSON-t ad vissza az azonosított entitásokról.

DocumentGenerationAssistant (abstract)

PDF dokumentum generáló tool-okhoz. Nem @Service – a plugin fejlesztő leszármaztatja és felülírja a szükséges metódusokat, a konkrét osztályt @Service-ként regisztrálja.

@Service
public class MyPdfAssistant extends DocumentGenerationAssistant {
    // Szükséges konstruktor és domain-specifikus metódusok
}

8.4   Spark transformer – párhuzamos batch LLM hívások

A platform Spark-alapú transformer osztályokat biztosít, amelyek sok sort párhuzamosan, konfigurálható batch mérettel dolgoznak fel LLM hívásokkal. Ezek nemcsak ingestion pipeline-okban hasznosak – tetszőleges adatforrásból leszedett adatokon is alkalmazhatók, a chatbot funkciótól teljesen függetlenül.

A részletes leírás, kódminták és saját transformer implementálási útmutató a Haladó: Spark batch LLM feldolgozás fejezetben olvasható.

8.5   Tetszőleges LLM hívás – közvetlen kliens injektálás

Ha az asszisztens réteg nem elegendő – például streaming callback-et szeretnél, egyedi válaszformátumot, vagy teljes kontrollt akarsz az LLM kérés felett –, a platform LLM klienseit közvetlenül is injektálhatod.

Két kliens érhető el:

⚠️
Adatvédelmi figyelmeztetés. A CloudOpenAIClient használatakor a kérésbe kerülő adatok – beleértve a felhasználói kérdést, a kontextust és bármilyen belső adatot – elhagyják a szervezet infrastruktúráját és egy külső felhőszolgáltatóhoz kerülnek. Egészségügyi, közigazgatási vagy jogi domain-ekben ez GDPR és adatvédelmi kockázatot jelent. Csak olyan adatot adj át a cloud kliensnek, amelynek felhőbe küldéséhez explicit engedély van.

Injektálás

@Autowired
private OpenAIClient openAIClient;          // helyi vLLM – ajánlott

@Autowired
private CloudOpenAIClient cloudOpenAIClient; // felhő – körültekintéssel

Szinkron (blokkoló) hívás

Callback nélkül hívva a kliens blokkoló módon vár a teljes válaszra és azt szövegként adja vissza. A stream(true) flag ilyenkor is kötelező – a platform mindig streaming módban kommunikál az LLM-mel, a szinkron/aszinkron viselkedést a callback jelenléte vagy hiánya szabályozza.

import hu.medical.rag.common.openai.beans.OpenAIRequestVO;
import hu.medical.rag.common.openai.beans.TextChatMessageVO;

List<Object> messages = List.of(
    TextChatMessageVO.builder().role("system").content("Te egy összefoglaló asszisztens vagy.").build(),
    TextChatMessageVO.builder().role("user").content(userPrompt).build()
);

OpenAIRequestVO request = OpenAIRequestVO.builder()
    .model("my-model")         // a platform yaml-ban konfigurált modell neve
    .stream(true)              // mindig true – kötelező
    .messages(messages)
    .temperature(0.3f)
    .build();

// Blokkoló hívás – callback nélkül
String response = openAIClient.chat(request);

Aszinkron (streaming) hívás callbackkel

Ha a válasz tokeneit azonnal a kliensnek szeretnéd streamelni, adj át egy CallbackWithErrorHandler implementációt. Ebben az esetben a hívás azonnal visszatér, a tokenek a callback onSuccess() metódusán keresztül érkeznek.

openAIClient.chat(request, new CallbackWithErrorHandler<OpenAIResponseVO, Exception>() {
    @Override
    public void onSuccess(String callId, OpenAIResponseVO chunk) {
        if (chunk.getMessage() != null && chunk.getMessage().getContent() != null) {
            if (chunk.isDone()) {
                // utolsó chunk – a teljes szöveg elérhető
            } else {
                // köztes token – streameld tovább a kliensnek
                String token = chunk.getMessage().getContent();
            }
        }
    }

    @Override
    public void onFailure(Call call, String callId, Exception e) {
        // hibakezelés
    }
}, true, null);
💡
Az esetek nagy részében az ILlmAssistant vagy a platform asszisztensek elegendők. A közvetlen kliens injektálást akkor érdemes választani, ha strukturált JSON-on túlmutató válaszra van szükség, egyedi streaming logikát szeretnél, vagy a hívást pontosan kontrollálni akarod (pl. saját retry, timeout).

8.6   TOON formátum – strukturált adat LLM-nek

A TOON (Tabled Object Notation) pipe-elválasztós, tömörített adatreprezentáció, amelyet az LLM-ek JSON-nál hatékonyabban dolgoznak fel – kevesebb tokent fogyaszt, és a modell jobban "olvassa" a rekordok közötti határokat.

Az SDK két megközelítést kínál TOON adat előállítására – a felhasználás helyétől függően érdemes választani:

ToonUtil.toToon()ToonInstructionContentVO
Típus Statikus segédmetódus → plain String VO osztály → JsonChatMessageVO content listájába kerül
Mikor Tool CONTEXT vagy ANSWER szövegébe ágyazva, pl. lekérdezési eredmény megjelenítéséhez Strukturált LLM prompt felépítéséhez, ahol kiemelés (headColumns) is szükséges
headColumns Nem támogatott Igen – egyes mezők leírással kiemelhetők a TOON blokk elé

ToonUtil – gyors szövegbeágyazás

Ha a tool saját maga állít össze szöveges választ strukturált adatból (pl. adatbázisból kiolvasott lista), a ToonUtil.toToon() egyetlen hívással elkészíti a formázott blokkot:

@Autowired
private ObjectMapper objectMapper;

// employees: List<EmployeeRecord> vagy Map
String text = "A szakrendelésen dolgozó orvosok:\n"
    + ToonUtil.toToon(objectMapper, employees);

return ToolExecutionResult.asAnswer(getName(), TextAnswer.asSet(text));

A metódus kivételt nem dob – ha a konverzió bármilyen okból sikertelen, üres stringet ad vissza.

ToonInstructionContentVO – strukturált LLM prompt

Ha az LLM-nek küldött promptba kell TOON formátumú adatot illeszteni (pl. egy asszisztens hívásában), a ToonInstructionContentVO a megfelelő eszköz. Támogatja a headColumns-t: egyes mezők leírással és értékkel a TOON blokk elé kerülnek, így hangsúlyozva a legfontosabb adatokat.

ToonInstructionContentVO content = ToonInstructionContentVO.builder()
    .dataMap(dataAsMap)
    .headColumns(new LinkedHashMap<>(Map.of(
        "patientName", "Páciens neve",
        "diagnosis",   "Diagnózis"
    )))
    .build();

JsonChatMessageVO message = JsonChatMessageVO.builder()
    .role("user")
    .content(List.of(content))
    .build();

8.7   Mikor melyiket

Helyzet Ajánlott megközelítés
Tool futtatási paraméterek kinyerése a kérdésből ILlmAssistant – JSON schema-kényszerített, request bean-re mappolva
Orvosi szaktérület, triage, tünetelemzés MedicalAIAssistant injektálva
Web keresési paraméterek kinyerése WebSearchAssistant injektálva
Egy dokumentum összefoglalása tool-ban BaseSummaryAssistant injektálva
Entitás kinyerés gráfhoz EntityAnalysisAssistant injektálva
PDF generálás tool-ban DocumentGenerationAssistant leszármaztatva
Sok dokumentum összefoglalása ingestion-ban SummaryToolTransformer vagy BatchedSummaryTransformer
Egyedi batch LLM feldolgozás ingestion-ban BaseLLMTransformer leszármaztatva
Teljes kontroll, egyedi streaming, egyedi retry OpenAIClient közvetlen injektálás (helyi vLLM)
Felhő LLM – nem személyes, nem érzékeny adattal CloudOpenAIClient – adatvédelmi egyeztetés után