본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 25. 23:59

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)를 만듭니다:

  1. .md 또는 .txt 파일 폴더를 로컬 벡터 저장소 (vector store)에 인덱싱합니다.
  2. 로컬 LLM을 사용하여 해당 파일들에 대한 질문에 답변합니다.
  3. 답변이 어떤 문서에서 왔는지 인용합니다.

이 과정을 마치면, 여러분의 엔지니어링 위키, 개인 메모, 또는 코드베이스를 지정하여 데이터가 머신 외부로 나가지 않고도 자연어로 질문을 던질 수 있게 됩니다.

기술 스택

  • 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(`
...

MATCHsqlite-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가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0