Q&A-Bot bauen
Diese Anleitung baut den Kern eines Bots, der Fragen aus Ihrer Wissensbasis beantwortet: in einem Chat-Tool, in Ihrem Help-Center oder in Ihrem Produkt. Der Assistent findet das passende Wissen und antwortet, wobei er die verwendeten Karten zitiert; Ihr Code sendet die Frage und zeigt das Ergebnis.
Sie brauchen einen Schlüssel mit den Scopes search und ask. Die Antwort verwendet nur
die Karten, die der Schlüssel lesen darf. Rolle, Datenprofil und Sammlungen des Schlüssels bestimmen also, worüber der
Bot sprechen kann. Geben Sie einem Bot für Kunden einen Schlüssel, der nur öffentliche Sammlungen sieht.
Drei Wege zu einer Antwort
| Endpunkt | Gibt zurück | Verwenden Sie ihn für |
|---|---|---|
POST /v1/search | Die passenden Textstellen, die beste zuerst. Keine formulierte Antwort. | Ein Suchfeld, „ähnliche Artikel“, Ihre eigene Rangfolge. |
POST /v1/ask | Eine vollständige Antwort mit ihren Quellen in einer einzigen Antwort. | Bots, die eine Nachricht pro Antwort posten; Backend-Jobs. |
POST /v1/chat | Die Antwort als Stream von Ereignissen, während sie entsteht. | Chat-Oberflächen, in denen die Antwort sichtbar entstehen soll. |
Suchen
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"}'Jeder Treffer ist eine Textstelle einer Karte: doc_id und doc_name der Karte, heading des Abschnitts, text der
Textstelle, die Seiten oder Tabellenzeilen, in denen sie gefunden wurde, sowie Vertrauenswert und Prüfstatus der Karte.
collection wählt die zu durchsuchende Sammlung; ohne diesen Parameter wird die Sammlung mit dem Namen default
verwendet. tags und card_ids beschränken die Suche auf Karten mit diesen Tags oder auf bestimmte Karten.
Fragen und eine vollständige Antwort erhalten
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"}'Der Text der Antwort in answer ist Markdown. Seine Verweise wie [1] oder [1, 3] entsprechen ref der Einträge in
sources; verlinken Sie jeden davon mit der Karte (doc_id), damit Lesende die Antwort überprüfen können.
Eine Zahl im Text, die nicht anhand der Dokumente bestätigt werden konnte, kann als Markierung wie ⟦n:a1⟧ erscheinen.
verification.items beschreibt jede Markierung: ihren marker, den reason und den Text aus original. Wenn Ihr
Code keine Markierungen verarbeitet, ersetzen Sie jede durch ihren Text aus original.
Wenn es keine Antwort gibt
Die Antwort ist immer 200. Wenn der Assistent nicht antworten kann, sagt empty, warum, und answer enthält eine
kurze Meldung, die Sie anzeigen können:
empty | Bedeutung |
|---|---|
no_hits | Nichts in der Wissensbasis passt zur Frage. |
restricted | Das Thema steht in Karten, die der Schlüssel nicht lesen darf. |
guardrail | Die Frage liegt außerhalb dessen, was der Assistent nach dem Willen der Organisation beantworten darf. |
no_data_for_period | Die Frage betrifft einen Zeitraum, für den es keine Dokumente gibt. |
no_hits ist ein nützliches Signal: Es zeigt, wonach Ihre Benutzer fragen, was die Wissensbasis noch nicht abdeckt.
Schnell und präzise
Übergeben Sie "quality": "precise" für eine gründlichere Antwort auf eine schwierige Frage, die dafür mehr Zeit
braucht. Der Standardwert "fast" passt zu den meisten Fragen.
Antwort streamen
Zeigen Sie die Antwort in einer Chat-Oberfläche, während sie entsteht. POST /v1/chat
mit "stream": true antwortet mit text/event-stream: ein Ereignis pro Nachricht, jedes mit einer Zeile id: (seine
Nummer im Stream, ab 1) und einer Zeile data: mit JSON. Das Feld type im JSON sagt, um welches Ereignis es sich
handelt.
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 | Was zu tun ist |
|---|---|
meta | Die Antwort hat begonnen. Bewahren Sie answer_id auf, wenn Sie Antworten protokollieren. |
sources | Die Karten, auf denen die Antwort beruht. Kann mehrmals kommen; das letzte gilt. |
token | Ein Stück des Texts der Antwort. Hängen Sie es an das Angezeigte an. |
done | Die Antwort ist vollständig. Es hat dieselben Felder wie die Antwort von /v1/ask. |
error | Die Antwort wurde abgebrochen. Der Stream endet ohne done. Siehe unten. |
followups | Nach done: vorgeschlagene Folgefragen, die Sie als Schaltflächen anbieten können. |
| alles andere | Überspringen Sie es. Neue Ereignistypen können jederzeit hinzukommen. |
Eine Zeile, die mit : beginnt (etwa : ping, alle 15 Sekunden gesendet, während die Antwort vorbereitet wird), ist
ein Kommentar. Überspringen Sie sie.
messages enthält das bisherige Gespräch, die älteste Nachricht zuerst, und endet mit der Frage des Benutzers. Senden
Sie die letzten Gesprächsrunden mit, damit Folgefragen wie „und für Bestellungen aus dem Ausland?“ verstanden werden.
// 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;
}Ohne "stream": true gibt POST /v1/chat wie /v1/ask die vollständige Antwort als JSON zurück.
Abgebrochenen Stream fortsetzen
Verbindungen brechen ab: Ein Mobilfunknetz wechselt, ein Proxy läuft in ein Timeout, Ihr Server startet neu. Geben Sie
jeder Frage eine request_id, und die Antwort übersteht den Abbruch:
- Mit
request_idwird die Antwort unabhängig von der Verbindung vorbereitet. Ein Verbindungsabbruch hält sie nicht auf: Die Antwort wird fertiggestellt und gespeichert. - Zum Fortsetzen senden Sie dieselbe Anfrage erneut, mit derselben
request_idund der Nummer des zuletzt empfangenen Ereignisses im HeaderLast-Event-ID. - Verwenden Sie eine
request_id(zum Beispiel eine UUID) pro Frage und dieselbe bei jeder Wiederholung dieser Frage. Eine neue Frage erhält eine neuerequest_id.
Was die wiederholte Anfrage erhält, hängt davon ab, wo die Antwort gerade steht:
| Situation | Die wiederholte Anfrage erhält |
|---|---|
| Die Antwort entsteht noch oder ist gerade fertig geworden | Die Ereignisse nach Last-Event-ID mit denselben Nummern: Der Stream läuft einfach weiter. |
| Die Antwort ist seit einigen Minuten fertig | Die gespeicherte Antwort als ein token mit dem ganzen Text und ein done, nummeriert ab 1. Die Frage wird nicht erneut gestellt. |
| Der Stream kann ab dieser Nummer nicht weiterlaufen | Die Antwort von Anfang an, nummeriert ab 1. |
Ihr Code behandelt alle drei Fälle mit einer Regel: Hat das erste empfangene Ereignis eine Nummer, die nicht größer
als Last-Event-ID ist, beginnt die Antwort von vorn; leeren Sie das Angezeigte und bauen Sie es neu auf. Andernfalls
hängen Sie an.
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));
}
}
}Die Sursa-API kann auf mehreren Servern laufen. Ein Stream setzt ab Last-Event-ID nur auf dem Server fort, der die
Antwort gerade schreibt. Erreicht Ihre wiederholte Anfrage einen anderen, erhält sie die gespeicherte Antwort, sobald
diese fertig ist, oder die Antwort wird von Anfang an neu geschrieben. Die Regel oben deckt beide Fälle ab, und die
Frage wird nie doppelt gezählt.
Ohne request_id bricht das Schließen der Verbindung die Antwort ab.
Fehler in einem Stream
Fehler vor dem Start des Streams sind gewöhnliche HTTP-Fehler: 401, 403, 422 oder 429 mit Retry-After, wenn
die Organisation ihr Fragenlimit aufgebraucht hat oder zu viele
gleichzeitig laufende Antworten hat.
Sobald der Stream begonnen hat, ist sein HTTP-Status immer 200, und ein Problem kommt als Ereignis error an:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truebedeutet, dass die Antwort wegen des Servers abgebrochen wurde, zum Beispiel während eines Updates, und Sie dieselbe Anfrage unverändert erneut senden können.- Andere Codes wie
timeoutsind in der API-Referenz aufgeführt. Behandeln Sie einen unbekannten Code wie jeden anderen Stream-Fehler: Informieren Sie die Person und lassen Sie sie erneut fragen.
Bewertungen von Antworten ermöglichen
Jede Antwort hat eine question_id. Senden Sie die Bewertung an
POST /v1/questions/{question_id}/feedback: 1 für hilfreich, -1 für
nicht hilfreich, optional mit reason und note. Bewertungen helfen der Organisation zu erkennen, welche Karten
überarbeitet werden müssen.
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."}'Checkliste für einen Bot im Produktivbetrieb
- Ein Schlüssel nur mit
searchundask, beschränkt auf die Sammlungen, die die Zielgruppe des Bots sehen darf. - Eine
request_idpro Frage und die Regel zum Fortsetzen von oben. - Ein Timeout beim Lesen des Streams, das länger als 15 Sekunden ist, damit die Kommentare
: pingihn am Leben halten. - Antworten als Markdown dargestellt, mit Verweisen, die auf ihre Quellen verlinken.
- Eine klare Meldung, wenn
emptygesetzt ist, und ein Weg, einen Menschen zu erreichen. 429behandelt, indem aufRetry-Aftergewartet wird.- Bewertungen werden zurückgesendet, damit die Wissensbasis mit der Zeit besser wird.