LangChain.js로 간단한 RAG 앱 구현

# LangChain.js로 간단한 RAG 앱 구현

![RAG Architecture](https://images.unsplash.com/photo-1555949963-ff9fe0c870eb?q=80&w=2070&auto=format&fit=crop)

안녕하세요! 오늘은 **LangChain.js**를 사용해서 간단한 **RAG(Retrieval-Augmented Generation)** 앱을 만들어보겠습니다. RAG는 AI 챗봇에 맞춤형 지식을 추가하는 가장 인기 있는 방법 중 하나입니다! 🚀

## RAG란 무엇인가요?

**RAG(Retrieval-Augmented Generation)**는 두 단계로 작동합니다:

1. **검색(Retrieval):** 사용자 질문과 관련된 문서를 데이터베이스에서 찾습니다
2. **생성(Generation):** 찾은 문서를 컨텍스트로 사용해서 AI가 답변을 생성합니다

**왜 RAG를 쓰나요?**
– ✅ AI가 학습하지 않은 최신 정보 제공
– ✅ 회사/개인 데이터 활용 가능
– ✅ 할루시네이션(거짓 정보 생성) 감소
– ✅ 답변에 출처 표시 가능

## 프로젝트 준비

먼저 새로운 프로젝트를 설정해볼까요?

“`bash
npx create-next-app@latest my-rag-app
cd my-rag-app
npm install @langchain/openai @langchain/community langchain faiss-node
“`

## 기본 구조 이해하기

RAG 앱의 핵심 구성 요소들:

“`
문서 로드 → 텍스트 분할 → 임베딩 생성 → 벡터 저장 → 질문 시 검색 → 답변 생성
“`

## 1. 문서 로드하기

먼저 검색할 문서를 로드해야 합니다:

“`typescript
import { TextLoader } from “langchain/document_loaders/fs/text”;

// 텍스트 파일에서 문서 로드
const loader = new TextLoader(“./data/my-docs.txt”);
const docs = await loader.load();

console.log(`로드된 문서: ${docs.length}개`);
console.log(docs[0].pageContent);
“`

## 2. 텍스트 분할하기

긴 문서는 더 작은 청크(chunk)로 나눠야 합니다:

“`typescript
import { RecursiveCharacterTextSplitter } from “langchain/text_splitter”;

const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});

const splitDocs = await splitter.splitDocuments(docs);
console.log(`분할된 청크: ${splitDocs.length}개`);
“`

**파라미터 설명:**
– `chunkSize`: 각 청크의 최대 길이
– `chunkOverlap`: 청크 간 중복되는 길이 (맥락 유지용)

## 3. 임베딩 생성하기

텍스트를 숫자 벡터로 변환합니다:

“`typescript
import { OpenAIEmbeddings } from “@langchain/openai”;

const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
});
“`

## 4. 벡터 저장소 만들기

FAISS를 사용해서 로컬 벡터 DB를 만듭니다:

“`typescript
import { FaissStore } from “@langchain/community/vectorstores/faiss”;

// 벡터 저장소 생성
const vectorStore = await FaissStore.fromDocuments(
splitDocs,
embeddings
);

// 저장소 저장
await vectorStore.save(“./faiss-store”);
“`

## 5. 검색과 생성 연결하기

이제 전체 파이프라인을 만들어봅시다:

“`typescript
import { ChatOpenAI } from “@langchain/openai”;
import { RetrievalQAChain } from “langchain/chains”;

// 벡터 저장소 로드
const loadedVectorStore = await FaissStore.load(
“./faiss-store”,
embeddings
);

// LLM 초기화
const llm = new ChatOpenAI({
modelName: “gpt-4”,
temperature: 0,
});

// 체인 생성
const chain = RetrievalQAChain.fromLLM(
llm,
loadedVectorStore.asRetriever()
);

// 질문하기
const response = await chain.call({
query: “이 문서에서 핵심 내용은 무엇인가요?”,
});

console.log(response.text);
“`

## 전체 코드 예제

실제 앱에서는 이렇게 구현합니다:

“`typescript
// lib/rag.ts
import { OpenAIEmbeddings } from “@langchain/openai”;
import { ChatOpenAI } from “@langchain/openai”;
import { RecursiveCharacterTextSplitter } from “langchain/text_splitter”;
import { FaissStore } from “@langchain/community/vectorstores/faiss”;
import { RetrievalQAChain } from “langchain/chains”;
import { TextLoader } from “langchain/document_loaders/fs/text”;

export async function createVectorStore(docPath: string, storePath: string) {
// 1. 문서 로드
const loader = new TextLoader(docPath);
const docs = await loader.load();

// 2. 텍스트 분할
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
const splitDocs = await splitter.splitDocuments(docs);

// 3. 벡터 저장소 생성
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY!,
});

const vectorStore = await FaissStore.fromDocuments(
splitDocs,
embeddings
);

// 4. 저장
await vectorStore.save(storePath);

return vectorStore;
}

export async function queryRAG(query: string, storePath: string) {
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY!,
});

// 벡터 저장소 로드
const vectorStore = await FaissStore.load(storePath, embeddings);

// LLM과 체인 설정
const llm = new ChatOpenAI({
modelName: “gpt-4”,
temperature: 0,
});

const chain = RetrievalQAChain.fromLLM(
llm,
vectorStore.asRetriever({
k: 3, // 상위 3개 관련 문서 반환
})
);

// 질의응답
const response = await chain.call({
query,
});

return response.text;
}
“`

## API 라우트 만들기

Next.js API 라우트로 구현해볼까요?

“`typescript
// app/api/rag/route.ts
import { NextRequest, NextResponse } from “next/server”;
import { createVectorStore, queryRAG } from “@/lib/rag”;

export async function POST(req: NextRequest) {
const { query, action } = await req.json();

try {
if (action === “create”) {
// 벡터 저장소 생성
await createVectorStore(
“./data/my-docs.txt”,
“./faiss-store”
);
return NextResponse.json({
success: true,
message: “벡터 저장소가 생성되었습니다”,
});
} else if (action === “query”) {
// RAG 질의
const answer = await queryRAG(query, “./faiss-store”);
return NextResponse.json({
success: true,
answer,
});
} else {
return NextResponse.json(
{ error: “알 수 없는 액션” },
{ status: 400 }
);
}
} catch (error) {
console.error(“RAG 오류:”, error);
return NextResponse.json(
{ error: “서버 오류” },
{ status: 500 }
);
}
}
“`

## 프론트엔드 UI

간단한 채팅 인터페이스:

“`tsx
// app/page.tsx
“use client”;

import { useState } from “react”;

export default function RAGChat() {
const [query, setQuery] = useState(“”);
const [answer, setAnswer] = useState(“”);
const [loading, setLoading] = useState(false);

const handleQuery = async () => {
if (!query.trim()) return;

setLoading(true);
try {
const res = await fetch(“/api/rag”, {
method: “POST”,
headers: { “Content-Type”: “application/json” },
body: JSON.stringify({ query, action: “query” }),
});

const data = await res.json();
setAnswer(data.answer);
} catch (error) {
console.error(error);
setAnswer(“오류가 발생했습니다”);
} finally {
setLoading(false);
}
};

return (

📚 RAG 챗봇


setQuery(e.target.value)}
placeholder=”문서에 대해 물어보세요…”
disabled={loading}
/>

{answer && (

답변:

{answer}

)}

);
}
“`

## 더 나은 RAG를 위한 팁

### 1. 관련 문서 수 조정

“`typescript
const chain = RetrievalQAChain.fromLLM(
llm,
vectorStore.asRetriever({
k: 4, // 더 많은 문서 검색
searchType: “mmr”, // MMR 알고리즘으로 다양성 증가
})
);
“`

### 2. 커스텀 프롬프트 사용

“`typescript
import { PromptTemplate } from “langchain/prompts”;

const prompt = PromptTemplate.fromTemplate(
`다음 문서를 참고해서 질문에 답변해주세요:
문서: {context}

질문: {question}

문서에 없는 내용은 “알 수 없습니다”라고 하고, 답변에 문서 내용을 인용해주세요.`
);

const chain = RetrievalQAChain.fromLLM(
llm,
vectorStore.asRetriever(),
{
prompt,
}
);
“`

### 3. 소스 문서 반환

“`typescript
const chain = new RetrievalQAChain({
combineDocumentsChain: loadQARefineChain(llm),
retriever: vectorStore.asRetriever(),
returnSourceDocuments: true,
});

const response = await chain.call({ query });

// 소스 문서 확인
response.sourceDocuments.forEach((doc, i) => {
console.log(`소스 ${i + 1}:`, doc.pageContent);
});
“`

## 다양한 문서 로더

LangChain은 다양한 형식을 지원합니다:

“`typescript
// PDF 로더
import { PDFLoader } from “langchain/document_loaders/fs/pdf”;
const pdfLoader = new PDFLoader(“./doc.pdf”);

// 웹페이지 로더
import { CheerioWebBaseLoader } from “langchain/document_loaders/web/cheerio”;
const webLoader = new CheerioWebBaseLoader(“https://example.com”);

// CSV 로더
import { CSVLoader } from “langchain/document_loaders/fs/csv”;
const csvLoader = new CSVLoader(“./data.csv”);

// JSON 로더
import { JSONLoader } from “langchain/document_loaders/fs/json”;
const jsonLoader = new JSONLoader(“./data.json”);
“`

## 환경 변수 설정

`.env.local` 파일:

“`env
OPENAI_API_KEY=your-api-key-here
“`

## 프로젝트 실행

“`bash
npm run dev
“`

`http://localhost:3000`에서 테스트해보세요!

## 결론

축하합니다! 🎉 이제 LangChain.js로 RAG 앱을 만들 수 있습니다.

**핵심 포인트:**
– ✅ 문서 로드 → 분할 → 임베딩 → 저장 파이프라인
– ✅ FAISS로 로컬 벡터 DB 구현
– ✅ RetrievalQAChain으로 검색과 생성 연결
– ✅ 다양한 문서 형식 지원

## 다음 단계

– 🗄️ Pinecone 같은 클라우드 벡터 DB 사용
– 📊 여러 문서 병합해서 검색
– 🔐 사용자별 개인 데이터 RAG
– 🎨 더 예쁜 UI 만들기

## 참고 자료

– [LangChain.js 문서](https://js.langchain.com/)
– [RAG 튜토리얼](https://js.langchain.com/docs/use_cases/question_answering/)
– [FAISS 문서](https://github.com/facebookresearch/faiss)

질문이 있거나 막히는 부분이 있다면 댓글로 알려주세요! 함께 해결해봐요 😊

**다음 포스팅:** GPT-4o vs Claude 3.5 Sonnet 비교