Bygg en fråga-svar-bot
Den här guiden bygger kärnan i en bot som besvarar frågor utifrån din kunskapsbas: i ett chattverktyg, i ditt hjälpcenter eller i din produkt. Assistenten hittar rätt kunskap och svarar, med hänvisningar till korten som den använde; din kod skickar frågan och visar resultatet.
Du behöver en nyckel med omfången search och ask. Svaret använder bara de kort som
nyckeln får läsa, så nyckelns roll, dataprofil och samlingar avgör vad boten kan tala om. Ge en bot för kunder en
nyckel som bara ser offentliga samlingar.
Tre sätt att få ett svar
| Slutpunkt | Returnerar | Använd den för |
|---|---|---|
POST /v1/search | De utdrag som matchar, bästa först. Inget skrivet svar. | En sökruta, ”relaterade artiklar”, din egen rangordning. |
POST /v1/ask | Ett komplett svar med dess källor, i ett enda svar. | Botar som publicerar ett meddelande per svar; bakgrundsjobb. |
POST /v1/chat | Svaret som en ström av händelser, medan det skrivs. | Chattgränssnitt, där användaren ska se svaret växa fram. |
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"}'Varje träff är ett utdrag ur ett kort: kortets doc_id och doc_name, avsnittets heading, utdragets text, sidor
eller tabellrader där det hittades samt kortets förtroende och verifiering.
collection väljer samlingen som du söker i; utan den används samlingen som heter default. tags och card_ids
begränsar sökningen till kort med de taggarna eller till vissa kort.
Fråga och få ett komplett 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"}'Svarstexten i answer är Markdown. Dess hänvisningar, till exempel [1] eller [1, 3], motsvarar ref i posterna i
sources; länka var och en till kortet (doc_id) så att läsaren kan kontrollera svaret.
Ett tal i texten som inte kunde bekräftas mot dokumenten kan visas som en markering, till exempel ⟦n:a1⟧.
verification.items beskriver varje markering: dess marker, orsaken reason och texten original. Om din kod inte
hanterar markeringar ersätter du var och en med dess original-text.
När det inte finns något svar
Svaret är alltid 200. När assistenten inte kan svara anger empty varför, och answer innehåller ett kort
meddelande som du kan visa:
empty | Betydelse |
|---|---|
no_hits | Inget i kunskapsbasen matchar frågan. |
restricted | Ämnet finns i kort som nyckeln inte får läsa. |
guardrail | Frågan ligger utanför det som organisationen tillåter assistenten att svara på. |
no_data_for_period | Frågan gäller en period som det inte finns några dokument för. |
no_hits är en användbar signal: den visar vad dina användare frågar om som kunskapsbasen ännu inte täcker.
Lägena Fast och Precise
Skicka "quality": "precise" för ett grundligare svar på en svår fråga, till priset av längre tid. Standardläget,
"fast", passar för de flesta frågor.
Strömma svaret
I ett chattgränssnitt visar du svaret medan det skrivs. POST /v1/chat med
"stream": true svarar med text/event-stream: en händelse per meddelande, var och en med en rad id: (dess nummer i
strömmen, från 1) och en rad data: med JSON. Fältet type i JSON-koden anger vilken typ av händelse det är.
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 | Vad du gör |
|---|---|
meta | Svaret har börjat. Spara answer_id om du loggar svar. |
sources | Korten som svaret bygger på. Kan komma mer än en gång; den sista gäller. |
token | En bit av svarstexten. Lägg till den efter det som du visar. |
done | Svaret är klart. Händelsen har samma fält som svaret från /v1/ask. |
error | Svaret avbröts. Strömmen slutar utan done. Se nedan. |
followups | Efter done: förslag på följdfrågor som du kan erbjuda som knappar. |
| allt annat | Hoppa över det. Nya händelsetyper kan tillkomma när som helst. |
En rad som börjar med : (till exempel : ping, som skickas var 15:e sekund medan svaret förbereds) är en kommentar.
Hoppa över den.
messages innehåller konversationen hittills, äldst först, och slutar med användarens fråga. Skicka de senaste
omgångarna så att följdfrågor som ”och för beställningar från utlandet?” blir förstådda.
// 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;
}Utan "stream": true returnerar POST /v1/chat hela svaret som JSON, precis som /v1/ask.
Återuppta en avbruten ström
Anslutningar bryts: ett mobilnät byts, en proxy når sin tidsgräns, din server startar om. Ge varje fråga ett
request_id, så överlever svaret avbrottet:
- Med
request_idförbereds svaret oberoende av anslutningen. Ett avbrott stoppar det inte: det slutförs och sparas. - För att återuppta skickar du samma anrop igen, med samma
request_id, och numret på den senaste händelsen som du tog emot i huvudetLast-Event-ID. - Använd ett
request_id(till exempel ett UUID) per fråga och samma vid varje nytt försök med den frågan. En ny fråga får ett nyttrequest_id.
Vad det upprepade anropet får beror på var svaret befinner sig:
| Situation | Det upprepade anropet får |
|---|---|
| Svaret skrivs fortfarande eller har just blivit klart | Händelserna efter Last-Event-ID, med samma nummer: strömmen fortsätter helt enkelt. |
| Svaret blev klart för några minuter sedan | Det sparade svaret som en token med hela texten och en done, numrerade från 1. Frågan ställs inte igen. |
| Strömmen kan inte fortsätta från det numret | Svaret från början, numrerat från 1. |
Din kod hanterar alla tre fallen med en enda regel: om den första händelse som du tar emot har ett nummer som inte är
större än Last-Event-ID börjar svaret om; töm det som du visade och bygg upp det igen. Annars lägger du till.
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 kan köras på flera servrar. En ström fortsätter från Last-Event-ID bara på den server som skriver svaret;
om ditt upprepade anrop når en annan server får det det sparade svaret när det är klart, eller så skrivs svaret igen
från början. Regeln ovan täcker båda fallen, och frågan räknas aldrig två gånger.
Utan request_id avbryts svaret när anslutningen stängs.
Fel i en ström
Fel innan strömmen startar är vanliga HTTP-fel: 401, 403, 422 eller 429 med Retry-After när organisationen
har förbrukat sin gräns för frågor eller har för många
pågående svar samtidigt.
När strömmen har startat är dess HTTP-status alltid 200, och ett problem kommer som en händelse error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truebetyder att svaret avbröts på grund av servern, till exempel under en uppdatering, och att du kan skicka samma anrop igen oförändrat.- Andra koder, till exempel
timeout, listas i API-referensen. Behandla en okänd kod som vilket annat strömfel som helst: berätta det för användaren och låt användaren fråga igen.
Låt användarna betygsätta svar
Varje svar har ett question_id. Skicka användarens betyg till
POST /v1/questions/{question_id}/feedback: 1 för hjälpsamt, -1 för inte
hjälpsamt, med valfria reason och note. Betygen hjälper organisationen att se vilka kort som behöver förbättras.
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."}'Checklista för en bot i produktion
- En nyckel med bara
searchochask, begränsad till de samlingar som botens målgrupp får se. - Ett
request_idper fråga och regeln för återupptagning ovan. - En tidsgräns för läsningen av strömmen som är längre än 15 sekunder, så att kommentarerna
: pinghåller den vid liv. - Svar som återges som Markdown, med hänvisningar som länkar till sina källor.
- Ett tydligt meddelande när
emptyär satt, och ett sätt att nå en människa. 429som hanteras genom att vänta påRetry-After.- Betyg som skickas tillbaka, så att kunskapsbasen blir bättre med tiden.