Tisztázó kérdezés
Ha egy tool nem tudja egyértelműen kinyerni a szükséges paramétereket a felhasználói
kérdésből, interaktív Q&A körrel pontosíthatja az igényt – anélkül hogy a pipeline-t
újraindítaná. Ez a fejezet az IClarifiableTool életciklusát, a
ClarifyingQuestion összeállítását és a tipikus implementációs sémát mutatja be.
7.1 Mikor indokolt?
Az LLM-alapú paraméter-kinyerés legtöbbször elegendő: a tool az
getInstruction()-ban leírt mezőket a felhasználó üzenetéből vonja ki.
Tisztázó kérdezés akkor indokolt, ha egy kritikus paraméter hiánya miatt a keresés
értelmetlen lenne (pl. szakrendelés-kereső életkorcsoport nélkül), és az opciók
előre ismertek – tehát egy zárt vagy félig nyitott kérdéssel pótolhatók.
Ha az összes lehetséges paraméter-kombináció megfelelő eredményt ad, ne kérdezz vissza. A felesleges tisztázó kör ront a felhasználói élményen.
AbstractMcpTool) a tisztázó kör
életciklusát a platform kezeli: a tool a hívások előtt egyetlen metódushívással
választást, szabad szöveges választ vagy hiányzó paramétereket kérhet a felhasználótól
(→ 14.5).
7.2 IClarifiableTool interfész
Az IClarifiableTool<Request> kiterjeszti az
IChatbotTool<Request> és az IOrchestratorActivatedTool
interfészeket. Ebből következik:
- Kizárólag orchestrátoron keresztül aktiválható – a
ToolRegistrykizárja a direkt LLM routing-ból. - A
getPriority()kötelező; tipikus érték:0(a RAG retrieval előtt fut). - A szokásos
IChatbotToolmetódusok (getName(),getDomain(),getInstruction(),getRequestClass(),executeWithResult()) ugyanúgy kötelezők.
Az interfész egyetlen saját metódusa:
ToolExecutionResult executeWithClarification(
ClarificationSession session,
FluxSink<TextGenerationResponse> progressEmitter,
CallbackWithErrorHandler<OpenAIResponseVO, Exception> finalCallback
);
Az orchestrátor ezt hívja, amikor a felhasználó megválaszolt egy kérdést és az adott
tool folytatásra vár. A session tartalmazza az eddigi részleges
paramétereket és az épp megválaszolt kérdés azonosítóját.
ClarificationSession a getSystems() accessoron keresztül a
kérés eredeti system-szűrő listáját is elérhetővé teszi. Ezt az orchestrátor tölti fel
az első tisztázó kérdés kiküldésekor, és a kérdés-válasz kör minden további fordulóján
automatikusan megőrzi – egy executeWithClarification-t implementáló toolnak
emiatt nem kell a system-listát saját maga elmentenie/visszatöltenie a részleges request
JSON-jában, ha a folytatáshoz (pl. dokumentum-lekérdezéshez) szüksége van rá.
7.3 Az életciklus lépésről lépésre
1. executeWithResult()
↓ LLM kinyeri a paramétereket, de valami hiányzik
→ return ToolExecutionResult.needsClarification(question, partialJson, getName())
2. Platform: INTERACTIVE_QUESTION SSE → kliens megjelení a kérdést
3. Felhasználó válaszol → POST /api/chatbot/clarify {chatId, answer}
4. executeWithClarification(session, ...)
↓ applyAnswer() alkalmazza a választ
↓ detectNextQuestion() megvizsgálja, kell-e még kérdés
→ ha igen: return needsClarification(...) ← újabb kör
→ ha nem: adatbázis / API hívás → return asGuaranteedContext(...)
/api/chatbot/clarify végpont ugyanolyan SSE stream-et ad vissza,
mint a /message. A válasz feldolgozása után a pipeline az utolsó
DONE eseménnyel zárul – a kliensnek nem kell külön kezelnie.
7.4 ClarifyingQuestion összeállítása
A ClarifyingQuestion bean tartalmazza azt, amit a kliens megjelenít:
ClarifyingQuestion.builder()
.questionId("adultOrChild") // gépi azonosító – erre hivatkozik applyAnswer()
.text("Felnőtt vagy gyermek szakrendelést keres?")
.options(List.of("Felnőtt", "Gyermek", "Mindkettő érdekel"))
.allowFreeText(false) // false = csak az opciók egyike fogadható el
.build();
questionId– azapplyAnswer()-ben erre a azonosítóra hivatkozol a válasz értelmezésekor.options– ha üres, a kliens csak szabad szöveges bevitelt kínál.allowFreeText–trueesetén az opciók mellett szabad szöveg is elfogadott (pl. helyszín megadása).
7.5 Implementációs séma
Az alábbi minta egy tipikus interaktív, több kérdéskört kezelő tool felépítését mutatja. A következő három privát metódus alkotja a magtot – célszerű ezt a sémát követni:
detectNextQuestion() – mi hiányzik még?
Sorban ellenőrzi a kötelező paramétereket. Az első hiányos paraméterhez visszaad
egy kérdést; ha minden megvan, null-lal tér vissza.
private ClarifyingQuestion detectNextQuestion(MyRequest request) {
if (request.getAdultOrChild() == null
|| AdultOrChild.UNKNOWN.equals(request.getAdultOrChild())) {
return ClarifyingQuestion.builder()
.questionId("adultOrChild")
.text("Felnőtt vagy gyermek szakrendelést keres?")
.options(List.of("Felnőtt", "Gyermek", "Mindkettő érdekel"))
.allowFreeText(false)
.build();
}
if (request.getServicePlace() == null || request.getServicePlace().isBlank()) {
return ClarifyingQuestion.builder()
.questionId("servicePlace")
.text("Melyik helyszínen keres szakrendelést?")
.options(List.of("Debrecen", "Berettyóújfalu"))
.allowFreeText(true)
.build();
}
return null; // minden megvan
}
applyAnswer() – a válasz alkalmazása
A questionId alapján meghatározza, melyik mezőt kell kitölteni,
és az érkező szöveget a megfelelő típusra konvertálja.
private void applyAnswer(MyRequest request, String questionId, String userAnswer) {
if ("adultOrChild".equals(questionId)) {
String lc = userAnswer.toLowerCase();
if (lc.contains("feln")) request.setAdultOrChild(ADULT);
else if (lc.contains("gyermek") || lc.contains("gyerek")) request.setAdultOrChild(CHILD);
else request.setAdultOrChild(BOTH);
// Ha egyik sem illeszkedik, marad UNKNOWN → következő körben újra kérdez
} else if ("servicePlace".equals(questionId)) {
request.setServicePlace(userAnswer.trim());
}
}
executeWithRequest() – közös belső belépési pont
Mind az executeWithResult(), mind az executeWithClarification()
ide hív be. Ez tartja össze a logikát: ha még hiányzik paraméter, visszakérdez;
ha minden megvan, elvégzi a keresést.
private ToolExecutionResult executeWithRequest(MyRequest request) {
try {
ClarifyingQuestion nextQuestion = detectNextQuestion(request);
if (nextQuestion != null) {
String partialJson = objectMapper.writeValueAsString(request);
return ToolExecutionResult.needsClarification(nextQuestion, partialJson, getName());
}
// Minden megvan – végrehajtás
Set<UniqueTextDocument> docs = queryDatabase(request);
return ToolExecutionResult.asGuaranteedContext(getName(), docs);
} catch (Exception e) {
logger.error("executeWithRequest hiba: {}", e.getMessage());
return null;
}
}
7.6 Teljes példa – IClarifiableTool vázlata
@Component
public class MyServiceTool implements IClarifiableTool<MyRequest> {
private final ObjectMapper objectMapper;
// ... egyéb függőségek
@Override public String getName() { return "my_service"; }
@Override public String getDomain() { return "MyDomain"; }
@Override public int getPriority() { return 0; }
// ── Első kör: LLM kinyeri a paramétereket ──────────────────────────────
@Override
public ToolExecutionResult executeWithResult(List<String> systems, String userPrompt, String userLang,
List<String> importantWords, FluxSink<TextGenerationResponse> emitter,
CallbackWithErrorHandler<OpenAIResponseVO, Exception> cb) {
try {
Map<String, Object> resp = llmAssistant.generateToolRequest(userPrompt, SYSTEM_PROMPT, this);
if (resp != null && resp.containsKey(getName())) {
MyRequest request = objectMapper.convertValue(resp.get(getName()), MyRequest.class);
return executeWithRequest(request);
}
} catch (Exception e) { /* log */ }
return null;
}
// ── Folytatás: felhasználó megválaszolta a kérdést ─────────────────────
@Override
public ToolExecutionResult executeWithClarification(ClarificationSession session,
FluxSink<TextGenerationResponse> emitter,
CallbackWithErrorHandler<OpenAIResponseVO, Exception> cb) {
try {
MyRequest request = objectMapper.readValue(
session.getPartialRequestJson(), MyRequest.class);
applyAnswer(request, session.getPendingQuestionId(), session.getPendingAnswer());
return executeWithRequest(request);
} catch (Exception e) {
logger.error("Clarification folytatás sikertelen: {}", e.getMessage());
return null;
}
}
// ── Privát segédmetódusok ──────────────────────────────────────────────
private ToolExecutionResult executeWithRequest(MyRequest request) { ... }
private ClarifyingQuestion detectNextQuestion(MyRequest request) { ... }
private void applyAnswer(MyRequest request, String questionId, String answer) { ... }
// ── IChatbotTool – kötelező metódusok (getRequestAsJsonNode() nincs
// felülírva, a default interfész-implementáció elegendő) ──────────────
@Override public String getInstruction() { return "..."; }
@Override public Class<MyRequest> getRequestClass() { return MyRequest.class; }
@Override public String getExecutableInstruction() { return "my_service: true ha ..."; }
}
7.7 Több tool visszakérdezése egy körben
Ha egy prioritáscsoportban több IClarifiableTool is
NEEDS_CLARIFICATION-nel tér vissza, a platform automatikusan sorba
állítja a kérdéseket: az első azonnal kiküldésre kerül, a többi az előző megválaszolása
után következik. A plugin fejlesztőnek ezzel nincs teendője.
ClarificationSession objektum az executeWithClarification()
hívásban read-only – ne módosítsd közvetlenül, csak az általa nyújtott adatokat olvasd.
7.8 Multi-hop és clarification egyidejűleg
Ha az orchestrátor multi-hop feldolgozás közben küld ki NEEDS_CLARIFICATION-t,
a platform védi a párhuzamos hozzáférést: a felhasználó válasza csak akkor kerül
feldolgozásra, amikor az orchestrátor már befejezte a folyamatban lévő munkát.
Szerverleállás esetén a védelem automatikusan feloldódik – nincs szükség
manuális beavatkozásra.
Plugin fejlesztőnek ezzel nincs teendője – az AbstractOrchestratorTool
kezeli.