이 글에서는 Upstash Vector, Upstash Redis, Hugging Face Inference API, Replicate LLAMA-2-70B Chat 모델, Vercel을 활용해 오픈소스 기반의 커스텀 콘텐츠 RAG 챗봇을 구축한 과정을 소개합니다. Upstash Vector는 벡터 삽입·조회와 함께 사용자 메시지마다 관련 컨텍스트를 동적으로 생성하고 갱신하는 역할을 담당했으며, Upstash Redis는 챗봇의 대화 내역을 저장하는 데 활용되었습니다.
사전 준비 사항
시작하기 전에 다음 항목들이 필요합니다.
- Node.js 18 이상
- Upstash 계정
- Hugging Face 계정
- Replicate 계정
- Vercel 계정
기술 스택
| 기술 | 설명 |
|---|---|
| Upstash | 서버리스 데이터베이스 플랫폼입니다. 이 프로젝트에서는 벡터 저장을 위한 Upstash Vector와 대화 내역 저장을 위한 Upstash Redis 두 가지를 모두 사용합니다. |
| Next.js | 웹을 위한 React 프레임워크입니다. 빠른 프로토타이핑을 위해 널리 쓰이는 shadcn/ui를 함께 활용합니다. |
| Replicate | 오픈소스 모델을 실행하고 파인튜닝할 수 있는 플랫폼입니다. 여기서는 LLAMA-2-70B Chat 모델을 사용합니다. |
| Hugging Face | 머신러닝 커뮤니티가 모델·데이터셋·애플리케이션으로 협업하는 플랫폼입니다. 임베딩 생성을 위해 Hugging Face Inference API를 사용합니다. |
| LangChain | 언어 모델 기반 애플리케이션 개발을 위한 프레임워크입니다. |
| TailwindCSS | 커스텀 디자인을 손쉽게 구축할 수 있는 CSS 프레임워크입니다. |
| Vercel | 웹 애플리케이션을 배포하고 확장할 수 있는 클라우드 플랫폼입니다. |
Upstash Redis 설정하기
Upstash 계정을 생성하고 로그인한 뒤, Redis 탭으로 이동하여 데이터베이스를 생성합니다.


데이터베이스 생성이 완료되면 Details 탭으로 이동합니다. 화면을 아래로 스크롤해 Connect your database 섹션을 찾은 후, 해당 내용을 복사해 안전한 곳에 보관하세요.

마찬가지로 아래로 스크롤해 REST API 섹션을 찾고 .env 버튼을 선택합니다. 표시된 내용을 복사해 안전하게 저장해 둡니다.

Upstash Vector 설정하기
Upstash 계정에 로그인한 상태에서 Vector 탭으로 이동해 인덱스(Index)를 생성합니다.


인덱스 생성 후 화면을 아래로 스크롤해 Connect 섹션을 찾고 .env 버튼을 선택합니다. 표시된 내용을 복사해 안전하게 보관하세요.

프로젝트 설정하기
앱 저장소를 클론한 뒤 이 가이드를 따라가면 프로젝트의 모든 구성 요소를 익힐 수 있습니다. 프로젝트를 포크하려면 다음 명령어를 실행하세요.
git clone https://github.com/rishi-raj-jain/custom-rag-chatbot-upstash-vector
cd custom-rag-chatbot-upstash-vector
pnpm install
저장소를 클론한 후 .env 파일을 생성하고, 앞선 단계에서 저장해 둔 값들을 추가합니다.
완성된 파일은 대략 다음과 같습니다.
# .env
# 위의 단계에서 얻은 값들
# Upstash Redis URL 및 Token
UPSTASH_REDIS_REST_URL="https://....upstash.io"
UPSTASH_REDIS_REST_TOKEN="..."
# Upstash Vector URL 및 Token
UPSTASH_VECTOR_REST_URL="https://...-vector.upstash.io"
UPSTASH_VECTOR_REST_TOKEN="..."
# Replicate API Key
REPLICATE_API_TOKEN="r8_..."
# Hugging Face Inference API Key
HUGGINGFACEHUB_API_KEY="hf_..."
위 단계를 마치면 다음 명령어로 로컬 환경을 실행할 수 있습니다.
pnpm dev
저장소 구조 살펴보기
아래는 프로젝트의 주요 폴더 구조입니다. 빨간색으로 표시된 파일들은 이 글에서 자세히 다루는 부분으로, 학습된 커스텀 컨텍스트를 기반으로 AI와 대화하는 API 라우트 생성, 그리고 기존 인덱스에 벡터를 upsert하여 컨텍스트를 갱신하는 로직이 담겨 있습니다.

