Klausimų ir atsakymų boto kūrimas
Ši instrukcija parodo, kaip sukurti boto pagrindą. Botas atsako į klausimus pagal jūsų žinių bazę pokalbių įrankyje, jūsų pagalbos centre ar jūsų produkte. Asistentas randa reikiamas žinias ir atsako, nurodydamas panaudotas korteles; jūsų kodas išsiunčia klausimą ir parodo rezultatą.
Jums reikia rakto su apimtimis search ir ask. Atsakymas naudoja tik korteles, kurias
raktui leidžiama skaityti, todėl apie ką botas gali kalbėti, lemia rakto vaidmuo, duomenų profilis ir rinkiniai.
Klientams skirtam botui suteikite raktą, kuris mato tik viešus rinkinius.
Trys būdai gauti atsakymą
| Operacija | Grąžina | Kam naudoti |
|---|---|---|
POST /v1/search | Atitinkančias ištraukas, geriausios – pirmos. Parašyto atsakymo nėra. | Paieškos laukui, „susijusiems straipsniams“, savo rikiavimui. |
POST /v1/ask | Visą atsakymą su šaltiniais viename atsake. | Botams, kurie kiekvienam atsakymui skelbia vieną žinutę; serverio pusės darbams. |
POST /v1/chat | Atsakymą kaip įvykių srautą, kol jis rašomas. | Pokalbių sąsajoms, kuriose naudotojas turėtų matyti, kaip atsiranda atsakymas. |
Paieška
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"}'Kiekvienas atitikmuo – kortelės ištrauka: kortelės doc_id ir doc_name, skyriaus heading, ištraukos text,
puslapiai ar lentelės eilutės, kuriuose ji rasta, ir kortelės patikimumas bei tikrinimas.
collection pasirenka rinkinį, kuriame ieškoma; be jo naudojamas rinkinys, pavadintas default. tags ir card_ids
susiaurina paiešką iki kortelių su tomis žymomis arba iki konkrečių kortelių.
Klausimas ir visas atsakymas
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"}'Atsakymo tekstas lauke answer yra Markdown formatu. Jo nuorodos, pavyzdžiui, [1] arba [1, 3], atitinka sources
įrašų ref; kiekvieną susiekite su kortele (doc_id), kad skaitytojas galėtų patikrinti atsakymą.
Skaičius tekste, kurio nepavyko patvirtinti pagal dokumentus, gali būti parodytas kaip ženklas, pavyzdžiui, ⟦n:a1⟧.
verification.items aprašo kiekvieną ženklą: jo marker, reason ir original tekstą. Jei jūsų kodas ženklų
neapdoroja, kiekvieną jų pakeiskite jo original tekstu.
Kai atsakymo nėra
Atsakas visada yra 200. Kai asistentas negali atsakyti, empty nurodo priežastį, o answer pateikia trumpą
pranešimą, kurį galima parodyti:
empty | Reikšmė |
|---|---|
no_hits | Žinių bazėje nėra nieko, kas atitiktų klausimą. |
restricted | Tema yra kortelėse, kurių raktui neleidžiama skaityti. |
guardrail | Klausimas nepatenka į sritį, kurioje organizacija leidžia asistentui atsakyti. |
no_data_for_period | Klausiama apie laikotarpį, už kurį dokumentų nėra. |
no_hits – naudingas signalas: jis parodo, ko jūsų naudotojai klausia, bet žinių bazė dar neaprėpia.
„Fast“ ir „Precise“
Perduokite "quality": "precise", kad į sudėtingą klausimą gautumėte išsamesnį atsakymą, nors tai užtrunka ilgiau.
Numatytasis "fast" tinka daugumai klausimų.
Atsakymo transliavimas srautu
Pokalbių sąsajoje rodykite atsakymą, kol jis rašomas. POST /v1/chat su "stream": true
atsako text/event-stream formatu: po vieną įvykį kiekvienai žinutei, kiekvienas su eilute id: (jo numeris sraute,
nuo 1) ir eilute data: su JSON. JSON laukas type nurodo, koks tai įvykis.
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 | Ką daryti |
|---|---|
meta | Atsakymas pradėtas. Išsaugokite answer_id, jei registruojate atsakymus. |
sources | Kortelės, kuriomis remiasi atsakymas. Gali ateiti daugiau nei kartą; galioja paskutinis. |
token | Atsakymo teksto dalis. Pridėkite ją prie to, ką rodote. |
done | Atsakymas baigtas. Jis turi tuos pačius laukus kaip /v1/ask atsakas. |
error | Atsakymas nutrūko. Srautas baigiasi be done. Žr. toliau. |
followups | Po done: siūlomi tolesni klausimai, kuriuos galite pateikti kaip mygtukus. |
| bet kas kita | Praleiskite. Nauji įvykių tipai gali atsirasti bet kada. |
Eilutė, prasidedanti : (pavyzdžiui, : ping, siunčiama kas 15 sekundžių, kol atsakymas rengiamas), yra komentaras.
Ją praleiskite.
messages pateikia iki šiol vykusį pokalbį: seniausios replikos – pirmos, paskutinė – naudotojo klausimas. Siųskite
kelias paskutines replikas, kad būtų suprasti tolesni klausimai, pavyzdžiui, „o užsakymams iš užsienio?“.
// 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;
}Be "stream": true POST /v1/chat grąžina visą atsakymą JSON formatu, kaip /v1/ask.
Nutrūkusio srauto tęsimas
Ryšiai nutrūksta: pasikeičia mobilusis tinklas, tarpiniame serveryje baigiasi laiko limitas, jūsų serveris
paleidžiamas iš naujo. Kiekvienam klausimui suteikite request_id, ir atsakymas išliks nepaisant nutrūkimo:
- Su
request_idatsakymas rengiamas nepriklausomai nuo ryšio. Atsijungimas jo nesustabdo: jis užbaigiamas ir išsaugomas. - Kad tęstumėte, vėl išsiųskite tą pačią užklausą su tuo pačiu
request_id, o antraštėjeLast-Event-IDnurodykite paskutinio gauto įvykio numerį. - Kiekvienam klausimui naudokite vieną
request_id(pavyzdžiui, UUID) ir tą patį – kiekviename to klausimo pakartojime. Naujas klausimas gauna naująrequest_id.
Ką gauna pakartota užklausa, priklauso nuo to, kokiame etape yra atsakymas:
| Situacija | Pakartota užklausa gauna |
|---|---|
| Atsakymas dar rašomas arba ką tik baigtas | Įvykius po Last-Event-ID, su tais pačiais numeriais: srautas tiesiog tęsiasi. |
| Atsakymas baigtas prieš kelias minutes | Išsaugotą atsakymą kaip token su visu tekstu ir done, numeruotus nuo 1. Klausimas iš naujo neužduodamas. |
| Srauto nuo to numerio tęsti negalima | Atsakymą nuo pradžios, numeruotą nuo 1. |
Jūsų kodas visus tris atvejus apdoroja viena taisykle: jei pirmojo gauto įvykio numeris ne didesnis už
Last-Event-ID, atsakymas pradedamas iš naujo; išvalykite tai, ką rodėte, ir sudarykite iš naujo. Kitu atveju
pridėkite.
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 gali veikti keliuose serveriuose. Srautas nuo Last-Event-ID tęsiamas tik tame serveryje, kuris rašo
atsakymą; jei jūsų pakartota užklausa pasiekia kitą serverį, ji gauna išsaugotą atsakymą, kai šis bus baigtas, arba
atsakymas rašomas iš naujo nuo pradžios. Aukščiau aprašyta taisyklė apima abu atvejus, o klausimas niekada
neįskaičiuojamas du kartus.
Be request_id ryšio uždarymas atšaukia atsakymą.
Klaidos sraute
Klaidos prieš prasidedant srautui yra įprastos HTTP klaidos: 401, 403, 422 arba 429 su Retry-After, kai
organizacija išnaudojo savo klausimų limitą arba vienu metu turi per daug
rengiamų atsakymų.
Kai srautas prasidėjo, jo HTTP būsena visada yra 200, o problema atkeliauja kaip įvykis error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: truereiškia, kad atsakymas nutrūko dėl serverio, pavyzdžiui, atnaujinimo metu, ir tą pačią užklausą galite išsiųsti dar kartą be pakeitimų.- Kiti kodai, pavyzdžiui,
timeout, išvardyti API žinyne. Nežinomą kodą vertinkite kaip bet kurią kitą srauto klaidą: praneškite naudotojui ir leiskite jam paklausti dar kartą.
Atsakymų vertinimas
Kiekvienas atsakymas turi question_id. Naudotojo įvertinimą siųskite į
POST /v1/questions/{question_id}/feedback: 1 – naudingas, -1 –
nenaudingas, galima pridėti reason ir note. Įvertinimai padeda organizacijai matyti, kurias korteles reikia
tobulinti.
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."}'Kontrolinis sąrašas boto paleidimui
- Raktas tik su
searchirask, apribotas rinkiniais, kuriuos gali matyti boto auditorija. request_idkiekvienam klausimui ir aukščiau aprašyta tęsimo taisyklė.- Srauto skaitymo laiko limitas, ilgesnis nei 15 sekundžių, kad
: pingkomentarai palaikytų ryšį. - Atsakymai atvaizduojami kaip Markdown, nuorodos susietos su jų šaltiniais.
- Aiškus pranešimas, kai nustatytas
empty, ir būdas susisiekti su žmogumi. 429apdorojamas laukiantRetry-After.- Įvertinimai siunčiami atgal, kad žinių bazė laikui bėgant gerėtų.