在此示例中,我們將構建一個完整的應用程序,該應用程序使用Pinecone提供的檢索增強生成(RAG),以在聊天機器人中提供準確且上下文相關的響應。
RAG是一種強大的工具,可以結合基於檢索的模型和生成模型的好處。與可能在維護最新信息或訪問特定領域知識的傳統聊天機器人不同的是,基於抹布的聊天機器人使用從爬行的URL創建的知識庫來提供上下文相關的響應。
將Vercel的AI SDK納入我們的應用程序將使我們輕鬆地設置聊天機器人工作流程並更有效地利用流媒體,尤其是在邊緣環境中,增強了我們聊天機器人的響應性和性能。
在本教程結束時,您將擁有一個上下文感知的聊天機器人,該聊天機器人在不幻覺的情況下提供了準確的響應,從而確保了更有效且引人入勝的用戶體驗。讓我們開始構建此功能強大的工具(完整代碼列表)。
Next.js是一個功能強大的JavaScript框架,使我們能夠使用React構建服務器端渲染和靜態Web應用程序。由於其易於設置,出色的性能和內置功能,例如路由和API路由,這是我們項目的絕佳選擇。
要創建一個新的next.js應用,請運行以下命令:
npx create-next-app chatbot接下來,我們將添加ai軟件包:
npm install ai如果您想與教程一起構建,則可以使用依賴項的完整列表。
在此步驟中,我們將使用Vercel SDK在Next.js應用程序中建立聊天機器人的後端和前端。在此步驟結束時,我們的基本聊天機器人將啟動並運行,為我們準備在以下階段添加上下文感知功能。讓我們開始。
現在,讓我們專注於聊天機器人的前端組件。我們將構建機器人的面向用戶的元素,創建用戶與應用程序交互的接口。這將涉及在我們的下一個js應用程序中製定聊天界面的設計和功能。
首先,我們將創建Chat組件,以渲染聊天接口。
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 ;此組件將顯示消息列表和用戶發送消息的輸入表格。呈現聊天消息的Messages組件:
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 >
) ;
}我們的Page組件將管理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 ;有用的useChat掛鉤將管理Chat組件中顯示的消息的狀態。它將:
接下來,我們將設置聊天機器人API端點。這是服務器端組件,它將處理我們聊天機器人的請求和響應。我們將創建一個名為api/chat/route.ts的新文件,並添加以下依賴項:
import { Configuration , OpenAIApi } from "openai-edge" ;
import { Message , OpenAIStream , StreamingTextResponse } from "ai" ;第一個依賴性是openai-edge軟件包,它使在邊緣環境中與OpenAI的API進行交互變得更加容易。第二個依賴關係是ai軟件包,我們將使用該軟件包來定義Message和OpenAIStream類型,我們將用來將其從OpenAI返回到客戶端。
接下來初始化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 ) ;要將此端點定義為邊緣函數,我們將定義和導出runtime變量
export const runtime = "edge" ;接下來,我們將定義端點處理程序:
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 ;
}
}在這裡,我們從帖子中解構消息,並創建我們的初始提示。我們將提示和消息用作createChatCompletion方法的輸入。然後,我們將響應轉換為流並將其返回給客戶端。請注意,在此示例中,我們僅將用戶的消息發送到OpenAI(而不是包括機器人的消息)。
當我們潛入建立聊天機器人時,了解上下文的作用很重要。在聊天機器人的響應中添加上下文是創建更自然,更自然的對話用戶體驗的關鍵。沒有上下文,聊天機器人的回答可能會感到脫節或無關緊要。通過了解用戶查詢的上下文,我們的聊天機器人將能夠提供更準確,相關和引人入勝的響應。現在,讓我們開始牢記這個目標。
首先,我們首先專注於播種知識庫。我們將創建一個軌道和一個種子腳本,並設置一個爬網端點。這將使我們能夠收集和組織聊天機器人將使用上下文相關響應的信息。
在填充知識庫之後,我們將從嵌入中檢索匹配。這將使我們的聊天機器人能夠根據用戶查詢找到相關信息。
接下來,我們將邏輯包裝到getContext功能中,並更新聊天機器人的提示。這將通過確保聊天機器人的提示相關且引人入勝,簡化我們的代碼並改善用戶體驗。
最後,我們將添加一個上下文面板和關聯的上下文端點。這些將為聊天機器人提供一個用戶界面,並為其提供每個用戶查詢的必要上下文的方法。
此步驟是為了向我們的聊天機器人提供所需的信息,並設置必要的基礎架構,以便有效地檢索和使用該信息。讓我們開始。
現在,我們將繼續播種知識庫,即知識庫,這將為我們的聊天機器人的響應提供信息。此步驟涉及收集和組織我們聊天機器人需要有效運行的信息。在本指南中,我們將使用從各個網站檢索到的數據,稍後我們將能夠提出有關問題的問題。為此,我們將創建一個軌道,將刮擦網站的數據,嵌入並將其存儲在Pinecone中。
為了簡潔起見,您可以在此處找到爬網的完整代碼。這是相關部分:
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
) ;
}
} Crawler類是一個網絡橫梁,從給定點開始訪問URL,並從中收集信息。它在構造函數中定義的一定深度和最大頁面內運行。爬網方法是啟動爬行過程的核心函數。
輔助方法獲取,parsehtml和提取物分別處理頁面的HTML內容,解析HTML以提取文本,並從頁面上提取所有URL,以排隊以排隊以排隊。該課程還保留了訪問的URL記錄以避免重複。
seed功能為了將事物結合在一起,我們將創建一個種子功能,該功能將使用爬蟲來播種知識庫。在代碼的這一部分中,我們將初始化爬網並獲取給定的URL,然後將其內容分成塊,最後將其嵌入和索引在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 ;
}
}為了彌補內容,我們將使用以下方法之一:
RecursiveCharacterTextSplitter此分離器將文本分成給定尺寸的塊,然後遞歸將塊分成較小的塊,直到達到塊大小為止。此方法對於長文檔很有用。MarkdownTextSplitter此分離器將文本根據Markdown標頭拆分為塊。此方法對於已經使用Markdown構建的文檔很有用。該方法的好處是,它將基於標題將文檔分為塊,這對於我們的聊天機器人了解文檔的結構非常有用。我們可以假設標題下的每個文本單元是內部連貫的信息單元,並且當用戶提出問題時,檢索到的上下文也將是內部連貫的。crawl端點crawl端點的端點非常簡單。它只是調用seed功能並返回結果。
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" } ) ;
}
}現在,我們的後端能夠爬網,嵌入內容並將嵌入在生皮內酮中。端點將返回我們抓取的檢索網頁中的所有段,因此我們可以顯示它們。接下來,我們將編寫一組功能,這些功能將在這些嵌入中構建上下文。
為了從索引中檢索最相關的文檔,我們將在Pinecone SDK中使用query功能。此函數採用向量,並從索引返回最相似的向量。給定一些嵌入,我們將使用此功能從索引中檢索最相關的文檔。
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 } ` )
}
}該功能帶有嵌入式,TOPK參數和名稱空間,並返回Pinecone索引的TOPK匹配。它首先獲得了Pinecone客戶端,檢查索引列表中是否存在所需的索引,並在錯誤的情況下引發錯誤。然後獲得特定的松果索引。然後,該功能使用定義的請求查詢Pinecone索引並返回匹配項。
getContext包裹東西我們將在getContext函數中將內容包裝在一起。此功能將接收message並返回上下文 - 以字符串形式或一組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 ) ;
} ; getContext chat/route.ts 。
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 , "" ) ;最後,我們將更新提示,以包括我們從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.
` ,
} ,
] ;在此提示中,我們添加了一個START CONTEXT BLOCK和END OF CONTEXT BLOCK以指示應插入上下文的位置。我們還添加了一條行,以指出AI助手將考慮對話中提供的任何上下文塊。
接下來,我們需要將上下文面板添加到CHAT UI中。我們將添加一個稱為Context (完整代碼)的新組件。
我們要允許界面指出已使用所檢索內容的哪些部分用於生成響應。為此,我們將添加另一個端點,該端點將調用相同的getContext 。
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 ( ) ;
}
}每當用戶爬網時,上下文面板都會顯示檢索到的網頁的所有段。每當後端完成回發送消息時,前端都會觸發將檢索此上下文的效果:
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使用劇作家進行最終測試。
運行所有測試:
npm run test:e2e
默認情況下,在本地運行時,如果遇到錯誤,劇作家將打開HTML報告,顯示哪些測試失敗以及哪些瀏覽器驅動程序。
要在本地顯示最新的測試報告,請運行:
npm run test:show