ToolExecutionResult – visszatérési módok
Minden tool az executeWithResult() végén egy
ToolExecutionResult-ot ad vissza – vagy null-t.
Ez az objektum mondja meg a pipeline-nak, mit tegyen ezután: adjon kontextust az
LLM-nek, kézbesítse a kész választ, vagy tegyen fel kérdést a felhasználónak.
A visszatérési mód megválasztása a tool legfontosabb tervezési döntése.
9.1 A négy mód áttekintése
| Mód | Mit jelent | Mi történik ezután |
|---|---|---|
CONTEXT |
A tool dokumentumokat ad az LLM kontextusához. | A pipeline LLM-mel generál választ a dokumentumok alapján. |
ANSWER |
A tool előre elkészítette a kész választ. | Az LLM hívás kihagyásra kerül; a tool tartalma közvetlenül a klienshez kerül. |
ANSWERED |
A tool a választ már el is küldte (maga streamelt). | A pipeline visszatér, nincs további teendő. |
NEEDS_CLARIFICATION |
Hiányos paraméter – a tool visszakérdez. | Lásd 6. fejezet. |
Ha a tool null-lal tér vissza, a platform úgy értelmezi,
hogy a tool nem tudta kezelni a kérést – a pipeline folytatódik a RAG
retrieval-lel, mintha a tool nem is lett volna aktiválva.
9.2 CONTEXT – dokumentumok az LLM-nek
A CONTEXT mód a leggyakoribb: a tool lekérdez valamit (adatbázis, API, gráf),
és az eredményt UniqueTextDocument objektumok halmazaként adja vissza.
A pipeline ezt a halmazt az LLM kontextusába szövi, amely természetes nyelvű
választ generál belőle.
// Egyszerű eset: dokumentumok HashSet-ben
HashSet<UniqueTextDocument> docs = queryDatabase(request);
return ToolExecutionResult.asContext(getName(), docs);
asContext() vs asGuaranteedContext() – mikor melyik?
Ez a két factory metódus azonos struktúrájú eredményt hoz létre, de eltérő jelzéssel látja el a pipeline-t.
asContext() esetén az orchestrátor a csoport összes tool-jának
futása után közbülső minőségi kaput futtat: LLM-hívással értékeli, hogy az
összegyűjtött kontextus elégséges-e a válaszhoz. Ha igen, a maradék
(alacsonyabb prioritású) csoportok – köztük a RAG retrieval – kihagyásra kerülnek.
Ha nem, a pipeline folytatódik.
asGuaranteedContext() esetén a tool jelzi, hogy maga kezeskedik a
kontextus elégségességéért – a közbülső minőségi kapu kihagyódik, és a query
bekerül a garantált kontextussal rendelkező lekérdezések halmazába.
Ez azt is jelenti, hogy ha ugyanez a query egy következő körben (pl. clarification
re-entry után) újra feldolgozásra kerülne, a RAG retrieval tool az adott query-re
nem aktiválódik – a platform tudja, hogy már van megbízható kontextus.
asGuaranteedContext() akkor indokolt, ha a tool saját maga is
ellenőrzi a kontextus minőségét – például LLM-hívással vagy más megbízható
mechanizmussal. Ha a tool egyszerűen lekérdez és visszaad (minőségellenőrzés
nélkül), asContext() a helyes választás.
Az IClarifiableTool implementációk tipikusan
asGuaranteedContext()-ot adnak vissza, mert az interaktív kérdezés
garantálja, hogy minden szükséges paraméter megvan a lekérdezéshez.
9.3 UniqueTextDocument – a kontextus egysége
A UniqueTextDocument egyetlen mondatot vagy szöveg-blokkot képvisel.
A kontextus mindig HashSet<UniqueTextDocument>-ként utazik a
pipeline-ban – a halmaz garantálja, hogy ugyanaz az oldal-mondat pár nem kerül
be duplán, még ha több tool is visszaadja.
Konstruktor és mezők
new UniqueTextDocument(
Long documentId, // dokumentum egyedi azonosítója
String documentName, // megjelenített dokumentumnév
String documentUrl, // forrás URL (hivatkozásokhoz)
Integer page, // oldalszám
Integer sentenceNumber, // mondatszám az oldalon
String text, // a szöveg tartalma
Double sentenceScore // relevancia-pontszám
)
TextAnswer – egyszerűsített szöveges válasz
Ha a tool nem dokumentumból kinyert kontextust, hanem saját maga által előállított
szövegblokkot ad vissza (pl. egy elvégzett művelet eredményét), a
TextAnswer osztályt érdemes használni. Ez a UniqueTextDocument
leszármazottja, amely a documentId, page és
sentenceNumber mezőket automatikusan kezeli – a plugin fejlesztőjének
csak a szöveget kell megadni.
// Csak a szöveget kell megadni – az ID és a HashSet kezelés automatikus
return ToolExecutionResult.asAnswer(getName(), TextAnswer.asSet("A rekord sikeresen frissítve."));
A TextAnswer belsőleg egy saját statikus IdGenerator
példányt tart fenn (nodeId=0), amely Snowflake-kompatibilis egyedi azonosítót
generál minden példányhoz. Ez kizárólag a HashSet egyediségét
szolgálja – nem adatbázisba mentett érték, és nem ütközik a platform
IdGenerator bean-jével.
A TextAnswer három factory metódust kínál:
| Metódus | Mikor használd |
|---|---|
TextAnswer.asSet(String text) |
Egyetlen szövegblokk visszaadása – a leggyakoribb eset. |
TextAnswer.asSet(List<String> textList) |
Több szövegblokk egyszerre: a lista elemei automatikusan 1-től
sorszámozódnak (sentenceNumber), így mindegyik egyedi
helyet foglal a HashSet-ben. |
TextAnswer.newInstance(String text, Integer sentenceNumber) |
Kézzel megadott sorrendi számmal rendelkező példány – akkor hasznos,
ha a tool maga állítja össze a HashSet-et és a sorrend
explicit vezérlése szükséges. |
equals, hashCode, compareTo – a HashSet szabályai
Az egyenlőség vizsgálat és a hash kizárólag a
documentId + page + sentenceNumber hármason alapul –
a text és a sentenceScore nem vesz részt benne.
Ez azt jelenti:
- Ugyanaz a mondat (azonos documentId/page/sentenceNumber) csak egyszer kerülhet a halmazba, még ha más tool is visszaadja eltérő score-ral.
- Ha a tool saját szöveg-blokkot ad vissza (nem adatbázisból kinyert dokumentumot),
a
documentId,pageéssentenceNumbermezőknek egyedinek kell lenniük a halmazon belül – különben ütközés keletkezik és az elem nem kerül be.
A compareTo a rendezési sorrendet adja meg:
documentId → page → sentenceNumber. A PromptGenerator
erre támaszkodik, amikor a dokumentumokat oldalanként csoportosítja és a mondatokat
sorba rendezi az LLM prompt összeállításakor.
A −1-es mondatszám
A sentenceNumber = -1 speciális szerepű: az oldal összefoglalóját
vagy egy technikai szövegblokkot jelöl, amelyet a tool összefoglalóként szán az
adott oldalhoz. A PromptGenerator ezt automatikusan felismeri:
a −1-es elemet kiszedi a mondatok közül, és a prompt oldal-szintű
summary mezőjeként illeszti be – nem a mondatok sorában jelenik meg,
hanem az oldal bevezető szövegeként.
// Oldal összefoglalója: sentenceNumber = -1
docs.add(new UniqueTextDocument(docId, docName, docUrl, page, -1,
"Ez az oldal a kardiovaszkuláris szövődményeket tárgyalja.", 1.0));
// Az oldal mondatai: sentenceNumber >= 0
docs.add(new UniqueTextDocument(docId, docName, docUrl, page, 0, "...", score));
docs.add(new UniqueTextDocument(docId, docName, docUrl, page, 1, "...", score));
9.4 ANSWER és ANSWERED – feladatot végző agentek
Nem minden agent célja, hogy kontextust adjon egy chatbot válaszhoz. Vannak agentek, amelyek a vállalati rendszerben hajtanak végre feladatot: dokumentumot generálnak, adatot írnak, folyamatot indítanak el, vagy külső API-t hívnak. Ezeknek az eredménye nem kontextus, hanem egy kész kimenet – vagy egyáltalán nincs szöveges visszajelzésük, csak annyi, hogy a feladat elvégzett.
ANSWER – a tool előállította a kész tartalmat
Az ANSWER mód esetén a tool egy UniqueTextDocument-be
csomagolja a kész tartalmat, amelyet a platform LLM-hívás nélkül továbbít a
kliensnek. Tipikus eset: PDF dokumentum generálás.
// PDF generálás: a text mező Base64-kódolt PDF tartalmát hordozza
HashSet<UniqueTextDocument> result = new HashSet<>();
result.add(new UniqueTextDocument(1L, "Beutaló", null, 0, 0, base64PdfContent, 1.0));
return ToolExecutionResult.asAnswer(getName(), result, "BASE64_PDF");
Ha a kimenet egyszerű plain text (pl. összefoglaló szöveg a végrehajtott
feladatról), a contentType paraméter elhagyható.
Ilyen esetekre a TextAnswer kényelmi osztály ajánlott:
// Szöveges összefoglaló – TextAnswer automatikusan kezeli az ID-t és a HashSet-et
return ToolExecutionResult.asAnswer(getName(),
TextAnswer.asSet("Az adatok sikeresen exportálva. 42 rekord feldolgozva."));
ANSWERED – a tool maga gondoskodott a kézbesítésről
Az ANSWERED mód azt jelzi, hogy a tool már elküldte a választ –
streamelt, adatbázisba írt, vagy más módon lezárta a feladatot. A pipeline
azonnal visszatér, nincs további feldolgozás.
// A tool saját maga streamelt, vagy a feladat elvégzett – nincs szöveges kimenet
return ToolExecutionResult.asAnswered(getName());
ANSWER és ANSWERED módok az orchestrátorban
terminálisak: azonnal visszatérést váltanak ki, a maradék
prioritáscsoportok (köztük a RAG retrieval) nem futnak le. Csak akkor használd,
ha biztos, hogy a feladat lezárult és nincs szükség további kontextusra.
Többszegmenses ANSWERED válasz – SEGMENT_BREAK
Ha egy ANSWERED módú tool maga állítja elő és streameli a teljes
választ (nem az LLM-en keresztül), és a választ a kliens felé nem egyetlen
folytonos szövegként, hanem több önálló, egymást követő szegmensre tagolva akarja
megjeleníteni – pl. egy több lépésből álló szimuláció, ahol minden lépés külön
egységként jelenjen meg –, a progressEmitter-en keresztül maga is
küldhet ilyen jelzéseket. Ehhez két, a platform által lefoglalt
progressStatus értéket kell használni közvetlenül, a
toolProgressEmitter() becsomagoló segédmetódus megkerülésével
(az becsomagolná egy TOOL_PROGRESS:... névtérbe, ahol a platform már
nem ismerné fel):
"LLM_GENERATION"tartalommal – egy szövegdarab; ugyanazt a státuszt használja, mint a platform saját LLM streaming kimenete, így a kliens ugyanúgy jeleníti meg, mintha valódi LLM-válasz érkezne"SEGMENT_BREAK"tartalom nélkül – jelzi, hogy egy önálló szegmens véget ért, de a stream nem zárul le, még jön a következő
Ha egy egész szegmens szövegét egyetlen LLM_GENERATION eseményben
küldöd ki, a kliensen egyszerre, egy csapásra jelenik meg – nem úgy hat, mintha
LLM generálná. Ahhoz, hogy valódi, karakterenkénti/szavankénti LLM streamingnek
hasson, a szöveget szavak mentén érdemes szétvágni, és minden szót külön
LLM_GENERATION eseményként, kis késleltetéssel kiküldeni:
private void emitContent(FluxSink<TextGenerationResponse> emitter, String text) {
if (emitter == null) {
return;
}
// Space-ek mentén szétvágjuk
String[] words = text.split("(?= )");
for (String word : words) {
if (word != null) {
TextGenerationResponse tgr = new TextGenerationResponse("LLM_GENERATION");
tgr.setContent(word);
emitter.next(tgr);
}
try {
Thread.sleep(20);
} catch (InterruptedException ignore) {
Thread.currentThread().interrupt();
}
}
}
A (?= ) lookahead-alapú vágás a szóközöket a következő szó elején
hagyja, így a szóközök nem vesznek el a darabolás során. A szegmensek közötti
hívási minta:
FluxSink<TextGenerationResponse> emitter = progressEmitter; // NEM toolProgressEmitter(...)
StringBuilder combined = new StringBuilder();
for (int i = 0; i < segments.size(); i++) {
String segmentText = renderSegment(segments.get(i));
combined.append(segmentText);
emitContent(emitter, segmentText);
if (i < segments.size() - 1) {
emitter.next(new TextGenerationResponse("SEGMENT_BREAK")); // csak szegmensek KÖZÖTT
}
}
// A stream lezárása és a history mentés a szokásos ANSWERED befejezés szerint történik –
// a finalCallback-nek a teljes, összefűzött szöveget kell megkapnia.
finalCallback.onSuccess(getName(), doneResponseWith(combined.toString()));
return ToolExecutionResult.asAnswered(getName());
SEGMENT_BREAK csak a szegmensek között menjen ki, az
utolsó után nem – a stream lezárása (DONE) a finalCallback
szokásos ANSWERED-befejezésén keresztül történik, ahogy egy
egyszerű, egyszegmenses ANSWERED válasznál is.
9.5 null visszatérés – a tool nem aktiválódott
Ha az executeWithResult() null-lal tér vissza, a
platform úgy kezeli, mintha a tool nem lett volna aktiválva: az orchestrátor
a következő prioritáscsoport felé halad, végül a RAG retrieval fut le.
Ez a megfelelő visszatérés, ha a tool megvizsgálta a kérést, de arra a következtetésre jutott, hogy nem tudja kezelni – például nincs releváns találat az adatbázisban, vagy a kinyert paraméterek nem elégségesek egy értelmes lekérdezéshez.
9.6 Factory metódusok összefoglalója
| Factory metódus | Mód | Guaranteed | Tipikus használat |
|---|---|---|---|
asContext(name, docs) |
CONTEXT | ❌ | Általános lekérdezés, minőség nem ismert előre |
asContext(name, promptContexts) |
CONTEXT | ❌ | Multi-hop: explicit prompt–kontextus párok |
asGuaranteedContext(name, docs) |
CONTEXT | ✅ | Strukturált lekérdezés, interaktív tool (pl. clarifiable) |
asGuaranteedContext(name, promptContexts) |
CONTEXT | ✅ | Multi-hop guaranteed változat |
asAnswer(name, docs) |
ANSWER | – | Kész szöveges válasz, LLM kihagyva |
asAnswer(name, docs, contentType) |
ANSWER | – | Tipizált kimenet, pl. "BASE64_PDF" |
asAnswered(name) |
ANSWERED | – | Tool maga gondoskodott a kézbesítésről (streaming) |
needsClarification(question, partialJson, name) |
NEEDS_CLARIFICATION | – | Hiányos paraméter, interaktív kérdéskör indul |