Ollama와 TypeScript를 사용하여 로컬 전용 RAG 시스템 구축하기
요약
Ollama와 TypeScript를 활용하여 외부 API 호출 없이 로컬에서만 작동하는 RAG 시스템 구축 방법을 소개합니다. SQLite-vec을 사용하여 가볍고 빠른 벡터 검색을 구현하며, 개인 데이터의 보안을 유지하며 자연어로 질문할 수 있는 환경을 제공합니다.
핵심 포인트
- Ollama를 통한 로컬 LLM 및 임베딩 모델 실행
- SQLite-vec을 활용한 경량 벡터 데이터베이스 구축
- 데이터 외부 유출 없는 완전한 로컬 RAG 파이프라인 구현
- TypeScript와 Node.js 기반의 간결한 개발 스택
Ollama와 TypeScript를 사용하여 로컬 전용 RAG 시스템 구축하기
대부분의 RAG 튜토리얼은 사용자의 개인 문서를 OpenAI로 전송합니다. 여기서는 문서를 여러분의 노트북에 그대로 유지하는 방법을 소개합니다.
이 포스트에서는 완전히 로컬 머신에서 실행되는 전체 검색 증강 생성 (Retrieval-Augmented Generation, RAG) 파이프라인을 단계별로 설명합니다. API 키도, 제3자 호출도, 월간 청구서도 필요 없습니다. 단 200줄의 TypeScript와 단일 바이너리만 있으면 됩니다.
구축하게 될 내용
다음 기능을 수행하는 명령줄 도구 (command-line tool)를 만듭니다:
.md또는.txt파일 폴더를 로컬 벡터 저장소 (vector store)에 인덱싱합니다.- 로컬 LLM을 사용하여 해당 파일들에 대한 질문에 답변합니다.
- 답변이 어떤 문서에서 왔는지 인용합니다.
이 과정을 마치면, 여러분의 엔지니어링 위키, 개인 메모, 또는 코드베이스를 지정하여 데이터가 머신 외부로 나가지 않고도 자연어로 질문을 던질 수 있게 됩니다.
기술 스택
- Ollama — LLM 및 임베딩 모델 (embedding model)을 실행합니다.
@xenova/transformers— 두 번째 Ollama 모델을 사용하고 싶지 않을 경우를 위한 대체 임베딩 라이브러리입니다.sqlite-vec— 벡터 유사도 검색 (vector similarity search) 기능을 추가하는 SQLite 확장 기능입니다. 작고 빠르며 별도의 데이터베이스 서버가 필요 없습니다.- TypeScript + Node 22 — 이 모든 것을 하나로 결합합니다.
왜 Chroma나 Qdrant 대신 SQLite를 사용할까요? 100만 개 미만의 청크 (chunks)를 가진 컬렉션의 경우, SQLite가 더 빠르고 배포가 간편하며 데몬 (daemon)이 필요하지 않습니다. 여러분의 "벡터 데이터베이스"는 단 하나의 파일이 됩니다.
설정
ollama pull nomic-embed-text # 임베딩 모델
ollama pull qwen2.5:7b # 답변 모델
pnpm add better-sqlite3 sqlite-vec
1단계: 문서 청킹 및 임베딩
import fs from "node:fs";
import path from "node:path";
...
nomic-embed-text는 768차원 벡터를 반환합니다. 수천 개의 문서 코퍼스 (corpus)를 몇 분 안에 재인덱싱할 수 있을 정도로 충분히 빠릅니다.
2단계: SQLite에 저장
import Database from "better-sqlite3";
import * as sqliteVec from "sqlite-vec";
...
3단계: 검색
async function search(query: string, k = 4) {
const queryVec = await embed(query);
const rows = db.prepare(`
...
MATCH는 sqlite-vec의 코사인 유사도 (cosine similarity)를 트리거합니다. 작은 말뭉치(corpora)에서는 밀리초 미만의 속도를 보여줍니다.
4단계: LLM에게 질문하기
async function ask(question: string) {
const matches = await search(question, 4);
...
전체 과정 합치기
// 폴더 인덱싱
const files = fs.readdirSync("./notes").map((f) => path.join("./notes", f));
for (const f of files) await indexFile(f);
...
마크다운 파일 500개를 인덱싱하는 총 실행 시간은 M2 MacBook에서 약 3분 정도 소요되었습니다. 질문당 지연 시간 (latency)은 2초 미만입니다.
이것이 중요한 이유
팀의 문서가 누군가 처음부터 끝까지 다 읽을 수 있는 수준(약 100페이지)을 넘어섰다면, 로컬 RAG (Retrieval-Augmented Generation)는 그 위키를 다시 유용한 도구로 바꿔줍니다. 다음 사례에도 동일하게 적용됩니다:
- 코드베이스 (Codebases) — "속도 제한기 (rate limiter)가 어디에 구현되어 있나요?"라는 질문에 답변
- 고객 지원 아카이브 (Customer support archives) — "우리의 환불 정책은 무엇인가요?"라는 질문에 답변
- 연구 노트 (Research notes) — "6개월 전에 X에 대해 뭐라고 적었었죠?"라는 질문에 답변
- 법률 문서 (Legal documents) — "우리의 MSA (Master Service Agreement)에서 면책 (indemnification)에 대해 뭐라고 명시되어 있나요?"라는 질문에 답변
마지막 항목이 중요합니다. 현재 모든 리걸테크 (legal-tech) 스타트업들이 이것의 클라우드 버전을 구축하고 있습니다. 하지만 당신의 것은 노트북에서 실행됩니다.
실제로 효과가 있는 튜닝 방법
- 청크 크기 (Chunk size) 800-1200자가 가장 적절합니다. 청크가 너무 작으면 문맥 (context)을 잃고, 너무 크면 관련성 (relevance)이 희석됩니다.
- **청크 크기의 10-15% 오버랩 (Overlap)**을 설정하면 생각의 중간에서 잘린 문장을 포착할 수 있습니다.
- 속도보다 정밀도가 더 중요하다면 크로스 인코더 (cross-encoder)로 상위 k개(top-k)를 재순위화 (Re-rank) 하세요. 100ms 정도의 시간이 추가되지만, 관련성을 70%에서 90%로 끌어올리는 경우가 많습니다.
- **임베딩 (embeddings)을 캐싱 (Cache)**하되, 콘텐츠 해시 (content hash)를 키로 사용하여 재인덱싱이 증분식 (incremental)으로 이루어지도록 하세요.
다음 단계
이 시리즈의 이전 포스트에서는 함수 호출 (function calling)을 다루었습니다. 함수 호출과 RAG를 결합하면 문서를 읽고 행동을 취할 수 있는 로컬 에이전트 (local agent)를 가질 수 있습니다: "데이터 거주성 (data residency)에 대해 우리 MSA가 어떻게 말하고 있는지 요약해서 법무팀에 보낼 이메일 초안을 작성해줘" — MSA 청크를 읽고, 초안을 작성하고, 이메일 도구를 호출하는 식입니다.
이것이 진정한 어시스턴트입니다. 그리고 그 어떤 데이터도 당신의 기기를 떠나지 않습니다.
다음 포스트: 실시간 UI를 위한 프로덕션 패턴인 Next.js에서 Server-Sent Events (SSE)를 통해 Ollama 응답을 스트리밍하는 방법
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기