Δημιουργία bot ερωτήσεων και απαντήσεων
Αυτός ο οδηγός δημιουργεί τον πυρήνα ενός bot που απαντά σε ερωτήσεις από τη βάση γνώσεών σας: σε ένα εργαλείο συνομιλίας, στο κέντρο βοήθειάς σας ή μέσα στο προϊόν σας. Ο Βοηθός AI βρίσκει τη γνώση που χρειάζεται και απαντά, παραπέμποντας στις κάρτες που χρησιμοποίησε· ο κώδικάς σας στέλνει την ερώτηση και δείχνει το αποτέλεσμα.
Χρειάζεστε ένα κλειδί με τις εμβέλειες search και ask. Η απάντηση χρησιμοποιεί μόνο τις
κάρτες που επιτρέπεται να διαβάζει το κλειδί, οπότε ο ρόλος, το προφίλ δεδομένων και οι συλλογές του κλειδιού
καθορίζουν για τι μπορεί να μιλήσει το bot. Σε ένα bot για πελάτες δώστε ένα κλειδί που βλέπει μόνο δημόσιες
συλλογές.
Τρεις τρόποι για να λάβετε απάντηση
| Τελικό σημείο | Επιστρέφει | Χρησιμοποιήστε το για |
|---|---|---|
POST /v1/search | Τα αποσπάσματα που ταιριάζουν, με το καλύτερο πρώτο. Χωρίς γραπτή απάντηση. | Ένα πεδίο αναζήτησης, «σχετικά άρθρα», τη δική σας κατάταξη. |
POST /v1/ask | Μια πλήρη απάντηση με τις πηγές της, σε μία απόκριση. | Bots που δημοσιεύουν ένα μήνυμα ανά απάντηση· εργασίες στον διακομιστή. |
POST /v1/chat | Την απάντηση ως ροή συμβάντων, καθώς γράφεται. | Περιβάλλοντα συνομιλίας, όπου ο χρήστης πρέπει να βλέπει την απάντηση να εμφανίζεται. |
Αναζήτηση
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"}'Κάθε αποτέλεσμα είναι ένα απόσπασμα μιας κάρτας: το doc_id και το doc_name της κάρτας, το heading της ενότητας,
το text του αποσπάσματος, οι σελίδες ή οι γραμμές πίνακα όπου βρέθηκε, καθώς και η εμπιστοσύνη και η επαλήθευση της
κάρτας.
Το collection επιλέγει τη συλλογή στην οποία γίνεται η αναζήτηση· χωρίς αυτό, χρησιμοποιείται η συλλογή με όνομα
default. Τα tags και card_ids περιορίζουν την αναζήτηση σε κάρτες με αυτές τις ετικέτες ή σε συγκεκριμένες
κάρτες.
Ερώτηση και πλήρης απάντηση
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 είναι σε Markdown. Οι παραπομπές του, όπως [1] ή [1, 3], αντιστοιχούν στο
ref των καταχωρίσεων του sources· συνδέστε την καθεμία με την κάρτα (doc_id), ώστε ο αναγνώστης να μπορεί να
ελέγξει την απάντηση.
Ένας αριθμός στο κείμενο που δεν ήταν δυνατό να επιβεβαιωθεί από τα έγγραφα μπορεί να εμφανιστεί ως σήμανση, όπως
⟦n:a1⟧. Το verification.items περιγράφει κάθε σήμανση: το marker, το reason και το κείμενο original. Αν ο
κώδικάς σας δεν χειρίζεται σημάνσεις, αντικαταστήστε την καθεμία με το κείμενο original.
Όταν δεν υπάρχει απάντηση
Η απόκριση είναι πάντα 200. Όταν ο Βοηθός δεν μπορεί να απαντήσει, το empty λέει γιατί, και το answer περιέχει
ένα σύντομο μήνυμα που μπορείτε να δείξετε:
empty | Σημασία |
|---|---|
no_hits | Τίποτα στη βάση γνώσεων δεν ταιριάζει με την ερώτηση. |
restricted | Το θέμα βρίσκεται σε κάρτες που το κλειδί δεν επιτρέπεται να διαβάσει. |
guardrail | Η ερώτηση είναι εκτός των θεμάτων στα οποία ο οργανισμός επιτρέπει στον Βοηθό να απαντά. |
no_data_for_period | Η ερώτηση αφορά μια περίοδο για την οποία δεν υπάρχουν έγγραφα. |
Το no_hits είναι χρήσιμο σήμα: δείχνει τι ρωτούν οι χρήστες σας που η βάση γνώσεων δεν καλύπτει ακόμη.
«Fast» και «Precise»
Περάστε "quality": "precise" για μια πιο ενδελεχή απάντηση σε μια δύσκολη ερώτηση· μια τέτοια απάντηση χρειάζεται περισσότερο χρόνο.
Η προεπιλογή, "fast", ταιριάζει στις περισσότερες ερωτήσεις.
Ροή της απάντησης
Σε ένα περιβάλλον συνομιλίας, δείχνετε την απάντηση καθώς γράφεται. Το POST /v1/chat με
"stream": true απαντά με text/event-stream: ένα συμβάν ανά μήνυμα, το καθένα με μια γραμμή id: (ο αριθμός του
στη ροή, από το 1) και μια γραμμή data: με JSON. Το πεδίο type του JSON λέει τι είναι το συμβάν.
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 | Τι να κάνετε |
|---|---|
meta | Η απάντηση ξεκίνησε. Κρατήστε το answer_id, αν καταγράφετε τις απαντήσεις. |
sources | Οι κάρτες στις οποίες βασίζεται η απάντηση. Μπορεί να έρθει περισσότερες από μία φορές· ισχύει το τελευταίο. |
token | Ένα κομμάτι του κειμένου της απάντησης. Προσθέστε το σε ό,τι δείχνετε. |
done | Η απάντηση ολοκληρώθηκε. Έχει τα ίδια πεδία με την απόκριση του /v1/ask. |
error | Η απάντηση σταμάτησε. Η ροή τελειώνει χωρίς done. Δείτε παρακάτω. |
followups | Μετά το done: προτεινόμενες επόμενες ερωτήσεις που μπορείτε να προσφέρετε ως κουμπιά. |
| οτιδήποτε άλλο | Παραλείψτε το. Νέοι τύποι συμβάντων μπορεί να εμφανιστούν ανά πάσα στιγμή. |
Μια γραμμή που ξεκινά με : (όπως το : ping, που στέλνεται κάθε 15 δευτερόλεπτα όσο ετοιμάζεται η απάντηση) είναι
σχόλιο. Παραλείψτε τη.
Το messages περιέχει τη συνομιλία μέχρι τώρα, τα παλαιότερα πρώτα, και τελειώνει με την ερώτηση του χρήστη. Στείλτε
τους τελευταίους γύρους, ώστε να γίνονται κατανοητές οι συμπληρωματικές ερωτήσεις, όπως «και για παραγγελίες από το
εξωτερικό;».
// 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;
}Χωρίς "stream": true, το POST /v1/chat επιστρέφει την πλήρη απάντηση ως JSON, όπως το /v1/ask.
Συνέχιση ροής που διακόπηκε
Οι συνδέσεις διακόπτονται: ένα κινητό δίκτυο αλλάζει, ένας διακομιστής μεσολάβησης λήγει, ο διακομιστής σας
επανεκκινείται. Δώστε σε κάθε ερώτηση ένα request_id και η απάντηση επιβιώνει της διακοπής:
- Με
request_id, η απάντηση ετοιμάζεται ανεξάρτητα από τη σύνδεση. Μια αποσύνδεση δεν τη σταματά: ολοκληρώνεται και αποθηκεύεται. - Για να συνεχίσετε, στείλτε ξανά το ίδιο αίτημα, με το ίδιο
request_id, και τον αριθμό του τελευταίου συμβάντος που λάβατε στην κεφαλίδαLast-Event-ID. - Χρησιμοποιείτε ένα
request_id(για παράδειγμα ένα UUID) ανά ερώτηση και το ίδιο σε κάθε επανάληψη αυτής της ερώτησης. Μια νέα ερώτηση παίρνει νέοrequest_id.
Τι λαμβάνει το επαναλαμβανόμενο αίτημα εξαρτάται από το πού βρίσκεται η απάντηση:
| Περίπτωση | Το επαναλαμβανόμενο αίτημα λαμβάνει |
|---|---|
| Η απάντηση γράφεται ακόμη ή μόλις ολοκληρώθηκε | Τα συμβάντα μετά το Last-Event-ID, με τους ίδιους αριθμούς: η ροή απλώς συνεχίζει. |
| Η απάντηση ολοκληρώθηκε πριν από μερικά λεπτά | Την αποθηκευμένη απάντηση ως ένα token με όλο το κείμενο και ένα done, με αρίθμηση από το 1. Η ερώτηση δεν υποβάλλεται ξανά. |
| Η ροή δεν μπορεί να συνεχίσει από αυτόν τον αριθμό | Την απάντηση από την αρχή, με αρίθμηση από το 1. |
Ο κώδικάς σας χειρίζεται και τις τρεις περιπτώσεις με έναν κανόνα: αν το πρώτο συμβάν που λαμβάνετε έχει αριθμό όχι
μεγαλύτερο από το Last-Event-ID, η απάντηση ξεκινά από την αρχή· καθαρίστε ό,τι δείξατε και δημιουργήστε το ξανά.
Διαφορετικά, προσθέστε στο τέλος.
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 μπορεί να εκτελείται σε πολλούς διακομιστές. Μια ροή συνεχίζει από το Last-Event-ID μόνο στον
διακομιστή που γράφει την απάντηση· αν το επαναλαμβανόμενο αίτημά σας φτάσει σε άλλον, λαμβάνει την αποθηκευμένη
απάντηση μόλις ολοκληρωθεί ή η απάντηση γράφεται ξανά από την αρχή. Ο παραπάνω κανόνας καλύπτει και τις δύο
περιπτώσεις, και η ερώτηση δεν μετριέται ποτέ δύο φορές.
Χωρίς request_id, το κλείσιμο της σύνδεσης ακυρώνει την απάντηση.
Σφάλματα σε μια ροή
Τα σφάλματα πριν ξεκινήσει η ροή είναι συνηθισμένα σφάλματα HTTP: 401, 403, 422 ή 429 με Retry-After, όταν ο
οργανισμός έχει εξαντλήσει το όριο ερωτήσεών του ή έχει ταυτόχρονα υπερβολικά πολλές
απαντήσεις σε εξέλιξη.
Μόλις ξεκινήσει η ροή, η κατάσταση HTTP της είναι πάντα 200, και ένα πρόβλημα φτάνει ως συμβάν error:
{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
- Το
retry: trueσημαίνει ότι η απάντηση σταμάτησε εξαιτίας του διακομιστή, για παράδειγμα κατά τη διάρκεια μιας ενημέρωσης, και μπορείτε να στείλετε ξανά το ίδιο αίτημα χωρίς αλλαγές. - Οι άλλοι κωδικοί, όπως το
timeout, αναφέρονται στην αναφορά API. Χειριστείτε έναν άγνωστο κωδικό όπως κάθε άλλο σφάλμα ροής: ενημερώστε τον χρήστη και αφήστε τον να ρωτήσει ξανά.
Αξιολόγηση απαντήσεων από τους χρήστες
Κάθε απάντηση έχει ένα question_id. Στείλτε την αξιολόγηση του χρήστη στο
POST /v1/questions/{question_id}/feedback: 1 για χρήσιμη, -1 για μη
χρήσιμη, με προαιρετικά reason και note. Οι αξιολογήσεις βοηθούν τον οργανισμό να δει ποιες κάρτες χρειάζονται
δουλειά.
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."}'Λίστα ελέγχου για bot σε παραγωγή
- Ένα κλειδί μόνο με
searchκαιask, περιορισμένο στις συλλογές που επιτρέπεται να βλέπει το κοινό του bot. - Ένα
request_idανά ερώτηση και ο παραπάνω κανόνας συνέχισης. - Ένα χρονικό όριο ανάγνωσης της ροής μεγαλύτερο από 15 δευτερόλεπτα, ώστε τα σχόλια
: pingνα την κρατούν ενεργή. - Απαντήσεις που αποδίδονται ως Markdown, με τις παραπομπές συνδεδεμένες με τις πηγές τους.
- Ένα σαφές μήνυμα όταν έχει οριστεί το
emptyκαι ένας τρόπος να επικοινωνήσει κανείς με έναν άνθρωπο. - Χειρισμός του
429με αναμονή όσο λέει τοRetry-After. - Αποστολή των αξιολογήσεων, ώστε η βάση γνώσεων να βελτιώνεται με τον καιρό.