Byg en Q&A-bot
I denne vejledning bygger du kernen i en bot, der besvarer spørgsmål ud fra din vidensbase: i et chatværktøj, i dit hjælpecenter eller i dit produkt. Assistenten finder den relevante viden og svarer med henvisning til de kort, den har brugt; din kode sender spørgsmålet og viser resultatet.
Du skal bruge en nøgle med scopes search og ask. Svaret bruger kun de kort, som nøglen
har lov til at læse, så nøglens rolle, dataprofil og samlinger bestemmer, hvad botten kan tale om. Giv en bot til
kunder en nøgle, der kun ser offentlige samlinger.
Tre måder at få et svar på
| Operation | Returnerer | Brug den til |
|---|---|---|
POST /v1/search | De uddrag, der matcher, bedste først. Intet skrevet svar. | Et søgefelt, »relaterede artikler«, din egen rangering. |
POST /v1/ask | Et komplet svar med kilder i ét HTTP-svar. | Bots, der sender én besked pr. svar; baggrundsjob. |
POST /v1/chat | Svaret som en stream af hændelser, mens det skrives. | Chatgrænseflader, hvor brugeren skal se svaret dukke op. |
Søg
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"}'Hvert hit er et uddrag af et kort: kortets doc_id og doc_name, afsnittets heading, uddragets text, de sider
eller tabelrækker, hvor det blev fundet, samt kortets tillidsscore og verificeringsstatus.
collection vælger den samling, der skal søges i; uden den bruges samlingen med navnet default. tags og
card_ids begrænser søgningen til kort med disse tags eller til bestemte kort.
Spørg, og få et komplet svar
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"}'Svarets tekst i answer er Markdown. Dens henvisninger, fx [1] eller [1, 3], svarer til ref i posterne i
sources; link hver af dem til kortet (doc_id), så læseren kan kontrollere svaret.
Et tal i teksten, som ikke kunne bekræftes i dokumenterne, kan stå som en markør som ⟦n:a1⟧. verification.items
beskriver hver markør: dens marker, årsagen reason og teksten original. Håndterer din kode ikke markører, så
erstat hver af dem med dens tekst fra original.
Når der ikke er noget svar
HTTP-svaret er altid 200. Når assistenten ikke kan svare, fortæller empty hvorfor, og answer indeholder en kort
besked, som du kan vise:
empty | Betydning |
|---|---|
no_hits | Intet i vidensbasen matcher spørgsmålet. |
restricted | Emnet findes i kort, som nøglen ikke har lov til at læse. |
guardrail | Spørgsmålet ligger uden for det, som organisationen tillader assistenten at besvare. |
no_data_for_period | Spørgsmålet handler om en periode, som der ikke findes dokumenter for. |
no_hits er et nyttigt signal: Det viser, hvad dine brugere spørger om, som vidensbasen endnu ikke dækker.
»Fast« og »Precise«
Angiv "quality": "precise" for at få et grundigere svar på et svært spørgsmål, men det tager længere tid.
Standardværdien, "fast", passer til de fleste spørgsmål.
Stream svaret
I en chatgrænseflade viser du svaret, mens det skrives. POST /v1/chat med
"stream": true svarer med text/event-stream: én hændelse pr. besked, hver med en id:-linje (dens nummer i
streamen, fra 1) og en data:-linje med JSON. JSON’ens felt type fortæller, hvilken hændelse det er.
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 | Hvad du gør |
|---|---|
meta | Svaret er startet. Gem answer_id, hvis du logger svar. |
sources | De kort, svaret bygger på. Kan komme mere end én gang; den sidste gælder. |
token | Et stykke af svarets tekst. Føj det til det, du viser. |
done | Svaret er færdigt. Hændelsen har de samme felter som svaret fra /v1/ask. |
error | Svaret stoppede. Streamen slutter uden done. Se nedenfor. |
followups | Efter done: foreslåede næste spørgsmål, som du kan tilbyde som knapper. |
| alt andet | Spring det over. Nye hændelsestyper kan dukke op når som helst. |
En linje, der begynder med : (fx : ping, som sendes hvert 15. sekund, mens svaret forberedes), er en kommentar.
Spring den over.
messages indeholder samtalen indtil nu, ældste først, og slutter med brugerens spørgsmål. Send de seneste runder, så
opfølgende spørgsmål som »og for ordrer fra udlandet?« bliver forstået.
// 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;
}Uden "stream": true returnerer POST /v1/chat det komplette svar som JSON ligesom /v1/ask.
Genoptag en afbrudt stream
Forbindelser afbrydes: Et mobilnetværk skifter, en proxy får timeout, din server genstarter. Giv hvert spørgsmål et
request_id, så overlever svaret afbrydelsen:
- Med
request_idforberedes svaret uafhængigt af forbindelsen. En afbrydelse stopper det ikke: Det gøres færdigt og gemmes. - For at genoptage sender du den samme anmodning igen med det samme
request_idog nummeret på den sidste hændelse, du modtog, i headerenLast-Event-ID. - Brug ét
request_id(fx et UUID) pr. spørgsmål og det samme ved hvert nyt forsøg med det spørgsmål. Et nyt spørgsmål får et nytrequest_id.
Hvad den gentagne anmodning får, afhænger af, hvor langt svaret er:
| Situation | Den gentagne anmodning modtager |
|---|---|
| Svaret er stadig ved at blive skrevet eller er lige blevet færdigt | Hændelserne efter Last-Event-ID med de samme numre: Streamen fortsætter blot. |
| Svaret blev færdigt for nogle minutter siden | Det gemte svar som en token med hele teksten og en done, nummereret fra 1. Spørgsmålet stilles ikke igen. |
| Streamen kan ikke fortsætte fra det nummer | Svaret fra begyndelsen, nummereret fra 1. |
Din kode håndterer alle tre tilfælde med én regel: Har den første hændelse, du modtager, et nummer, der ikke er
større end Last-Event-ID, starter svaret forfra; ryd det, du har vist, og byg det op igen. Ellers føjer du til.
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));
}
}
}Sursas API kan køre på flere servere. En stream fortsætter kun fra Last-Event-ID på den server, der skriver svaret;
rammer din gentagne anmodning en anden server, modtager den det gemte svar, når det er færdigt, eller svaret skrives
igen fra begyndelsen. Reglen ovenfor dækker begge tilfælde, og spørgsmålet tælles aldrig to gange.
Uden request_id annulleres svaret, når forbindelsen lukkes.
Fejl i en stream
Fejl, før streamen starter, er almindelige HTTP-fejl: 401, 403, 422 eller 429 med Retry-After, når
organisationen har brugt sin grænse for spørgsmål eller har for mange
svar i gang på samme tid.
Når streamen er startet, er dens HTTP-status altid 200, og et problem kommer som en error-hændelse:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truebetyder, at svaret stoppede på grund af serveren, fx under en opdatering, og at du kan sende den samme anmodning igen uændret.- Andre koder, fx
timeout, står i API-referencen. Behandl en ukendt kode som enhver anden fejl i streamen: Fortæl brugeren det, og lad vedkommende spørge igen.
Lad brugerne vurdere svar
Hvert svar har et question_id. Send brugerens vurdering til
POST /v1/questions/{question_id}/feedback: 1 for nyttigt, -1 for ikke
nyttigt, med valgfri reason og note. Vurderingerne hjælper organisationen med at se, hvilke kort der skal arbejdes
på.
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."}'Tjekliste for en bot i produktion
- En nøgle med kun
searchogask, begrænset til de samlinger, som bottens målgruppe må se. - Et
request_idpr. spørgsmål og reglen for genoptagelse ovenfor. - En timeout for læsning af streamen, der er længere end 15 sekunder, så kommentarerne
: pingholder den i live. - Svar, der vises som Markdown, med henvisninger linket til deres kilder.
- En tydelig besked, når
emptyer sat, og en mulighed for at komme i kontakt med en person. 429håndteret ved at vente det antal sekunder, der står iRetry-After.- Vurderinger, der sendes tilbage, så vidensbasen bliver bedre over tid.