Construiți un bot de întrebări și răspunsuri
Acest ghid construiește partea de bază a unui bot care răspunde la întrebări din baza de cunoștințe: într-o aplicație de chat, în centrul de asistență sau în interiorul produsului. Asistentul găsește cunoștințele necesare și răspunde, citând fișele pe care le-a folosit; codul dumneavoastră trimite întrebarea și afișează rezultatul.
Aveți nevoie de o cheie cu domeniile de acces search și ask. Răspunsul folosește
doar fișele pe care cheia are voie să le citească, așa că rolul, profilul de date și colecțiile cheii decid despre ce
poate vorbi botul. Unui bot pentru clienți dați-i o cheie care vede doar colecțiile publice.
Trei moduri de a obține un răspuns
| Endpoint | Returnează | Folosiți-l pentru |
|---|---|---|
POST /v1/search | Pasajele care corespund, cele mai potrivite primele. Fără răspuns scris. | O casetă de căutare, „articole similare”, propria ordonare. |
POST /v1/ask | Un răspuns complet, cu sursele lui, într-un singur răspuns HTTP. | Boți care publică un mesaj pentru fiecare răspuns; joburi pe server. |
POST /v1/chat | Răspunsul ca flux de evenimente, în timp ce este scris. | Interfețe de chat, în care utilizatorul trebuie să vadă cum apare răspunsul. |
Căutare
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"}'Fiecare rezultat este un pasaj dintr-o fișă: doc_id și doc_name ale fișei, heading al secțiunii, text al
pasajului, paginile sau rândurile de tabel în care a fost găsit, precum și încrederea și verificarea fișei.
collection alege colecția în care se caută; fără el, se folosește colecția numită default. tags și card_ids
restrâng căutarea la fișele cu acele etichete sau la anumite fișe.
Întrebare și răspuns complet
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"}'Textul răspunsului din answer este Markdown. Trimiterile din el, cum ar fi [1] sau [1, 3], corespund câmpului
ref al intrărilor din sources; legați fiecare trimitere de fișă (doc_id), ca cititorul să poată verifica
răspunsul.
Un număr din text care nu a putut fi confirmat pe baza documentelor poate apărea ca marcaj, de exemplu ⟦n:a1⟧.
verification.items descrie fiecare marcaj: câmpul lui marker, motivul (reason) și textul original. Dacă codul
nu tratează marcajele, înlocuiți-le pe fiecare cu textul lui original.
Când nu există răspuns
Răspunsul este întotdeauna 200. Când asistentul nu poate răspunde, empty spune de ce, iar answer conține un
mesaj scurt pe care îl puteți afișa:
empty | Semnificație |
|---|---|
no_hits | Nimic din baza de cunoștințe nu corespunde întrebării. |
restricted | Subiectul se află în fișe pe care cheia nu are voie să le citească. |
guardrail | Întrebarea depășește ce permite organizația asistentului să răspundă. |
no_data_for_period | Întrebarea se referă la o perioadă pentru care nu există documente. |
no_hits este un semnal util: arată ce întreabă utilizatorii și nu este încă acoperit de baza de cunoștințe.
Modurile „Fast” și „Precise”
Transmiteți "quality": "precise" pentru un răspuns mai temeinic la o întrebare dificilă, cu prețul unui timp mai
lung. Valoarea implicită, "fast", se potrivește majorității întrebărilor.
Transmiterea răspunsului în flux
Într-o interfață de chat, afișați răspunsul în timp ce este scris. POST /v1/chat cu
"stream": true răspunde cu text/event-stream: câte un eveniment pentru fiecare mesaj, fiecare cu un rând id:
(numărul lui în flux, de la 1) și un rând data: cu JSON. Câmpul type din JSON spune ce eveniment este.
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 | Ce trebuie făcut |
|---|---|
meta | Răspunsul a început. Păstrați answer_id dacă înregistrați răspunsurile în jurnal. |
sources | Fișele pe care se bazează răspunsul. Poate veni de mai multe ori; contează ultimul. |
token | O bucată din textul răspunsului. Adăugați-o la ce afișați. |
done | Răspunsul este complet. Are aceleași câmpuri ca răspunsul lui /v1/ask. |
error | Răspunsul s-a oprit. Fluxul se încheie fără done. Vedeți mai jos. |
followups | După done: întrebări următoare sugerate, pe care le puteți oferi ca butoane. |
| orice altceva | Ignorați-l. Pot apărea oricând tipuri noi de evenimente. |
Un rând care începe cu : (de exemplu : ping, trimis la fiecare 15 secunde cât timp răspunsul este pregătit) este
un comentariu. Ignorați-l.
messages conține conversația de până acum, de la cel mai vechi mesaj, încheiată cu întrebarea utilizatorului.
Trimiteți ultimele câteva schimburi, ca întrebările suplimentare, de exemplu „și pentru comenzile din străinătate?”, să
fie înțelese.
// 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;
}Fără "stream": true, POST /v1/chat returnează răspunsul complet ca JSON, la fel ca /v1/ask.
Reluarea unui flux întrerupt
Conexiunile se întrerup: se schimbă rețeaua mobilă, un proxy închide conexiunea la expirare, serverul dumneavoastră
repornește. Dați fiecărei întrebări un request_id, iar răspunsul supraviețuiește întreruperii:
- Cu
request_id, răspunsul este pregătit independent de conexiune. O deconectare nu îl oprește: el este finalizat și salvat. - Pentru a relua, trimiteți din nou aceeași cerere, cu același
request_id, și numărul ultimului eveniment primit în antetulLast-Event-ID. - Folosiți un singur
request_id(de exemplu un UUID) pentru fiecare întrebare și același la fiecare reîncercare a acelei întrebări. O întrebare nouă primește unrequest_idnou.
Ce primește cererea repetată depinde de stadiul răspunsului:
| Situație | Cererea repetată primește |
|---|---|
| Răspunsul este încă scris sau tocmai s-a încheiat | Evenimentele de după Last-Event-ID, cu aceleași numere: fluxul pur și simplu continuă. |
| Răspunsul s-a încheiat acum câteva minute | Răspunsul salvat, ca un token cu tot textul și un done, numerotate de la 1. Întrebarea nu este pusă din nou. |
| Fluxul nu poate continua de la acel număr | Răspunsul de la început, numerotat de la 1. |
Codul tratează toate cele trei cazuri cu o singură regulă: dacă primul eveniment primit are un număr cel mult egal
cu Last-Event-ID, răspunsul începe din nou; ștergeți ce ați afișat și construiți-l din nou. Altfel, adăugați la
ce aveți.
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));
}
}
}API-ul Sursa poate rula pe mai multe servere. Un flux continuă de la Last-Event-ID doar pe serverul care scrie
răspunsul; dacă cererea repetată ajunge la alt server, ea primește răspunsul salvat după ce acesta s-a încheiat sau
răspunsul este scris din nou de la început. Regula de mai sus acoperă ambele cazuri, iar întrebarea nu este socotită
niciodată de două ori.
Fără request_id, închiderea conexiunii anulează răspunsul.
Erori într-un flux
Erorile de dinainte de pornirea fluxului sunt erori HTTP obișnuite: 401, 403, 422 sau 429 cu Retry-After,
când organizația și-a epuizat limita de întrebări sau are prea multe
răspunsuri în lucru în același timp.
După ce fluxul a pornit, statusul lui HTTP este întotdeauna 200, iar o problemă sosește ca eveniment error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: trueînseamnă că răspunsul s-a oprit din cauza serverului, de exemplu în timpul unei actualizări, și puteți trimite din nou aceeași cerere, neschimbată.- Celelalte coduri, cum ar fi
timeout, sunt enumerate în referința API. Tratați un cod necunoscut ca pe orice altă eroare de flux: anunțați utilizatorul și lăsați-l să întrebe din nou.
Evaluarea răspunsurilor de către utilizatori
Fiecare răspuns are un question_id. Trimiteți evaluarea utilizatorului la
POST /v1/questions/{question_id}/feedback: 1 pentru util, -1 pentru
inutil, cu un reason și o note opționale. Evaluările ajută organizația să vadă ce fișe trebuie îmbunătățite.
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."}'Listă de verificare pentru un bot în producție
- O cheie doar cu
searchșiask, limitată la colecțiile pe care le poate vedea publicul botului. - Câte un
request_idpentru fiecare întrebare și regula de reluare de mai sus. - Un timp de expirare la citirea fluxului mai lung de 15 secunde, ca comentariile
: pingsă-l mențină activ. - Răspunsurile afișate ca Markdown, cu trimiterile legate de sursele lor.
- Un mesaj clar când
emptyeste setat și o modalitate de a ajunge la o persoană. 429tratat prin așteptarea indicată deRetry-After.- Evaluările trimise înapoi, ca baza de cunoștințe să se îmbunătățească în timp.