Next.js App Router에서 Chat 라우트 설정하기
이 섹션에서는 app/api/chat/route.js 라우트를 어떻게 구성했는지 설명합니다. 이 라우트는 서버리스 데이터베이스에 대화를 동기화하고, 문자열의 임베딩을 동적으로 생성하며, 지정된 인덱스에서 관련 벡터를 조회해 컨텍스트를 만들고, LLAMA-2-70B Chat 모델에 예측을 요청하는 역할을 수행합니다. 이해를 돕기 위해 몇 가지 단계로 나누어 설명하겠습니다.
대화 내역 저장하기
Upstash Redis와의 대화를 캐싱하기 위해 Redis List를 활용합니다. 사용자로부터 응답할 새 메시지가 들어오면, 조건부로 챗봇의 이전 응답을 리스트에 push합니다. 그다음 사용자의 최신 메시지도 리스트에 push하여 저장한 후 응답 생성 절차를 진행합니다.
// File: app/api/chat/route.js
import { Redis } from '@upstash/redis'
// Instantiate the Upstash Redis
const upstashRedis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
export async function POST(req) {
try {
// the whole chat as array of messages
const { messages } = await req.json()
// assuming user - assistant chat
// add assitant's response to the chat history
if (messages.length > 1) {
await upstashRedis.lpush('unique_conversation_id', JSON.stringify(messages[messages.length - 2]))
}
// add user's request to the chat history
await upstashRedis.lpush('unique_conversation_id', JSON.stringify(messages[messages.length - 1]))
// Proceed to create a response
}
최신 메시지의 임베딩 생성하기
사용자가 제공한 전체 컨텍스트(즉, 커스텀 콘텐츠)를 반영해 최신 메시지에 효과적으로 답변하려면, 기존 인덱스에서 관련 컨텍스트(유사한 벡터)를 검색하는 데 활용할 임베딩을 생성해야 합니다. Hugging Face Inference API를 LangChain과 함께 사용하면 엣지 환경에서 API 호출 한 번으로 임베딩을 만들 수 있으며, 생성된 벡터를 Upstash Vector 인덱스 생성 시 설정한 길이(여기서는 256)만큼 잘라냅니다.
// File: app/api/chat/route.js
import { HuggingFaceInferenceEmbeddings } from '@langchain/community/embeddings/hf'
// Instantiate the Hugging Face Inference API
const embeddings = new HuggingFaceInferenceEmbeddings()
export async function POST(req) {
try {
// ...
// get the latest question stored in the last message of the chat array
const userMessages = messages.filter((i) => i.role === 'user')
const lastMessage = userMessages[userMessages.length - 1].content
// generate embeddings of the latest question
const queryVector = (await embeddings.embedQuery(lastMessage)).slice(0, 256)
// Proceed to create a response
}
최신 메시지 기반으로 관련 컨텍스트 벡터 조회하기
메시지마다 사용자가 제공한 모든 컨텍스트를 불러오는 것은 비용이 큰 작업입니다. 우리는 사용자의 최신 메시지와 관련 있는 컨텍스트만 골라 시스템 프롬프트 형태로 LLAMA-2-70B Chat 모델에 전달하고자 합니다. 이를 위해 기존 벡터 집합을 쿼리해 가장 관련성 높은 2개의 벡터와 메타데이터를 가져온 뒤, 신뢰도 점수가 70%를 초과하는 결과만 필터링합니다.
// File: app/api/chat/route.js
import { Index } from '@upstash/vector'
// Instantiate the Upstash Vector Index
const upstashVectorIndex = new Index()
export async function POST(req) {
try {
// ...
// query the relevant vectors from the embedding vector
const queryResult = await upstashVectorIndex.query({
vector: queryVector,
// get the top 2 relevant results
topK: 2,
// do not include the whole set of embeddings in the response
includeVectors: false,
// include the meta data so that can get the description out of the index
includeMetadata: true,
})
// console.log('The query result came in', queryResult.length)
// using the resulting set of relevant vectors
// filter the one that have score of greater than 70% match
// and get the description we stored while training
const queryPrompt = queryResult
.filter((match) => match.score && match.score > 0.7)
.map((match) => match.metadata.description)
.join('\n')
// console.log('The query prompt is', queryPrompt)
// Proceed to create a response
}
컨텍스트를 담아 LLAMA-2-70B Chat 모델에 예측 요청하기
관련 컨텍스트를 문자열로 확보했다면, 마지막 단계는 llama-2-70B chat 모델에 프롬프트를 전달해 사용자의 최신 메시지에 대한 응답을 생성하는 것입니다. 여기서는 Vercel AI SDK의 experimental_buildLlama2Prompt 메서드를 활용합니다. 이 메서드는 llama-2-70B chat 모델에 적합한 프롬프트 형식을 알아서 만들어 줍니다.
// File: app/api/chat/route.js
import Replicate from 'replicate'
import { experimental_buildLlama2Prompt } from 'ai/prompts'
import { ReplicateStream, StreamingTextResponse } from 'ai'
// Instantiate the Replicate API
const replicate = new Replicate({
auth: process.env.REPLICATE_API_TOKEN,
})
export async function POST(req) {
try {
// ...
const response = await replicate.predictions.create({
// You must enable streaming.
stream: true,
// The model must support streaming. See https://replicate.com/docs/streaming
// This is the model ID for Llama 2 70b Chat
version: '2c1608e18606fad2812020dc541930f2d0495ce32eee50074220b87300bc16e1',
// Format the message list into the format expected by Llama 2
input: {
prompt: experimental_buildLlama2Prompt([
{
// create a system content message to be added as
// the llama2prompt generator will supply it as the context with the API
role: 'system',
content: queryPrompt.substring(0, Math.min(queryPrompt.length, 2000)),
},
// also, pass the whole conversation!
...messages,
]),
},
})
// stream the result to the frontend
const stream = await ReplicateStream(response)
return new StreamingTextResponse(stream)
}
Next.js App Router에서 Train 라우트 설정하기
이 섹션에서는 app/api/train/route.js 라우트를 어떻게 구성했는지 설명합니다. 이 라우트는 요청 객체에 담겨 전달된 문자열들의 임베딩을 동적으로 생성하고, 이를 Upstash Vector 인덱스에 추가하는 역할을 합니다. 단계별로 나누어 살펴보겠습니다.
문자열의 임베딩 생성하기
기존 인덱스를 설정하거나 갱신할 수 있도록 문자열의 임베딩을 생성합니다. 이렇게 하면 챗봇의 향후 응답에 반영될 컨텍스트를 항상 최신 상태로 유지할 수 있습니다. Hugging Face Inference API를 LangChain과 함께 사용하면 엣지 환경에서 API 호출 한 번으로 임베딩을 만들 수 있습니다.
// File: app/api/train/route.js
import { HuggingFaceInferenceEmbeddings } from '@langchain/community/embeddings/hf'
// Instantiate the Hugging Face Inference API
const embeddings = new HuggingFaceInferenceEmbeddings()
export async function POST(req) {
try {
// a default set of messages to create vector embeddings on
let messagesToVectorize = [
'Rishi is pretty much active on Twitter nowadays.',
'Rishi loves writing for Upstash',
"Rishi's recent article on building chatbot using Upstash went viral",
'Rishi is enjoying building launchfa.st.',
]
// if the POST request is of type application/json
if (req.headers.get('Content-Type') === 'application/json') {
// and if the request contains array of messages to train on
const { messages } = await req.json()
if (typeof messages !== 'string' && messages.length > 0) {
messagesToVectorize = messages
}
}
// Call the Hugging Face Inference API to get emebeddings on the messages
const generatedEmbeddings = await Promise.all(messagesToVectorize.map((i) => embeddings.embedQuery(i)))
// ...
}
유사도 검색을 위한 벡터 저장하기
생성된 임베딩을 벡터 인덱스에 추가하려면, Upstash Vector 인덱스 생성 시 설정한 길이(여기서는 256)만큼 벡터를 자른 뒤 upsert 메서드로 메타데이터, 즉 원본 문자열과 함께 삽입합니다. 이렇게 하면 유사한 벡터를 검색할 때 해당 문자열들을 되찾을 수 있고, LLAMA-2-70B Chat 모델이 응답을 생성할 때 대화의 지식 기반으로 활용할 수 있습니다.
// File: app/api/train/route.js
import { Index } from '@upstash/vector'
// Instantiate the Upstash Vector Index
const upstashVectorIndex = new Index()
export async function POST(req) {
try {
// ...
// Slice the vector into lengths of upto 256
await Promise.all(
generatedEmbeddings
.map((i) => i.slice(0, 256))
.map((vector, index) =>
// Upsert the vector with description to be further as the context to upcoming questions
upstashVectorIndex.upsert({
vector,
id: index.toString(),
metadata: { description: messagesToVectorize[index] },
}),
),
)
// Once done, return with a successful 200 response
return new Response(JSON.stringify({ code: 1 }), { status: 200, headers: { 'Content-Type': 'application/json' } })
}
배울 것이 정말 많았죠! 이제 모든 준비가 끝났습니다 ✨
Vercel에 배포하기
이제 저장소를 Vercel에 배포할 준비가 되었습니다. 다음 단계를 따라 배포를 진행하세요 👇🏻
- 먼저 앱 코드를 담은 GitHub 저장소를 생성합니다.
- Vercel 대시보드로 이동해 New Project를 생성합니다.
- 새 프로젝트를 방금 만든 GitHub 저장소와 연결합니다.
- Settings에서
Environment Variables를 로컬.env파일의 값과 일치하도록 설정합니다. - 배포 시작! 🚀
추가 참고 자료
더 자세한 내용이 궁금하다면 이 글에서 인용한 참고 자료들을 확인해 보세요.
맺음말
이 프로젝트를 통해 임베딩 생성, 기존 벡터 집합에서의 조회, 그리고 확보한 컨텍스트를 바탕으로 LLAMA-2-70B Chat 모델로 관련성 높은 응답을 만드는 방법을 배울 수 있었습니다. 무엇보다 사용량에 따라 유연하게 확장되는 Upstash 덕분에 인프라 부담 없이 개발에 집중할 수 있었다는 점이 가장 큰 장점이었습니다. 이 가이드가 여러분만의 RAG 챗봇을 구축하는 데 도움이 되기를 바랍니다.