Anar al contingut
Sursa Developers

Crear un bot de preguntes i respostes

Aquesta guia construeix el nucli d’un bot que respon preguntes a partir de la vostra base de coneixement: en una eina de xat, al vostre centre d’ajuda o dins del vostre producte. L’assistent troba el coneixement necessari i respon, citant les entrades que ha fet servir; el vostre codi envia la pregunta i mostra el resultat.

Necessiteu una clau amb els àmbits search i ask. La resposta només fa servir les entrades que la clau pot llegir, de manera que el rol, el perfil de dades i les col·leccions de la clau decideixen de què pot parlar el bot. A un bot per a clients, doneu-li una clau que només vegi col·leccions públiques.

Tres maneres d’obtenir una resposta

Punt finalRetornaFeu-lo servir per a
POST /v1/searchEls extractes que coincideixen, primer els més adequats. Sense resposta redactada.Un quadre de cerca, «articles relacionats», una ordenació pròpia.
POST /v1/askUna resposta completa amb les seves fonts, d’un sol cop.Bots que publiquen un missatge per resposta; treballs del vostre servidor.
POST /v1/chatLa resposta com un flux d’esdeveniments, mentre s’escriu.Interfícies de xat, en què l’usuari ha de veure com apareix la resposta.
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"}'

Cada resultat és un extracte d’una entrada: doc_id i doc_name de l’entrada, heading de la secció, text de l’extracte, les pàgines o les files de taula on s’ha trobat, i la confiança i la verificació de l’entrada.

collection selecciona la col·lecció on es cerca; sense aquest paràmetre, es fa servir la col·lecció amb el nom default. tags i card_ids restringeixen la cerca a les entrades amb aquestes etiquetes o a entrades concretes.

Preguntar i obtenir una resposta completa

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"}'

El text de la resposta a answer és Markdown. Les seves referències, com ara [1] o [1, 3], corresponen al ref dels elements de sources; enllaceu cadascuna amb l’entrada (doc_id) perquè qui llegeix pugui comprovar la resposta.

Un nombre del text que no s’ha pogut confirmar amb els documents pot aparèixer com un marcador, com ara ⟦n:a1⟧. verification.items descriu cada marcador: el seu marker, el reason i el text original. Si el vostre codi no gestiona marcadors, substituïu cadascun pel seu text original.

Quan no hi ha resposta

La resposta sempre és 200. Quan l’assistent no pot respondre, empty indica per què, i answer conté un missatge breu que podeu mostrar:

emptySignificat
no_hitsNo hi ha res a la base de coneixement que coincideixi amb la pregunta.
restrictedEl tema és en entrades que la clau no pot llegir.
guardrailLa pregunta queda fora del que l’organització permet que respongui l’assistent.
no_data_for_periodLa pregunta es refereix a un període per al qual no hi ha documents.

no_hits és un senyal útil: mostra què pregunten els vostres usuaris que la base de coneixement encara no cobreix.

Respostes «Fast» (ràpida) i «Precise» (precisa)

Passeu "quality": "precise" per obtenir una resposta més exhaustiva a una pregunta difícil, a canvi de més temps. El valor per defecte, "fast", convé a la majoria de preguntes.

Transmetre la resposta en temps real

En una interfície de xat, mostreu la resposta mentre s’escriu. POST /v1/chat amb "stream": true respon amb text/event-stream: un esdeveniment per missatge, cadascun amb una línia id: (el seu número dins del flux, a partir d’1) i una línia data: amb JSON. El camp type del JSON indica quin esdeveniment és.

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":[…]}
typeQuè cal fer
metaLa resposta ha començat. Conserveu answer_id si registreu les respostes.
sourcesLes entrades en què es basa la resposta. Pot arribar més d’una vegada; val l’última.
tokenUn tros del text de la resposta. Afegiu-lo al que mostreu.
doneLa resposta és completa. Té els mateixos camps que la resposta de /v1/ask.
errorLa resposta s’ha aturat. El flux acaba sense done. Vegeu més avall.
followupsDesprés de done: preguntes de seguiment suggerides que podeu oferir com a botons.
qualsevol altreOmeteu-lo. Poden aparèixer tipus d’esdeveniment nous en qualsevol moment.

