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 |
@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ódus | Mit 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.
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;
}
}
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:
- Első
%s→tasksparaméter értéke - Második
%s→tool.getInstruction()visszatérési értéke
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:
- Szaktérület routing: tünetből meghatározza az illetékes orvosi szaktérületet (kardiológia, neurológia, bőrgyógyászat stb.)
- Sürgősségi triage: életveszély esetén Sürgősségi betegellátást (SBO) javasol
- Trauma detekció: baleseti, mozgásszervi sérülésnél Traumatológiát javasol
@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:
-
OpenAIClient– a platform helyi, on-premise vLLM példányára hív. API kulcs nem szükséges. Az adatok nem hagyják el a szervezet infrastruktúráját. -
CloudOpenAIClient– felhőalapú LLM végpontra hív (pl. OpenAI, Azure). Az adatok elhagyják az on-premise infrastruktúrát.
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);
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 |