Kysymys–vastaus-botin rakentaminen
Tässä oppaassa rakennetaan sellaisen botin perusosat, joka vastaa kysymyksiin tietopankkisi pohjalta: chat-työkalussa, tukikeskuksessasi tai tuotteesi sisällä. Avustaja löytää tarvittavan tiedon ja vastaa ja viittaa käyttämiinsä kortteihin; koodisi lähettää kysymyksen ja näyttää tuloksen.
Tarvitset avaimen, jolla on käyttöalueet search ja ask. Vastaus käyttää vain kortteja,
joita avain saa lukea, joten avaimen rooli, dataprofiili ja kokoelmat ratkaisevat, mistä botti voi puhua. Anna
asiakkaille tarkoitetulle botille avain, joka näkee vain julkiset kokoelmat.
Kolme tapaa saada vastaus
| Päätepiste | Palauttaa | Käyttökohde |
|---|---|---|
POST /v1/search | Vastaavat tekstikatkelmat, paras ensin. Ei kirjoitettua vastausta. | Hakukenttä, ”aiheeseen liittyvät artikkelit”, oma osumien järjestys. |
POST /v1/ask | Valmiin vastauksen lähteineen yhdessä vastauksessa. | Botit, jotka julkaisevat yhden viestin vastausta kohden; taustatyöt. |
POST /v1/chat | Vastauksen tapahtumien virtana sitä mukaa kuin se kirjoitetaan. | Chat-käyttöliittymät, joissa käyttäjän pitää nähdä vastauksen ilmestyvän. |
Haku
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"}'Jokainen osuma on kortin tekstikatkelma: kortin doc_id ja doc_name, osion heading, katkelman text, sivut tai
taulukon rivit, joilta se löytyi, sekä kortin luottamus ja vahvistus.
collection valitsee haettavan kokoelman; ilman sitä käytetään kokoelmaa, jonka nimi on default. tags ja
card_ids rajaavat haun kortteihin, joilla on nämä tunnisteet, tai tiettyihin kortteihin.
Kysyminen ja valmis vastaus
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"}'answer-kentän vastauksen teksti on Markdownia. Sen viittaukset, kuten [1] tai [1, 3], vastaavat
sources-kentän kohteiden ref-arvoja; linkitä kukin viittaus korttiin (doc_id), jotta lukija voi tarkistaa
vastauksen.
Tekstissä oleva luku, jota ei voitu varmistaa asiakirjoista, voi näkyä merkkinä, kuten ⟦n:a1⟧.
verification.items kuvaa jokaisen merkin: sen marker-arvon, syyn (reason) ja original-tekstin. Jos koodisi
ei käsittele merkkejä, korvaa kukin niistä sen original-tekstillä.
Kun vastausta ei ole
Vastaus on aina 200. Kun avustaja ei pysty vastaamaan, empty kertoo syyn, ja answer sisältää lyhyen viestin,
jonka voit näyttää:
empty | Merkitys |
|---|---|
no_hits | Mikään tietopankissa ei vastaa kysymystä. |
restricted | Aihe on korteissa, joita avain ei saa lukea. |
guardrail | Kysymys on sen ulkopuolella, mihin organisaatio sallii avustajan vastata. |
no_data_for_period | Kysymys koskee ajanjaksoa, jolta ei ole asiakirjoja. |
no_hits on hyödyllinen signaali: se näyttää, mitä käyttäjäsi kysyvät mutta mitä tietopankki ei vielä kata.
”Fast” ja ”Precise”
Anna "quality": "precise" (”Precise”, tarkka), kun haluat vaikeaan kysymykseen perusteellisemman vastauksen, vaikka
se vie enemmän aikaa. Oletus "fast" (”Fast”, nopea) sopii useimpiin kysymyksiin.
Vastauksen välittäminen virtana
Näytä chat-käyttöliittymässä vastaus sitä mukaa kuin se kirjoitetaan. POST /v1/chat
parametrilla "stream": true vastaa text/event-stream-muodossa: yksi tapahtuma viestiä kohden, ja kussakin on
id:-rivi (tapahtuman numero virrassa ykkösestä alkaen) ja data:-rivi, jossa on JSON. JSON-objektin type-kenttä
kertoo, mistä tapahtumasta on kyse.
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 | Mitä tehdä |
|---|---|
meta | Vastaus on alkanut. Säilytä answer_id, jos kirjaat vastauksia lokiin. |
sources | Kortit, joihin vastaus perustuu. Voi tulla useammin kuin kerran; viimeinen on voimassa. |
token | Osa vastauksen tekstistä. Lisää se näyttämäsi tekstin perään. |
done | Vastaus on valmis. Siinä on samat kentät kuin /v1/ask-pyynnön vastauksessa. |
error | Vastaus keskeytyi. Virta päättyy ilman done-tapahtumaa. Ks. alla. |
followups | done-tapahtuman jälkeen: ehdotetut jatkokysymykset, joita voit tarjota painikkeina. |
| mikä tahansa muu | Ohita se. Uusia tapahtumatyyppejä voi ilmestyä milloin tahansa. |
Rivi, joka alkaa merkillä : (kuten : ping, joka lähetetään 15 sekunnin välein vastauksen valmistelun aikana), on
kommentti. Ohita se.
messages sisältää tähänastisen keskustelun vanhimmasta alkaen, ja viimeisenä on käyttäjän kysymys. Lähetä muutama
viimeisin vuoro, jotta jatkokysymykset, kuten ”entä ulkomailta tulevat tilaukset?”, ymmärretään.
// 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;
}Ilman "stream": true -parametria POST /v1/chat palauttaa valmiin vastauksen JSON-muodossa, kuten /v1/ask.
Jatka katkennutta virtaa
Yhteydet katkeavat: mobiiliverkko vaihtuu, välityspalvelimen aikakatkaisu laukeaa, palvelimesi käynnistyy uudelleen.
Anna jokaiselle kysymykselle request_id, niin vastaus säilyy katkoksen yli:
- Kun
request_idon annettu, vastaus muodostetaan yhteydestä riippumatta. Yhteyden katkeaminen ei pysäytä sitä: vastaus valmistuu ja tallennetaan. - Jatka lähettämällä sama pyyntö uudelleen samalla
request_id-tunnuksella ja viimeisen vastaanottamasi tapahtuman numerollaLast-Event-ID-otsakkeessa. - Käytä yhtä
request_id-tunnusta (esimerkiksi UUID) kysymystä kohden ja samaa tunnusta saman kysymyksen jokaisessa uusintayrityksessä. Uusi kysymys saa uudenrequest_id-tunnuksen.
Se, mitä toistettu pyyntö saa, riippuu vastauksen vaiheesta:
| Tilanne | Toistettu pyyntö saa |
|---|---|
| Vastausta kirjoitetaan vielä, tai se on juuri valmistunut | Last-Event-ID-kohdan jälkeiset tapahtumat samoilla numeroilla: virta yksinkertaisesti jatkuu. |
| Vastaus valmistui joitakin minuutteja sitten | Tallennetun vastauksen token-tapahtumana, jossa on koko teksti, ja done-tapahtuman, numeroituina ykkösestä alkaen. Kysymystä ei esitetä uudelleen. |
| Virta ei voi jatkua kyseisestä numerosta | Vastauksen alusta alkaen, numeroituna ykkösestä. |
Koodisi käsittelee kaikki kolme tapausta yhdellä säännöllä: jos ensimmäisen vastaanottamasi tapahtuman numero on
enintään Last-Event-ID, vastaus alkaa alusta; tyhjennä näyttämäsi teksti ja rakenna se uudelleen. Muuten lisää
uudet tapahtumat perään.
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 voi toimia useana API-instanssina. Virta jatkuu Last-Event-ID-kohdasta vain siinä instanssissa, joka
kirjoittaa vastausta; jos toistettu pyyntösi päätyy toiseen instanssiin, se saa tallennetun vastauksen, kun vastaus on
valmis, tai vastaus kirjoitetaan uudelleen alusta. Yllä oleva sääntö kattaa molemmat tapaukset, eikä kysymystä
koskaan lasketa kahdesti.
Ilman request_id-tunnusta yhteyden sulkeminen peruu vastauksen.
Virheet virrassa
Ennen virran alkamista tulevat virheet ovat tavallisia HTTP-virheitä: 401, 403, 422 tai 429 ja Retry-After,
kun organisaatio on käyttänyt kysymysrajansa tai sillä on samaan aikaan liian monta
käynnissä olevaa vastausta.
Kun virta on alkanut, sen HTTP-tila on aina 200, ja ongelma saapuu error-tapahtumana:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truetarkoittaa, että vastaus keskeytyi Sursan puolella, esimerkiksi päivityksen aikana, ja voit lähettää saman pyynnön uudelleen muuttamattomana.- Muut koodit, kuten
timeout, luetellaan API-viitteessä. Käsittele tuntematon koodi kuten mikä tahansa muu virran virhe: kerro käyttäjälle ja anna hänen kysyä uudelleen.
Vastausten arviointi
Jokaisella vastauksella on question_id. Lähetä käyttäjän arvio päätepisteeseen
POST /v1/questions/{question_id}/feedback: 1 tarkoittaa hyödyllistä,
-1 ei hyödyllistä, ja mukaan voi liittää valinnaiset reason ja note. Arviot auttavat organisaatiota näkemään,
mitkä kortit vaativat työtä.
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."}'Tuotantobotin tarkistuslista
- Avain, jolla on vain
searchjaaskja joka on rajattu kokoelmiin, jotka botin yleisö saa nähdä. request_idkysymystä kohden ja yllä kuvattu jatkamissääntö.- Virran lukemisen aikakatkaisu, joka on pidempi kuin 15 sekuntia, jotta
: ping-kommentit pitävät yhteyden auki. - Vastaukset näytetään Markdown-muotoisina, ja viittaukset linkitetään lähteisiinsä.
- Selkeä viesti, kun
emptyon asetettu, ja keino tavoittaa ihminen. 429käsitellään odottamallaRetry-After-ajan verran.- Arviot lähetetään takaisin, jotta tietopankki paranee ajan mittaan.