모델이 아무리 좋아도 회사 핸드북, 운영 문서, 제품 노트, 내부 정책 같은 정보는 기본적으로 알지 못합니다.
그래서 RAG, 즉 retrieval-augmented generation이 필요합니다. 목표는 모델 자체를 더 똑똑하게 만드는 것이 아니라, 우리 데이터를 근거로 답하게 만드는 것입니다.
Supabase는 초기 팀에게 실용적인 선택지입니다. 별도 벡터 데이터베이스를 처음부터 도입하지 않아도, PostgreSQL 안에서 관계형 데이터와 벡터 검색을 함께 다룰 수 있기 때문입니다.
이 글은 Supabase 기반 RAG 챗봇이 실제로 어떻게 동작하는지, 무엇부터 만들어야 하는지, 그리고 왜 내부 문서 챗봇이 기대보다 부정확해지는지를 실전 기준으로 설명합니다.
RAG가 해결하는 문제
일반적인 챗봇 모델은 학습된 지식과 현재 프롬프트 안의 정보로 답합니다. 하지만 다음 같은 경우에는 그것만으로 부족합니다.
- 사내 문서
- 최근에 바뀐 제품 스펙
- 고객별 데이터
- 개인 노트나 내부 위키
RAG는 먼저 관련 문서를 검색한 뒤, 그 검색 결과를 문맥으로 넣어 답을 생성하게 만듭니다.
핵심 흐름은 단순합니다.
- 문서를 검색 가능한 형태로 저장
- 질문과 관련된 조각을 retrieval
- 그 조각을 모델에 context로 전달
- 그 context 안에서만 답하게 하기
retrieval이 약하면 모델이 좋아도 결과가 흔들립니다.
왜 Supabase가 좋은 시작점일까
전용 벡터 DB도 강력하지만, 초기 프로젝트에서는 Supabase가 주는 단순함이 큽니다.
- PostgreSQL이 익숙한 팀이 많다
pgvector를 기존 관계형 데이터 옆에 둘 수 있다- metadata filter, auth, 앱 데이터가 한 시스템 안에 남는다
- 작은 팀이 운영하고 설명하기 쉽다
모든 규모에 최적이라는 뜻은 아니지만, “처음 제대로 만드는 RAG”에는 매우 현실적인 선택입니다.
최소한의 테이블 구조
첫 버전부터 복잡한 스키마가 필요한 것은 아닙니다.
보통 최소한 이 정도면 시작할 수 있습니다.
- 텍스트 chunk
- embedding vector
- source, title, team, visibility 같은 metadata
예를 들면:
create table documents (
id bigserial primary key,
content text not null,
source text,
section text,
embedding vector(1536)
);
여기서 metadata가 중요한 이유는, retrieval 품질이 벡터 유사도만으로 결정되지 않기 때문입니다.
ingestion이 RAG 품질의 절반이다
RAG 품질의 절반은 ingestion에서 결정됩니다.
실전 ingestion 흐름은 보통 이렇습니다.
- 원본 문서 수집
- 문서를 chunk로 분할
- 각 chunk의 embedding 생성
- 텍스트와 metadata를 Supabase에 저장
초보 시스템이 자주 틀리는 이유는, 채팅 응답 화면부터 만들고 정작 ingestion 파이프라인을 대충 만드는 데 있습니다.
chunking은 진짜 품질 결정이다
chunk가 너무 크면 검색이 흐려지고, 너무 작으면 문맥이 끊깁니다.
무난한 시작점은 보통 이렇습니다.
- 가능하면 section이나 heading 기준으로 나누기
- source metadata 유지하기
- 서로 다른 주제를 한 chunk에 섞지 않기
- 한 chunk가 하나의 일관된 아이디어를 담도록 하기
좋은 chunking은 프롬프트 수정 몇 줄보다 더 큰 차이를 만들 수 있습니다.
embedding을 만들고 저장하기
chunk가 준비되면 embedding을 만들고 텍스트와 함께 저장합니다.
import { createClient } from '@supabase/supabase-js';
import OpenAI from 'openai';
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function insertDocument(content: string, source: string) {
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: content,
});
const embedding = embeddingResponse.data[0].embedding;
await supabase.from('documents').insert({
content,
source,
embedding,
});
}
이 단계 자체는 단순합니다. 더 어려운 부분은 저장되는 chunk가 깨끗하고, 최신이며, metadata가 충분한지입니다.
retrieval이 승부를 가른다
사용자가 질문하면 질문도 embedding으로 바꾸고, 유사한 chunk를 검색한 뒤, 그 결과를 모아 모델 프롬프트를 구성합니다.
이때 retrieval 품질은 단순 유사도만으로 정해지지 않습니다.
실제로는 이런 요소가 자주 필요합니다.
- metadata filter
- source 범위 제한
- freshness 규칙
- score threshold
- top-k 조정
retrieval이 엉뚱한 chunk를 가져오면 모델은 약한 근거로 답할 수밖에 없습니다.
Supabase에서 retrieval 흐름 만들기
DB 함수로 similarity search를 수행하고, 앱에서 상위 결과를 context로 묶는 구조가 흔합니다.
async function askQuestion(question: string) {
const embeddingResponse = await openai.embeddings.create({
model: 'text-embedding-3-small',
input: question,
});
const queryEmbedding = embeddingResponse.data[0].embedding;
const { data: matches, error } = await supabase.rpc('match_documents', {
query_embedding: queryEmbedding,
match_threshold: 0.75,
match_count: 4,
});
if (error) throw error;
const contextText = matches.map((doc: { content: string }) => doc.content).join('\n\n---\n\n');
return openai.responses.create({
model: 'gpt-4.1-mini',
input: [
{
role: 'system',
content: 'Answer only from the provided context. If the answer is not supported by the context, say you do not know.',
},
{
role: 'user',
content: `Question: ${question}\n\nContext:\n${contextText}`,
},
],
});
}
세부 API 형태는 바뀔 수 있어도 설계는 같습니다. 먼저 검색하고, 그 다음 명시적으로 grounding 규칙을 준 채 답하게 만드는 것입니다.
프롬프트도 중요하지만, 그것만으로는 부족하다
많은 팀이 RAG 품질을 프롬프트 문제로만 생각합니다. 실제로는 그렇지 않은 경우가 많습니다.
프롬프트는 단순하고 엄격하면 충분합니다.
- 제공된 context에서만 답하기
- 없는 사실은 만들지 않기
- 모르면 모른다고 말하기
- 가능하면 source나 근거를 함께 보여주기
하지만 retrieval이 약하면 좋은 프롬프트도 한계가 있습니다.
RAG 챗봇이 부정확해지는 흔한 실수
1. chunking이 나쁘다
서로 다른 주제가 한 chunk에 섞이거나, 중요한 문맥이 반쯤 끊깁니다.
2. metadata filtering이 없다
제품, 팀, 문서 유형, 고객 구분이 필요한데 전체 문서를 다 뒤집니다.
3. threshold 없이 retrieval한다
품질이 낮은 매칭도 그대로 프롬프트에 들어가 답을 흐립니다.
4. freshness 전략이 없다
이미 바뀐 문서가 계속 검색돼 오래된 답을 만듭니다.
5. RAG가 환각을 완전히 없앨 거라고 기대한다
RAG는 unsupported answer를 줄여주지만, 완벽한 진실성을 자동으로 보장하지는 않습니다.
6. evaluation 루프가 없다
실제 질문 세트로 retrieval과 answer 품질을 검증하지 않으면, 좋아졌는지 나빠졌는지조차 알 수 없습니다.
실전 프로젝트에서 무엇부터 만들까
첫 usable version은 보통 이 순서면 충분합니다.
- 좁은 문서 집합 하나를 정하기
- chunking과 ingestion 만들기
- embedding과 metadata 저장하기
- threshold가 있는 retrieval 만들기
- context 밖 답변을 제한하는 프롬프트 넣기
- 실제 질문 세트로 평가하기
채팅 UI부터 만들고 grounding을 나중에 붙이는 것보다 훨씬 안전한 순서입니다.
정말 좋아지고 있는지 어떻게 판단할까
데모 질문 하나가 그럴듯하다고 성공이라고 보면 안 됩니다.
다음 기준으로 봐야 합니다.
- 올바른 source 문서를 찾는가
- context가 없을 때 억지로 답하지 않는가
- 반복되는 실제 질문에서 품질이 좋아지는가
- 오래된 문서가 남아 있을 때 성능이 무너지는가
평가 기준은 “똑똑해 보이냐”가 아니라 “올바른 근거를 찾고 그 근거 안에서 답하느냐”입니다.
FAQ
Q. 처음부터 별도 벡터 DB 대신 왜 Supabase를 쓰나요?
관계형 데이터, metadata, auth, vector search를 한 스택 안에 둘 수 있어서 초기 제품에는 더 단순하고 실용적이기 때문입니다.
Q. prompt engineering과 retrieval 중 뭐가 더 중요하나요?
초반에는 retrieval 품질이 더 중요합니다. 나쁜 chunking과 엉뚱한 매칭은 좋은 프롬프트로도 완전히 복구되지 않습니다.
Q. RAG가 환각을 완전히 없애주나요?
아니요. retrieval과 grounding이 좋으면 unsupported answer를 줄여주지만, 완벽함을 보장하지는 않습니다.
Read Next
- AI 시스템 평가 구조를 더 잘 잡고 싶다면 Harness Engineering Guide로 이어가세요.
- 에이전트 시스템을 더 넓게 이해하고 싶다면 AI Agent Guide가 다음 글입니다.
Related Posts
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.