Membuat bot tanya jawab
Panduan ini membangun inti bot yang menjawab pertanyaan dari basis pengetahuan Anda: di alat chat, di pusat bantuan Anda, atau di dalam produk Anda. Asisten menemukan pengetahuan yang relevan dan menjawab, sambil merujuk kartu yang dipakainya; kode Anda mengirim pertanyaan dan menampilkan hasilnya.
Anda membutuhkan kunci dengan cakupan search dan ask. Jawaban hanya memakai kartu
yang boleh dibaca oleh kunci, jadi peran, profil data, dan koleksi kunci menentukan apa yang dapat dibicarakan bot.
Berikan bot untuk pelanggan kunci yang hanya melihat koleksi publik.
Tiga cara mendapatkan jawaban
| Endpoint | Mengembalikan | Pakai untuk |
|---|---|---|
POST /v1/search | Petikan yang cocok, yang terbaik lebih dulu. Tanpa jawaban tertulis. | Kotak pencarian, “artikel terkait”, pemeringkatan Anda sendiri. |
POST /v1/ask | Jawaban lengkap beserta sumbernya, dalam satu respons. | Bot yang memposting satu pesan per jawaban; job back-end. |
POST /v1/chat | Jawaban sebagai stream peristiwa, selagi ditulis. | Antarmuka chat, tempat pengguna sebaiknya melihat jawaban muncul. |
Pencarian
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"}'Setiap hasil adalah petikan dari sebuah kartu: doc_id dan doc_name kartu, heading bagian, text petikan,
halaman atau baris tabel tempat petikan itu ditemukan, serta kepercayaan dan verifikasi kartu.
collection memilih koleksi tempat pencarian dilakukan; tanpa parameter itu, koleksi yang bernama default dipakai.
tags dan card_ids mempersempit pencarian ke kartu dengan tag tersebut atau ke kartu tertentu.
Bertanya dan mendapatkan jawaban lengkap
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"}'Teks jawaban di answer berupa Markdown. Rujukannya, seperti [1] atau [1, 3], sesuai dengan ref dari entri di
sources; tautkan masing-masing ke kartunya (doc_id) agar pembaca dapat memeriksa jawabannya.
Angka dalam teks yang tidak dapat dikonfirmasi terhadap dokumen dapat muncul sebagai penanda seperti ⟦n:a1⟧.
verification.items menjelaskan setiap penanda: marker, reason, dan teks original-nya. Jika kode Anda tidak
menangani penanda, ganti masing-masing dengan teks original-nya.
Saat tidak ada jawaban
Responsnya selalu 200. Jika asisten tidak dapat menjawab, empty menyebutkan alasannya, dan answer berisi pesan
singkat yang dapat Anda tampilkan:
empty | Artinya |
|---|---|
no_hits | Tidak ada apa pun di basis pengetahuan yang cocok dengan pertanyaan. |
restricted | Topiknya ada di kartu yang tidak boleh dibaca oleh kunci. |
guardrail | Pertanyaan berada di luar hal yang diizinkan organisasi untuk dijawab asisten. |
no_data_for_period | Pertanyaan menanyakan periode yang tidak memiliki dokumen. |
no_hits adalah sinyal yang berguna: sinyal ini menunjukkan apa yang ditanyakan pengguna Anda yang belum dicakup
basis pengetahuan.
Fast dan Precise
Berikan "quality": "precise" untuk jawaban yang lebih menyeluruh atas pertanyaan yang sulit, dengan konsekuensi waktu
yang lebih lama. Default-nya, "fast", cocok untuk sebagian besar pertanyaan.
Mengalirkan jawaban
Di antarmuka chat, tampilkan jawaban selagi ditulis. POST /v1/chat dengan
"stream": true merespons dengan text/event-stream: satu peristiwa per pesan, masing-masing dengan baris id:
(nomornya dalam stream, mulai dari 1) dan baris data: berisi JSON. Field type dari JSON itu menyebutkan jenis
peristiwanya.
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 | Yang perlu dilakukan |
|---|---|
meta | Jawaban sudah dimulai. Simpan answer_id jika Anda mencatat jawaban. |
sources | Kartu yang menjadi dasar jawaban. Dapat datang lebih dari sekali; yang terakhir yang berlaku. |
token | Sepenggal teks jawaban. Tambahkan ke apa yang Anda tampilkan. |
done | Jawaban sudah lengkap. Memiliki field yang sama dengan respons /v1/ask. |
error | Jawaban berhenti. Stream berakhir tanpa done. Lihat di bawah. |
followups | Setelah done: saran pertanyaan berikutnya yang dapat Anda tawarkan sebagai tombol. |
| lainnya | Lewati. Jenis peristiwa baru dapat muncul kapan saja. |
Baris yang diawali : (seperti : ping, yang dikirim setiap 15 detik selama jawaban sedang disiapkan) adalah
komentar. Lewati baris itu.
messages berisi percakapan sejauh ini, yang terlama lebih dulu, diakhiri dengan pertanyaan pengguna. Kirim beberapa
giliran terakhir agar pertanyaan lanjutan seperti “dan untuk pesanan dari luar negeri?” dapat dipahami.
// 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;
}Tanpa "stream": true, POST /v1/chat mengembalikan jawaban lengkap sebagai JSON, seperti /v1/ask.
Melanjutkan stream yang terputus
Koneksi bisa putus: jaringan seluler berganti, proxy terkena timeout, server Anda dimulai ulang. Berikan setiap
pertanyaan request_id, dan jawabannya bertahan dari putusnya koneksi:
- Dengan
request_id, jawaban disiapkan terlepas dari koneksi. Koneksi yang terputus tidak menghentikannya: jawaban diselesaikan dan disimpan. - Untuk melanjutkan, kirim lagi permintaan yang sama, dengan
request_idyang sama, dan nomor peristiwa terakhir yang Anda terima di headerLast-Event-ID. - Pakai satu
request_id(misalnya UUID) per pertanyaan, dan yang sama di setiap percobaan ulang pertanyaan itu. Pertanyaan baru mendapatrequest_idbaru.
Apa yang diterima permintaan yang diulang bergantung pada keadaan jawabannya:
| Situasi | Permintaan yang diulang menerima |
|---|---|
| Jawaban masih ditulis, atau baru saja selesai | Peristiwa setelah Last-Event-ID, dengan nomor yang sama: stream cukup berlanjut. |
| Jawaban sudah selesai beberapa menit yang lalu | Jawaban yang tersimpan sebagai satu token berisi seluruh teks dan satu done, bernomor mulai dari 1. Pertanyaan tidak diajukan lagi. |
| Stream tidak dapat berlanjut dari nomor itu | Jawaban dari awal, bernomor mulai dari 1. |
Kode Anda menangani ketiga kasus dengan satu aturan: jika peristiwa pertama yang Anda terima memiliki nomor yang
tidak lebih besar daripada Last-Event-ID, jawaban dimulai dari awal; hapus apa yang sudah Anda tampilkan dan bangun
lagi. Jika tidak, tambahkan.
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));
}
}
}API Sursa dapat berjalan di beberapa server. Stream berlanjut dari Last-Event-ID hanya di server yang sedang menulis
jawaban; jika permintaan yang diulang sampai ke server lain, permintaan itu menerima jawaban yang tersimpan setelah
jawaban selesai, atau jawaban ditulis lagi dari awal. Aturan di atas mencakup kedua kasus, dan pertanyaan tidak pernah
dihitung dua kali.
Tanpa request_id, menutup koneksi akan membatalkan jawaban.
Error dalam stream
Error sebelum stream dimulai adalah error HTTP biasa: 401, 403, 422, atau 429 dengan Retry-After saat
organisasi sudah memakai habis batas pertanyaannya atau memiliki terlalu banyak
jawaban yang sedang diproses sekaligus.
Setelah stream dimulai, status HTTP-nya selalu 200, dan masalah datang sebagai peristiwa error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
retry: trueberarti jawaban berhenti karena server, misalnya saat pembaruan, dan Anda dapat mengirim lagi permintaan yang sama tanpa perubahan.- Kode lain, seperti
timeout, dicantumkan di referensi API. Perlakukan kode yang tidak dikenal seperti error stream lainnya: beri tahu pengguna, dan biarkan mereka bertanya lagi.
Memungkinkan pengguna menilai jawaban
Setiap jawaban memiliki question_id. Kirim penilaian pengguna ke
POST /v1/questions/{question_id}/feedback: 1 untuk membantu, -1 untuk
tidak membantu, dengan reason dan note opsional. Penilaian membantu organisasi melihat kartu mana yang perlu
diperbaiki.
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."}'Daftar periksa untuk bot produksi
- Kunci yang hanya memiliki
searchdanask, dibatasi pada koleksi yang boleh dilihat audiens bot. request_idper pertanyaan, dan aturan melanjutkan di atas.- Timeout pembacaan stream yang lebih lama dari 15 detik, agar komentar
: pingmenjaganya tetap hidup. - Jawaban dirender sebagai Markdown, dengan rujukan yang ditautkan ke sumbernya.
- Pesan yang jelas saat
emptydiisi, dan cara untuk menghubungi seseorang. 429ditangani dengan menunggu selamaRetry-After.- Penilaian dikirim kembali, agar basis pengetahuan makin baik dari waktu ke waktu.