SDK
Haladó fejezet

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 jellegeMintaFő 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 minták nem zárják ki egymást. Egy komplexebb plugin jellemzően több mintát is használ egyszerre – pl. egy NER ingestion tool (D minta) adja a tudásgráf alapját, amelyre egy orchestrátor-aktivált tool (B minta) épít gráfbejárással (F minta), és a végén saját maga streameli a választ (E minta).

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:

  1. Request bean megírása (→ 3.2) – plain Lombok @Data, Javadoc a fejlesztőnek, nem az LLM-nek szól.
  2. IChatbotTool<Request> implementálása: getName(), getDomain()/getEligibleSystem(), getInstruction() (paraméter-kinyerési útmutató), getExecutableInstruction() (LLM routing – mikor aktiválódjon a tool), getRequestClass().
  3. executeWithResult(...)-ban exractToolParam(llmAssistant, objectMapper, userPrompt) a paraméter-kinyeréshez (→ 8. fejezet), majd a saját lekérdezés/ számítás, végül ToolExecutionResult.asContext(...) vagy asAnswer(...) a visszatéréshez (→ 8. fejezet).
  4. @Component + a plugin META-INF/services/hu.medical.rag.common.tool.intf.IChatbotTool fájljába felvenni a teljes csomagnevet (→ 11. fejezet) – enélkül a ServiceLoader nem tölti be, a @Component önmagában nem elég.
⚠️
A 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:

  1. Minden lépés saját IOrchestratorActivatedTool – kötelező getPriority(): kisebb szám korábban fut, azonos prioritásúak párhuzamosan (→ 4. fejezet).
  2. 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 az executeWithPriorContext(...) 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).
  3. 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 és executeWithRequiredContext(...), amely a függőségek kontextusát paraméterként kapja meg, nem instance-mezőn keresztül (→ 4.8).
  4. Az orchestrátor tool (AbstractOrchestratorTool leszármazott) nem sorolja fel explicit módon a priority-lánc tagjait – azokat a ToolRegistry a 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:

  1. IClarifiableTool<Request> implementálása – kiterjeszti az IChatbotTool-t és az IOrchestratorActivatedTool-t egyszerre (→ 7. fejezet).
  2. 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.
  3. detectNextQuestion(Request): sorban megvizsgálja a kötelező mezőket, és az elsőt, ami hiányzik, egy ClarifyingQuestion-nel kérdezi vissza. Ha nincs több hiányzó mező, null-t ad vissza, és a tool a tényleges feldolgozással folytatja.
  4. applyAnswer(Request, questionId, answer): a felhasználó válaszát a megfelelő mezőbe írja, majd a hívó újra meghívja detectNextQuestion-t a következő hiányzó mezőért.
  5. Ha nincs több hiányzó mező: ToolExecutionResult.asGuaranteedContext(...) vagy más, nem NEEDS_CLARIFICATION mód a tényleges válasszal.
ℹ️
Az állapotot (melyik kérdésnél tartunk, mi az eddig kitöltött request) a platform Redis-ben, 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:

  1. AbstractNERIngestionTool leszármazottban implementáld a defineEntityTypes()/defineRelationTypes()-t a domain rendes, LLM-alapú entitás- és relációkinyeréséhez (→ 5.8, → 10. fejezet).
  2. Í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).
  3. 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.
  4. 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.
⚠️
A 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):

  1. É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.
  2. Streameld a progressEmitter-en keresztül: minden szövegdarabra new TextGenerationResponse("LLM_GENERATION") + setContent(...), majd emitter.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.
  3. A végén hívd meg a finalCallback.onSuccess(getName(), doneChunk)-ot egy OpenAIResponseVO-val, amelyen setDone(true) és egy TextChatMessageVO.builder().content(teljesVálasz).build() szerepel.
  4. Térj vissza ToolExecutionResult.asAnswered(getName())-nel.
💡
Az 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):

  1. Normalizáld a kiinduló kulcsszót/label-t KnowledgeGraphUtil.normalizeLabel(...)-lal.
  2. Hívd meg knowledgeGraphService.findConnectedConcepts(domain, normalizedLabels, relationType, reverse, targetType)-t – az eredmény a talált fogalmak key/label/type metaadata.
  3. 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 normalizedLabels paramé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.
⚠️
Ha a bejárt relációtípust 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):

  1. 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).
  2. 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 kapott TaskInfo-t változatlanul add vissza.
  3. 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 a requiredToolNames halmazá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 a decideConditionalSubtask()-ban dönts róla (→ 6.4).
  4. A kapott taskInfo pé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.
  5. 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 az IClarifiableContextConsumerTool-t) – a puffer feltöltése és paraméterként való átadása automatikus, a coordinatornak ehhez nincs teendője (→ 6.7, → 4.8).
💡
Egy adott coordinator legfeljebb egyszer fut le egy feladaton – vagy valamelyik ősén –, függetlenül attól, hogy hány tisztázó körön megy át a feldolgozás; ezt a 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á.
⚠️
A G minta rokon, de nem azonos a B mintával: a B minta egy fix, kódban rögzített priority-lánc tagjai közötti, statikus együttműködés, míg a G minta a lánc tagjait – a rész-lépéseket és a rajtuk futó tool-okat – futásidőben, a feladat tartalma alapján hozza létre. A kettő kombinálható: egy coordinator által létrehozott rész-lépésen ugyanúgy futhat egy priority-lánc, mint egy közvetlenül a felhasználó kérdéséből indulón.

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:

  1. 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 kapott IResponseEmitter (TextGenerationResponseEmitter) egy lehetségesen null-t wrapelő, no-op-safe implementáció is lehet.
  2. Ha a kérés eddigre megszakításra került, egyszerűen térj vissza (pl. null-lal a CONTEXT mó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.
  3. 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 kapott responseEmitter.isCancelled()-en keresztül kérdezi le (→ API referencia).
⚠️
Az 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.