AssoAI RAG 아키텍처 딥다이브 — 18개 모듈의 비밀

4 minute read

학생회 SaaS AssoAI의 심장부에는 18개 TypeScript 모듈로 이루어진 RAG(Retrieval-Augmented Generation) 시스템이 뛰고 있습니다. “이번 달 예산 얼마 남았어?”라는 한 줄 질문이 정확한 답변으로 돌아오기까지, 내부에서는 놀라울 만큼 정교한 파이프라인이 작동합니다. 오늘은 그 여정을 코드 레벨에서 낱낱이 해부해 보겠습니다 🐧

전체 아키텍처 한눈에 보기

사용자 질문
    │
    ▼
┌─────────────────────────────────────────────┐
│  1. context-synthesizer  ← 대화 맥락 합성    │
│  2. query-analyzer       ← 의도·키워드 분석   │
│  3. date-parser          ← 날짜 표현 파싱     │
├─────────────────────────────────────────────┤
│          검색 레이어 (병렬 실행)               │
│  4. view-query     ← VIEW 패턴 매칭          │
│  5. db-query       ← NL→SQL 변환·실행        │
│  6. Vector Search  ← 임베딩 유사도 검색       │
│  7. notion-search  ← Notion 하이브리드 검색   │
│  8. regulation-search ← 규정 벡터+텍스트      │
│  9. comprehensive-search ← 멀티소스 통합      │
├─────────────────────────────────────────────┤
│  10. reranker           ← 중복 제거 + 재점수  │
│  11. schema-info        ← DB 스키마 제공      │
│  12. context-builder    ← 프로덕션 컨텍스트   │
│  13. answer-enhancer    ← 인용·링크 생성      │
├─────────────────────────────────────────────┤
│  14. action-planner     ← 액션 의도 감지      │
│  15. action-executor    ← DB 쓰기 + 실행취소  │
│  16. follow-up-generator← 후속 질문 추천      │
├─────────────────────────────────────────────┤
│  17. evaluation         ← 30문항 평가셋       │
│  18. evaluation-runner  ← 품질 벤치마크       │
├─────────────────────────────────────────────┤
│       orchestrator.ts  ← 전체 파이프라인 지휘  │
└─────────────────────────────────────────────┘
    │
    ▼
  SSE 스트리밍 응답 (thinking steps + answer)

핵심 모듈 딥다이브

1️⃣ Orchestrator — 파이프라인 지휘자

orchestrator.tsRAGOrchestrator 클래스가 전체 흐름을 10단계로 지휘합니다:

export class RAGOrchestrator {
  constructor(options: RAGOptions = {}) {
    this.options = {
      maxIterations: options.maxIterations ?? 3,
      similarityThreshold: options.similarityThreshold ?? 0.5,
      resultsPerQuery: options.resultsPerQuery ?? 5,
      enableReranking: options.enableReranking ?? true,
    }
  }
}

각 단계마다 ThinkingStep을 생성해 프론트엔드에 실시간 SSE 스트리밍합니다. “질문 분석 중 → 데이터베이스 조회 중 → 결과 분석 중 → 답변 생성 중” — 사용자가 AI의 사고 과정을 지켜볼 수 있죠.

2️⃣ Query Analyzer — AI 기반 의도 분석

LLM에게 사용자 질문을 JSON으로 구조화하게 합니다:

const analysis = await analyzeQuery(effectiveQuery, enhancedContext)
// → { intent, keywords, entities, expandedQueries,
//    dataSource, relevantTables }

dataSource가 핵심입니다. "structured", "document", "both", "none" 중 하나를 반환하여 이후 검색 전략을 결정합니다. 대화형 질문(“안녕”, “넌 뭘 할 수 있어?”)은 "none"으로 분류되어 RAG를 완전히 바이패스합니다.

3️⃣ Context Synthesizer — 멀티턴 대화 핵심

짧은 후속 질문(“숫자로 이야기해줘”)을 이전 맥락과 결합합니다:

export function isShortFollowUpQuery(query: string): boolean {
  const wordCount = query.trim().split(/\s+/).length
  const hasModifierWords = /숫자로|자세히|더|다시|그거|그것|아까/.test(query)
  return wordCount <= 5 || hasModifierWords
}

“그것”, “거기서”, “아까” 같은 대명사를 실제 대상으로 해석(resolve) 하여, 짧은 후속 질문도 독립적인 완전한 쿼리로 확장합니다.

4️⃣ View Query — 패턴 매칭 고속 경로

