In diesem Beispiel erstellen wir eine Vollstapelanwendung, die von Pinecone mit Abruf Augmented Generation (RAG) verwendet wird, um genaue und kontextbezogene Antworten in einem Chatbot zu liefern.
RAG ist ein leistungsstarkes Tool, das die Vorteile von retrievalbasierten Modellen und generativen Modellen kombiniert. Im Gegensatz zu herkömmlichen Chatbots, die mit der Aufrechterhaltung aktueller Informationen oder mit dem Zugriff auf domänenspezifisches Wissen zu kämpfen haben, verwendet ein auf RAG-basierter Chatbot eine aus krabbelten URLs erstellte Wissensbasis, um kontextbezogene Antworten zu liefern.
Durch die Einbeziehung von Vercels AI SDK in unsere Anwendung können wir den Chatbot -Workflow einfach einrichten und das Streaming effizienter nutzen, insbesondere in Randumgebungen, wodurch die Reaktionsfähigkeit und Leistung unseres Chatbots verbessert wird.
Am Ende dieses Tutorials haben Sie einen kontextbewussten Chatbot, der genaue Antworten ohne Halluzination liefert und eine effektivere und ansprechendere Benutzererfahrung gewährleistet. Beginnen wir mit dem Erstellen dieses leistungsstarken Tools (vollständige Codeauflistung).
Next.js ist ein leistungsstarkes JavaScript-Framework, mit dem wir serverseitige und statische Webanwendungen mit React erstellen können. Dies ist eine gute Wahl für unser Projekt, da es sich um eine einfache Einrichtung, eine hervorragende Leistung und integrierte Funktionen wie Routing- und API-Routen handelt.
Führen Sie den folgenden Befehl aus, um eine neue Next.js -App zu erstellen:
npx create-next-app chatbot Als nächstes fügen wir das ai -Paket hinzu:
npm install aiSie können die vollständige Liste der Abhängigkeiten verwenden, wenn Sie zusammen mit dem Tutorial zusammenarbeiten möchten.
In diesem Schritt werden wir das Vercel SDK verwenden, um das Backend und Frontend unseres Chatbots in der nächsten Anwendung zu ermitteln. Am Ende dieses Schritts wird unser grundlegender Chatbot in Betrieb sein, und es ist bereit, in den folgenden Phasen kontextbezogene Funktionen hinzuzufügen. Fangen wir an.
Konzentrieren wir uns nun auf die Frontend -Komponente unseres Chatbots. Wir erstellen die benutzergerichteten Elemente unseres Bots und erstellen die Oberfläche, über die Benutzer mit unserer Anwendung interagieren. Dies beinhaltet das Erstellen des Designs und der Funktionalität der Chat -Schnittstelle in unserer nächsten Anwendung.
Zunächst erstellen wir die Chat -Komponente, die die Chat -Oberfläche rendert.
import React , { FormEvent , ChangeEvent } from "react" ;
import Messages from "./Messages" ;
import { Message } from "ai/react" ;
interface Chat {
input : string ;
handleInputChange : ( e : ChangeEvent < HTMLInputElement > ) => void ;
handleMessageSubmit : ( e : FormEvent < HTMLFormElement > ) => Promise < void > ;
messages : Message [ ] ;
}
const Chat : React . FC < Chat > = ( {
input ,
handleInputChange ,
handleMessageSubmit ,
messages ,
} ) => {
return (
< div id = "chat" className = "..." >
< Messages messages = { messages } / >
< >
< form onSubmit = { handleMessageSubmit } className = "..." >
< input
type = "text"
className = "..."
value = { input }
onChange = { handleInputChange }
/ >
< span className = "..." > Press ⮐ to send < / span >
< / form >
< / >
< / div >
) ;
} ;
export default Chat ; Diese Komponente zeigt die Liste der Nachrichten und das Eingabeformular an, damit der Benutzer Nachrichten senden kann. Die Messages zum Rendern der Chat -Nachrichten:
import { Message } from "ai" ;
import { useRef } from "react" ;
export default function Messages ( { messages } : { messages : Message [ ] } ) {
const messagesEndRef = useRef < HTMLDivElement | null > ( null ) ;
return (
< div className = "..." >
{ messages . map ( ( msg , index ) => (
< div
key = { index }
className = { ` ${
msg . role === "assistant" ? "text-green-300" : "text-blue-300"
} ... ` }
>
< div className = "..." > { msg . role === "assistant" ? "?" : "?" } < / div >
< div className = "..." > { msg . content } < / div >
< / div >
) ) }
< div ref = { messagesEndRef } / >
< / div >
) ;
} Unsere Page verwaltet den Status für die in der Chat -Komponente angezeigten Nachrichten:
"use client" ;
import Header from "@/components/Header" ;
import Chat from "@/components/Chat" ;
import { useChat } from "ai/react" ;
const Page : React . FC = ( ) => {
const [ context , setContext ] = useState < string [ ] | null > ( null ) ;
const { messages , input , handleInputChange , handleSubmit } = useChat ( ) ;
return (
< div className = "..." >
< Header className = "..." / >
< div className = "..." >
< Chat
input = { input }
handleInputChange = { handleInputChange }
handleMessageSubmit = { handleSubmit }
messages = { messages }
/ >
< / div >
< / div >
) ;
} ;
export default Page ; Der nützliche useChat -Hook verwaltet den Status für die in der Chat -Komponente angezeigten Nachrichten. Es wird:
Als nächstes richten wir den Chatbot -API -Endpunkt ein. Dies ist die serverseitige Komponente, die Anforderungen und Antworten für unseren Chatbot behandelt. Wir erstellen eine neue Datei namens api/chat/route.ts und fügen die folgenden Abhängigkeiten hinzu:
import { Configuration , OpenAIApi } from "openai-edge" ;
import { Message , OpenAIStream , StreamingTextResponse } from "ai" ; Die erste Abhängigkeit ist das openai-edge , das es einfacher macht, mit OpenAIs APIs in einer Kantenumgebung zu interagieren. Die zweite Abhängigkeit ist das ai -Paket, mit dem wir die Message und OpenAIStream -Typen definieren werden, mit der wir die Antwort von OpenAI zurück zum Client zurückstreamieren werden.
Als nächstes initialisieren Sie den OpenAI -Kunden:
// Create an OpenAI API client (that's edge friendly!)
const config = new Configuration ( {
apiKey : process . env . OPENAI_API_KEY ,
} ) ;
const openai = new OpenAIApi ( config ) ; Um diesen Endpunkt als Kantenfunktion zu definieren, definieren und exportieren wir die runtime
export const runtime = "edge" ;Als nächstes definieren wir den Endpoint -Handler:
export async function POST ( req : Request ) {
try {
const { messages } = await req . json ( ) ;
const prompt = [
{
role : "system" ,
content : `AI assistant is a brand new, powerful, human-like artificial intelligence.
The traits of AI include expert knowledge, helpfulness, cleverness, and articulateness.
AI is a well-behaved and well-mannered individual.
AI is always friendly, kind, and inspiring, and he is eager to provide vivid and thoughtful responses to the user.
AI has the sum of all knowledge in their brain, and is able to accurately answer nearly any question about any topic in conversation.
AI assistant is a big fan of Pinecone and Vercel.
` ,
} ,
] ;
// Ask OpenAI for a streaming chat completion given the prompt
const response = await openai . createChatCompletion ( {
model : "gpt-3.5-turbo" ,
stream : true ,
messages : [
... prompt ,
... messages . filter ( ( message : Message ) => message . role === "user" ) ,
] ,
} ) ;
// Convert the response into a friendly text-stream
const stream = OpenAIStream ( response ) ;
// Respond with the stream
return new StreamingTextResponse ( stream ) ;
} catch ( e ) {
throw e ;
}
} Hier dekonstruieren wir die Nachrichten aus dem Beitrag und erstellen unsere erste Eingabeaufforderung. Wir verwenden die Eingabeaufforderung und die Nachrichten als Eingabe für die Methode createChatCompletion . Wir konvertieren dann die Antwort in einen Stream und geben sie an den Client zurück. Beachten Sie, dass wir in diesem Beispiel nur die Nachrichten des Benutzers an OpenAI senden (im Gegensatz zu den Nachrichten des Bots).
Während wir in den Aufbau unseres Chatbots eintauchen, ist es wichtig, die Rolle des Kontextes zu verstehen. Das Hinzufügen von Kontext zu den Antworten unseres Chatbot ist der Schlüssel, um ein natürlicheres, konversierendes Benutzererlebnis zu schaffen. Ohne Kontext können sich die Antworten eines Chatbots unzusammenhängend oder irrelevant anfühlen. Durch das Verständnis des Kontextes der Abfrage eines Benutzers kann unser Chatbot genauere, relevantere und ansprechende Antworten liefern. Lassen Sie uns nun mit diesem Ziel aufbauen.
Zunächst konzentrieren wir uns zunächst darauf, die Wissensbasis zu säen. Wir werden einen Crawler und ein Samenskript erstellen und einen Crawl -Endpunkt einrichten. Auf diese Weise können wir die Informationen sammeln und organisieren, die unser Chatbot verwenden wird, um kontextbezogene Antworten zu liefern.
Nachdem wir unsere Wissensbasis besiedelt haben, werden wir Übereinstimmungen aus unseren Einbettungen abrufen. Dadurch können unser Chatbot relevante Informationen basierend auf Benutzeranfragen finden.
Als nächstes wickeln wir unsere Logik in die GetContext -Funktion ein und aktualisieren die Eingabeaufforderung unseres Chatbot. Dadurch wird unser Code optimiert und die Benutzererfahrung verbessert, indem sichergestellt wird, dass die Eingabeaufforderungen des Chatbot relevant und engagiert sind.
Schließlich fügen wir ein Kontextbereich und einen zugehörigen Kontextendpunkt hinzu. Diese bieten eine Benutzeroberfläche für den Chatbot und eine Möglichkeit, den erforderlichen Kontext für jede Benutzerabfrage abzurufen.
In diesem Schritt geht es darum, unseren Chatbot die von ihm benötigten Informationen zu füttern und die erforderliche Infrastruktur für sie zum Abrufen und Nutzen dieser Informationen effektiv einzurichten. Fangen wir an.
Jetzt werden wir die Wissensbasis, die grundlegende Datenquelle, die die Antworten unseres Chatbots beeinflusst, übergehen. Dieser Schritt beinhaltet das Sammeln und Organisieren der Informationen, die unser Chatbot benötigt, um effektiv zu arbeiten. In diesem Leitfaden verwenden wir Daten, die von verschiedenen Websites abgerufen werden, über die wir später Fragen stellen können. Dazu werden wir einen Crawler erstellen, der die Daten von den Websites abkratzt, sie einbettet und in Tinecone speichern wird.
Aus Gründen der Kürze können Sie hier den vollständigen Code für den Crawler finden. Hier sind die relevanten Teile:
class Crawler {
private seen = new Set < string > ( ) ;
private pages : Page [ ] = [ ] ;
private queue : { url : string ; depth : number } [ ] = [ ] ;
constructor ( private maxDepth = 2 , private maxPages = 1 ) { }
async crawl ( startUrl : string ) : Promise < Page [ ] > {
// Add the start URL to the queue
this . addToQueue ( startUrl ) ;
// While there are URLs in the queue and we haven't reached the maximum number of pages...
while ( this . shouldContinueCrawling ( ) ) {
// Dequeue the next URL and depth
const { url , depth } = this . queue . shift ( ) ! ;
// If the depth is too great or we've already seen this URL, skip it
if ( this . isTooDeep ( depth ) || this . isAlreadySeen ( url ) ) continue ;
// Add the URL to the set of seen URLs
this . seen . add ( url ) ;
// Fetch the page HTML
const html = await this . fetchPage ( url ) ;
// Parse the HTML and add the page to the list of crawled pages
this . pages . push ( { url , content : this . parseHtml ( html ) } ) ;
// Extract new URLs from the page HTML and add them to the queue
this . addNewUrlsToQueue ( this . extractUrls ( html , url ) , depth ) ;
}
// Return the list of crawled pages
return this . pages ;
}
// ... Some private methods removed for brevity
private async fetchPage ( url : string ) : Promise < string > {
try {
const response = await fetch ( url ) ;
return await response . text ( ) ;
} catch ( error ) {
console . error ( `Failed to fetch ${ url } : ${ error } ` ) ;
return "" ;
}
}
private parseHtml ( html : string ) : string {
const $ = cheerio . load ( html ) ;
$ ( "a" ) . removeAttr ( "href" ) ;
return NodeHtmlMarkdown . translate ( $ . html ( ) ) ;
}
private extractUrls ( html : string , baseUrl : string ) : string [ ] {
const $ = cheerio . load ( html ) ;
const relativeUrls = $ ( "a" )
. map ( ( _ , link ) => $ ( link ) . attr ( "href" ) )
. get ( ) as string [ ] ;
return relativeUrls . map (
( relativeUrl ) => new URL ( relativeUrl , baseUrl ) . href
) ;
}
} Die Crawler -Klasse ist ein Web -Crawler, der URLs besucht, ab einem bestimmten Punkt, und sammelt Informationen von ihnen. Es arbeitet innerhalb einer bestimmten Tiefe und einer maximalen Anzahl von Seiten, die im Konstruktor definiert sind. Die Crawl -Methode ist die Kernfunktion, die den Kriechprozess startet.
Die Helfer -Methoden fetchpage, Parsehtml und Extrakten verarbeiten jeweils den HTML -Inhalt einer Seite, analysiert das HTML, um Text zu extrahieren und alle URLs aus einer Seite zu extrahieren, die für das nächste Krabbeln in die Warteschlange gestellt werden soll. Die Klasse unterhält auch eine Aufzeichnung besuchter URLs, um eine Duplikation zu vermeiden.
seedUm die Dinge zusammenzubinden, werden wir eine Samenfunktion erstellen, die den Crawler verwendet, um die Wissensbasis zu säen. In diesem Teil des Codes initialisieren wir das Crawl und holen eine bestimmte URL, dann teilen Sie den Inhalt in Stücke und setzen schließlich die Stücke in Tinecone ein und indizieren sie schließlich.
async function seed ( url : string , limit : number , indexName : string , options : SeedOptions ) {
try {
// Initialize the Pinecone client
const pinecone = new Pinecone ( ) ;
// Destructure the options object
const { splittingMethod , chunkSize , chunkOverlap } = options ;
// Create a new Crawler with depth 1 and maximum pages as limit
const crawler = new Crawler ( 1 , limit || 100 ) ;
// Crawl the given URL and get the pages
const pages = await crawler . crawl ( url ) as Page [ ] ;
// Choose the appropriate document splitter based on the splitting method
const splitter : DocumentSplitter = splittingMethod === 'recursive' ?
new RecursiveCharacterTextSplitter ( { chunkSize , chunkOverlap } ) : new MarkdownTextSplitter ( { } ) ;
// Prepare documents by splitting the pages
const documents = await Promise . all ( pages . map ( page => prepareDocument ( page , splitter ) ) ) ;
// Create Pinecone index if it does not exist
const indexList = await pinecone . listIndexes ( ) ;
const indexExists = indexList . some ( index => index . name === indexName )
if ( ! indexExists ) {
await pinecone . createIndex ( {
name : indexName ,
dimension : 1536 ,
waitUntilReady : true ,
} ) ;
}
const index = pinecone . Index ( indexName )
// Get the vector embeddings for the documents
const vectors = await Promise . all ( documents . flat ( ) . map ( embedDocument ) ) ;
// Upsert vectors into the Pinecone index
await chunkedUpsert ( index ! , vectors , '' , 10 ) ;
// Return the first document
return documents [ 0 ] ;
} catch ( error ) {
console . error ( "Error seeding:" , error ) ;
throw error ;
}
}Um den Inhalt zu unterteilen, verwenden wir eine der folgenden Methoden:
RecursiveCharacterTextSplitter - Dieser Splitter spaltet den Text in Stücke einer bestimmten Größe und spaltet dann die Stücke rekursiv in kleinere Stücke, bis die Stücke erreicht ist. Diese Methode ist für lange Dokumente nützlich.MarkdownTextSplitter - Dieser Splitter spaltet den Text basierend auf Markdown -Headern in Stücke auf. Diese Methode ist nützlich für Dokumente, die bereits unter Verwendung von Markdown strukturiert sind. Der Vorteil dieser Methode besteht darin, dass sie das Dokument basierend auf den Headern aufteilt, was für unseren Chatbot nützlich ist, um die Struktur des Dokuments zu verstehen. Wir können davon ausgehen, dass jede Texteinheit unter einem Header eine intern kohärente Informationseinheit ist. Wenn der Benutzer eine Frage stellt, wird auch der abgerufene Kontext intern kohärent sein.crawl -Endpunkt hinzu " Der Endpunkt für den crawl -Endpunkt ist ziemlich einfach. Es ruft einfach die seed auf und gibt das Ergebnis zurück.
import seed from "./seed" ;
import { NextResponse } from "next/server" ;
export const runtime = "edge" ;
export async function POST ( req : Request ) {
const { url , options } = await req . json ( ) ;
try {
const documents = await seed ( url , 1 , process . env . PINECONE_INDEX ! , options ) ;
return NextResponse . json ( { success : true , documents } ) ;
} catch ( error ) {
return NextResponse . json ( { success : false , error : "Failed crawling" } ) ;
}
}Jetzt kann unser Backend eine bestimmte URL kriechen, den Inhalt einbetten und die Einbettungen in Tinecone indexieren. Der Endpunkt gibt alle Segmente auf der von uns kriechens abgerufenen Webseite zurück, sodass wir sie anzeigen können. Als nächstes schreiben wir eine Reihe von Funktionen, die den Kontext aus diesen Einbettungen herausbauen.
Um die relevantesten Dokumente aus dem Index abzurufen, verwenden wir die query im Pinecone SDK. Diese Funktion nimmt einen Vektor ein und gibt die ähnlichsten Vektoren aus dem Index zurück. Wir werden diese Funktion verwenden, um die relevantesten Dokumente aus dem Index abzurufen, wenn einige Einbettungen.
const getMatchesFromEmbeddings = async ( embeddings : number [ ] , topK : number , namespace : string ) : Promise < ScoredPineconeRecord < Metadata > [ ] > => {
// Obtain a client for Pinecone
const pinecone = new Pinecone ( ) ;
const indexName : string = process . env . PINECONE_INDEX || '' ;
if ( indexName === '' ) {
throw new Error ( 'PINECONE_INDEX environment variable not set' )
}
// Retrieve the list of indexes to check if expected index exists
const indexes = await pinecone . listIndexes ( )
if ( indexes . filter ( i => i . name === indexName ) . length !== 1 ) {
throw new Error ( `Index ${ indexName } does not exist` )
}
// Get the Pinecone index
const index = pinecone ! . Index < Metadata > ( indexName ) ;
// Get the namespace
const pineconeNamespace = index . namespace ( namespace ?? '' )
try {
// Query the index with the defined request
const queryResult = await pineconeNamespace . query ( {
vector : embeddings ,
topK ,
includeMetadata : true ,
} )
return queryResult . matches || [ ]
} catch ( e ) {
// Log the error and throw it
console . log ( "Error querying embeddings: " , e )
throw new Error ( `Error querying embeddings: ${ e } ` )
}
}Die Funktion nimmt Emetten, einen Topk -Parameter und einen Namespace auf und gibt die TOPK -Übereinstimmungen aus dem Pinecone -Index zurück. Es erhält zuerst einen Pinecone -Client, prüft, ob der gewünschte Index in der Liste der Indizes vorhanden ist, und wirft einen Fehler aus, wenn nicht. Dann wird der spezifische Tinecone -Index angezeigt. Die Funktion fragt dann den Pinecone -Index mit der definierten Anforderung ab und gibt die Übereinstimmungen zurück.
getContext ein Wir werden die Dinge in der getContext -Funktion zusammenschließen. Diese Funktion wird in einer message aufgenommen und den Kontext zurückgegeben - entweder in Stringform oder als Satz von ScoredVector .
export const getContext = async (
message : string ,
namespace : string ,
maxTokens = 3000 ,
minScore = 0.7 ,
getOnlyText = true
) : Promise < string | ScoredVector [ ] > => {
// Get the embeddings of the input message
const embedding = await getEmbeddings ( message ) ;
// Retrieve the matches for the embeddings from the specified namespace
const matches = await getMatchesFromEmbeddings ( embedding , 3 , namespace ) ;
// Filter out the matches that have a score lower than the minimum score
const qualifyingDocs = matches . filter ( ( m ) => m . score && m . score > minScore ) ;
// If the `getOnlyText` flag is false, we'll return the matches
if ( ! getOnlyText ) {
return qualifyingDocs ;
}
let docs = matches
? qualifyingDocs . map ( ( match ) => ( match . metadata as Metadata ) . chunk )
: [ ] ;
// Join all the chunks of text together, truncate to the maximum number of tokens, and return the result
return docs . join ( "n" ) . substring ( 0 , maxTokens ) ;
} ; Zurück in chat/route.ts fügen wir den Anruf hinzu, um getContext zu erhalten:
const { messages } = await req . json ( ) ;
// Get the last message
const lastMessage = messages [ messages . length - 1 ] ;
// Get the context from the last message
const context = await getContext ( lastMessage . content , "" ) ; Schließlich werden wir die Eingabeaufforderung aktualisieren, den von uns abgerufenen Kontext aus der getContext -Funktion einzuschließen.
const prompt = [
{
role : "system" ,
content : `AI assistant is a brand new, powerful, human-like artificial intelligence.
The traits of AI include expert knowledge, helpfulness, cleverness, and articulateness.
AI is a well-behaved and well-mannered individual.
AI is always friendly, kind, and inspiring, and he is eager to provide vivid and thoughtful responses to the user.
AI has the sum of all knowledge in their brain, and is able to accurately answer nearly any question about any topic in conversation.
AI assistant is a big fan of Pinecone and Vercel.
START CONTEXT BLOCK
${ context }
END OF CONTEXT BLOCK
AI assistant will take into account any CONTEXT BLOCK that is provided in a conversation.
If the context does not provide the answer to question, the AI assistant will say, "I'm sorry, but I don't know the answer to that question".
AI assistant will not apologize for previous responses, but instead will indicated new information was gained.
AI assistant will not invent anything that is not drawn directly from the context.
` ,
} ,
] ; In dieser Eingabeaufforderung haben wir einen START CONTEXT BLOCK und END OF CONTEXT BLOCK hinzugefügt, um anzugeben, wo der Kontext eingefügt werden soll. Wir haben auch eine Zeile hinzugefügt, um anzuzeigen, dass der AI -Assistent jeden Kontextblock berücksichtigt, der in einem Gespräch bereitgestellt wird.
Als nächstes müssen wir das Kontextfeld zur Chat -Benutzeroberfläche hinzufügen. Wir fügen eine neue Komponente namens Context (Vollcode) hinzu.
Wir möchten zulassen, dass die Schnittstelle angibt, welche Teile des abgerufenen Inhalts zur Generierung der Antwort verwendet wurden. Dazu werden wir einen weiteren Endpunkt hinzufügen, der denselben getContext nennt.
export async function POST ( req : Request ) {
try {
const { messages } = await req . json ( ) ;
const lastMessage =
messages . length > 1 ? messages [ messages . length - 1 ] : messages [ 0 ] ;
const context = ( await getContext (
lastMessage . content ,
"" ,
10000 ,
0.7 ,
false
) ) as ScoredPineconeRecord [ ] ;
return NextResponse . json ( { context } ) ;
} catch ( e ) {
console . log ( e ) ;
return NextResponse . error ( ) ;
}
}Immer wenn der Benutzer eine URL kriecht, zeigt das Kontextbereich alle Segmente der abgerufenen Webseite an. Immer wenn das Backend das Senden einer Nachricht abschließt, löst das Front -End einen Effekt aus, der diesen Kontext abholt:
useEffect ( ( ) => {
const getContext = async ( ) => {
const response = await fetch ( "/api/context" , {
method : "POST" ,
body : JSON . stringify ( {
messages ,
} ) ,
} ) ;
const { context } = await response . json ( ) ;
setContext ( context . map ( ( c : any ) => c . id ) ) ;
} ;
if ( gotMessages && messages . length >= prevMessagesLengthRef . current ) {
getContext ( ) ;
}
prevMessagesLengthRef . current = messages . length ;
} , [ messages , gotMessages ] ) ; Der Tinecone-Vercel-Starter verwendet Dramatiker für End-to-End-Tests.
Alle Tests durchführen:
npm run test:e2e
Wenn es standardmäßig ausführt, wird Dramatiker beim lokalen Ausführen, wenn Fehler auftreten, ausschließlich, welche Tests fehlgeschlagen sind und für welche Browser -Treiber.
Um den neuesten Testbericht lokal anzuzeigen, rennen Sie:
npm run test:show