Platform RAG 아키텍처
ADR 0021 에서 확정된 "전 tenant 공용 공공 판례·법령 RAG 층" 의 구현 SSoT. 설계 사유·대안 검토는 ADR, 아래는 런타임 구조·파일 위치·일일 운영 흐름 만 다룬다.
Tenant-격리 RAG (ADR 0015 · ADR 0017) 와 병렬 · 독립 으로 동작한다. 두 층 모두 AI 프롬프트에 동시 주입되지만, Firestore 경로·쓰기 권한·리스크 프로파일이 다르다.
두 RAG 층 비교
| 축 | Tenant RAG | Platform RAG |
|---|---|---|
| 출처 | 사무소 사적 OCR 서면 (ADR 0015 → 0017) | 공공 판례·법령 (법제처 · 법고을 · 대법원) |
| Firestore 경로 | tenants/{tid}/legacyDocuments/{id} | publicDocuments/{docId} (tenant 경로 밖) |
| 쓰기 권한 | tenant staff (OCR 승인 플로우) | masterAdmin only (Admin SDK + rules) |
| 읽기 범위 | 해당 tenant AI 만 | 모든 인증 tenant |
| 임베딩 격리 | findNearest 가 tenant 경로로 시작 → 아키텍처상 누출 불가 | 격리 대상 아님 (공공 데이터) |
| Cross-tenant 리스크 | 있음 (엄격 차단 · ADR 0015 핵심) | 없음 (원천적으로 공공) |
1. Firestore 스키마
publicDocuments/{docId} (tenant 읽기 · masterAdmin 쓰기)
sourceType: "court_precedent" | "statute" | "admin_rule" | "commentary"
origin: "beopgoeul_lx" | "law_go_kr" | "scourt_openapi" | "aihub_71723" | "manual"
title: string
content: string (임베딩 소스 · stripLawGoKrHtml 정제)
sourceUrl?: string (원본 링크)
tags: string[]
precedentMeta?: { court, caseNumber, decisionDate, caseName, holdingSummary, citedStatutes }
statuteMeta?: { lawName, articleNumber, effectiveDate, status }
embedding?: { vector[768], modelVersion, dims, generatedAtIso, chunkCount }
embeddedAt: Timestamp | null
embeddingError: string | null
uploadedByEmail: string
linkedRawDocId: string | null (publicDocumentsRaw 연결키)
createdAt: Timestamp
updatedAt: Timestamp
publicDocumentsRaw/{rawId} (masterAdmin 읽기 · Admin SDK 만 쓰기)
source: "law_go_kr" | ...
targetType: "prec" | "law"
nativeId: string (법제처 판례일련번호 등)
fetchedAtIso: string
raw: object (API 원본 JSON — LLM/임베딩 마이그레이션 대비)
linkedPublicDocId: string
schemaVersion: number
createdAt: Timestamp
publicDocumentsRaw 는 재파싱 복구용. 파싱 규칙 · HTML 정제 로직 · 신규 필드가 바뀌어도 원본이 있으면 네트워크 재호출 없이 publicDocuments 전부 재구축 가능 (scripts/rebuild-public-docs-from-raw.ts).
2. Firestore rules
firestore.rules 관련 블록:
match /publicDocuments/{docId} {
allow read: if isAuthenticated();
allow write: if isAuthenticated() &&
exists(/databases/$(database)/documents/masterAdmins/$(request.auth.token.email));
}
match /publicDocumentsRaw/{rawId} {
allow read: if isAuthenticated() &&
exists(/databases/$(database)/documents/masterAdmins/$(request.auth.token.email));
allow write: if false; // Admin SDK 만
}
match /publicRagScheduleLog/{docId} {
allow read: if isAuthenticated() &&
exists(/databases/$(database)/documents/masterAdmins/$(request.auth.token.email));
allow write: if false; // Cloud Function Admin SDK 만
}
firestore-rules-invariants.test.ts 가 이 블록들을 불변조건으로 assert — 누군가 실수로 write 권한을 tenant 에 열면 빌드 실패.
3. 데이터 수집 파이프라인
[법제처 API] ──fetch──▶ data/public-rag-raw/by-case/{id}.json
│ (로컬 · gitignore)
▼
scripts/verify-downloaded-data.ts (품질 게이트)
│
▼
[로컬 파일] ──upload──▶ publicDocumentsRaw/{rawId} + publicDocuments/{docId}
│
▼
functions/onPublicDocumentCreated (Vertex AI 임베딩)
│
▼
publicDocuments.embedding.vector[768]
│
▼
findNearestPublic (COSINE, ops + tenant 검색 경로)
3.1 수집 트리거 3 종
- ops 수동 1회성 —
apps/ops/app/(ops)/public-rag/의 fetch 버튼fetchAndIngestLawGoKronCall Function — 쿼리·display 지정 단건 수집- 소규모 시험·즉시 갱신 필요 시
- 스크립트 bulk —
scripts/download-law-go-kr-bulk.ts+upload-public-raw-to-firestore.ts- 최초 대량 시드·카테고리 확장·재수집
- bulk ingestion 가이드 절차
- Cloud Scheduler 증분 —
scheduledFetchLawGoKr(매일 02:00 KST)- core 21 카테고리 · page 1 (display=20) 자동 순회
- 멱등 (
court + caseNumber중복 skip) → 신규만 embedding 트리거 - 결과
publicRagScheduleLog/{YYYY-MM-DD}로 집계
3.2 멱등 키
| 유형 | 멱등 키 |
|---|---|
| 판례 | precedentMeta.court + precedentMeta.caseNumber |
| 법령 조문 | statuteMeta.lawName + statuteMeta.articleNumber |
| raw | source + nativeId |
4. 임베딩 파이프라인
Cloud Function: functions/src/on-public-document-created.ts
onCreate trigger
├─ prepareEmbedInput (ADR 0015 와 동일 정제 — apps 측 논리 복제)
│ HTML strip · [ \t]+ → " " · \n{3,} → \n\n · .trim()
├─ Vertex AI text-embedding-004 @ us-central1
│ REST API + GoogleAuth ADC · content 상한 2000 char
├─ 다중 chunk → meanPoolVectors 평균 풀링 → 768-dim 단일 벡터
├─ publicDocuments.embedding.{vector, modelVersion, dims} update
├─ 실패 시 embeddingError 기록 (재시도 UI 에서 복구)
└─ embeddedAt: serverTimestamp (검색 가능 게이트)
관련 상수:
- 모델:
text-embedding-004(upgrade 시scripts/reembed-public-docs.ts --model-version-mismatch로 전 문서 재처리) - 차원: 768
- 거리 메트릭:
COSINE
5. 검색 경로
5.1 Server 측
apps/web/app/(workspace)/cases/_actions/find-related-precedents-action.ts— 에디터 사이드바용 (사건 컨텍스트 기반 자동 쿼리)apps/web/app/(workspace)/library/_actions/search-library-action.ts— /library 독립 검색 (structured 필터 + keyword)apps/web/app/(workspace)/library/_actions/chat-library-action.ts— /library AI 대화 (RAG + Gemini)functions/src/ops-generate-test-doc.ts— masterAdmin 초안 생성 시뮬레이션functions/src/ops-rag-chat.ts— masterAdmin 법률 리서치 대화
공통 규칙: findNearestPublic 를 publicDocuments 루트에서 직접 호출. tenant path 하위가 아니므로 guardedFindNearest 의 테넌트 경로 검증 대상 아님. 대신 Firestore rules 읽기 권한이 경계 역할 (인증된 tenant 만 read).
5.2 프롬프트 병렬 주입
AI 다듬기 · RAG 답변 프롬프트는 tenant RAG 와 Platform RAG 를 라벨로 구분 하여 동시에 전달:
[내 사무실 과거 사건]
{tenant snippet #1}
{tenant snippet #2}
[공공 판례·법령]
[1] 대법원 2024다12345 (2024-03-15): {snippet}
[2] 민법 제750조: {snippet}
6. 운영 관찰성 (ops 콘솔)
apps/ops/app/(ops)/public-rag/page.tsx 의 패널:
| 패널 | 역할 |
|---|---|
PublicRagStatsCard | 총 문서 · 임베딩 성공률 · origin/sourceType 분포 · 최근 7일·30일 업로드 추이 |
PublicRagTestPanel | 자연어 쿼리 → findNearest 결과 상위 K 건 표시 (품질 회귀 감지) |
PublicRagFailurePanel | embeddingError != null 문서 리스트 + 단건 retryPublicDocEmbedding |
PublicRagFetchPanel | 수동 쿼리 실시간 수집 (fetchAndIngestLawGoKr) |
PublicRagDocGenTestPanel | 사건 컨텍스트 → RAG + Gemini 초안 시뮬레이션 |
PublicRagChatPanel | masterAdmin AI 법률 리서치 대화 |
7. 마이그레이션 시나리오
7.1 파싱 규칙 변경 (HTML 정제 강화 등)
# 원본은 publicDocumentsRaw 에 그대로 보존 → 네트워크 재호출 없이 재파싱
NODE_PATH=apps/web/node_modules npx tsx \
scripts/rebuild-public-docs-from-raw.ts --dry-run
content / title / precedentMeta 만 update — embedding·createdAt 보존. content 가 실제로 바뀌면 이후 7.2 재임베딩 필요.
7.2 임베딩 모델 업그레이드 (e.g. text-embedding-004 → 005)
# 1. 모델 상수 변경: functions/src/on-public-document-created.ts + scripts/reembed-public-docs.ts
# 2. 전 문서 재임베딩
NODE_PATH=apps/web/node_modules npx tsx \
scripts/reembed-public-docs.ts --model-version-mismatch
체크포인트 + rate limit 보호. --dry-run 으로 대상 수 확인 후 실제 실행.
7.3 임베딩 실패 누적 배치 재시도
ops 콘솔 PublicRagFailurePanel 은 단건 retryPublicDocEmbedding 호출. 수십 건 이상 쌓이면:
npx tsx scripts/reembed-public-docs.ts --only-failed
7.4 Firestore 데이터 유실
data/public-rag-raw/로컬 백업이 있다면upload-public-raw-to-firestore.ts재실행- 양방향이라
publicDocumentsRaw를 export 해도 복구 가능 (export 스크립트는 필요 시 추가)
8. 파일 위치 한눈에
Cloud Functions
- 임베딩 트리거 —
functions/src/on-public-document-created.ts - 단건 재시도 —
functions/src/retry-public-doc-embedding.ts - 수동 수집 —
functions/src/fetch-law-go-kr.ts - 증분 스케줄 —
functions/src/scheduled-fetch-law-go-kr.ts - ops 초안 생성 —
functions/src/ops-generate-test-doc.ts - ops RAG 대화 —
functions/src/ops-rag-chat.ts - 공용 어댑터 —
functions/src/law-go-kr-adapter.ts
Server Actions (web)
- 에디터 사이드바 검색 —
apps/web/app/(workspace)/cases/_actions/find-related-precedents-action.ts - /library 검색 —
apps/web/app/(workspace)/library/_actions/search-library-action.ts - /library 대화 —
apps/web/app/(workspace)/library/_actions/chat-library-action.ts
Scripts
- 판례 bulk 다운로드 —
scripts/download-law-go-kr-bulk.ts - 법령 조문 다운로드 —
scripts/download-law-go-kr-statutes.ts - 카테고리 매니페스트 —
scripts/law-go-kr-categories.ts - 법령 매니페스트 —
scripts/law-go-kr-statute-list.ts - 원본 업로드 —
scripts/upload-public-raw-to-firestore.ts - 법령 업로드 —
scripts/upload-statutes-to-firestore.ts - 검증 —
scripts/verify-downloaded-data.ts - 재구축 (파싱 변경) —
scripts/rebuild-public-docs-from-raw.ts - 재임베딩 —
scripts/reembed-public-docs.ts - 공용 어댑터 —
scripts/lib/law-go-kr-convert.ts
ops UI
- 페이지 —
apps/ops/app/(ops)/public-rag/page.tsx - 통계 카드 —
apps/ops/app/(ops)/public-rag/_components/PublicRagStatsCard.tsx - 통계 집계 —
apps/ops/app/(ops)/public-rag/_lib/public-rag-stats.ts - 어댑터 —
apps/ops/app/(ops)/public-rag/_lib/adapter-law-go-kr.ts
문서
- ADR —
apps/docs/content/product/decisions/0021-platform-rag.md - 이 문서 (런타임) —
apps/docs/content/architecture/platform-rag.md - bulk 운영 절차 —
apps/docs/content/operations/public-rag-bulk-ingestion.md
9. 비용·규모
초기 시드 (Pack 1 민사 채권):
- 판례 ~4,000 건 (실측 4,054 · 43MB)
- 법령 조문 ~3,600 건 (민법 1337 + 상법 1308 + 기타 · ~20MB)
- 임베딩 호출: 7,600 × text-embedding-004 ≈ $0.2 (1회성)
- Firestore 스토리지: ~70MB raw + ~70MB publicDocuments ≈ 무시
일일 증분 (Cloud Scheduler):
- 21 카테고리 × page 1 × 20건 = 420 API 호출 (법제처 무료)
- 신규 ≤ 20건/일 예상 → 임베딩 ~$0.001/일
10. 다음 단계
- AI Hub 71723 어댑터 — 25만 건 판례 데이터셋 포맷 파서
- Citation 번호 통합 — tenant RAG + Platform RAG 인용을 단일 [1][2] 체계로
- 품질 모니터링 — Platform RAG 인용이 실제 AI 답변에 얼마나 반영되는지 측정 (샘플링 + 수동 평가)
- 저작권 검사 UI —
commentary유형 업로드 시 저작권 범위 체크리스트 (ADR 0021 Minority Report P7)