미리 정의된 VIEW 패턴(v_current_month_expenses 등 8개)과 키워드를 매칭하여, SQL 생성 없이 Supabase VIEW를 직접 조회합니다:

const VIEW_PATTERNS = {
  'v_current_month_expenses': {
    keywords: ['이번 달 지출', '지출 총액', '얼마 썼', ...],
    description: '이번 달 지출 합계',
    resultType: 'single_value',
  },
  // ... 7개 더
}

“이번 달 지출 얼마야?”처럼 흔한 질문은 LLM SQL 생성을 건너뛰어 레이턴시를 수백ms 절약합니다.

5️⃣ DB Query — NL→SQL 변환

VIEW에 매칭되지 않는 구조화 쿼리는 LLM이 PostgreSQL을 생성합니다. 보안이 핵심입니다:

// 위험한 패턴 차단
const dangerousPatterns = [
  /\bINSERT\s+INTO\b/i, /\bDELETE\s+FROM\b/i,
  /\bDROP\s+(TABLE|INDEX|VIEW)\b/i, ...
]
// org_id 필터 강제
if (!response.toLowerCase().includes('org_id')) {
  return null // 쿼리 거부
}

SELECT만 허용하고, org_id 필터가 없으면 무조건 거부. 서브쿼리도 3개까지만 허용합니다.

6️⃣ Reranker — 4요소 가중 재점수

벡터 검색 결과를 단순 유사도 대신 4개 요소의 가중합으로 재정렬합니다:

const originalScore = result.similarity * 0.4   // 원본 유사도
const keywordScore = keywordDensity * 0.3        // 키워드 매칭
const positionScore = Math.exp(-chunkIndex*0.1) * 0.2  // 위치 보정
const recencyScore = 0.5 * 0.1                   // 최신성 (향후 확장)

Jaccard 유사도 0.8 이상인 중복 청크를 먼저 제거(deduplicateResults)하고, 남은 결과를 재점수합니다.

7️⃣ Context Builder — 프로덕션 프롬프트 구성

검색된 데이터를 계층적 프롬프트로 조립합니다:

[스키마 인식] → [대화 맥락] → [핵심 데이터] → [보조 정보]
→ [사용자 질문] → [인용 목록] → [응답 지침]

“데이터에 없는 정보는 절대 추측하지 마세요”라는 강제 지침이 할루시네이션 방지의 최후 방어선입니다.

8️⃣ Action System — 자연어로 CRUD

“다음 주 수요일 정기회의 잡아줘” → LLM이 구조화 데이터를 추출 → 사용자 확인 → DB 쓰기:

const actionPlan: ActionPlan = {
  type: 'create_meeting',
  table: 'meetings',
  operation: 'insert',
  data: { title: '정기회의', meeting_date: '2026-02-11T14:00:00' },
  preview: buildPreview(actionType, extracted),
  requiresConfirmation: true,  // 반드시 사용자 확인!
}

action-executor.ts는 모든 작업을 감사 로그에 기록하고, 30분 내 실행 취소(undo)를 지원합니다.

검색 품질 평가

assessSearchQuality가 검색 결과의 신뢰도를 0~1 스케일로 평가합니다:

if (dbQueryExecuted) confidence += 0.4  // 권위 있는 소스
if (dbRowCount > 0)  confidence += 0.2  // 실제 데이터 존재
if (vectorResults >= 1) confidence += 0.2
if (avgSimilarity >= 0.7) confidence += 0.2

신뢰도 0.3 미만이면 답변 대신 “근거 부족” 메시지를 반환합니다. 모르면 모른다고 하는 것 — 할루시네이션 방지의 핵심 원칙입니다.

평가 프레임워크

evaluation.ts에 30개 테스트 문항(구조화 15 + 문서 15)이 정의되어 있고, evaluation-runner.ts가 자동 벤치마크를 실행합니다:

  • Context Relevance — 기대 키워드가 검색 결과에 존재하는가?
  • Answer Relevance — 답변이 질문 유형(숫자/목록/날짜)에 맞는가?
  • Faithfulness — 답변이 검색된 컨텍스트에 근거하는가?

마무리

18개 모듈, 4000줄 이상의 코드가 “이번 달 예산 얼마야?”라는 질문 하나에 동원됩니다. 과하다고요? 하지만 정확성·보안·사용자 경험을 포기하지 않으려면, 이 정도의 정교함이 필요합니다 🐧

다음 포스트에서는 학생회 데이터에 최적화된 실전 노하우를 다루겠습니다!


AssoAI는 학생회·동아리를 위한 AI 운영 도구입니다. asso-ai.kr

Comments