Creare un bot di domande e risposte
Questa guida costruisce il nucleo di un bot che risponde alle domande in base alla tua knowledge base: in uno strumento di chat, nel tuo centro assistenza o all’interno del tuo prodotto. L’assistente trova le conoscenze pertinenti e risponde, citando le schede che ha usato; il tuo codice invia la domanda e mostra il risultato.
Ti serve una chiave con gli ambiti search e ask. La risposta usa solo le schede che la
chiave può leggere, quindi ruolo, profilo dati e raccolte della chiave decidono di cosa può parlare il bot. A un bot per
i clienti dai una chiave che veda solo le raccolte pubbliche.
Tre modi per ottenere una risposta
| Endpoint | Restituisce | Usalo per |
|---|---|---|
POST /v1/search | Gli estratti corrispondenti, a partire dal migliore. Nessuna risposta scritta. | Una casella di ricerca, “articoli correlati”, un tuo ordinamento. |
POST /v1/ask | Una risposta completa con le sue fonti, in un’unica risposta HTTP. | Bot che pubblicano un messaggio per ogni risposta; job di back-end. |
POST /v1/chat | La risposta come stream di eventi, mentre viene scritta. | Interfacce di chat, dove l’utente deve vedere comparire la risposta. |
Cercare
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"}'Ogni risultato è un estratto di una scheda: doc_id e doc_name della scheda, heading della sezione, text
dell’estratto, le pagine o le righe di tabella in cui è stato trovato, e l’affidabilità e la verifica della scheda.
collection seleziona la raccolta in cui cercare; senza questo parametro si usa la raccolta con il nome default.
tags e card_ids restringono la ricerca alle schede con quei tag o a schede specifiche.
Chiedere e ottenere una risposta 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"}'Il testo della risposta in answer è in Markdown. I suoi riferimenti, come [1] o [1, 3], corrispondono a ref
delle voci in sources; collega ciascuno alla scheda (doc_id) così chi legge può verificare la risposta.
Un numero nel testo che non è stato possibile confermare rispetto ai documenti può comparire come un marcatore, per
esempio ⟦n:a1⟧. verification.items descrive ogni marcatore: il suo marker, il reason e il testo original. Se
il tuo codice non gestisce i marcatori, sostituisci ciascuno con il suo testo original.
Quando non c’è una risposta
La risposta HTTP è sempre 200. Quando l’assistente non può rispondere, empty dice perché, e answer contiene un
breve messaggio che puoi mostrare:
empty | Significato |
|---|---|
no_hits | Nella knowledge base non c’è nulla che corrisponda alla domanda. |
restricted | L’argomento si trova in schede che la chiave non può leggere. |
guardrail | La domanda va oltre ciò a cui l’organizzazione consente all’assistente di rispondere. |
no_data_for_period | La domanda riguarda un periodo per il quale non ci sono documenti. |
no_hits è un segnale utile: mostra cosa chiedono i tuoi utenti che la knowledge base non copre ancora.
Modalità “Fast” e “Precise”
Passa "quality": "precise" (modalità “Precise”, precisa) per una risposta più approfondita a una domanda difficile, a
costo di più tempo. Il valore predefinito, "fast" (modalità “Fast”, rapida), è adatto alla maggior parte delle
domande.
Trasmettere la risposta in streaming
In un’interfaccia di chat, mostra la risposta mentre viene scritta. POST /v1/chat con
"stream": true risponde con text/event-stream: un evento per messaggio, ciascuno con una riga id: (il suo numero
nello stream, a partire da 1) e una riga data: con il JSON. Il campo type del JSON dice che evento è.
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 | Cosa fare |
|---|---|
meta | La risposta è iniziata. Conserva answer_id se registri le risposte. |
sources | Le schede su cui si basa la risposta. Può arrivare più di una volta; vale l’ultimo. |
token | Un pezzo del testo della risposta. Aggiungilo a ciò che mostri. |
done | La risposta è completa. Ha gli stessi campi della risposta di /v1/ask. |
error | La risposta si è interrotta. Lo stream termina senza done. Vedi più avanti. |
followups | Dopo done: domande successive suggerite, che puoi offrire come pulsanti. |
| qualsiasi altro valore | Ignoralo. Nuovi tipi di evento possono comparire in qualsiasi momento. |
Una riga che inizia con : (come : ping, inviata ogni 15 secondi mentre la risposta viene preparata) è un commento.
Ignorala.
messages contiene la conversazione fino a quel momento, dal messaggio più vecchio, e termina con la domanda
dell’utente. Invia gli ultimi turni, così le domande successive come “e per gli ordini dall’estero?” vengono capite.
// 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;
}Senza "stream": true, POST /v1/chat restituisce la risposta completa in JSON, come /v1/ask.
Riprendere uno stream interrotto
Le connessioni si interrompono: una rete mobile cambia, un proxy va in timeout, il tuo server si riavvia. Assegna a
ogni domanda un request_id e la risposta sopravvive all’interruzione:
- Con
request_id, la risposta viene preparata indipendentemente dalla connessione. Una disconnessione non la ferma: viene completata e salvata. - Per riprendere, invia di nuovo la stessa richiesta, con lo stesso
request_ide il numero dell’ultimo evento ricevuto nell’intestazioneLast-Event-ID. - Usa un
request_id(per esempio un UUID) per ogni domanda, e lo stesso in ogni nuovo tentativo di quella domanda. Una nuova domanda riceve un nuovorequest_id.
Cosa riceve la richiesta ripetuta dipende da dove si trova la risposta:
| Situazione | La richiesta ripetuta riceve |
|---|---|
| La risposta è ancora in fase di scrittura, oppure è appena terminata | Gli eventi successivi a Last-Event-ID, con gli stessi numeri: lo stream semplicemente continua. |
| La risposta è terminata da qualche minuto | La risposta salvata come un token con l’intero testo e un done, numerati a partire da 1. La domanda non viene posta di nuovo. |
| Lo stream non può continuare da quel numero | La risposta dall’inizio, numerata a partire da 1. |
Il tuo codice gestisce tutti e tre i casi con un’unica regola: se il primo evento che ricevi ha un numero non
superiore a Last-Event-ID, la risposta ricomincia; cancella ciò che hai mostrato e ricostruiscilo. Altrimenti,
aggiungi.
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 di Sursa può funzionare su più server. Uno stream continua da Last-Event-ID solo sul server che sta scrivendo
la risposta; se la tua richiesta ripetuta raggiunge un altro server, riceve la risposta salvata quando è terminata,
oppure la risposta viene scritta di nuovo dall’inizio. La regola precedente copre entrambi i casi, e la domanda non
viene mai conteggiata due volte.
Senza request_id, chiudere la connessione annulla la risposta.
Errori in uno stream
Gli errori prima dell’inizio dello stream sono normali errori HTTP: 401, 403, 422, oppure 429 con
Retry-After quando l’organizzazione ha esaurito il suo limite di domande o ha troppe
risposte in corso contemporaneamente.
Una volta iniziato lo stream, il suo stato HTTP è sempre 200 e un problema arriva come evento error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truesignifica che la risposta si è interrotta per una causa del server, per esempio durante un aggiornamento, e puoi inviare di nuovo la stessa richiesta senza modifiche.- Gli altri codici, come
timeout, sono elencati nel riferimento API. Tratta un codice sconosciuto come qualsiasi altro errore dello stream: avvisa l’utente e lascia che chieda di nuovo.
Permettere agli utenti di valutare le risposte
Ogni risposta ha un question_id. Invia la valutazione dell’utente a
POST /v1/questions/{question_id}/feedback: 1 per utile, -1 per non
utile, con reason e note facoltativi. Le valutazioni aiutano l’organizzazione a capire quali schede vanno
migliorate.
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."}'Checklist per un bot in produzione
- Una chiave con soli
searcheask, limitata alle raccolte che il pubblico del bot può vedere. - Un
request_idper ogni domanda e la regola di ripresa descritta sopra. - Un timeout di lettura dello stream superiore a 15 secondi, così i commenti
: pinglo mantengono attivo. - Risposte visualizzate come Markdown, con i riferimenti collegati alle loro fonti.
- Un messaggio chiaro quando
emptyè impostato e un modo per contattare una persona. 429gestito attendendo il tempo diRetry-After.- Valutazioni inviate a Sursa, così la knowledge base migliora nel tempo.