Dalam contoh ini, kami akan membangun aplikasi full-stack yang menggunakan pengambilan augmented generasi (RAG) yang ditenagai oleh Pinecone untuk memberikan respons yang akurat dan relevan secara kontekstual dalam chatbot.
Rag adalah alat yang kuat yang menggabungkan manfaat dari model berbasis pengambilan dan model generatif. Tidak seperti chatbots tradisional yang dapat berjuang dengan mempertahankan informasi terkini atau mengakses pengetahuan khusus domain, chatbot berbasis RAG menggunakan basis pengetahuan yang dibuat dari URL merangkak untuk memberikan respons yang relevan secara kontekstual.
Memasukkan AI SDK Vercel ke dalam aplikasi kami akan memungkinkan kami dengan mudah mengatur alur kerja chatbot dan memanfaatkan streaming secara lebih efisien, terutama di lingkungan tepi, meningkatkan respons dan kinerja chatbot kami.
Pada akhir tutorial ini, Anda akan memiliki chatbot sadar konteks yang memberikan respons yang akurat tanpa halusinasi, memastikan pengalaman pengguna yang lebih efektif dan menarik. Mari kita mulai membangun alat yang kuat ini (daftar kode lengkap).
Next.js adalah kerangka kerja JavaScript yang kuat yang memungkinkan kami untuk membangun aplikasi web sisi server yang diberikan dan statis menggunakan React. Ini adalah pilihan yang tepat untuk proyek kami karena kemudahan pengaturan, kinerja yang sangat baik, dan fitur bawaan seperti rute dan rute API.
Untuk membuat aplikasi Next.js baru, jalankan perintah berikut:
npx create-next-app chatbot Selanjutnya, kami akan menambahkan paket ai :
npm install aiAnda dapat menggunakan daftar lengkap dependensi jika Anda ingin membangun bersama dengan tutorial.
Pada langkah ini, kita akan menggunakan Vercel SDK untuk membuat backend dan frontend chatbot kita di dalam aplikasi Next.js. Pada akhir langkah ini, chatbot dasar kami akan berjalan dan berjalan, siap bagi kami untuk menambahkan kemampuan sadar konteks pada tahap berikut. Mari kita mulai.
Sekarang, mari kita fokus pada komponen frontend chatbot kita. Kami akan membangun elemen yang menghadap pengguna dari bot kami, membuat antarmuka di mana pengguna akan berinteraksi dengan aplikasi kami. Ini akan melibatkan kerajinan desain dan fungsionalitas antarmuka obrolan dalam aplikasi Next.js kami.
Pertama, kami akan membuat komponen Chat , yang akan membuat antarmuka obrolan.
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 ; Komponen ini akan menampilkan daftar pesan dan formulir input untuk pengguna untuk mengirim pesan. Komponen Messages untuk membuat pesan obrolan:
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 >
) ;
} Komponen Page utama kami akan mengelola status untuk pesan yang ditampilkan di komponen Chat :
"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 ; Kait useChat yang berguna akan mengelola status untuk pesan yang ditampilkan dalam komponen Chat . Itu akan:
Selanjutnya, kami akan mengatur titik akhir chatbot API. Ini adalah komponen sisi server yang akan menangani permintaan dan tanggapan untuk chatbot kami. Kami akan membuat file baru yang disebut api/chat/route.ts dan menambahkan dependensi berikut:
import { Configuration , OpenAIApi } from "openai-edge" ;
import { Message , OpenAIStream , StreamingTextResponse } from "ai" ; Ketergantungan pertama adalah paket openai-edge yang membuatnya lebih mudah untuk berinteraksi dengan API Openai di lingkungan tepi. Ketergantungan kedua adalah paket ai yang akan kami gunakan untuk menentukan jenis Message dan OpenAIStream , yang akan kami gunakan untuk mengalirkan kembali respons dari OpenAi kembali ke klien.
Selanjutnya inisialisasi klien openai:
// Create an OpenAI API client (that's edge friendly!)
const config = new Configuration ( {
apiKey : process . env . OPENAI_API_KEY ,
} ) ;
const openai = new OpenAIApi ( config ) ; Untuk mendefinisikan titik akhir ini sebagai fungsi tepi, kami akan mendefinisikan dan mengekspor variabel runtime
export const runtime = "edge" ;Selanjutnya, kami akan mendefinisikan penangan titik akhir:
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 ;
}
} Di sini kami mendekonstruksi pesan dari pos, dan membuat prompt awal kami. Kami menggunakan prompt dan pesan sebagai input ke metode createChatCompletion . Kami kemudian mengubah respons menjadi aliran dan mengembalikannya ke klien. Perhatikan bahwa dalam contoh ini, kami hanya mengirim pesan pengguna ke OpenAi (sebagai lawan termasuk pesan bot juga).
Saat kami menyelam untuk membangun chatbot kami, penting untuk memahami peran konteks. Menambahkan konteks ke tanggapan chatbot kami adalah kunci untuk menciptakan pengalaman pengguna yang lebih alami dan percakapan. Tanpa konteks, tanggapan chatbot dapat terasa terputus -putus atau tidak relevan. Dengan memahami konteks kueri pengguna, chatbot kami akan dapat memberikan tanggapan yang lebih akurat, relevan, dan menarik. Sekarang, mari kita mulai membangun dengan tujuan ini.
Pertama, pertama -tama kita akan fokus pada penyemaian basis pengetahuan. Kami akan membuat crawler dan skrip benih, dan mengatur titik akhir merangkak. Ini akan memungkinkan kami untuk mengumpulkan dan mengatur informasi yang akan digunakan chatbot kami untuk memberikan tanggapan yang relevan secara kontekstual.
Setelah kami mengisi basis pengetahuan kami, kami akan mengambil kecocokan dari embeddings kami. Ini akan memungkinkan chatbot kami untuk menemukan informasi yang relevan berdasarkan kueri pengguna.
Selanjutnya, kami akan membungkus logika kami ke fungsi GetContext dan memperbarui prompt chatbot kami. Ini akan merampingkan kode kami dan meningkatkan pengalaman pengguna dengan memastikan petunjuk chatbot relevan dan menarik.
Akhirnya, kami akan menambahkan panel konteks dan titik akhir konteks terkait. Ini akan menyediakan antarmuka pengguna untuk chatbot dan cara untuk mengambil konteks yang diperlukan untuk setiap kueri pengguna.
Langkah ini adalah semua tentang memberi makan chatbot kami informasi yang dibutuhkan dan menyiapkan infrastruktur yang diperlukan untuk mengambil dan menggunakan informasi itu secara efektif. Mari kita mulai.
Sekarang kita akan beralih untuk menyemai basis pengetahuan, sumber data dasar yang akan menginformasikan tanggapan chatbot kita. Langkah ini melibatkan pengumpulan dan pengorganisasian informasi yang diperlukan chatbot kami untuk beroperasi secara efektif. Dalam panduan ini, kita akan menggunakan data yang diambil dari berbagai situs web yang nantinya akan kita ajukan pertanyaan. Untuk melakukan ini, kami akan membuat perayap yang akan mengikis data dari situs web, menanamkannya, dan menyimpannya di Pinecone.
Demi singkatnya, Anda dapat menemukan kode lengkap untuk crawler di sini. Berikut bagian terkait:
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
) ;
}
} Kelas Crawler adalah perayap web yang mengunjungi URL, mulai dari titik tertentu, dan mengumpulkan informasi dari mereka. Ini beroperasi dalam kedalaman tertentu dan jumlah maksimum halaman seperti yang didefinisikan dalam konstruktor. Metode perayapan adalah fungsi inti yang memulai proses merangkak.
Metode helper fetchpage, parsehtml, dan extracturs masing -masing menangani mengambil konten HTML dari sebuah halaman, mem -parsing HTML untuk mengekstrak teks, dan mengekstraksi semua URL dari halaman yang akan diantar untuk merangkak berikutnya. Kelas ini juga menyimpan catatan URL yang dikunjungi untuk menghindari duplikasi.
seedUntuk mengikat semuanya, kami akan membuat fungsi benih yang akan menggunakan crawler untuk menyemai basis pengetahuan. Di bagian kode ini, kami akan menginisialisasi merangkak dan mengambil URL yang diberikan, lalu membagi isinya menjadi potongan -potongan, dan akhirnya menanamkan dan mengindeks potongan di pinecone.
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 ;
}
}Untuk memotong konten, kami akan menggunakan salah satu metode berikut:
RecursiveCharacterTextSplitter - Splitter ini membagi teks menjadi potongan -potongan dengan ukuran tertentu, dan kemudian secara rekursif membagi potongan menjadi potongan yang lebih kecil sampai ukuran chunk tercapai. Metode ini berguna untuk dokumen panjang.MarkdownTextSplitter - Splitter ini membagi teks menjadi potongan -potongan berdasarkan header Markdown. Metode ini berguna untuk dokumen yang sudah terstruktur menggunakan markdown. Manfaat dari metode ini adalah bahwa ia akan membagi dokumen menjadi potongan -potongan berdasarkan header, yang akan berguna bagi chatbot kita untuk memahami struktur dokumen. Kami dapat mengasumsikan bahwa setiap unit teks di bawah header adalah unit informasi yang koheren secara internal, dan ketika pengguna mengajukan pertanyaan, konteks yang diambil juga akan koheren secara internal.crawl Titik akhir untuk titik akhir crawl cukup mudah. Ini hanya memanggil fungsi seed dan mengembalikan hasilnya.
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" } ) ;
}
}Sekarang backend kami dapat merangkak URL yang diberikan, menanamkan konten dan mengindeks embeddings di pinecone. Titik akhir akan mengembalikan semua segmen di halaman web yang diambil yang kami rayam, jadi kami akan dapat menampilkannya. Selanjutnya, kita akan menulis serangkaian fungsi yang akan membangun konteks dari embeddings ini.
Untuk mengambil dokumen yang paling relevan dari indeks, kami akan menggunakan fungsi query di Pinecone SDK. Fungsi ini mengambil vektor dan mengembalikan vektor yang paling mirip dari indeks. Kami akan menggunakan fungsi ini untuk mengambil dokumen yang paling relevan dari indeks, mengingat beberapa embeddings.
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 } ` )
}
}Fungsi ini mengambil embeddings, parameter topk, dan namespace, dan mengembalikan kecocokan topk dari indeks pinecone. Pertama -tama mendapat klien Pinecone, memeriksa apakah indeks yang diinginkan ada dalam daftar indeks, dan melempar kesalahan jika tidak. Maka ia mendapatkan indeks pinecone spesifik. Fungsi kemudian menanyakan indeks Pinecone dengan permintaan yang ditentukan dan mengembalikan kecocokan.
getContext Kami akan membungkus semuanya dalam fungsi getContext . Fungsi ini akan menerima message dan mengembalikan konteks - baik dalam bentuk string, atau sebagai satu set 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 ) ;
} ; Kembali di chat/route.ts , kami akan menambahkan panggilan ke getContext :
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 , "" ) ; Akhirnya, kami akan memperbarui prompt untuk memasukkan konteks yang kami ambil dari fungsi getContext .
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.
` ,
} ,
] ; Dalam prompt ini, kami menambahkan START CONTEXT BLOCK dan END OF CONTEXT BLOCK untuk menunjukkan di mana konteks harus dimasukkan. Kami juga menambahkan garis untuk menunjukkan bahwa asisten AI akan memperhitungkan blok konteks apa pun yang disediakan dalam percakapan.
Selanjutnya, kita perlu menambahkan panel konteks ke UI obrolan. Kami akan menambahkan komponen baru yang disebut Context (kode lengkap).
Kami ingin memungkinkan antarmuka untuk menunjukkan bagian mana dari konten yang diambil telah digunakan untuk menghasilkan respons. Untuk melakukan ini, kami akan menambahkan titik akhir lain yang akan memanggil getContext yang sama.
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 ( ) ;
}
}Setiap kali pengguna merangkak URL, panel konteks akan menampilkan semua segmen halaman web yang diambil. Setiap kali backend selesai mengirim pesan kembali, ujung depan akan memicu efek yang akan mengambil konteks ini:
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 ] ) ; Pinecone-Vercel-Starter menggunakan penulis naskah untuk pengujian ujung ke ujung.
Untuk menjalankan semua tes:
npm run test:e2e
Secara default, saat berjalan secara lokal, jika kesalahan ditemui, penulis naskah akan membuka laporan HTML yang menunjukkan tes mana yang gagal dan untuk pengemudi browser mana.
Untuk menampilkan laporan uji terbaru secara lokal, jalankan:
npm run test:show