Zbuduj bota odpowiadającego na pytania
Ten przewodnik buduje podstawę bota, który odpowiada na pytania na podstawie Twojej bazy wiedzy: w komunikatorze, w Twoim centrum pomocy lub w Twoim produkcie. Asystent znajduje potrzebną wiedzę i odpowiada, powołując się na karty, z których skorzystał; Twój kod wysyła pytanie i pokazuje wynik.
Potrzebujesz klucza z zakresami search i ask. Odpowiedź korzysta tylko z kart, które
klucz może odczytywać, więc rola klucza, jego profil dostępu i kolekcje decydują o tym, o czym bot może mówić. Botowi
dla klientów daj klucz, który widzi tylko publiczne kolekcje.
Trzy sposoby uzyskania odpowiedzi
| Operacja | Zwraca | Zastosowanie |
|---|---|---|
POST /v1/search | Pasujące urywki, od najlepiej dopasowanego. Bez gotowej odpowiedzi tekstowej. | Pole wyszukiwania, „powiązane artykuły”, własny ranking. |
POST /v1/ask | Pełną odpowiedź ze źródłami w jednej odpowiedzi HTTP. | Boty, które publikują jedną wiadomość na odpowiedź; zadania po stronie serwera. |
POST /v1/chat | Odpowiedź jako strumień zdarzeń, w miarę jej powstawania. | Interfejsy czatu, w których użytkownik powinien widzieć, jak pojawia się odpowiedź. |
Wyszukiwanie
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"}'Każde trafienie to urywek karty: doc_id i doc_name karty, heading sekcji, text urywka, strony lub wiersze
tabeli, w których go znaleziono, oraz zaufanie i weryfikacja karty.
collection wybiera kolekcję do przeszukania; bez tego parametru używana jest kolekcja o nazwie default. tags
i card_ids zawężają wyszukiwanie do kart z tymi tagami lub do konkretnych kart.
Zadaj pytanie i otrzymaj pełną odpowiedź
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"}'Tekst odpowiedzi w answer jest w formacie Markdown. Jego odwołania, np. [1] lub [1, 3], odpowiadają polu ref
wpisów w sources; połącz każde z nich z kartą (doc_id), aby czytelnik mógł sprawdzić odpowiedź.
Liczba w tekście, której nie udało się potwierdzić w dokumentach, może pojawić się jako znacznik, np. ⟦n:a1⟧.
verification.items opisuje każdy znacznik: jego marker, przyczynę w reason i tekst z original. Jeśli Twój
kod nie obsługuje znaczników, zastąp każdy z nich jego tekstem z original.
Gdy nie ma odpowiedzi
Odpowiedź HTTP ma zawsze status 200. Gdy asystent nie może odpowiedzieć, empty podaje przyczynę, a answer
zawiera krótki komunikat, który możesz pokazać:
empty | Znaczenie |
|---|---|
no_hits | Nic w bazie wiedzy nie pasuje do pytania. |
restricted | Temat znajduje się w kartach, których klucz nie może odczytywać. |
guardrail | Pytanie wykracza poza to, na co organizacja pozwala asystentowi odpowiadać. |
no_data_for_period | Pytanie dotyczy okresu, dla którego nie ma dokumentów. |
no_hits to przydatny sygnał: pokazuje, o co pytają Twoi użytkownicy, a czego baza wiedzy jeszcze nie obejmuje.
Tryby „Fast” i „Precise”
Przekaż "quality": "precise", aby uzyskać dokładniejszą odpowiedź na trudne pytanie kosztem dłuższego czasu.
Wartość domyślna, "fast", sprawdza się w przypadku większości pytań.
Przesyłaj odpowiedź strumieniowo
W interfejsie czatu pokazuj odpowiedź w miarę jej powstawania. POST /v1/chat
z "stream": true odpowiada w formacie text/event-stream: jedno zdarzenie na wiadomość, każde z wierszem id:
(jego numer w strumieniu, od 1) i wierszem data: z JSON. Pole type w JSON mówi, czym jest zdarzenie.
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 | Co zrobić |
|---|---|
meta | Odpowiedź się rozpoczęła. Zachowaj answer_id, jeśli zapisujesz odpowiedzi w dzienniku. |
sources | Karty, na których opiera się odpowiedź. Może przyjść więcej niż raz; obowiązuje ostatnie. |
token | Fragment tekstu odpowiedzi. Dołącz go do tego, co już pokazujesz. |
done | Odpowiedź jest kompletna. Ma te same pola co odpowiedź /v1/ask. |
error | Odpowiedź została przerwana. Strumień kończy się bez done. Zob. poniżej. |
followups | Po done: propozycje kolejnych pytań, które możesz pokazać jako przyciski. |
| cokolwiek innego | Pomiń. Nowe typy zdarzeń mogą pojawić się w każdej chwili. |
Wiersz zaczynający się od : (np. : ping, wysyłany co 15 sekund podczas przygotowywania odpowiedzi) to komentarz.
Pomiń go.
messages zawiera dotychczasową rozmowę, od najstarszej wiadomości, kończąc na pytaniu użytkownika. Wysyłaj kilka
ostatnich tur, aby pytania uzupełniające, np. „a dla zamówień z zagranicy?”, były zrozumiałe.
// 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;
}Bez "stream": true POST /v1/chat zwraca pełną odpowiedź jako JSON, tak jak /v1/ask.
Wznów przerwany strumień
Połączenia się zrywają: zmienia się sieć komórkowa, serwer proxy przekracza limit czasu, Twój serwer uruchamia się
ponownie. Nadaj każdemu pytaniu request_id, a odpowiedź przetrwa przerwę:
- Z
request_idodpowiedź jest przygotowywana niezależnie od połączenia. Rozłączenie jej nie zatrzymuje: zostaje dokończona i zapisana. - Aby wznowić, wyślij ponownie to samo żądanie z tym samym
request_idi numerem ostatniego otrzymanego zdarzenia w nagłówkuLast-Event-ID. - Używaj jednego
request_id(np. UUID) na pytanie i tego samego przy każdym ponowieniu tego pytania. Nowe pytanie otrzymuje nowyrequest_id.
To, co otrzyma powtórzone żądanie, zależy od tego, na jakim etapie jest odpowiedź:
| Sytuacja | Powtórzone żądanie otrzymuje |
|---|---|
| Odpowiedź jest wciąż przygotowywana lub właśnie się zakończyła | Zdarzenia po Last-Event-ID, z tymi samymi numerami: strumień po prostu trwa dalej. |
| Odpowiedź zakończyła się kilka minut temu | Zapisaną odpowiedź jako token z całym tekstem i done, numerowane od 1. Pytanie nie jest zadawane ponownie. |
| Strumienia nie można kontynuować od tego numeru | Odpowiedź od początku, numerowaną od 1. |
Twój kod obsługuje wszystkie trzy przypadki jedną zasadą: jeśli pierwsze otrzymane zdarzenie ma numer nie większy
niż Last-Event-ID, odpowiedź zaczyna się od nowa; wyczyść to, co pokazano, i zbuduj ją ponownie. W przeciwnym
razie dołączaj kolejne fragmenty.
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 może działać na kilku serwerach. Strumień jest kontynuowany od Last-Event-ID tylko na serwerze, który
przygotowuje odpowiedź; jeśli powtórzone żądanie trafi na inny serwer, otrzyma zapisaną odpowiedź, gdy będzie gotowa,
albo odpowiedź zostanie przygotowana od początku. Powyższa zasada obejmuje oba przypadki, a pytanie nigdy nie jest
liczone dwukrotnie.
Bez request_id zamknięcie połączenia anuluje odpowiedź.
Błędy w strumieniu
Błędy przed rozpoczęciem strumienia to zwykłe błędy HTTP: 401, 403, 422 lub 429 z Retry-After, gdy
organizacja wykorzystała swój limit pytań lub ma naraz zbyt wiele
odpowiedzi w toku.
Po rozpoczęciu strumienia jego status HTTP to zawsze 200, a problem przychodzi jako zdarzenie error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: trueoznacza, że odpowiedź została przerwana z przyczyn po stronie serwera, np. podczas aktualizacji, i możesz wysłać to samo żądanie ponownie bez zmian.- Inne kody, np.
timeout, są wymienione w dokumentacji API. Nieznany kod traktuj jak każdy inny błąd strumienia: poinformuj użytkownika i pozwól mu zapytać ponownie.
Pozwól użytkownikom oceniać odpowiedzi
Każda odpowiedź ma question_id. Wyślij ocenę użytkownika do
POST /v1/questions/{question_id}/feedback: 1 – pomocna, -1 –
niepomocna, z opcjonalnymi reason i note. Oceny pomagają organizacji zobaczyć, które karty wymagają pracy.
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."}'Lista kontrolna bota produkcyjnego
- Klucz tylko z
searchiask, ograniczony do kolekcji, które mogą widzieć odbiorcy bota. request_iddla każdego pytania i powyższa zasada wznawiania.- Limit czasu odczytu strumienia dłuższy niż 15 sekund, aby komentarze
: pingpodtrzymywały połączenie. - Odpowiedzi renderowane jako Markdown, z odwołaniami połączonymi ze źródłami.
- Jasny komunikat, gdy ustawione jest
empty, i możliwość kontaktu z człowiekiem. - Obsługa
429przez odczekanie czasu zRetry-After. - Odsyłane oceny, aby baza wiedzy z czasem stawała się lepsza.