Bygg en bot for spørsmål og svar
Denne veiledningen bygger de sentrale delene av en bot som svarer på spørsmål ut fra kunnskapsbasen din: i et chatteverktøy, i hjelpesenteret ditt eller inne i produktet ditt. Assistenten finner den relevante kunnskapen og svarer, med henvisninger til kortene den brukte; koden din sender spørsmålet og viser resultatet.
Du trenger en nøkkel med scopene search og ask. Svaret bruker bare kortene som
nøkkelen har lov til å lese, så nøkkelens rolle, dataprofil og samlinger avgjør hva boten kan snakke om. Gi en bot for
kunder en nøkkel som bare ser offentlige samlinger.
Tre måter å få et svar på
| Operasjon | Returnerer | Bruk den til |
|---|---|---|
POST /v1/search | Utdragene som samsvarer, de beste først. Ikke noe skrevet svar. | Et søkefelt, «relaterte artikler», din egen rangering. |
POST /v1/ask | Et fullstendig svar med kildene, i ett HTTP-svar. | Boter som legger ut én melding per svar; jobber på serversiden. |
POST /v1/chat | Svaret som en strøm av hendelser mens det skrives. | Chattegrensesnitt der brukeren skal se svaret dukke opp. |
Søk
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 treff er et utdrag fra et kort: doc_id og doc_name for kortet, heading for seksjonen, text for
utdraget, sidene eller tabellradene der det ble funnet, og kortets tillitsscore og verifisering.
collection velger samlingen det skal søkes i; uten den brukes samlingen som heter default. tags og card_ids
begrenser søket til kort med disse taggene eller til bestemte kort.
Spør og få et fullstendig 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"}'Teksten i svaret i answer er Markdown. Henvisningene i den, for eksempel [1] eller [1, 3], svarer til ref i
oppføringene i sources; lenk hver av dem til kortet (doc_id), slik at leseren kan kontrollere svaret.
Et tall i teksten som ikke kunne bekreftes mot dokumentene, kan vises som en markør, for eksempel ⟦n:a1⟧.
verification.items beskriver hver markør: marker, reason og teksten i original. Hvis koden din ikke håndterer
markører, erstatter du hver av dem med teksten i original.
Når det ikke finnes noe svar
HTTP-svaret er alltid 200. Når assistenten ikke kan svare, sier empty hvorfor, og answer inneholder en kort
melding du kan vise:
empty | Betydning |
|---|---|
no_hits | Ingenting i kunnskapsbasen samsvarer med spørsmålet. |
restricted | Temaet finnes i kort som nøkkelen ikke har lov til å lese. |
guardrail | Spørsmålet ligger utenfor det organisasjonen tillater assistenten å svare på. |
no_data_for_period | Spørsmålet gjelder en periode det ikke finnes dokumenter for. |
no_hits er et nyttig signal: Det viser hva brukerne dine spør om som kunnskapsbasen ennå ikke dekker.
«Fast» og «Precise»
Send "quality": "precise" for et grundigere svar på et vanskelig spørsmål, på bekostning av mer tid.
Standardverdien, "fast", passer for de fleste spørsmål.
Strøm svaret
I et chattegrensesnitt viser du svaret mens det skrives. POST /v1/chat med
"stream": true svarer med text/event-stream: én hendelse per melding, hver med en id:-linje (nummeret i
strømmen, fra 1) og en data:-linje med JSON. Feltet type i JSON-en sier hva hendelsen 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 | Hva du gjør |
|---|---|
meta | Svaret har begynt. Ta vare på answer_id hvis du logger svar. |
sources | Kortene svaret bygger på. Kan komme mer enn én gang; den siste gjelder. |
token | En bit av teksten i svaret. Legg den til det du viser. |
done | Svaret er ferdig. Hendelsen har de samme feltene som svaret fra /v1/ask. |
error | Svaret stoppet. Strømmen slutter uten done. Se nedenfor. |
followups | Etter done: foreslåtte neste spørsmål som du kan tilby som knapper. |
| alt annet | Hopp over det. Nye hendelsestyper kan dukke opp når som helst. |
En linje som begynner med : (for eksempel : ping, som sendes hvert 15. sekund mens svaret forberedes), er en
kommentar. Hopp over den.
messages inneholder samtalen så langt, eldste først, og slutter med brukerens spørsmål. Send de siste rundene, slik
at oppfølgingsspørsmål som «og for bestillinger fra utlandet?» blir forstått.
// 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;
}Uten "stream": true returnerer POST /v1/chat hele svaret som JSON, som /v1/ask.
Gjenoppta en avbrutt strøm
Tilkoblinger brytes: Et mobilnett skifter, en proxy får tidsavbrudd, serveren din starter på nytt. Gi hvert spørsmål
en request_id, så overlever svaret bruddet:
- Med
request_idforberedes svaret uavhengig av tilkoblingen. En frakobling stopper det ikke: Det fullføres og lagres. - For å gjenoppta sender du den samme forespørselen på nytt, med den samme
request_idog nummeret på den siste hendelsen du mottok, i headerenLast-Event-ID. - Bruk én
request_id(for eksempel en UUID) per spørsmål, og den samme i hvert nytt forsøk på det spørsmålet. Et nytt spørsmål får en nyrequest_id.
Hva den gjentatte forespørselen får, avhenger av hvor langt svaret har kommet:
| Situasjon | Den gjentatte forespørselen mottar |
|---|---|
| Svaret skrives fortsatt eller er nettopp ferdig | Hendelsene etter Last-Event-ID, med de samme numrene: Strømmen fortsetter ganske enkelt. |
| Svaret ble ferdig for noen minutter siden | Det lagrede svaret som en token med hele teksten og en done, nummerert fra 1. Spørsmålet stilles ikke på nytt. |
| Strømmen kan ikke fortsette fra det nummeret | Svaret fra begynnelsen, nummerert fra 1. |
Koden din håndterer alle tre tilfellene med én regel: Hvis den første hendelsen du mottar, har et nummer som ikke er
større enn Last-Event-ID, begynner svaret på nytt; tøm det du har vist, og bygg det opp igjen. Ellers legger 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 kjøre på flere servere. En strøm fortsetter fra Last-Event-ID bare på serveren som skriver svaret;
hvis den gjentatte forespørselen din når en annen server, mottar den det lagrede svaret når det er ferdig, eller svaret
skrives på nytt fra begynnelsen. Regelen ovenfor dekker begge tilfellene, og spørsmålet telles aldri to ganger.
Uten request_id avbrytes svaret når tilkoblingen lukkes.
Feil i en strøm
Feil før strømmen starter, er vanlige HTTP-feil: 401, 403, 422 eller 429 med Retry-After når organisasjonen
har brukt opp grensen for spørsmål eller har for mange
svar under arbeid samtidig.
Når strømmen har startet, er HTTP-statusen alltid 200, og et problem kommer som en error-hendelse:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truebetyr at svaret stoppet på grunn av serveren, for eksempel under en oppdatering, og at du kan sende den samme forespørselen på nytt uendret.- Andre koder, for eksempel
timeout, er oppført i API-referansen. Behandle en ukjent kode som enhver annen feil i strømmen: Si fra til brukeren, og la vedkommende spørre på nytt.
La brukerne vurdere svarene
Hvert svar har en question_id. Send brukerens vurdering til
POST /v1/questions/{question_id}/feedback: 1 for nyttig, -1 for ikke
nyttig, med valgfri reason og note. Vurderingene hjelper organisasjonen med å se hvilke kort som trenger arbeid.
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."}'Sjekkliste for en bot i produksjon
- En nøkkel med bare
searchogask, begrenset til samlingene som botens målgruppe har lov til å se. - En
request_idper spørsmål og regelen for gjenopptakelse ovenfor. - Et tidsavbrudd for lesing av strømmen som er lengre enn 15 sekunder, slik at
: ping-kommentarene holder den i live. - Svar som vises som Markdown, med henvisningene lenket til kildene.
- En tydelig melding når
emptyer satt, og en måte å komme i kontakt med et menneske på. 429håndtert ved å vente iRetry-Aftersekunder.- Vurderinger som sendes tilbake, slik at kunnskapsbasen blir bedre over tid.