SDK
9. fejezet

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ódusMikor 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:

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());
⚠️
Az 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):

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());
⚠️
A 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