Agent-koordináció – IAgentCoordinator
Domain és system szűréssel, valamint az 5. fejezetben
látott felülírásokkal (buildRetrievalOptions(), a deep fallback hook-ok
stb.) a plugin fejlesztőnek eddig arra volt ráhatása, hogyan viselkedjen a
saját domain-jéhez tartozó orchestrátor. Az IAgentCoordinator ennél
egy szinttel feljebb visz: lehetővé teszi, hogy egy tool a saját, tetszőlegesen
összetett algoritmusa alapján maga tervezze meg, mely további tool-oknak kell
együttműködniük egy feladat megválaszolásához - milyen rész-lépésekre bomlik a
feladat, melyik rész-lépést melyik tool-nak kell feldolgoznia, és hogyan jut el az
egyik rész-lépés eredménye a másikhoz. Ez a fejezet ezt a tudást gyűjti össze egy
helyen: a rész-feladatokra bontás szerződését, a keletkező rész-lépés-fa
végrehajtási szabályait, és a tool-ok közötti együttműködés összes, a platform által
kínált csatornáját - a laza, opcionális kontextus-átvételtől a tool-ok közötti
közvetlen, kódbeli metódushívásig.
6.1 A szerződés – IAgentCoordinator interfész
Egy IOrchestratorActivatedTool a saját algoritmusa szerint ismeri
legjobban, milyen rész-lépésekre kellene bontani egy feladatot ahhoz, hogy a
megválaszolásához esetleg szükséges további tool-ok is felismerjék, hogy rájuk
is szükség van. Az IAgentCoordinator interfészt implementálva egy
tool ezt a tudást a feladat leírásába írhatja bele, mielőtt a suitability teszt
eldöntené, mely tool-ok aktiválódnak.
public interface IAgentCoordinator {
TaskInfo decomposeIntoSubtasks(TaskInfo taskInfo) throws Exception;
// Opcionális: döntés egy feltételesként megjelölt rész-lépés futtatásáról (6.4)
default SubtaskDecision decideConditionalSubtask(TaskInfo parentTask, TaskInfo nextSubtask,
Map<String, List<PromptAndContext>> completedSoFar) {
return SubtaskDecision.RUN;
}
}
Az implementáció maga dönti el, hogy a kapott feladat az ő algoritmusa
szempontjából releváns-e: ha nem, a bemenetet változatlanul adja vissza. Ha
igen, a kapott TaskInfo példányt módosítja - nem
újat hoz létre és ad vissza -, mert az orchestrátor a feladat állapotát (a
rész-lépéseket, a rajta már lefutott tool-ok kontextusát, egy esetleges
folyamatban lévő tisztázó kört) magán ezen a példányon tartja karban, akár
több, egymást követő tisztázó kérdés-válasz körön át is. Egy módosítás vagy a
feladat saját szövegének (taskPrompt) kiegészítéséből áll, vagy a
feladat rész-lépéseit tartalmazó subTasks mezőjének feltöltéséből
- a kettő kombinálható.
@Override
public TaskInfo decomposeIntoSubtasks(TaskInfo taskInfo) throws Exception {
MyDecompositionRequest decomposition =
DECOMPOSITION_SCHEMA.exractToolParam(llmAssistant, objectMapper, taskInfo.getTaskPrompt());
if (decomposition == null || !decomposition.isRelevant()) {
return taskInfo; // nem releváns - a feladat változatlan marad
}
TaskInfo firstStep = new TaskInfo(taskInfo.getTaskNo() + ".1",
"Önálló, önmagában is érthető szövegű első rész-lépés",
taskInfo.getImportantWords(), taskInfo.getPromptType());
firstStep.setRequiredToolNames(Set.of("egyik_szukseges_tool_neve"));
TaskInfo secondStep = new TaskInfo(taskInfo.getTaskNo() + ".2",
"Önálló, önmagában is érthető szövegű második rész-lépés",
taskInfo.getImportantWords(), taskInfo.getPromptType());
secondStep.setRequiredToolNames(Set.of("masik_szukseges_tool_neve"));
taskInfo.setSubTasks(List.of(firstStep, secondStep));
return taskInfo;
}
IOrchestratorActivatedTool-on
elhelyezhető - nem kell hozzá külön, csak erre a célra létrehozott tool-t
írni. A rész-feladatokra bontás algoritmusa így ugyanabban az osztályban él,
amelyik a feladatot ténylegesen fel is dolgozza.
6.2 Rész-lépések fája és végrehajtási sorrend
A subTasks mező TaskInfo-k listája - minden
rész-lépés a szülőjéhez hasonló, önálló feladat: saját azonosítóval (a
szokásos konvenció a szülő azonosítójának egy pontozott utótaggal való
kiegészítése, pl. "1.1"), saját, önmagában is érthető
taskPrompt-tal. Egy rész-lépés maga is rendelkezhet további,
ugyanígy feldolgozandó rész-lépésekkel - fát alkotva, egy beépített mélységi
korlátig.
Ha egy feladatnak van rész-lépése, az orchestrátor a feladat saját suitability tesztje és tool-aktiválása előtt rekurzívan, listasorrendben, egyenként maradéktalanul lezárja mindegyik rész-lépést - beleértve egy rész-lépés által esetlegesen igényelt tisztázó kérdés-válasz kört is. Ha egy rész-lépés tisztázó kérdést igényel, vagy önmaga ad terminális választ, a feldolgozás ott megszakad, és az eredmény felfelé propagálódik - a szülő feladat (és annak testvérei) csak azután folytatódik, hogy minden rész-lépés lezárult. Egy rész-lépésen aktiválódott tool(ok) eredménye a rész-lépés saját taszk-azonosítója alatt kerül abba a kontextus-pufferbe, amit a szülő feladat a saját tool-aktiválása előtt megkap - egy erre épülő tool tehát ugyanúgy hozzáfér a rész-lépés eredményéhez, mintha egy alacsonyabb prioritású tool állította volna elő ugyanabban a körben (lásd 6.7).
Az executeOrchestratorActivatedTools() a domain aktiválható
tool-jai közül azokat, amelyek IAgentCoordinator-t is
implementálnak, priority szerint növekvő sorrendben, egymás után futtatja a
feladaton - mindegyik az előző kimenetét kapja bemenetként -, mielőtt a
feladat saját suitability tesztje eldöntené, mely tool-oknak kell
aktiválódniuk rajta. Ez a lépés minden körben lefut, friss
hívásnál és egy folyamatban lévő tisztázó kör folytatásakor is - a
feladat saját appliedCoordinatorNames halmaza (lásd alább) önmaga
gondoskodik arról, hogy egy már egyszer lefuttatott coordinator ne fusson le
újra ugyanazon a feladaton. Egy coordinator hibája esetén a feladat az adott
coordinator előtti állapotában marad, a további coordinatorok futása nem
szakad meg.
6.3 appliedCoordinatorNames – egy coordinator legfeljebb egyszer fut le egy feladaton
A TaskInfo.appliedCoordinatorNames azoknak a coordinator
tool-oknak a nevét tartalmazza, amelyek ezen a feladaton - vagy valamelyik
ősén - már megkapták az esélyt a rész-lépésekre bontásra, függetlenül attól,
hogy éltek-e vele. Ha legalább egy coordinator lefutott egy körben, a neve
bekerül ebbe a halmazba, és ugyanez a - immár bővített - halmaz kerül be magára
a feladatra és annak frissen létrejött összes rész-lépésére is. Ez két dolgot
biztosít: egy adott coordinator sem magán a feladaton, sem egyetlen
leszármazottján nem fut le újra, miközben egy még nem próbált coordinator
mindkettőn továbbra is lefuthat - és mivel a halmaz a feladat saját, tartós
állapotán él, ez több, egymást követő tisztázó kör között is megmarad.
6.4 requiredToolNames – kényszerített, suitability teszt nélküli aktiválás
Egy coordinator gyakran már a bontás pillanatában, pontosan tudja, melyik
tool-nak kell aktiválódnia egy adott (rész-)feladaton - nem kell ezt egy
önálló szöveg-alapú suitability teszt eldöntésére bíznia. A
TaskInfo.requiredToolNames halmazba írt tool-nevek pontosan ezt
fejezik ki: ha ez a halmaz nem üres, a feladat tool-aktiválási döntése
kizárólag ebből épül fel - a szokásos, LLM-alapú alkalmassági vizsgálat ki
is marad. Ez különösen egy önállóan létrehozott rész-lépésnél hasznos, ahol a
taskPrompt szövege önmagában esetleg nem biztos, hogy egy
suitability tesztet meggyőzően aktiválna, miközben a coordinator saját
algoritmusa szerint egyértelmű, melyik tool-nak kell futnia rajta.
A kényszerítés kizárólagos: ha a halmaz nem üres, a feladaton csak
a megjelölt tool-ok futnak, a domain RAG retrieval tool-ja sem fut mellettük. A
halmazba a tool egyszerű neve (getName()) és a tool-registry által
domain-előtaggal kiegészített neve egyaránt írható - az orchestrátor mindkét
alakot felismeri.
Ha egy tool kizárólag coordinator által kijelölt rész-lépésen futhat, felhasználói
kérdésre soha, nem kell az orchestrátor kizárt tool-jai közé tenni: elég, ha a
getExecutableInstruction() igen/nem kérdése mindig nemleges választ ír
elő (pl. "my_tool: mindig false – csak koordinátor által kijelölt feladaton fut").
A suitability teszt így soha nem aktiválja, a requiredToolNames viszont
igen.
Feltételes rész-lépések – decideConditionalSubtask()
Előfordul, hogy egy rész-lépésre csak akkor van szükség, ha egy korábbi testvére nem
adott eredményt - pl. egy olcsó, szűk keresés után csak akkor kell egy drága,
teljes keresés, ha az előbbi nem talált választ. Ilyenkor a coordinator a
rész-lépést TaskInfo.setConditional(true)-val feltételesként jelöli
meg. Az orchestrátor közvetlenül egy feltételes rész-lépés indítása előtt meghívja
a rész-lépést létrehozó coordinator decideConditionalSubtask()
metódusát, és átadja a testvér rész-lépések addigi eredményének pillanatképét
(kulcs: taskNo:toolName completion-kulcs). A visszaadott
SubtaskDecision:
| Érték | Hatás |
|---|---|
RUN | A rész-lépés lefut (alapértelmezés) |
SKIP | A rész-lépés elmarad, a további rész-lépések feldolgozása folytatódik |
STOP | A rész-lépés és az összes hátralévő testvére elmarad; a szülő feladat saját tool-aktiválása lefut |
Hogy melyik coordinator dönt, azt a rész-lépés ownerCoordinatorName
mezője rögzíti - ezt az orchestrátor tölti ki a bontás után, a coordinatornak nem
kell beállítania. Ha a döntés hibát dob, a rész-lépés lefut.
TaskInfo cheapStep = new TaskInfo(taskInfo.getTaskNo() + ".1.n", query, words, type);
cheapStep.setRequiredToolNames(Set.of(neighborSearchTool.getName()));
TaskInfo fullStep = new TaskInfo(taskInfo.getTaskNo() + ".1.s", query, words, type);
fullStep.setRequiredToolNames(Set.of(searchTool.getName()));
fullStep.setConditional(true); // csak akkor fut, ha a szűk keresés nem adott választ
@Override
public SubtaskDecision decideConditionalSubtask(TaskInfo parentTask, TaskInfo nextSubtask,
Map<String, List<PromptAndContext>> completedSoFar) {
String pairedPrefix = nextSubtask.getTaskNo().replace(".s", ".n") + ":";
boolean answered = completedSoFar.entrySet().stream()
.filter(e -> e.getKey().startsWith(pairedPrefix))
.anyMatch(e -> e.getValue().stream().anyMatch(pac -> pac.getContext() != null && !pac.getContext().isEmpty()));
return answered ? SubtaskDecision.SKIP : SubtaskDecision.RUN;
}
6.5 promptContext – a végrehajtási szöveg elválasztása a suitability tesztektől
Amikor egy coordinator rész-lépéseket hoz létre, az orchestrátor minden
frissen létrejött rész-lépés promptContext mezőjét automatikusan
feltölti a szülő feladat saját, végrehajtásra kész szövegével. Ez a mező csak
a tool tényleges végrehajtásakor - a taskPrompt elé fűzve -
kerül át a ténylegesen átadott szövegbe; sem a suitability teszt, sem egy
további coordinator saját bontási döntése nem látja, kizárólag a
taskPrompt mezőt. Ez teszi lehetővé, hogy egy rész-lépés
taskPrompt-ja rövid, önmagában is érthető direktíva maradjon,
miközben a ténylegesen futó tool a teljes, eredeti kontextust is megkapja.
6.6 AGENT_COORDINATOR_MARKER és isCoordinatorRequested()
A ténylegesen végrehajtásra átadott szöveg - ha a feladatnak van
promptContext-je - tartalmazza az
IChatbotTool.AGENT_COORDINATOR_MARKER jelölő-szót is, ami azt
jelzi, hogy a szöveg (részben vagy egészben) egy coordinator által
meghatározott feladatot hordoz, nem kizárólag a felhasználó saját,
közvetlen üzenetét. Egy tool, amelynek a logikája (pl. egy suitability-szöveg
vagy egy paraméter-kinyerési instrukció) különbséget tenne a felhasználó
saját megfogalmazása és egy coordinator direktívája között, az
isCoordinatorRequested(text) alapértelmezett metódussal
ellenőrizheti ezt a kapott szövegen, jelölő-szó kézi keresése helyett.
A végrehajtási szöveg két része az AbstractOrchestratorTool statikus
segédeivel olvasható ki: az extractCoordinatorTask(text) a coordinator
által meghatározott feladatot, az extractCoordinatorOriginal(text) a
felhasználó eredeti üzenetét, vagyis a szülő feladat szövegét adja vissza. Ha egy
coordinator a rész-lépései számára strukturált adatot is átad, érdemes a szülő
feladat szövegét szerializált JSON-ként megadni - a rész-lépéseken futó tool-ok így
az extractCoordinatorOriginal() eredményéből közvetlenül
deszerializálhatják, szöveges feldolgozás nélkül.
Egy tool a jelölő alapján a viselkedését is igazíthatja: coordinator kérésére a
szokásos kontextus helyett a rész-lépés feladatára adott kész választ adhatja
vissza, ToolExecutionResult.asGuaranteedContext()-ként. A coordinator
így válaszokat gyűjt a rész-lépéseiről, nem nyers kontextust, és a válaszok
garantált kontextusként kerülnek a hívó elé.
6.7 Kontextus-megosztás a rész-lépések között
A rész-lépéseken aktiválódott tool-ok eredménye automatikusan bekerül abba a
kontextus-pufferbe, amit a szülő feladat saját tool-aktiválása előtt megkap -
ehhez a coordinatornak semmit nem kell tennie. Ha a szülő feladaton aktiválódó
tool-nak ehhez a pufferhez hozzá kell férnie (pl. hogy a rész-lépésekben
feloldott adatokra építsen), az IContextConsumerTool interfészt
kell implementálnia. Gyakori eset, hogy ez maga a coordinator: a rész-lépések
lezárulása után a szülő feladaton ő aktiválódik, és a rész-lépések eredményéből
állítja össze a sajátját.
Az orchestrátor egy IContextConsumerTool-t mindig az
executeWithPriorContext(...) metódusán keresztül hajt végre, és a
puffer addigi állapotának pillanatképét paraméterként adja át -
a tool-ok singleton példányok, ezért a kapott kontextust nem szabad
példánymezőben tárolni. A kontextus nélküli executeWithResult(...)
meghívása egy ilyen tool-on hiba: az interfész alapértelmezése
UnsupportedOperationException-t dob. Ha a tool tisztázó kérdést is
feltehet, az IClarifiableContextConsumerTool-t implementálja: a
tisztázó kör folytatása ott is a kontextussal történik
(executeWithClarificationAndPriorContext(...)). A szerződést és egy
teljes példát a 4. fejezet, 4.8 tárgyal, mert ez
a mechanizmus nem kizárólag coordinator által létrehozott rész-lépésekkel
működik, hanem bármely, ugyanabban a feldolgozási körben korábban lefutott
tool kimenetével.
6.8 Közvetlen hívás két tool között
A rész-lépésekre bontás mindig az orchestrátor teljes gépezetén megy
keresztül: suitability-döntés (vagy requiredToolNames),
kontextus-puffer, esetleges tisztázó kör. Ha egy tool-nak egy másik,
ugyanabban a Spring context-ben élő tool egy konkrét, előre ismert
metódusára van szüksége - nem arra, hogy az a másik tool a saját,
önálló aktiválási logikája szerint fusson le -, ennél egyszerűbb út is
rendelkezésre áll: a hívott tool közönséges @Autowired mezőként
injektálható a hívó tool-ba, és a szükséges metódusa közvetlenül, szinkron
Java-hívással elérhető.
@Component
public class InteractionAnalysisTool implements IChatbotTool<InteractionRequest>, IAgentCoordinator {
@Autowired
private SubstanceLookupTool substanceLookupTool;
private void analyzeCandidate(String candidateLabel, List<UniqueTextDocument> pages) {
// Nem az orchestrátoron, nem tool-aktiválásként - egyszerű metódushívás
// egy másik, Spring által kezelt tool-ra, amelynek publikus, erre a célra
// szolgáló metódusa van.
List<List<String>> substanceGroups =
substanceLookupTool.resolveSubstanceGroups(candidateLabel, pages);
// ... a kapott hatóanyag-csoportokkal folytatja a saját elemzését
}
}
Ez a minta akkor helyénvaló, ha a hívott logika nem önálló, orchestrátor szintű döntés eredménye (nincs suitability kérdés, nincs tisztázó kör, nincs prioritási sorrend), hanem egy tisztán funkcionális, paramétereiből determinisztikusan következő lépés, amit a hívó tool a saját algoritmusának egy adott pontján, pontosan tudva, mikor és mivel akarja meghívni. A hívott metódus ugyanúgy egy közönséges, tesztelhető, singleton scope-ú Spring bean metódusa, mint bármely más service - a hívó tool felelőssége, hogy a hívás a saját végrehajtásának megfelelő pontján történjen, mert az orchestrátor semmilyen bekötést, sorrendet vagy kontextus-pufferelést nem végez helyette.
getPriority(), suitability
vagy requiredToolNames alapú aktiválási logikája nem fut le -
a hívó egyenesen a metódustörzsét futtatja. Ha a hívott logikának mégis
szüksége lenne ezekre (pl. hogy tisztázó kérdést tehessen fel a
felhasználónak), a rész-lépésekre bontás (6.2)
vagy az IToolDependent (4.8) a
megfelelő eszköz, nem a közvetlen hívás.
6.9 Melyik együttműködési mechanizmust mikor
A platform négy, egymást kiegészítő módot kínál arra, hogy egy tool egy másik tool munkájára építsen - a különbség abban van, mennyire szoros a függés, és ki dönt arról, hogy a másik tool egyáltalán lefusson-e.
| Mechanizmus | Ki dönt a másik tool lefutásáról | Mikor használd |
|---|---|---|
IContextConsumerTool |
A szokásos suitability teszt (vagy egy coordinator) - nincs garancia | A másik tool kimenete csak kiegészíti az eredményt; hiányában is értelmes, csak esetleg általánosabb választ ad (4.8) |
IToolDependent |
Az orchestrátor - garantáltan, fix, kódban rögzített tool-halmazra | A működés a másik tool kimenete nélkül értelmezhetetlen, és ez a függés minden aktiváláskor ugyanaz (4.8) |
IAgentCoordinator |
A coordinator saját, futásidejű döntése - dinamikusan létrehozott rész-lépésekre, egyenként kényszeríthető tool-lal | A szükséges tool-ok köre és sorrendje a feladat tartalmától függ, nem rögzíthető előre kódban - ez a fejezet tárgya |
Közvetlen @Autowired hívás |
Kizárólag a hívó tool saját, kódbeli logikája - az orchestrátor nem vesz részt benne | Egy konkrét, előre ismert, funkcionális metódusra van szükség, orchestrátor-szintű aktiválási döntés nélkül (6.8) |
6.10 Mélykereső koordinátor – deep fallback coordinatorral
A platform kész mélykereső coordinatort ad deep fallback-hez (5. fejezet): ha a kontextus-kapuőr szerint az elsődleges retrieval kontextusa nem elegendő a kérdés pontos megválaszolásához, a coordinator a hiányzó információra célzott kereséseket indít a domain kereső tool-jaival, és az így talált válaszokkal egészíti ki a kontextust. A működése a platform része; a plugin fejlesztőjének csak a bekötése és a paraméterezése a feladata.
Bekötés
Az orchestrátor getDeepFallbackTool() metódusa ne közvetlenül a
kontextuskereső tool-t adja vissza, hanem egy AbstractDeepSearchCoordinatorTool
leszármazottat, amely megnevezi a domain kereső tool-ját:
@Component
public class MyDeepSearchCoordinatorTool extends AbstractDeepSearchCoordinatorTool {
@Autowired
private MyContextSearchTool contextSearchTool; // AbstractContextSearchTool
@Autowired
private MyNeighborPageSearchTool neighborPageSearchTool; // AbstractNeighborPageSearchTool
@Override
protected IChatbotTool<?> getSearchTool() { // kötelező
return contextSearchTool;
}
@Override
protected IChatbotTool<?> getNeighborSearchTool() { // opcionális
return neighborPageSearchTool;
}
}
// A domain orchestrátorában:
@Override
protected IChatbotTool<?> getDeepFallbackTool(PromptType type) {
return deepSearchCoordinatorTool;
}
A coordinatort és a kereső tool-okat nem kell kizárt tool-ként megadni: normál ágon
nem aktiválódnak. A kereső tool-ok getExecutableInstruction() kérdése
mindig nemleges választ írjon elő (pl. "my_tool: mindig false – csak
koordinátor által kijelölt feladaton fut").
Paraméterezés
| Hol | Mit |
|---|---|
getNeighborSearchTool() |
Opcionális, olcsóbb első lépés: egy AbstractNeighborPageSearchTool
leszármazott, amely a kontextus releváns oldalainak közvetlen környezetében keres,
mielőtt a kereső tool a teljes domainen keresne. Akkor érdemes, ha a hiányzó
információ jellemzően ugyanabban a dokumentumban, a megtalált rész közelében
van (pl. hosszú, összefüggő szövegek). |
AbstractNeighborPageSearchTool.getNeighborSearchWindow(String system) |
Az első, szűk környezet félszélessége oldalban, system-enként (alapértelmezés: 5). |
spring.retriever.deep-search-max-tasks |
A coordinator által indítható keresések felső korlátja (környezeti változó:
DEEP_SEARCH_MAX_TASKS, alapértelmezés: 3). |
spring.retriever.context-sufficiency-threshold |
A kapuőr küszöbe, amely alatt a mélykeresés elindul (alapértelmezés: 60). |
Nagy domain
Ha a domain mérete miatt a teljes domainre kiterjedő keresés nem megengedhető, a
kontextuskereső tool leszármazottja írja felül a getMaxBruteForcePageCount()
metódust: beállított érték esetén a szűretlen, teljes átvizsgálás nem fut le, és a
gráf- vagy kulcsszavas keresés ennél több oldalas találatára sem indulnak a drága
LLM-hívások. Ha egy kérdéstípusra egyáltalán nem kell mélykeresés, az orchestrátor
getDeepFallbackTool(PromptType) metódusa arra a típusra null-t
adjon vissza (5. fejezet).