Crear un bot de preguntes i respostes
Aquesta guia construeix el nucli d’un bot que respon preguntes a partir de la vostra base de coneixement: en una eina de xat, al vostre centre d’ajuda o dins del vostre producte. L’assistent troba el coneixement necessari i respon, citant les entrades que ha fet servir; el vostre codi envia la pregunta i mostra el resultat.
Necessiteu una clau amb els àmbits search i ask. La resposta només fa servir les
entrades que la clau pot llegir, de manera que el rol, el perfil de dades i les col·leccions de la clau decideixen de
què pot parlar el bot. A un bot per a clients, doneu-li una clau que només vegi col·leccions públiques.
Tres maneres d’obtenir una resposta
| Punt final | Retorna | Feu-lo servir per a |
|---|---|---|
POST /v1/search | Els extractes que coincideixen, primer els més adequats. Sense resposta redactada. | Un quadre de cerca, «articles relacionats», una ordenació pròpia. |
POST /v1/ask | Una resposta completa amb les seves fonts, d’un sol cop. | Bots que publiquen un missatge per resposta; treballs del vostre servidor. |
POST /v1/chat | La resposta com un flux d’esdeveniments, mentre s’escriu. | Interfícies de xat, en què l’usuari ha de veure com apareix la resposta. |
Cercar
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"}'Cada resultat és un extracte d’una entrada: doc_id i doc_name de l’entrada, heading de la secció, text de
l’extracte, les pàgines o les files de taula on s’ha trobat, i la confiança i la verificació de l’entrada.
collection selecciona la col·lecció on es cerca; sense aquest paràmetre, es fa servir la col·lecció amb el nom
default. tags i card_ids restringeixen la cerca a les entrades amb aquestes etiquetes o a entrades concretes.
Preguntar i obtenir una resposta completa
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"}'El text de la resposta a answer és Markdown. Les seves referències, com ara [1] o [1, 3], corresponen al ref
dels elements de sources; enllaceu cadascuna amb l’entrada (doc_id) perquè qui llegeix pugui comprovar la
resposta.
Un nombre del text que no s’ha pogut confirmar amb els documents pot aparèixer com un marcador, com ara ⟦n:a1⟧.
verification.items descriu cada marcador: el seu marker, el reason i el text original. Si el vostre codi no
gestiona marcadors, substituïu cadascun pel seu text original.
Quan no hi ha resposta
La resposta sempre és 200. Quan l’assistent no pot respondre, empty indica per què, i answer conté un missatge
breu que podeu mostrar:
empty | Significat |
|---|---|
no_hits | No hi ha res a la base de coneixement que coincideixi amb la pregunta. |
restricted | El tema és en entrades que la clau no pot llegir. |
guardrail | La pregunta queda fora del que l’organització permet que respongui l’assistent. |
no_data_for_period | La pregunta es refereix a un període per al qual no hi ha documents. |
no_hits és un senyal útil: mostra què pregunten els vostres usuaris que la base de coneixement encara no cobreix.
Respostes «Fast» (ràpida) i «Precise» (precisa)
Passeu "quality": "precise" per obtenir una resposta més exhaustiva a una pregunta difícil, a canvi de més temps. El
valor per defecte, "fast", convé a la majoria de preguntes.
Transmetre la resposta en temps real
En una interfície de xat, mostreu la resposta mentre s’escriu. POST /v1/chat amb
"stream": true respon amb text/event-stream: un esdeveniment per missatge, cadascun amb una línia id: (el seu
número dins del flux, a partir d’1) i una línia data: amb JSON. El camp type del JSON indica quin esdeveniment és.
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 | Què cal fer |
|---|---|
meta | La resposta ha començat. Conserveu answer_id si registreu les respostes. |
sources | Les entrades en què es basa la resposta. Pot arribar més d’una vegada; val l’última. |
token | Un tros del text de la resposta. Afegiu-lo al que mostreu. |
done | La resposta és completa. Té els mateixos camps que la resposta de /v1/ask. |
error | La resposta s’ha aturat. El flux acaba sense done. Vegeu més avall. |
followups | Després de done: preguntes de seguiment suggerides que podeu oferir com a botons. |
| qualsevol altre | Ometeu-lo. Poden aparèixer tipus d’esdeveniment nous en qualsevol moment. |
Una línia que comença per : (com ara : ping, que s’envia cada 15 segons mentre es prepara la resposta) és un
comentari. Ometeu-la.
messages conté la conversa fins ara, de més antic a més recent, i acaba amb la pregunta de l’usuari. Envieu els
últims torns perquè s’entenguin les preguntes de seguiment, com ara «i per a les comandes de l’estranger?».
// 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;
}Sense "stream": true, POST /v1/chat retorna la resposta completa en JSON, com /v1/ask.
Reprendre un flux interromput
Les connexions es tallen: canvia la xarxa mòbil, un servidor intermediari esgota el temps d’espera, el vostre
servidor es reinicia. Doneu un request_id a cada pregunta i la resposta sobreviurà al tall:
- Amb
request_id, la resposta es prepara independentment de la connexió. Una desconnexió no l’atura: es completa i es desa. - Per reprendre-la, torneu a enviar la mateixa sol·licitud, amb el mateix
request_id, i el número de l’últim esdeveniment que heu rebut a la capçaleraLast-Event-ID. - Feu servir un
request_id(per exemple, un UUID) per pregunta, i el mateix en tots els reintents d’aquesta pregunta. Una pregunta nova rep unrequest_idnou.
El que rep la sol·licitud repetida depèn de l’estat de la resposta:
| Situació | La sol·licitud repetida rep |
|---|---|
| La resposta encara s’està escrivint o acaba de finalitzar | Els esdeveniments posteriors a Last-Event-ID, amb els mateixos números: el flux simplement continua. |
| La resposta va acabar fa uns minuts | La resposta desada com un token amb tot el text i un done, numerats a partir d’1. La pregunta no es torna a fer. |
| El flux no pot continuar des d’aquell número | La resposta des del principi, numerada a partir d’1. |
El vostre codi gestiona els tres casos amb una sola regla: si el primer esdeveniment que rebeu té un número que no
supera Last-Event-ID, la resposta torna a començar; esborreu el que heu mostrat i torneu-ho a construir. Si no,
afegiu-hi el text nou.
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));
}
}
}L’API de Sursa es pot executar en diversos servidors. Un flux continua des de Last-Event-ID només al servidor que
escriu la resposta; si la vostra sol·licitud repetida arriba a un altre, rep la resposta desada un cop ha acabat, o la
resposta es torna a escriure des del principi. La regla anterior cobreix tots dos casos, i la pregunta no es compta
mai dues vegades.
Sense request_id, tancar la connexió cancel·la la resposta.
Errors en un flux
Els errors anteriors a l’inici del flux són errors HTTP normals: 401, 403, 422, o 429 amb Retry-After quan
l’organització ha esgotat el seu límit de preguntes o té massa
respostes en curs alhora.
Un cop ha començat el flux, el seu estat HTTP sempre és 200, i un problema arriba com un esdeveniment error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truevol dir que la resposta s’ha aturat per causa del servidor, per exemple durant una actualització, i que podeu tornar a enviar la mateixa sol·licitud sense canvis.- Els altres codis, com ara
timeout, figuren a la referència de l’API. Tracteu un codi desconegut com qualsevol altre error del flux: informeu-ne l’usuari i deixeu-lo tornar a preguntar.
Permetre que els usuaris valorin les respostes
Cada resposta té un question_id. Envieu la valoració de l’usuari a
POST /v1/questions/{question_id}/feedback: 1 per a útil, -1 per a no
útil, amb un reason i una note opcionals. Les valoracions ajuden l’organització a veure quines entrades cal
millorar.
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."}'Llista de comprovació per a un bot en producció
- Una clau amb només
searchiask, limitada a les col·leccions que pot veure el públic del bot. - Un
request_idper pregunta i la regla de represa anterior. - Un temps d’espera de lectura del flux superior a 15 segons, perquè els comentaris
: pingel mantinguin actiu. - El Markdown de les respostes mostrat amb format, amb les referències enllaçades a les seves fonts.
- Un missatge clar quan
emptyté valor, i una manera de contactar amb una persona. 429gestionat esperant el temps deRetry-After.- Valoracions enviades de tornada, perquè la base de coneixement millori amb el temps.