Plugin fejlesztési minták esetenként
Az előző fejezetek egyesével mutatták be az SDK építőelemeit. Ez a fejezet fordítva közelít: egy konkrét fejlesztési feladatból indul ki ("egy kérdésre egyszerűen kell válaszolni", "több lépésben kell adatot összegyűjteni és feldolgozni", "a felhasználótól hiányzó adatot kell bekérni" stb.), és megmutatja, melyik interfész-kombináció illik hozzá, milyen sorrendben kell megírni, és melyik korábbi fejezet tárgyalja a részleteket.
Melyik mintát mikor?
| A feladat jellege | Minta | Fő interfészek |
|---|---|---|
| Egy kérdésre egy lépésben, a platform saját retrieval-jével vagy egy egyszerű saját lekérdezéssel válaszolható | A – Egyszerű lookup tool | IChatbotTool |
| A válaszhoz több, egymásra épülő lépés kell, és a lépések közötti adatátadást nem akarod a hívó kódra bízni | B – Orchestrátor-aktivált tool priority-lánccal | IOrchestratorActivatedTool + IContextConsumerTool
vagy IToolDependent |
| A kérdés megválaszolásához hiányzó adatra van szükség, amit egy visszakérdezéssel be kell gyűjteni | C – Interaktív, tisztázó kérdéses tool | IClarifiableTool |
| Egy már ingestált dokumentumtípusból determinisztikusan (nem LLM-mel) kinyerhető, fix szerkezetű adatot kell a tudásgráfba írni | D – NER ingestion tool + determinisztikus utófeldolgozás | AbstractNERIngestionTool.postProcessDomainSpecificData() |
| A válasz szerkezete (pl. egy kimerítő felsorolás vagy egy szimuláció) olyan, hogy a záró LLM kör kihagyásával, saját magad építve és streamelve megbízhatóbb, mint egy LLM-mel megfogalmaztatott összefoglaló | E – ANSWERED módú, önmagát streamelő tool | ToolExecutionResult.asAnswered() |
| Egy már felépített tudásgráfon kell tetszőleges relációtípus mentén, 1-hop bejárással kapcsolódó fogalmakat keresni | F – Tudásgráf-bejárás | KnowledgeGraphService.findConnectedConcepts() |
| A válaszhoz szükséges tool-ok köre és sorrendje nem rögzíthető előre kódban, hanem a feladat tényleges, futásidejű tartalmától függ | G – Dinamikus feladat-dekompozíció (agent-koordináció) | IAgentCoordinator |
| A tool végrehajtása több lépésben, hosszabb ideig tart, és a felhasználó a válasz megérkezése előtt lecsatlakozhat vagy explicit megszakíthatja a kérést | H – Megszakítható, hosszú futású tool | IResponseEmitter.isCancelled() |
A minta Egyszerű lookup tool
A legtöbb tool ebbe a kategóriába esik: a felhasználó kérdéséből egy-két paramétert kell kinyerni, ezekkel egy lekérdezést vagy számítást elvégezni, és az eredményt kontextusként vagy kész válaszként visszaadni.
Lépések:
- Request bean megírása (→ 3.2) – plain Lombok
@Data, Javadoc a fejlesztőnek, nem az LLM-nek szól. IChatbotTool<Request>implementálása:getName(),getDomain()/getEligibleSystem(),getInstruction()(paraméter-kinyerési útmutató),getExecutableInstruction()(LLM routing – mikor aktiválódjon a tool),getRequestClass().executeWithResult(...)-banexractToolParam(llmAssistant, objectMapper, userPrompt)a paraméter-kinyeréshez (→ 8. fejezet), majd a saját lekérdezés/ számítás, végülToolExecutionResult.asContext(...)vagyasAnswer(...)a visszatéréshez (→ 8. fejezet).@Component+ a pluginMETA-INF/services/hu.medical.rag.common.tool.intf.IChatbotToolfájljába felvenni a teljes csomagnevet (→ 11. fejezet) – enélkül aServiceLoadernem tölti be, a@Componentönmagában nem elég.
getInstruction() (paraméter-kinyerés) és a
getExecutableInstruction() (aktiválási döntés) két külön célt
szolgál, két külön LLM hívás mögött – nem szabad összekeverni a tartalmukat.
Az előbbi a mezőkitöltést írja le, az utóbbi egy true/false döntést.
B minta Orchestrátor-aktivált tool priority-lánccal
Akkor kell, ha egy válasz több, egymásra épülő lépésből áll, és a lépések közötti adatátadást nem a hívó kódra (pl. egy közös mezőre vagy konstruktor- paraméterre) akarod bízni, hanem a platform priority-csoportos végrehajtási mechanizmusára.
Lépések:
- Minden lépés saját
IOrchestratorActivatedTool– kötelezőgetPriority(): kisebb szám korábban fut, azonos prioritásúak párhuzamosan (→ 4. fejezet). - Ha egy későbbi (nagyobb prioritású) lépésnek szüksége van egy korábbi lépés
kimenetére, de nem akarsz közöttük közvetlen függőséget: a későbbi lépés
implementálja az
IContextConsumerTool-t: az orchestrátor azexecuteWithPriorContext(...)metódusát hívja, és a korábbi lépések eredményét paraméterként adja át - ebből olvassa ki a korábbi lépés eredményét egy megállapodás szerinti kulcs alatt, példánymezőben nem tárolva (→ 4.8). - Ha a függőség garantált és szigorúbb (a lépés nélkül a tool nem tud
dolgozni, és a végrehajtómotor az LLM aktiválási döntésétől függetlenül is
futtassa a függőséget):
IToolDependentésexecuteWithRequiredContext(...), amely a függőségek kontextusát paraméterként kapja meg, nem instance-mezőn keresztül (→ 4.8). - Az orchestrátor tool (
AbstractOrchestratorToolleszármazott) nem sorolja fel explicit módon a priority-lánc tagjait – azokat aToolRegistrya domain/system alapján automatikusan megtalálja; az orchestrátorban csak azokat a tool-okat kell explicit kizárni (setExcludedTools(...)), amelyeket kizárólag egy másik mechanizmus (pl. deep fallback) aktivál, nem a normál priority-lánc (→ 5. fejezet).
C minta Interaktív, tisztázó kérdéses tool
Akkor kell, ha a kérdés megválaszolásához olyan adat hiányzik, amit nem lehet kitalálni vagy alapértelmezni – például egy döntés, amit csak a felhasználó tud meghozni (kort, helyszínt, kategóriát stb.).
Lépések:
IClarifiableTool<Request>implementálása – kiterjeszti azIChatbotTool-t és azIOrchestratorActivatedTool-t egyszerre (→ 7. fejezet).- Egy közös belépési pont (pl.
executeWithRequest(Request)) írása, amelyet mind az első aktiváláskor (executeWithResult(...)-ból), mind a visszakérdezés folytatásakor (executeWithClarification(ClarificationSession, ...)-ból) meghívsz – ez tartja egy helyen a döntési logikát. detectNextQuestion(Request): sorban megvizsgálja a kötelező mezőket, és az elsőt, ami hiányzik, egyClarifyingQuestion-nel kérdezi vissza. Ha nincs több hiányzó mező,null-t ad vissza, és a tool a tényleges feldolgozással folytatja.applyAnswer(Request, questionId, answer): a felhasználó válaszát a megfelelő mezőbe írja, majd a hívó újra meghívjadetectNextQuestion-t a következő hiányzó mezőért.- Ha nincs több hiányzó mező:
ToolExecutionResult.asGuaranteedContext(...)vagy más, nemNEEDS_CLARIFICATIONmód a tényleges válasszal.
ClarificationSession-ként tárolja a körök
között – a tool-nak nem kell saját állapottárolást írnia hozzá.
D minta NER ingestion tool + determinisztikus utófeldolgozás
Akkor kell, ha egy dokumentumtípus fix, sablonos szerkezetű részletéből (pl. egy jogszabályi vagy sablon-eredetű, mindig ugyanúgy felépülő mondatból) strukturált adatot akarsz a tudásgráfba írni – de ez az adat nem alkalmas LLM-alapú NER-kinyerésre, mert a pontos szöveg visszaadása a lényeg, amiben egy regex megbízhatóbb, mint egy LLM.
Lépések:
AbstractNERIngestionToolleszármazottban implementáld adefineEntityTypes()/defineRelationTypes()-t a domain rendes, LLM-alapú entitás- és relációkinyeréséhez (→ 5.8, → 10. fejezet).- Írd felül a
postProcessDomainSpecificData(String domain, String pageEmbeddingCollectionName)hook-ot – ez a mention-írás után, batch-enként egyszer,_SUCCESS-fájllal védve fut le (→ 10.8). - A hook-ban
findPagesByNameSuffix(pageEmbeddingCollectionName, system, nameSuffix)-szal kérd le a releváns dokumentumtípus oldalait (List<PageTextEntry>) – ez a tárolt dokumentum-szerkezet ismerete nélkül, SDK-biztos módon adja vissza az oldal nevét és szövegét. - Regex-szel vagy más determinisztikus logikával nyerd ki a keresett
adatot, majd a meglévő, általános célú
knowledgeGraphService.upsertConcept(...)/upsertRelation(...)hívásokkal írd be a gráfba – saját, domain-specifikus fogalom- és relációtípusként, amely nem szerepel az LLM-alapú kinyerés vokabuláriumában.
KnowledgeGraphService szándékosan általános célú: nincs benne
semmilyen domain-specifikus entitástípus vagy mező beégetve. A
type/relationType mindig a hívó által megadott,
tetszőleges string – ne próbáld ezt a service-t domain-specifikus mezőkkel
bővíteni, a domain-specifikus jelentést a hívó kód adja a paraméterek
értékével.
E minta ANSWERED módú, önmagát streamelő tool
Akkor éri meg, ha a válasz szerkezete olyan, hogy egy determinisztikusan felépített szöveg megbízhatóbb, mint egy LLM-mel megfogalmaztatott összefoglaló – például egy kimerítő felsorolás, ahol a záró LLM kör kihagyhatna vagy hozzáköltyi elemeket adhatna a listához.
Lépések (→ 9.4):
- Építsd fel a végleges választ magad (pl.
StringBuilder-rel), determinisztikus logikával – nincs záró LLM kör, amit meg kellene bíznod a végső megfogalmazással. - Streameld a
progressEmitter-en keresztül: minden szövegdarabranew TextGenerationResponse("LLM_GENERATION")+setContent(...), majdemitter.next(...). Több önálló szegmensnél a szegmensek között"SEGMENT_BREAK"progress-státusszal jelezd a törést – a stream ettől nem zárul le. - A végén hívd meg a
finalCallback.onSuccess(getName(), doneChunk)-ot egyOpenAIResponseVO-val, amelyensetDone(true)és egyTextChatMessageVO.builder().content(teljesVálasz).build()szerepel. - Térj vissza
ToolExecutionResult.asAnswered(getName())-nel.
ANSWERED mód a kérést terminálisan lezárja: ha egy magasabb
prioritású (később futó) lépés is aktiválódna ugyanabban a körben, az már
nem fut le. Ez szándékosan van így – egy önmagát kézbesítő tool után nincs
mit hozzáadnia egy további lépésnek.
F minta Tudásgráf-bejárás egy már felépített gráfon
Akkor kell, ha egy másik tool-nak (akár a D minta szerint épített, akár a platform rendes NER pipeline-ja által épített) tudásgráfon kell tetszőleges relációtípus mentén, egy lépésben kapcsolódó fogalmakat keresni.
Lépések (→ KnowledgeGraphService fejezet):
- Normalizáld a kiinduló kulcsszót/label-t
KnowledgeGraphUtil.normalizeLabel(...)-lal. - Hívd meg
knowledgeGraphService.findConnectedConcepts(domain, normalizedLabels, relationType, reverse, targetType)-t – az eredmény a talált fogalmakkey/label/typemetaadata. - Több lépéses bejárásnál (pl. fogalom → köztes fogalom → végső fogalom) a
kimenetet add be a következő hívás
normalizedLabelsparaméterének – nincs beépített többlépéses bejáró metódus, a láncolást a hívó kód végzi.
AbstractNERIngestionTool-on keresztül,
LLM-alapú kinyeréssel hozták létre, az iránya (melyik végpont került a
from, melyik a to oldalra) kinyerésenként eltérhet
– erre sem a prompt, sem a kód nem ad garanciát. Ilyen relációtípusnál
érdemes mindkét irányban lekérdezni (reverse=false és
reverse=true), és a találatokat a key mező alapján
deduplikálni. Ez a kockázat nem áll fenn a saját, kódból hívott
upsertRelation-nel létrehozott relációkra (pl. a D minta
szerint), ott az irány mindig a hívó kódban rögzített, garantált.
G minta Dinamikus feladat-dekompozíció (agent-koordináció)
Akkor kell, ha egy feladat megválaszolásához szükséges tool-ok köre és sorrendje nem rögzíthető előre, fix priority-lánc formájában, mert a feladat tényleges, futásidejű tartalmától függ – például egy összetett kérdés több, egymástól eltérő számú és típusú named entitást is tartalmazhat, és mindegyikhez egy-egy önálló rész-lépés kell.
Lépések (→ 6. fejezet):
- Implementáld az
IAgentCoordinator-t egy már meglévőIOrchestratorActivatedTool-on – nem kell hozzá külön, csak erre a célra létrehozott tool (→ 6.1). decomposeIntoSubtasks(TaskInfo)-ban döntsd el (saját LLM-hívással vagy determinisztikus logikával), hogy a kapott feladat a te bontásod szempontjából releváns-e – ha nem, a kapottTaskInfo-t változatlanul add vissza.- Ha igen, hozz létre rész-lépéseket: minden rész-lépés egy önálló,
önmagában is érthető
taskPrompt-tal rendelkezőTaskInfo. Ha már a bontás pillanatában tudod, melyik tool-nak kell futnia egy adott rész-lépésen, állítsd be arequiredToolNameshalmazát – ezzel a szokásos, LLM-alapú suitability teszt is kimarad, és a rész-lépésen kizárólag ezek a tool-ok futnak. Ha egy rész-lépésre csak egy korábbi testvér eredményétől függően van szükség, jelöld feltételesnek (setConditional(true)), és adecideConditionalSubtask()-ban dönts róla (→ 6.4). - A kapott
taskInfopéldányt módosítsd (taskInfo.setSubTasks(...)) – ne hozz létre és adj vissza egy újat, mert az orchestrátor a feladat állapotát ugyanezen a példányon tartja karban, akár több tisztázó körön át is. - Ha a szülő feladaton futó tool-nak hozzá kell férnie a rész-lépéseken
aktiválódott tool-ok kimenetéhez, implementálja az
IContextConsumerTool-t (tisztázó kérdés esetén azIClarifiableContextConsumerTool-t) – a puffer feltöltése és paraméterként való átadása automatikus, a coordinatornak ehhez nincs teendője (→ 6.7, → 4.8).
TaskInfo.appliedCoordinatorNames tartja
nyilván (→ 6.3). Egy ismételt
aktiválástól tehát nem kell tartani, és nem kell saját idempotencia-jelzőt
írni hozzá.
H minta Megszakítható, hosszú futású tool
Akkor érdemes rá figyelni, ha egy tool végrehajtása több, egymást követő drága lépésből áll (több szekvenciális LLM hívás, egy Spark job indítása, egy nagyobb, több oldalas gráf- vagy dokumentumbetöltés) – ilyenkor a felhasználó a végleges válasz megérkezése előtt lecsatlakozhat, vagy egy explicit megszakítás-kérést küldhet, és kár lenne a hátralévő, már senki által nem várt lépéseket is végigfuttatni.
Lépések:
- Minden drága lépés – egy újabb LLM kör, egy Spark job indítása, egy
nagyobb oldalbetöltés – előtt ellenőrizd a kapott
responseEmitterállapotát:responseEmitter != null && responseEmitter.isCancelled(). A null-check kötelező, mert platform módban a kapottIResponseEmitter(TextGenerationResponseEmitter) egy lehetségesennull-t wrapelő, no-op-safe implementáció is lehet. - Ha a kérés eddigre megszakításra került, egyszerűen térj vissza (pl.
null-lal aCONTEXTmód helyett) – nincs szükség explicit hiba dobására vagy takarításra, a pipeline a megszakított válaszfolyamot már nem továbbítja sehova. - A tényleges megszakítás-állapotot a platform tartja karban egy
JVM-egész, kérésazonosító szerint kulcsolt nyilvántartásban
(
CancellationRegistry) – ide a kliens lecsatlakozása vagy egy explicit REST megszakítás-kérés ír, a tool-nak ehhez sosincs közvetlen hozzáférése, kizárólag a kapottresponseEmitter.isCancelled()-en keresztül kérdezi le (→ API referencia).
isCancelled() ellenőrzés checkpoint-alapú, nem szakítja
meg automatikusan egy már elindított, önmagában hosszan futó hívást – egy
folyamatban lévő LLM-streaming vagy egy már elindított Spark job a saját
lefutásáig fut, az ellenőrzés csak a checkpontok közötti, még el nem
indított következő drága lépést akadályozza meg.