이 예에서는 Pinecone이 구동하는 검색 증강 생성 (RAG)을 사용하여 챗봇에서 정확하고 상황에 맞는 응답을 제공하는 풀 스택 응용 프로그램을 구축합니다.
Rag는 검색 기반 모델과 생성 모델의 이점을 결합한 강력한 도구입니다. 최신 정보를 유지하거나 도메인 별 지식에 액세스하는 데 어려움을 겪을 수있는 전통적인 챗봇과 달리 Rag 기반 챗봇은 크롤링 된 URL에서 생성 된 지식 기반을 사용하여 상황에 맞는 응답을 제공합니다.
Vercel의 AI SDK를 애플리케이션에 통합하면 챗봇 워크 플로우를 쉽게 설정하고 특히 에지 환경에서보다 효율적으로 스트리밍을 활용하여 챗봇의 응답 성과 성능을 향상시킬 수 있습니다.
이 튜토리얼이 끝나면 환각없이 정확한 응답을 제공하는 컨텍스트 인식 챗봇이있어보다 효과적이고 매력적인 사용자 경험을 보장합니다. 이 강력한 도구 (전체 코드 목록)를 구축하기 시작합시다.
Next.js는 강력한 JavaScript 프레임 워크로서 REACT를 사용하여 서버 측 렌더링 및 정적 웹 응용 프로그램을 구축 할 수 있습니다. 설정이 용이하며 성능이 뛰어나고 라우팅 및 API 경로와 같은 내장 기능으로 인해 프로젝트에 훌륭한 선택입니다.
새로운 Next.js 앱을 만들려면 다음 명령을 실행하십시오.
npx create-next-app chatbot 다음으로 ai 패키지를 추가하겠습니다.
npm install ai튜토리얼과 함께 빌드하려면 전체 종속성 목록을 사용할 수 있습니다.
이 단계에서는 Vercel SDK를 사용하여 다음.js 응용 프로그램 내에서 챗봇의 백엔드와 프론트 엔드를 설정합니다. 이 단계가 끝날 무렵, 기본 챗봇은 UP 및 실행 중이며 다음 단계에서 컨텍스트 인식 기능을 추가 할 수 있습니다. 시작합시다.
이제 챗봇의 프론트 엔드 구성 요소에 집중합시다. 우리는 봇의 사용자를 대상으로하는 요소를 구축하여 사용자가 응용 프로그램과 상호 작용할 인터페이스를 만듭니다. 여기에는 Next.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 패키지로, Edge 환경에서 OpenAI의 API와 쉽게 상호 작용할 수 있습니다. 두 번째 종속성은 Message 및 OpenAIStream 유형을 정의하는 데 사용할 ai 패키지이며, 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에 저장하는 크롤러를 만들어냅니다.
간결함을 위해 Crawler의 전체 코드를 여기에서 찾을 수 있습니다. 관련 부분은 다음과 같습니다.
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을 방문하여 정보를 수집하는 웹 크롤러입니다. 생성자에 정의 된대로 특정 깊이와 최대 페이지 수 내에서 작동합니다. 크롤링 방법은 크롤링 프로세스를 시작하는 핵심 기능입니다.
도우미 방법은 각각 페이지의 HTML 컨텐츠를 가져오고, 텍스트를 추출하기 위해 HTML을 구문 분석하고, 다음 크롤링에 대해 대기 할 페이지에서 모든 URL을 추출하여 각각 페이지의 HTML 컨텐츠를 가져 오는 것을 처리하고 추출한 방법을 가져옵니다. 클래스는 또한 중복을 피하기 위해 방문한 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" } ) ;
}
}이제 우리의 백엔드는 주어진 URL을 크롤링하고, 내용을 포함시키고, 임베딩을 Pinecone에 색인 할 수 있습니다. 엔드 포인트는 우리가 크롤링 한 검색된 웹 페이지에서 모든 세그먼트를 반환하므로 표시 할 수 있습니다. 다음으로, 우리는 이러한 임베딩에서 컨텍스트를 구축 할 기능 세트를 작성합니다.
인덱스에서 가장 관련성이 높은 문서를 검색하려면 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 인덱스를 가져옵니다. 그런 다음 함수는 정의 된 요청으로 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 ) ;
} ; chat/route.ts 로 돌아가서 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 , "" ) ; 마지막으로 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 조수가 대화에서 제공되는 컨텍스트 블록을 고려할 것임을 나타내는 라인을 추가했습니다.
다음으로 컨텍스트 패널을 채팅 UI에 추가해야합니다. Context (Full Code)라는 새 구성 요소를 추가하겠습니다.
검색된 컨텐츠의 어떤 부분이 응답을 생성하는 데 사용되었는지를 나타 내기 위해 인터페이스를 허용하려고합니다. 이를 위해 동일한 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 ( ) ;
}
}사용자가 URL을 크롤링 할 때마다 컨텍스트 패널은 검색된 웹 페이지의 모든 세그먼트를 표시합니다. 백엔드가 메시지를 다시 보내는 것을 완료 할 때마다 프론트 엔드는이 컨텍스트를 검색하는 효과를 트리거합니다.
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는 End-End Testing을 위해 극작가를 사용합니다.
모든 테스트를 실행하려면 :
npm run test:e2e
기본적으로 로컬로 실행할 때 오류가 발생하면 극작가는 어떤 테스트 실패 및 브라우저 드라이버를 보여주는 HTML 보고서를 열게됩니다.
로컬로 최신 테스트 보고서를 표시하려면 다음을 실행하십시오.
npm run test:show