Vytvorte bota na otázky a odpovede
Tento návod vytvorí základ bota, ktorý odpovedá na otázky z vašej bázy znalostí: v chatovacom nástroji, vo vašom centre pomoci alebo priamo vo vašom produkte. Asistent nájde potrebné znalosti a odpovie, pričom cituje karty, ktoré použil; váš kód odošle otázku a zobrazí výsledok.
Potrebujete kľúč s rozsahmi search a ask. Odpoveď používa len karty, ktoré kľúč smie
čítať, takže o tom, o čom bot môže hovoriť, rozhodujú rola kľúča, dátový profil a kolekcie. Botovi pre zákazníkov
dajte kľúč, ktorý vidí len verejné kolekcie.
Tri spôsoby, ako získať odpoveď
| Operácia | Vracia | Na čo ju použiť |
|---|---|---|
POST /v1/search | Pasáže, ktoré zodpovedajú dopytu, najlepšie prvé. Bez napísanej odpovede. | Vyhľadávacie pole, „súvisiace články“, vlastné poradie výsledkov. |
POST /v1/ask | Úplnú odpoveď s jej zdrojmi v jednej odpovedi HTTP. | Boty, ktoré posielajú jednu správu na odpoveď; úlohy na serveri. |
POST /v1/chat | Odpoveď ako stream udalostí, priebežne, ako vzniká. | Chatovacie rozhrania, kde má používateľ vidieť, ako sa odpoveď objavuje. |
Vyhľadávanie
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ýsledok je pasáž karty: doc_id a doc_name karty, heading sekcie, text pasáže, strany alebo riadky
tabuľky, kde sa našla, a dôvera a overenie karty.
collection vyberá kolekciu, v ktorej sa vyhľadáva; bez neho sa použije kolekcia s názvom default. tags
a card_ids zúžia vyhľadávanie na karty s týmito štítkami alebo na konkrétne karty.
Otázka s úplnou odpoveďou
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 odpovede v answer je v Markdowne. Jeho odkazy, napríklad [1] alebo [1, 3], zodpovedajú ref položiek
v sources; každý z nich prepojte s kartou (doc_id), aby si čitateľ mohol odpoveď overiť.
Číslo v texte, ktoré sa nepodarilo potvrdiť podľa dokumentov, sa môže objaviť ako značka, napríklad ⟦n:a1⟧.
verification.items opisuje každú značku: jej marker, dôvod reason a text original. Ak váš kód značky
nespracúva, nahraďte každú z nich jej textom original.
Keď odpoveď chýba
Odpoveď HTTP je vždy 200. Keď asistent nevie odpovedať, empty uvádza prečo a answer obsahuje krátku správu,
ktorú môžete zobraziť:
empty | Význam |
|---|---|
no_hits | Otázke nezodpovedá nič v báze znalostí. |
restricted | Téma je v kartách, ktoré kľúč nesmie čítať. |
guardrail | Otázka je mimo toho, na čo organizácia asistentovi dovoľuje odpovedať. |
no_data_for_period | Otázka sa pýta na obdobie, za ktoré neexistujú žiadne dokumenty. |
no_hits je užitočný signál: ukazuje, na čo sa vaši používatelia pýtajú a čo báza znalostí zatiaľ nepokrýva.
„Fast“ a „Precise“
Na dôkladnejšiu odpoveď na ťažkú otázku odošlite "quality": "precise", za cenu dlhšieho času. Predvolený režim
"fast" vyhovuje väčšine otázok.
Streamovanie odpovede
V chatovacom rozhraní zobrazujte odpoveď priebežne, ako vzniká. POST /v1/chat
s "stream": true odpovedá vo formáte text/event-stream: jedna udalosť na správu, každá s riadkom id: (jej
číslo v streame, od 1) a riadkom data: s JSON. Pole type v JSON hovorí, o akú udalosť ide.
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 | Čo robiť |
|---|---|
meta | Odpoveď sa začala. Ak odpovede zapisujete do logu, uložte si answer_id. |
sources | Karty, z ktorých odpoveď vychádza. Môže prísť viackrát; platí posledná. |
token | Časť textu odpovede. Pripojte ju k tomu, čo zobrazujete. |
done | Odpoveď je hotová. Má rovnaké polia ako odpoveď /v1/ask. |
error | Odpoveď sa zastavila. Stream skončí bez done. Pozri nižšie. |
followups | Po done: odporúčané ďalšie otázky, ktoré môžete ponúknuť ako tlačidlá. |
| čokoľvek iné | Preskočte. Nové typy udalostí sa môžu objaviť kedykoľvek. |
Riadok, ktorý začína na : (napríklad : ping, ktorý sa posiela každých 15 sekúnd, kým sa odpoveď pripravuje), je
komentár. Preskočte ho.
messages obsahuje doterajšiu konverzáciu, od najstaršieho kola, a končí otázkou používateľa. Posielajte niekoľko
posledných kôl, aby asistent porozumel aj nadväzujúcim otázkam, napríklad „a pri objednávkach zo zahraničia?“.
// 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 vráti POST /v1/chat úplnú odpoveď ako JSON, rovnako ako /v1/ask.
Obnovte prerušený stream
Spojenia sa prerušujú: mobilná sieť sa zmení, na proxy vyprší časový limit, váš server sa reštartuje. Dajte každej
otázke request_id a odpoveď prerušenie prežije:
- S
request_idsa odpoveď pripravuje nezávisle od spojenia. Odpojenie ju nezastaví: dokončí sa a uloží. - Ak chcete pokračovať, odošlite znova tú istú požiadavku s rovnakým
request_ida s číslom poslednej prijatej udalosti v hlavičkeLast-Event-ID. - Pre každú otázku použite jedno
request_id(napríklad UUID) a to isté pri každom opakovaní tejto otázky. Nová otázka dostane novérequest_id.
Čo opakovaná požiadavka dostane, závisí od toho, v akom stave je odpoveď:
| Situácia | Opakovaná požiadavka dostane |
|---|---|
| Odpoveď sa ešte píše alebo práve skončila | Udalosti po Last-Event-ID s rovnakými číslami: stream jednoducho pokračuje. |
| Odpoveď skončila pred niekoľkými minútami | Uloženú odpoveď ako token s celým textom a done, číslované od 1. Otázka sa znova nekladie. |
| Stream nemôže od tohto čísla pokračovať | Odpoveď od začiatku, číslovanú od 1. |
Váš kód zvládne všetky tri prípady jedným pravidlom: ak má prvá prijatá udalosť číslo nie väčšie ako
Last-Event-ID, odpoveď začína odznova; vymažte, čo ste zobrazili, a zostavte ju znova. Inak pripájajte.
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 bežať na niekoľkých serveroch. Stream pokračuje od Last-Event-ID len na serveri, ktorý odpoveď
píše; ak sa vaša opakovaná požiadavka dostane na iný server, dostane uloženú odpoveď, keď je hotová, alebo sa
odpoveď napíše znova od začiatku. Pravidlo vyššie pokrýva oba prípady a otázka sa nikdy nezapočíta dvakrát.
Bez request_id zatvorenie spojenia odpoveď zruší.
Chyby v streame
Chyby pred začiatkom streamu sú bežné chyby HTTP: 401, 403, 422 alebo 429 s Retry-After, keď organizácia
vyčerpala svoj limit otázok alebo má naraz príliš veľa
rozpracovaných odpovedí.
Keď sa stream začne, jeho stavový kód HTTP je vždy 200 a problém príde ako udalosť error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: trueznamená, že odpoveď sa zastavila pre server, napríklad počas aktualizácie, a tú istú požiadavku môžete odoslať znova bez zmeny.- Ďalšie kódy, napríklad
timeout, uvádza referencia API. Neznámy kód spracujte ako každú inú chybu streamu: povedzte to používateľovi a nechajte ho opýtať sa znova.
Hodnotenie odpovedí používateľmi
Každá odpoveď má question_id. Hodnotenie používateľa odošlite na
POST /v1/questions/{question_id}/feedback: 1 pre užitočnú, -1 pre
neužitočnú, s nepovinným reason a note. Hodnotenia pomáhajú organizácii vidieť, ktoré karty treba zlepšiť.
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ý zoznam pre produkčného bota
- Kľúč len so
searchaask, obmedzený na kolekcie, ktoré smú vidieť adresáti bota. request_idpre každú otázku a pravidlo obnovenia uvedené vyššie.- Časový limit na čítanie streamu dlhší ako 15 sekúnd, aby ho komentáre
: pingudržali pri živote. - Odpovede vykreslené ako Markdown s odkazmi prepojenými na ich zdroje.
- Jasná správa, keď je nastavené
empty, a možnosť obrátiť sa na človeka. 429spracované čakaním podľaRetry-After.- Hodnotenia odosielané späť, aby sa báza znalostí časom zlepšovala.