Una línia que comença per : (com ara : ping, que s’envia cada 15 segons mentre es prepara la resposta) és un comentari. Ometeu-la.

messages conté la conversa fins ara, de més antic a més recent, i acaba amb la pregunta de l’usuari. Envieu els últims torns perquè s’entenguin les preguntes de seguiment, com ara «i per a les comandes de l’estranger?».

// 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;
}

Sense "stream": true, POST /v1/chat retorna la resposta completa en JSON, com /v1/ask.

Reprendre un flux interromput

Les connexions es tallen: canvia la xarxa mòbil, un servidor intermediari esgota el temps d’espera, el vostre servidor es reinicia. Doneu un request_id a cada pregunta i la resposta sobreviurà al tall:

  • Amb request_id, la resposta es prepara independentment de la connexió. Una desconnexió no l’atura: es completa i es desa.
  • Per reprendre-la, torneu a enviar la mateixa sol·licitud, amb el mateix request_id, i el número de l’últim esdeveniment que heu rebut a la capçalera Last-Event-ID.
  • Feu servir un request_id (per exemple, un UUID) per pregunta, i el mateix en tots els reintents d’aquesta pregunta. Una pregunta nova rep un request_id nou.

El que rep la sol·licitud repetida depèn de l’estat de la resposta:

SituacióLa sol·licitud repetida rep
La resposta encara s’està escrivint o acaba de finalitzarEls esdeveniments posteriors a Last-Event-ID, amb els mateixos números: el flux simplement continua.
La resposta va acabar fa uns minutsLa resposta desada com un token amb tot el text i un done, numerats a partir d’1. La pregunta no es torna a fer.
El flux no pot continuar des d’aquell númeroLa resposta des del principi, numerada a partir d’1.

El vostre codi gestiona els tres casos amb una sola regla: si el primer esdeveniment que rebeu té un número que no supera Last-Event-ID, la resposta torna a començar; esborreu el que heu mostrat i torneu-ho a construir. Si no, afegiu-hi el text nou.

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));
    }
  }
}

L’API de Sursa es pot executar en diversos servidors. Un flux continua des de Last-Event-ID només al servidor que escriu la resposta; si la vostra sol·licitud repetida arriba a un altre, rep la resposta desada un cop ha acabat, o la resposta es torna a escriure des del principi. La regla anterior cobreix tots dos casos, i la pregunta no es compta mai dues vegades.

Sense request_id, tancar la connexió cancel·la la resposta.

Errors en un flux

Els errors anteriors a l’inici del flux són errors HTTP normals: 401, 403, 422, o 429 amb Retry-After quan l’organització ha esgotat el seu límit de preguntes o té massa respostes en curs alhora.

Un cop ha començat el flux, el seu estat HTTP sempre és 200, i un problema arriba com un esdeveniment error:

{"type": "error", "code": "server_restart", "retry": true, "message": "…", "answer_id": "…"}
  • retry: true vol dir que la resposta s’ha aturat per causa del servidor, per exemple durant una actualització, i que podeu tornar a enviar la mateixa sol·licitud sense canvis.
  • Els altres codis, com ara timeout, figuren a la referència de l’API. Tracteu un codi desconegut com qualsevol altre error del flux: informeu-ne l’usuari i deixeu-lo tornar a preguntar.

Permetre que els usuaris valorin les respostes

Cada resposta té un question_id. Envieu la valoració de l’usuari a POST /v1/questions/{question_id}/feedback: 1 per a útil, -1 per a no útil, amb un reason i una note opcionals. Les valoracions ajuden l’organització a veure quines entrades cal millorar.

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."}'

Llista de comprovació per a un bot en producció

  • Una clau amb només search i ask, limitada a les col·leccions que pot veure el públic del bot.
  • Un request_id per pregunta i la regla de represa anterior.
  • Un temps d’espera de lectura del flux superior a 15 segons, perquè els comentaris : ping el mantinguin actiu.
  • El Markdown de les respostes mostrat amb format, amb les referències enllaçades a les seves fonts.
  • Un missatge clar quan empty té valor, i una manera de contactar amb una persona.
  • 429 gestionat esperant el temps de Retry-After.
  • Valoracions enviades de tornada, perquè la base de coneixement millori amb el temps.