Orchestrátor tool fejlesztés
Az orchestrátor a domain-specifikus vezérlési logika otthona – minden kérés ide csatornázódik be először. Ez a fejezet az implementáció teljes képét mutatja, beleértve a NER ingestion tool kapcsolatát, amely a domain grafikus tudásának egyetlen forrása.
5.1 Mi az orchestrátor és mikor kell?
Az orchestrátor akkor indokolt, ha a domain kérései egynél összetettebb feldolgozást igényelnek: több tool koordinálása, sub-query bontás, domain-specifikus döntési logika vagy deep fallback keresés szükséges. Minden kérés – domain egyezés esetén – először az orchestrátoron megy keresztül, amelynek lehetősége van tool-okat aktiválni, a kérdést al-kérdésekre bontani, vagy visszalépni a RAG pipeline-ba (null visszatérés).
Ha a domainhez nincs regisztrált (Spring-managed vagy plugin) orchestrátor, a platform két módon reagálhat – lásd lentebb. Az orchestrátor tehát a plugin fejlesztő szempontjából opcionális – de ha van, teljes kontrollt kap.
Több orchestrátor egy domainhez – system alapú kiválasztás
Egy domainhez több orchestrátor is regisztrálható – jellemzően úgy, hogy mindegyik
a domain más-más system-jéhez tartozik (getEligibleSystem()). Kérésenként
azonban mindig pontosan egy orchestrátor aktiválódik: a platform először a domain
egyezés alapján szűr, majd a jogosultságot nézi – egy orchestrátor akkor jogosult,
ha getEligibleSystem() null-t ad vissza (tehát bármely
kérésre illik), vagy a kérésben egyáltalán nincs system megadva, vagy a kérés
system listája tartalmazza az orchestrátor eligible system-jét.
A jogosult jelöltek közül a kiválasztás a következő elv szerint történik: ha a
kérésben nincs system megadva, a null eligible system-ű (domain-szintű
alapértelmezett) orchestrátor élvez elsőbbséget a system-specifikusakkal szemben.
Ha a kérésben van system, és emiatt egynél több jelölt is jogosult marad, a platform
egyet választ közülük, de ez a választás nem determinisztikus
prioritási sorrend alapján történik.
Ha nincs regisztrált orchestrátor a domainhez – a platform default orchestrátora
Ha a kért domain+system, illetve domain-only szűrésre sem talál a platform
regisztrált (Spring-managed vagy plugin) orchestrátort, a döntés a domain
admin-felületen beállított defaultOrchestrator kapcsolóján múlik:
-
Ha a kérésben egyáltalán nincs domain megadva, vagy a domain
defaultOrchestratorbeállításafalse/hiányzik, a platform orchestrátor nélkül, közvetlenül az LLM routing-ot és az alap RAG pipeline-t futtatja – ez a hagyományos, orchestrátor nélküli viselkedés. -
Ha a domain
defaultOrchestratorbeállításatrue, a platform egy saját, belső default orchestrátort épít fel a kérés domain nevére – ez a szokásos prioritásos tool-végrehajtást futtatja (executeOrchestratorActivatedTools()), egy domain-független RAG retrieval tool-lal fallback-ként, és egy generikus (NER ingestion tool nélküli) deep fallback context search tool-lal. Ez azért hasznos, mert egy frissen létrehozott domainhez – mielőtt bármilyen plugin deployolásra kerülne – így is érvényesül a PromptType-alapú retrieval hangolás, és ha a domainhez időközben mégis regisztrálódik néhányIOrchestratorActivatedTool(de teljes orchestrátor még nem), azok is lefutnak.
A defaultOrchestrator kapcsoló kizárólag a domain admin
konfigurációjának része – a plugin fejlesztőnek nincs ráhatása és nem is
kell vele foglalkoznia. Ha egy plugin később valódi, domain-specifikus
orchestrátort regisztrál, az a következő kérésnél automatikusan átveszi a
vezérlést a default orchestrátortól; ha a plugin eltávolításra kerül, a
platform automatikusan visszaesik a default orchestrátorra (vagy a
defaultOrchestrator=false esetben az orchestrátor nélküli
módra) – ehhez sincs szükség semmilyen plugin-oldali teendőre.
5.2 Az osztályhierarchia
AbstractOrchestrator ← a domain tudás és hook metódusok otthona
└── AbstractOrchestratorTool ← a platform pipeline integráció (final metódusok)
└── MyOrchestratorTool ← a plugin fejlesztő implementációja
A plugin fejlesztő mindig az AbstractOrchestratorTool-ból örököl.
Az AbstractOrchestrator tartalmazza a domain-tudás hook-jait
(gráf szűrők, deep fallback), az AbstractOrchestratorTool pedig
a pipeline integrációt (clarification session kezelés, sub-query bontás,
prioritás-csoportos tool futtatás). Az executeWithResult() és az
executeOrchestratorActivatedTools() metódusok final-ok –
nem írhatók felül.
5.3 Kötelező elemek
Annotációk
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // kötelező – per-request mutable állapot
public class MyOrchestratorTool extends AbstractOrchestratorTool {
@Scope("prototype") nem elhagyható. Az orchestrátor per-request
mutable állapotot tárol (progressEmitter, finalCallback, chatId, userLang,
classification, retrievalOptions). Singleton esetén párhuzamos kérések felülírnák
egymás állapotát.
Konstruktor
A plugin-aktiválási mechanizmus miatt minden plugin tool és
orchestrátor osztálynak publikus, paraméter nélküli konstruktorral kell
rendelkeznie – ez alól az AbstractOrchestratorTool leszármazottak
sem kivételek. A discovery Java ServiceLoader-en keresztül történik
(ami maga is no-arg konstruktort követel meg minden felfedezett osztálytól), és
orchestrátorok esetén a PluginLoader is közvetlenül,
getDeclaredConstructor().newInstance()-szel példányosít – paraméter
nélkül. Az AbstractOrchestrator ősosztály függőségei
(orchestratorAssistant, objectMapper,
toolRegistry stb.) @Autowired mezőként vannak felvéve
már az alaposztályban; ezeket a példányosítás után a platform
(autowireBean()) automatikusan feltölti – nincs szükség rájuk
konstruktor-paraméterként.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyOrchestratorTool extends AbstractOrchestratorTool {
public MyOrchestratorTool() {
super();
}
@Override
public String getDomain() {
return "MyDomain";
}
}
Mivel a konstruktor paraméter nélküli, a domain NER ingestion tool-ját nem lehet
konstruktoron keresztül átadni. Helyette a NER tool-t @Autowired
mezőként kell felvenni, és egy @PostConstruct metódusban regisztrálni
az AbstractOrchestrator védett setNerIngestionTool(...)
setterén keresztül – ez már azután fut le, hogy a platform az
autowireBean()-nel feltöltötte a mezőket:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyOrchestratorTool extends AbstractOrchestratorTool {
@Autowired
private MyNERIngestionTool myNerIngestionTool;
public MyOrchestratorTool() {
super();
}
@PostConstruct
public void init() {
setNerIngestionTool(myNerIngestionTool);
}
@Override
public String getDomain() {
return "MyDomain";
}
}
A NER ingestion tool bekötéséről (mit ad át, mire használja az orchestrátor) részletesen az 5.7 alfejezet szól.
getDomain()
@Override
public String getDomain() {
return "MyDomain"; // egyezik a platform admin által konfigurált domain névvel
}
5.4 Az orchestrate() metódus
Ez az egyetlen kötelezően implementálandó abstract metódus. Minden kéréshez (vagy sub-queryhez, ha multi-hop) meghívódik.
protected abstract ToolExecutionResult orchestrate(
List<String> systems, // aktív system szűrők
TaskInfo taskInfo, // az al-kérdés szövege, típusa, fontos szavak
RetrievalOptions opts // az osztályozásból levezetett retrieval profil
);
Visszatérési értékek:
null– az orchestrátor nem aktivált tool-t; a platform a RAG pipeline-nal folytatja azoptsprofil alapjánToolExecutionResult– valamelyik tool aktiválódott és eredményt adott; a pipeline ennek megfelelően folytatja
A két megközelítés
1. Közvetlen tool hívás – az orchestrátor maga dönt, melyik tool-t hívja és mikor. Tipikus eset: PromptType alapján elágazás.
@Override
protected ToolExecutionResult orchestrate(List<String> systems,
TaskInfo taskInfo, RetrievalOptions opts) {
PromptType type = taskInfo.getPromptType();
if (PromptType.REASONING.equals(type) || PromptType.COMPARATIVE.equals(type)) {
// Entitás-elemző tool közvetlen hívása
ToolExecutionResult res = characterTool.executeWithResult(
systems, taskInfo.getTaskPrompt(), userLang, taskInfo.getImportantWords(),
progressEmitter, finalCallback);
if (res != null) return res;
}
// Ha nem volt aktiválható tool, delegálunk vagy null-t adunk vissza
return null;
}
2. Delegálás az automatikus rendszernek – az orchestrátor
átadja a döntést az executeOrchestratorActivatedTools()-nak,
amely az összes regisztrált IOrchestratorActivatedTool-t aktiválja
prioritás szerint.
@SuppressWarnings("rawtypes")
@Override
protected ToolExecutionResult orchestrate(List<String> systems,
TaskInfo taskInfo, RetrievalOptions opts) {
// Ha egyes tool-okat ki akarunk zárni az automatizmusból:
setExcludedTools(List.of(contextSearchTool.getName()));
return executeOrchestratorActivatedTools(
(Map) CurrentUserUtil.getCurrentUser(), systems, taskInfo, opts);
}
A két megközelítés kombinálható is: egyes PromptType-oknál közvetlen hívás, másokra delegálás.
5.5 executeOrchestratorActivatedTools()
Az automatikus tool futtatás engine-je. Az összes regisztrált
IOrchestratorActivatedTool-t aktiválja a domain-ben:
- LLM-alapú suitability teszt dönti el, melyik tool-t kell aktiválni
(
AbstractRAGRetrievalToolkizárva – az mindig fut fallback-ként) - Az
IToolDependent-et implementáló tool-ok megjelölt függőségeit az engine az LLM döntésétől függetlenül is aktiválja – lásd 4.8 - Tool-ok prioritás szerint csoportosítva futnak (TreeMap, kisebb = korábban)
- Azonos prioritásúak párhuzamosan (CompletableFuture)
- Ha egy csoport már garantált kontextust ad, a maradék csoportok (pl. RAG) kihagyásra kerülnek
Ha az orchestrátor kizár bizonyos tool-okat az automatizmusból (mert maga kezelte):
setExcludedTools(List.of("summary_tool", "character_tool"));
A userData paraméter – saját jogosultsági logika
Az executeOrchestratorActivatedTools() első paramétere (userData)
minden egyes aktiválni kívánt tool isEligible(Map userData) hívásába
változatlanul továbbadódik – ez dönti el, hogy egy tool egyáltalán szóba jöhet-e az
adott felhasználónak, még az LLM routing előtt. A platform a saját autentikációs
rendszerében (Keycloak) azonosítja a hívó felhasználót; ezt a CurrentUserUtil-on
keresztül lehet lekérdezni.
A userData paraméter típusa nyers Map, és a platform nem írja
elő, minek kell benne lennie. Ki, mihez jogosult – ez a döntés nem a platform, hanem a
plugin fejlesztőjének hatásköre. A minta példákban látható
(Map) CurrentUserUtil.getCurrentUser() csak egy kényelmi alapeset, amikor
a plugin-nak nincs saját jogosultsági rétege, és egyszerűen a platform user objektumát
adja tovább. Ha a domain tool-jai nem mindenki számára futtathatók, a plugin
felépítheti a saját autorizációs logikáját is:
@SuppressWarnings("rawtypes")
@Override
protected ToolExecutionResult orchestrate(List<String> systems,
TaskInfo taskInfo, RetrievalOptions opts) {
Long platformUserId = CurrentUserUtil.getCurrentUserId();
// A saját autorizációs rendszer id alapján ad vissza jogosultsági attribútumokat –
// ennek tartalma és formája kizárólag a plugin fejlesztőjén múlik.
Map userData = myAuthorizationService.resolvePermissions(platformUserId);
return executeOrchestratorActivatedTools(userData, systems, taskInfo, opts);
}
Az így összeállított userData map jelenik meg ezután minden tool
isEligible(Map userData) implementációjában – a platform user objektumától
függetlenül, tetszőleges saját kulcsokkal és attribútumokkal.
5.6 Hook metódusok
usePerSubQueryAnswering()
Multi-hop esetén (ha az LLM a kérdést több al-kérdésre bontja) dönti el, hogy az al-kérdések egyenként kapnak-e saját LLM kört és válasz szöveget, vagy az összes kontextus egyetlen LLM hívásba kerül.
// Alapértelmezett: false – egyetlen összevont LLM kör
// true: al-kérdésenként külön LLM kör (SEGMENT_BREAK SSE-vel elválasztva)
@Override
protected boolean usePerSubQueryAnswering() {
return true;
}
true esetén a pipeline méri a kontextus-előállítási időt, és
ha az gyors volt, automatikusan synthesis módra vált: az al-válaszokat
összegyűjti, majd egyetlen szintézis LLM hívással generál végső választ.
Ha lassú volt, a szegmensek egymás után streamelnek SEGMENT_BREAK-kel.
Ez a platform saját, automatikus szegmentálása – egy tool ANSWERED módban
saját maga is küldhet SEGMENT_BREAK-et, ettől függetlenül
(→ 8.4).
getDeepFallbackTool(PromptType)
Ha a kontextus gyenge vagy üres, a pipeline ezt a tool-t aktiválja mélyebb
analízisre. Tipikusan egy AbstractContextSearchTool leszármazott:
entitás-alapú gráf keresés, gráf-találat hiányában (nincs azonosítható entitás,
vagy van entitás, de nincs gráf-egyezés) LLM-mel kiválasztott kifejezésre BM25
kulcsszavas keresés, és csak ha egyik sem talál, szűretlen teljes domain-átvizsgálás
– majd Spark-alapú batch értékelés dönti el, elegendő-e a talált kontextus.
@Override
protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
return contextSearchTool; // minden PromptType-ra ugyanaz a fallback
// Vagy típusra szabva:
// return PromptType.ENTITY_LOOKUP.equals(type) ? entitySearchTool : contextSearchTool;
}
Ha null-t adsz vissza (alapértelmezett), a deep fallback nem aktiválódik.
A deep fallback akkor indul, ha a kontextus-kapuőr pontszáma a
spring.retriever.context-sufficiency-threshold küszöb (alapértelmezés: 60)
alatt marad, és a kérdés a domainhez kapcsolódik. Ajánlott a kontextuskereső tool
helyett a platform mélykereső coordinatorának leszármazottját visszaadni, amely a
kereső tool-t célzott keresésekre használja - bekötése és paraméterezése:
6.10.
@Override
protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
return deepSearchCoordinatorTool; // AbstractDeepSearchCoordinatorTool leszármazott
}
AbstractContextSearchTool leszármazottja
felülírhatja a getMaxBruteForcePageCount() hook-ot (alapértelmezés:
null, nincs korlát). Beállított érték esetén a szűretlen, teljes
domain-átvizsgálás egyáltalán nem fut le, ha sem a gráf-, sem a BM25-ág nem talál
oldalt; emellett a gráf- vagy BM25-ágról érkező találathalmaz mérete is ehhez az
értékhez van mérve, mielőtt a Spark-alapú, batch-enkénti kontextus-elégségességi
LLM-hívások elindulnának – efölött a tool a mély-kereső LLM-hívásokat kihagyva
null-lal tér vissza.
getClassificationPromptSupplement()
Domain-specifikus kiegészítő szöveg a sub-query bontási LLM classification
promptjához. Alapértelmezetten null – ilyenkor a prompt változatlan
marad. Ha nem null/üres a visszatérési érték, a szöveg a bontási
alapszabályok után kerül a promptba. Jó eszköz arra, hogy egy domain-specifikus
mondatszerkezetet kizárj a felesleges al-kérdésekre bontásból (pl. egy állítás és
annak valószínűsége/súlyossága egyetlen összefüggő kérdésnek számítson).
@Override
protected String getClassificationPromptSupplement() {
return """
Egy adott állapot fennállására és annak valószínűségére, súlyosságára vagy
alátámasztó/cáfoló körülményeire vonatkozó kérdés mindig egyetlen összefüggő
kérdésnek számít – ezeket ne bontsd külön al-kérdésekre.
""";
}
getDefaultAnswer()
Akkor releváns, ha egy domainhez több, system-enként külön regisztrált
orchestrátor tartozik (a kérés system megjelölésével aktiválja a megfelelőt).
Ha egy ilyen, konkrét system-hez kötött orchestrátor aktiválódott, de a kérésre
egyetlen tool sem reagált, az alapértelmezett viselkedés (null
visszaadása) a RAG pipeline-nak adja át a vezérlést az opts profil
alapján. Ha a domain ezen system-jéhez nem tartozik statikus RAG adatbázis
(pl. az orchestrátor kizárólag tool-okon keresztül dolgozik), ez a hook
felülírható, hogy ehelyett egy kész választ adjon vissza, amely jelzi, hogy
a kérés nem tartozik az aktivált agent hatáskörébe.
@Override
protected ToolExecutionResult getDefaultAnswer() {
return ToolExecutionResult.asAnswer(getName(), TextAnswer.asSet("""
A kérdés alapján nem volt azonosítható olyan feladat, ami ennek az
agentnek a hatásköréhez tartozna.
"""));
}
getDefaultAnswer()-t felülírod, mert a system-hez nem tartozik
statikus RAG adat, az orchestrate()-ben zárd ki a RAG retrieval
tool-t is setExcludedTools(List.of("rag_retrieval_tool"))-val –
különben az mindig fallback-ként aktiválódna, és irreleváns kontextust adna,
mielőtt a getDefaultAnswer() egyáltalán lefutna.
usePageLevelIngestion(String system)
Domain+system alapú döntés arról, hogy az adott system nagy korpusza miatt
a mondat-szintű helyett a page-level ingestion/retrieval mód indokolt-e:
true esetén nincs mondat-szintű DocumentEmbedding1024
collection az adott system-hez – kizárólag az oldalak pontos szövegének
egyetlen vektora és BM25-bejegyzése készül el, és a retrieval is ezen fut.
Alapértelmezetten false minden system-re – visszafelé
kompatibilis, a meglévő domainek viselkedése nem változik.
@Override
public boolean usePageLevelIngestion(String system) {
return "NAGY_CORPUS_SYSTEM".equals(system);
}
A hook mind az ingestion pipeline-t (az ingestion time hívó a domainhez
tartozó, system-hez illő orchestrátort keresi meg és kérdezi le), mind a
chat-time retrieval profilt (RetrievalOptions.pageLevelMode())
egyetlen tudásforrásból vezérli. Ugyanazon domainen belül system-enként
eltérő lehet – egy domain néhány, már felöltött, kis korpuszú system-je
maradhat mondat-szintű, míg egy új, nagy korpuszú system page-level módban
ingestálódik. Részletek: 9.5 alfejezet.
getContextExpansionMode(PromptType)
Kérdéstípusonként megadja, mit kapjon az LLM a keresési találatokból:
csak a releváns mondatokat (SENTENCE_ONLY), a mondatokat és az
oldalak összefoglalóját (PAGE_SUMMARY), a találati oldalak teljes
szövegét (FULL_PAGE), vagy a kontextus méretétől függő
választást (AUTO). Alapértelmezetten a FACTOID, ENTITY_LOOKUP és
DEFINITION típus PAGE_SUMMARY, a PROCEDURAL, COMPARATIVE és
REASONING típus FULL_PAGE, a CONTEXTUAL típus AUTO.
Akkor érdemes felülírni, ha a domain korpuszán más mód ad megbízhatóbb
kontextust, például ha az oldal-összefoglalók nem elég pontosak, és minden
kérdéshez a teljes oldal kell.
@Override
protected ContextExpansionMode getContextExpansionMode(PromptType type) {
return ContextExpansionMode.FULL_PAGE;
}
5.7 A NER ingestion tool bekötése – az egyetlen tudásforrás elve
Az orchestrátorba opcionálisan bekötheted a domain NER ingestion tool-ját
(@Autowired mező + @PostConstruct init, lásd 5.3).
Ez egy kulcsfontosságú tervezési döntés: a domain-specifikus tudás
(entitástípusok, relációtípusok, PromptType→gráf mapping) egyetlen helyen
él, és mind az ingestion pipeline, mind az orchestrátor onnan olvassa.
Ha be van kötve az ingestion tool, az orchestrátor automatikusan delegál:
| Orchestrátor hook | Honnan jön az érték |
|---|---|
getGraphRelationTypesForPromptType(type) |
nerIngestionTool.getRelationTypesForPromptType(type) |
isGraphBoostEnabled(type) |
nerIngestionTool.isGraphBoostEnabled(type) |
Ezeket nem kell az orchestrátorban felülírni – elég az ingestion tool-ban definiálni. Ha nincs ingestion tool bekötve, a gráf bejárás nem szűrt (minden él követhető) és a graph boost ki van kapcsolva.
5.8 AbstractNERIngestionTool leszármaztatása
A NER ingestion tool a domain grafikus tudásának gazdája. Az alábbi metódusokat kell implementálni:
getEligibleSystem()
Megadja, hogy ez a tool melyik system értékű dokumentumokat dolgozza fel.
Az ingestion pipeline a domain összes NER tool-ját system-szűrés nélkül futtatja le,
így null esetén az ingestion közben az összes dokumentum feldolgozásra kerül
a domainben. Ha a domain több forrásból tölt be tartalmat (pl. orvosi PDF-ek és
weboldal-scrape), domainenként több NER tool is regisztrálható – mindegyik a saját
system-jét kezeli.
@Override
public String getEligibleSystem() {
return "DOCUMENTS"; // null = minden dokumentum a domainben (ingestion közben)
}
null visszatérés csak az ingestion pipeline-ban jelent univerzális
elérhetőséget. Ha valamelyik retrieval tool a system-szűrt
ToolRegistry.getIngestionTools(domain, systems) lekérdezéssel ellenőrzi a
gráf-eligibility-t (pl. a graph enrichment bekapcsolásához), egy null
deklarációjú NER tool nem fog megjelenni, ha a kérésben explicit system szűrő van –
ilyenkor az IChatbotTool.ALL-t kell visszaadni null helyett.
"DOCUMENTS") a feltöltött dokumentumokból épít fogalmakat és
relációkat, míg egy másik NER tool (system="WEB") egy weboldal-scrape
alapján szervezeti, infrastrukturális entitásokat keres. Az orchestrátorba csak az
elsődleges NER tool kerül bekötésre (@Autowired mező +
@PostConstruct init, lásd 5.3) – az határozza meg a gráfbejárás
szűrőit és a boost logikát.
defineEntityTypes()
A domain entitástípusait és azok leírását definiálja.
Az EntityTypeDescriptor két paramétert vár: a típus nevét (NAGY_BETŰS)
és egy természetes nyelvű leírást az LLM számára. Ebből automatikusan épül fel:
- A NER JSON schema (enum értékek a constrained decoding-hoz)
- A NER prompt instrukció (
getInstruction()) - Az entitásanalízis LLM promptja (
getEntityTypeDescriptions())
@Override
protected List<EntityTypeDescriptor> defineEntityTypes() {
return List.of(
new EntityTypeDescriptor("DISEASE", "betegségek, kórképek, tünetek, állapotok"),
new EntityTypeDescriptor("DRUG", "gyógyszerek, hatóanyagok, készítmények"),
new EntityTypeDescriptor("PROCEDURE", "beavatkozások, vizsgálatok, kezelési eljárások"),
new EntityTypeDescriptor("ANATOMY", "anatómiai struktúrák, szervek, testrészek"),
new EntityTypeDescriptor("OTHER", "egyéb fontos fogalom")
);
}
defineRelationTypes()
A domain relációtípusait definiálja. Ugyanúgy automatikusan épül be a NER promptba.
@Override
protected List<RelationTypeDescriptor> defineRelationTypes() {
return List.of(
new RelationTypeDescriptor("HAS_SYMPTOM", "betegségnek adott tünete van"),
new RelationTypeDescriptor("TREATED_BY", "betegség kezelési módja"),
new RelationTypeDescriptor("CONTRAINDICATED_WITH","ellenjavallat két entitás között"),
new RelationTypeDescriptor("CAUSES", "okozati összefüggés"),
new RelationTypeDescriptor("RELATED_TO", "általános tematikus kapcsolat")
);
}
getAdditionalRules() – opcionális
Extra szabályok a NER prompthoz (pl. ragozási elvek, kizárási feltételek, szimbolikus vs. köznapi tárgy megkülönböztetése).
@Override
protected String getAdditionalRules() {
return """
- A label mező MINDIG alany esetben álljon – ragozott alakokat soha ne adj label-ként
- DRUG típust csak konkrét hatóanyagra vagy készítményre adj, nem terápiás csoportra
""";
}
getRelationTypesForPromptType(PromptType) – az orchestrátor is ezt kapja
PromptType-onként szűri, hogy a gráf bejáráskor milyen éltípusok követhetők.
Ez az orchestrátor getGraphRelationTypesForPromptType()-ja által
meghívott metódus – egyetlen helyen definiált, mindkettő ebből olvas.
@Override
public Set<String> getRelationTypesForPromptType(PromptType type) {
return switch (type) {
case PROCEDURAL -> Set.of("TREATED_BY", "HAS_SYMPTOM", "PART_OF");
case REASONING -> Set.of("CAUSES", "HAS_SYMPTOM", "TREATED_BY");
case COMPARATIVE-> Set.of("CONTRAINDICATED_WITH", "RELATED_TO");
case FACTOID -> Set.of("PART_OF", "CAUSES");
default -> null; // null = minden él követhető
};
}
isGraphBoostEnabled(PromptType)
Megadja, hogy az adott PromptType-nál a tudásgráf által azonosított oldalak kapnak-e score-boostot a rerank előtt.
@Override
public boolean isGraphBoostEnabled(PromptType type) {
return List.of(PromptType.DEFINITION, PromptType.ENTITY_LOOKUP, PromptType.FACTOID)
.contains(type);
}
Hangolható normalizálási paraméterek – opcionális
Az alaposztály két küszöbértéket alkalmaz az entitás- és alias-zaj kiszűrésére az ingestion pipeline végén. Mindkettő felülírható, ha a korpusz mérete vagy az LLM megbízhatósága eltér az alapértelmezettől.
getAliasMinPageCount() – egy alias csak akkor kerül a gráfba,
ha legalább ennyi különböző oldalon visszaigazolódik. Alapértelmezett: 2.
Kis korpusznál (egyetlen könyv, néhány dokumentum) 1-re csökkenthető;
zajos LLM esetén 3-ra vagy magasabbra emelhető.
@Override
protected int getAliasMinPageCount() {
return 1; // alapértelmezett: 2
}
getLowFrequencyEntityThreshold() – az ennél kevesebb oldalon
megjelenő entitások „ritka"-nak minősülnek, és a pipeline token-laza
illesztéssel próbál etablált anyaentitást keresni számukra (ragozott
vagy elírt alakok megmentése). Alapértelmezett: 2.
@Override
protected int getLowFrequencyEntityThreshold() {
return 1; // alapértelmezett: 2
}
getAdditionalRules()-ban
ezért nem szükséges explicit névegyesítési utasításokat megadni – a pipeline
automatikusan kezeli a névformák összevonását. Az additional rules inkább a típus-elhatárolásra
és a kizárási feltételekre való.
5.9 Teljes példa – minimális orchestrátor
Az alábbi példa egy tiszta delegálásra épülő orchestrátor mintáját mutatja: minden kérést az automatikus tool rendszernek delegál, engedélyezi a multi-hop per-segment válaszadást, és bekötött NER ingestion tool-lal rendelkezik.
// ── NER Ingestion Tool ─────────────────────────────────────────────────────
@Component
public class MyNERIngestionTool extends AbstractNERIngestionTool {
public MyNERIngestionTool() {
super();
// A SparkSessionStorage és a KnowledgeGraphService @Autowired(required=false)
// settereken keresztül töltődik fel az alaposztályban, a példányosítás után.
}
@Override public String getDomain() { return "MyDomain"; }
@Override public String getEligibleSystem() { return null; } // teljes domain
@Override
protected List<EntityTypeDescriptor> defineEntityTypes() {
return List.of(
new EntityTypeDescriptor("PERSON", "személyek, szakértők"),
new EntityTypeDescriptor("CONCEPT", "fogalmak, elvek, módszerek"),
new EntityTypeDescriptor("OTHER", "egyéb fontos fogalom")
);
}
@Override
protected List<RelationTypeDescriptor> defineRelationTypes() {
return List.of(
new RelationTypeDescriptor("RELATED_TO", "általános kapcsolat"),
new RelationTypeDescriptor("CAUSES", "okozati összefüggés")
);
}
@Override
public Set<String> getRelationTypesForPromptType(PromptType type) {
return null; // minden él követhető minden típusnál
}
@Override
public boolean isGraphBoostEnabled(PromptType type) {
return PromptType.ENTITY_LOOKUP.equals(type);
}
}
// ── Orchestrátor ───────────────────────────────────────────────────────────
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class MyOrchestratorTool extends AbstractOrchestratorTool {
@Autowired
private MyContextSearchTool contextSearchTool;
@Autowired
private MyNERIngestionTool myNerIngestionTool;
public MyOrchestratorTool() {
super();
}
@PostConstruct
public void init() {
// A gráf tudás (reláció szűrők, boost) a nerIngestionTool-ból automatikusan jön
setNerIngestionTool(myNerIngestionTool);
}
@Override
public String getDomain() { return "MyDomain"; }
@SuppressWarnings("rawtypes")
@Override
protected ToolExecutionResult orchestrate(List<String> systems,
TaskInfo taskInfo, RetrievalOptions opts) {
// Az összes IOrchestratorActivatedTool-t aktiválja prioritás szerint
// (contextSearchTool kizárva, mert deep fallback tool, nem normál aktiválandó)
setExcludedTools(List.of(contextSearchTool.getName()));
return executeOrchestratorActivatedTools(
(Map) CurrentUserUtil.getCurrentUser(), systems, taskInfo, opts);
}
@Override
protected boolean usePerSubQueryAnswering() {
return true; // multi-hop: al-kérdésenként külön LLM válasz
}
@Override
protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
return contextSearchTool; // gyenge kontextus esetén mélyebb keresés
}
}
5.10 getIdentityDescription() – a chatbot-identitás válasz tartalma
A platform egy beépített, minden domain-en aktiválódó tool-lal (IdentityTool)
válaszol az olyan kérdésekre, amelyek nem a tudásbázis tartalmára, hanem magára a chatbotra
vonatkoznak – „ki vagy te", „mit tudsz", „miben tudsz segíteni", „milyen rendszerekhez van
hozzáférésed". Ezekre a kérdésekre nincs válasz a tudásbázis dokumentumaiban, ezért az
IdentityTool nem RAG kontextust ad az LLM-nek, hanem a regisztrált orchestrátorok
és tool-ok getIdentityDescription() visszatérési értékeiből épít saját, dedikált
system prompt-ot, és maga hívja meg az LLM-et.
A metódus az IChatbotTool-on van deklarálva (default null), tehát
bármely orchestrátor és bármely sima tool felülírhatja. A választás, hogy hol
érdemes megírni a leírást, a plugin telepítési egységétől függ:
-
Orchestrátoros plugin – ha a plugin egy domainhez kötött orchestrátorral
települ, elegendő az orchestrátor
getIdentityDescription()-jében leírni a teljes plugin által nyújtott tudást/képességeket. Mivel a domain összes tool-ja mindig ugyanazzal az orchestrátorral együtt települ, nem kell minden egyes tool-on külön-külön megismételni a leírást – az orchestrátor egy helyen, egyben adja a domain teljes „ki vagyok / mit tudok" válaszát. -
Önálló, orchestrátor nélküli plugin – ha a plugin egyetlen (vagy néhány),
önmagában telepíthető tool-ból áll, amihez nem tartozik saját orchestrátor (jellemzően
getDomain()ésgetEligibleSystem()isALL, mint egy domain-független külső szolgáltatást hívó tool-nál), akkor egyetlen domain orchestrátora sem tudja megbízhatóan felsorolni a képességét – hiszen a plugin jelenléte telepítésenként változhat, független attól, melyik domain-ek vannak aktívak. Ilyenkor magának a tool-nak kell felülírnia agetIdentityDescription()-t.
Az IdentityTool egy adott kérésnél mindkét forrást összegyűjti és összefűzi:
a kérés domain-jéhez illeszkedő orchestrátor-példányok leírását, és a
ToolRegistry domain+system illesztésének megfelelő tool-példányok leírását
(a getChatTools és getOrchestratorActivatedTools eredményéből).
A kettő nem zárja ki egymást – egy orchestrátoros domainen belül egy önálló, ALL
domain-ű tool leírása ugyanúgy bekerül az összegzésbe, mint az orchestrátoré.
// ── Orchestrátoros plugin: a teljes domain tudását az orchestrátor írja le ──
@Override
public String getIdentityDescription() {
return """
Rövid, felhasználóknak szóló összefoglaló arról, milyen témákban és milyen
adatforrások alapján tud segíteni a domain – a plugin összes tool-jának
tudását egybefoglalva.
""";
}
// ── Önálló, orchestrátor nélküli plugin: a tool írja le saját magát ──
@Override
public String getDomain() { return IChatbotTool.ALL; }
@Override
public String getEligibleSystem() { return IChatbotTool.ALL; }
@Override
public String getIdentityDescription() {
return """
Rövid, önmagában értelmezhető leírás arról, mire képes ez az egy tool –
nem tételez fel semmilyen domain-specifikus kontextust, mert bármely
domainen, más pluginoktól függetlenül is települhet.
""";
}
getIdentityDescription()-t ne keverd össze a
getSystemPromptSupplement()-tel: az utóbbi a normál RAG-válaszok
rendszerpromptját egészíti ki (ArangoDB-ben tárolva, admin által is szerkeszthető), és
csak akkor kerül bele, ha a tool ténylegesen aktiválódott az adott kérésre. A
getIdentityDescription() ezzel szemben kizárólag az identitás-kérdésekre
adott válaszhoz épül fel, kódban rögzített szöveg, és attól függetlenül bekerül az
összegzésbe, hogy az adott tool aktiválódott-e a beszélgetés során.
5.11 Feladatok rész-lépésekre bontása – IAgentCoordinator
Egy IOrchestratorActivatedTool az IAgentCoordinator
interfészt implementálva, még a suitability teszt előtt, rész-lépésekre
bonthatja a feladatot a saját algoritmusa szerint - meghatározva, mely
további tool-oknak kell aktiválódniuk, és milyen sorrendben. Ez az
executeOrchestratorActivatedTools() minden körében lefut, és a
keletkező rész-lépés-fát rekurzívan, a feladat saját tool-aktiválása előtt
dolgozza fel az orchestrátor. A teljes szerződést, a rész-lépés-fa
végrehajtási szabályait és a tool-ok közötti együttműködés további
csatornáit (kontextus-megosztás, garantált függőség, közvetlen hívás) az
6. fejezet tárgyalja részletesen.