Vytvořte bota pro otázky a odpovědi
Tento návod sestaví jádro bota, který odpovídá na otázky z vaší znalostní báze: v chatovacím nástroji, ve vašem centru nápovědy nebo přímo ve vašem produktu. Asistent najde potřebné znalosti a odpoví s odkazy na karty, které použil; váš kód odešle otázku a zobrazí výsledek.
Potřebujete klíč s rozsahy search a ask. Odpověď používá jen karty, které klíč smí
číst, takže o tom, o čem bot může mluvit, rozhoduje role, datový profil a kolekce klíče. Botovi pro zákazníky dejte
klíč, který vidí jen veřejné kolekce.
Tři způsoby, jak získat odpověď
| Operace | Vrací | Použijte pro |
|---|---|---|
POST /v1/search | Odpovídající pasáže, nejlepší první. Bez napsané odpovědi. | Vyhledávací pole, „související články“, vlastní pořadí výsledků. |
POST /v1/ask | Úplnou odpověď se zdroji v jedné odpovědi HTTP. | Boty, kteří na každou odpověď pošlou jednu zprávu; úlohy na straně serveru. |
POST /v1/chat | Odpověď jako stream událostí, zatímco vzniká. | Chatovací rozhraní, kde má uživatel vidět, jak se odpověď objevuje. |
Vyhledávání
curl -X POST https://api.sursa.com/v1/search \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "how do I return a damaged item", "collection": "support"}'Každý výsledek je pasáž karty: doc_id a doc_name karty, heading oddílu, text pasáže, stránky nebo řádky
tabulky, kde byla nalezena, a důvěra a ověření karty.
collection určuje kolekci, ve které se vyhledává; bez ní se použije kolekce s názvem default. tags
a card_ids zúží vyhledávání na karty s danými štítky nebo na konkrétní karty.
Otázka s úplnou odpovědí
curl -X POST https://api.sursa.com/v1/ask \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "How do I return a damaged item?", "collection": "support"}'Text odpovědi v answer je ve formátu Markdown. Jeho odkazy, například [1] nebo [1, 3], odpovídají ref
položek v sources; každý propojte s kartou (doc_id), aby si čtenář mohl odpověď ověřit.
Číslo v textu, které se nepodařilo potvrdit podle dokumentů, se může objevit jako značka, například ⟦n:a1⟧.
verification.items popisuje každou značku: její marker, důvod reason a text original. Pokud váš kód značky
nezpracovává, nahraďte každou z nich jejím textem original.
Když odpověď chybí
Odpověď je vždy 200. Když asistent odpovědět nedokáže, empty uvádí proč a answer obsahuje krátkou zprávu,
kterou můžete zobrazit:
empty | Význam |
|---|---|
no_hits | Otázce nic ve znalostní bázi neodpovídá. |
restricted | Téma je v kartách, které klíč nesmí číst. |
guardrail | Otázka je mimo to, na co organizace asistentovi dovoluje odpovídat. |
no_data_for_period | Otázka se týká období, pro které nejsou k dispozici žádné dokumenty. |
no_hits je užitečný signál: ukazuje, na co se vaši uživatelé ptají a co znalostní báze zatím nepokrývá.
Režimy Fast a Precise
Pro důkladnější odpověď na obtížnou otázku předejte "quality": "precise", za cenu delšího čekání. Výchozí
"fast" se hodí pro většinu otázek.
Streamování odpovědi
V chatovacím rozhraní zobrazujte odpověď průběžně, jak vzniká. POST /v1/chat
s "stream": true odpovídá ve formátu text/event-stream: jedna událost na zprávu, každá s řádkem id: (její číslo
ve streamu, od 1) a řádkem data: s JSON. Pole type v JSON říká, o jakou událost jde.
id: 1
data: {"type":"meta","sources":3,"timings":{…},"answer_id":"…"}
id: 2
data: {"type":"sources","sources":[{"ref":1,"doc_id":"8c1d…","doc_name":"Returns","text":"…"}]}
id: 3
data: {"type":"token","text":"Send a photo of the item "}
id: 4
data: {"type":"token","text":"within 7 days [1]."}
id: 5
data: {"type":"done","answer":"Send a photo of the item within 7 days [1].","question_id":9812,"sources":[…]}
type | Co dělat |
|---|---|
meta | Odpověď začala. Pokud odpovědi zaznamenáváte, uschovejte si answer_id. |
sources | Karty, ze kterých odpověď vychází. Může přijít víckrát; platí ta poslední. |
token | Část textu odpovědi. Připojte ji k tomu, co zobrazujete. |
done | Odpověď je hotová. Má stejná pole jako odpověď /v1/ask. |
error | Odpověď se zastavila. Stream skončí bez done. Viz níže. |
followups | Po done: navrhované další otázky, které můžete nabídnout jako tlačítka. |
| cokoli jiného | Přeskočte. Nové typy událostí se mohou objevit kdykoli. |
Řádek, který začíná na : (například : ping, posílaný každých 15 sekund, zatímco se odpověď připravuje), je
komentář. Přeskočte ho.
messages obsahuje dosavadní konverzaci od nejstaršího kola a končí otázkou uživatele. Posílejte posledních několik
kol, aby asistent porozuměl navazujícím otázkám typu „a u objednávek ze zahraničí?“.
// Streams an answer and calls onText with each piece of text. Node.js 20+ or a browser.
export async function streamAnswer({ messages, collection, requestId, lastEventId, onText }) {
const response = await fetch("https://api.sursa.com/v1/chat", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SURSA_API_KEY}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
...(lastEventId ? { "Last-Event-ID": String(lastEventId) } : {}),
},
body: JSON.stringify({ messages, collection, stream: true, request_id: requestId }),
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
const decoder = new TextDecoder();
let buffer = "";
let event = { id: 0, data: "" };
let done = null;
for await (const piece of response.body) {
buffer += decoder.decode(piece, { stream: true });
let newline;
while ((newline = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, newline).replace(/\r$/, "");
buffer = buffer.slice(newline + 1);
if (line.startsWith("id:")) event.id = Number(line.slice(3).trim());
else if (line.startsWith("data:")) event.data += line.slice(5).trim();
else if (line === "" && event.data) {
const message = JSON.parse(event.data);
if (message.type === "token") onText(message.text, event.id);
else if (message.type === "done") done = message;
else if (message.type === "error") throw Object.assign(new Error(message.message), { event: message, id: event.id });
event = { id: 0, data: "" };
}
}
}
if (!done) throw new Error("the stream ended before the answer was complete"); // resume it
return done;
}Bez "stream": true vrací POST /v1/chat úplnou odpověď jako JSON, stejně jako /v1/ask.
Obnovte přerušený stream
Spojení se přerušují: změní se mobilní síť, proxy vyprší časový limit, váš server se restartuje. Dejte každé otázce
request_id a odpověď přerušení přežije:
- S
request_idse odpověď připravuje nezávisle na spojení. Odpojení ji nezastaví: dokončí se a uloží. - Chcete-li stream obnovit, odešlete znovu stejný požadavek se stejným
request_ida s číslem poslední přijaté události v hlavičceLast-Event-ID. - Pro každou otázku použijte jedno
request_id(například UUID) a stejné ve všech opakováních této otázky. Nová otázka dostane novérequest_id.
Co opakovaný požadavek dostane, závisí na tom, v jakém stavu odpověď je:
| Situace | Opakovaný požadavek dostane |
|---|---|
| Odpověď se stále píše, nebo právě skončila | Události po Last-Event-ID se stejnými čísly: stream prostě pokračuje. |
| Odpověď skončila před několika minutami | Uloženou odpověď jako token s celým textem a done, číslované od 1. Otázka se znovu neklade. |
| Stream nemůže od tohoto čísla pokračovat | Odpověď od začátku, číslovanou od 1. |
Váš kód zvládne všechny tři případy jedním pravidlem: pokud má první přijatá událost číslo, které není větší než
Last-Event-ID, odpověď začíná znovu; vymažte zobrazený text a sestavte ji znovu. Jinak text připojujte.
import { randomUUID } from "node:crypto";
async function answerWithResume(messages, collection, show) {
const requestId = randomUUID();
let text = "";
let lastId = 0;
for (let attempt = 1; ; attempt++) {
let first = true;
try {
return await streamAnswer({
messages, collection, requestId, lastEventId: lastId,
onText(piece, id) {
if (first && lastId && id <= lastId) text = ""; // the answer starts over
first = false;
text += piece;
lastId = id;
show(text);
},
});
} catch (error) {
const retry = error.event ? error.event.retry === true : true; // a network error, or a server restart
if (!retry || attempt === 5) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}Sursa API může běžet na několika serverech. Stream pokračuje od Last-Event-ID jen na serveru, který odpověď píše;
pokud váš opakovaný požadavek dorazí na jiný server, dostane uloženou odpověď, jakmile je hotová, nebo se odpověď
napíše znovu od začátku. Pravidlo výše pokrývá oba případy a otázka se nikdy nezapočítá dvakrát.
Bez request_id uzavření spojení odpověď zruší.
Chyby ve streamu
Chyby před začátkem streamu jsou běžné chyby HTTP: 401, 403, 422 nebo 429 s Retry-After, když organizace
vyčerpala svůj limit otázek nebo má najednou příliš mnoho
rozpracovaných odpovědí.
Jakmile stream začal, jeho stavový kód HTTP je vždy 200 a problém přichází jako událost error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: trueznamená, že se odpověď zastavila kvůli serveru, například během aktualizace, a stejný požadavek můžete odeslat znovu beze změny.- Další kódy, například
timeout, uvádí reference API. S neznámým kódem zacházejte jako s jakoukoli jinou chybou streamu: informujte uživatele a nechte ho zeptat se znovu.
Hodnocení odpovědí uživateli
Každá odpověď má question_id. Hodnocení uživatele odešlete na
POST /v1/questions/{question_id}/feedback: 1 pro užitečnou, -1 pro
neužitečnou, s volitelným reason a note. Hodnocení pomáhají organizaci zjistit, které karty je potřeba vylepšit.
curl -X POST https://api.sursa.com/v1/questions/9812/feedback \
-H "Authorization: Bearer $SURSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"value": -1, "reason": "outdated", "note": "The return window is now 14 days."}'Kontrolní seznam pro produkčního bota
- Klíč jen se
searchaask, omezený na kolekce, které smí vidět publikum bota. request_idpro každou otázku a pravidlo obnovení popsané výše.- Časový limit pro čtení streamu delší než 15 sekund, aby ho komentáře
: pingudržely naživu. - Odpovědi vykreslené jako Markdown s odkazy propojenými se zdroji.
- Srozumitelná zpráva, když je nastaveno
empty, a možnost obrátit se na člověka. 429zpracované čekáním podleRetry-After.- Odesílaná hodnocení, aby se znalostní báze časem zlepšovala.