ADR-14: AI 기반 스펙 문서 생성 파이프라인
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2026-01-18 | @KubrickCode | worker, web, infra |
배경
문제 정의
Specvital 플랫폼에서 테스트 파일 컬렉션을 사람이 읽을 수 있는 스펙 문서로 자동 변환 필요. 두 가지 인지 작업 포함:
- 분류: 도메인 및 기능별 테스트 그룹화 (의미적 이해)
- 변환: 테스트 이름을 동작 설명으로 변환 (언어 생성)
요구사항
| 요구사항 | 설명 |
|---|---|
| 규모 | 수천 개의 테스트가 있는 리포지토리 처리 (청크당 최대 500개 이상) |
| 비용 | 대량 처리를 위한 비용 효율적 모델 선택 필요 |
| 일관성 | 청크 간 도메인 할당의 일관성 유지 |
| 신뢰성 | 프로덕션 시스템에 장애 허용 및 우아한 저하 필요 |
| 지연 | Gemini 서버 측 5분 타임아웃이 요청당 범위 제약 |
제약사항
| 제약사항 | 영향 |
|---|---|
| PostgreSQL 백엔드 | 기존 River 큐 인프라와 캐싱 통합 |
| Content-Hash 캐싱 | (content_hash, language, model_id)로 문서 인덱싱 |
| Worker 아키텍처 | 기존 워커 서비스 패턴과 통합 필수 |
| 쿼터 시스템 | 캐시 히트 시 사용자 쿼터 미소비 (ADR-13) |
결정
Google Gemini 모델을 사용하는 2단계 AI 파이프라인과 대규모 리포지토리 처리를 위한 순차적 청킹 및 앵커 전파 채택.
파이프라인 아키텍처
Phase 1: 분류 (gemini-2.5-flash)
├── 입력: 테스트 파일 + 도메인 힌트
├── 청킹: 청크당 최대 500개 테스트, 50K 토큰
├── 출력: 도메인 그룹화, 기능 할당, 신뢰도 점수
└── 앵커 전파: 청크 간 도메인 결정 유지
Phase 2: 변환 (gemini-2.5-flash-lite)
├── 입력: 기능별 테스트 배치
├── 동시성: 최대 5개 병렬 API 호출
├── 출력: 테스트 이름 → 동작 설명
└── 폴백: 실패 시 원본 테스트 이름 + 0.0 신뢰도설정
| 파라미터 | 값 | 근거 |
|---|---|---|
| Phase 1 타임아웃 | 270초 | Gemini 5분 서버 제한 이하 |
| Phase 2 전체 타임아웃 | 7분 | 대규모 기능 세트 처리 허용 |
| Phase 2 기능 타임아웃 | 90초 | 부분 성공을 위한 기능별 격리 |
| Phase 2 동시성 | 5 | API 속도 제한과 처리량 균형 |
| 청크 간 지연 | 5초 | 청크 간 속도 제한 준수 |
| 청크당 최대 테스트 | 500 | 504 오류 방지를 위해 1000에서 축소 |
| 청크당 최대 토큰 | 50,000 | 응답 60초 이내 유지 |
| Thinking 모드 | 비활성화 | 구조화된 작업의 비용 최적화 |
신뢰성 패턴
| 메커니즘 | Phase 1 | Phase 2 |
|---|---|---|
| 서킷 브레이커 임계값 | 5회 실패 | 3회 실패 |
| 재시도 횟수 | 3회 | 2회 |
| 백오프 전략 | 지수적 | 지수적 |
| 속도 제한기 | 전역 공유 | 전역 공유 |
캐싱 전략
- 캐시 키:
(content_hash, language, model_id) - 캐시 히트 시 쿼터 소비 없이 기존 문서 반환
- 계층적 스키마:
spec_documents → spec_domains → spec_features → spec_behaviors
검토한 옵션
A. LLM 제공자 선택
| 옵션 | 결론 |
|---|---|
| Gemini 2.5 Flash (선택) | 1M 토큰 컨텍스트, $0.30/1M 비용 효율적, Phase 2용 Flash-Lite |
| Claude 3.5/Opus 4.5 | 우수한 추론이나 $5/1M (17배 비쌈), 200K 컨텍스트 제한 |
| GPT-4o/GPT-5.x | 광범위한 생태계이나 $2-3/1M, 128K 컨텍스트 제한 |
선택 근거:
- 비용 효율성: Flash $0.30/1M, Flash-Lite $0.10/1M 토큰
- 컨텍스트 윈도우: 1M 토큰 용량으로 복잡한 청킹 없이 대규모 테스트 파일 처리
- 작업 적합성: 분류와 변환은 고급 추론이 필요 없는 구조화된 작업
- 2단계 가용성: 분류 품질용 Flash, 대량 변환용 Flash-Lite
B. 파이프라인 아키텍처
| 옵션 | 결론 |
|---|---|
| 2단계 (분류 → 변환) (선택) | 관심사 분리, 다른 비용 프로필, 독립적 실패 도메인 |
| 단일 패스 파이프라인 | 더 단순하나 디버깅 어려움, 전부 아니면 전무 실패 |
| 의미적 분석 포함 다중 패스 | 최고 품질이나 3+ API 호출, 복잡한 오케스트레이션 |
선택 근거:
- 작업 전문화: 분류와 변환은 다른 최적 프롬프팅 전략 보유
- 비용 최적화: Phase 2에서 더 저렴한 Flash-Lite 모델 사용
- 실패 격리: Phase 2 실패 시 Phase 1 분류 손실 없이 폴백 가능
- 독립적 튜닝: 각 단계 개별 최적화 가능
C. 대규모 리포지토리 처리
| 옵션 | 결론 |
|---|---|
| 앵커 전파를 통한 순차 청킹 (선택) | 예측 가능한 메모리, 앵커를 통한 일관된 도메인 할당 |
| 병렬 청킹 | 최대 처리량이나 청크 간 도메인 불일치 |
| 스트림 처리 | 최소 메모리이나 문서 간 컨텍스트 없음 |
선택 근거:
- 일관성: 앵커 도메인이 청크 간 전파되어 "기능 드리프트" 방지
- 속도 제한 준수: 청크 간 지연(5초)이 자연스럽게 API 제한 준수
- 디버깅 용이성: 명확한 청크 경계로 특정 입력 부분집합의 문제 재현 가능
- 메모리 예측 가능성: 고정 청크 크기로 OOM 방지
구현 세부사항
모델 설정
결정론적 출력 설정:
| 파라미터 | 값 | 근거 |
|---|---|---|
| Temperature | 0.0 | 재현 가능한 출력을 위한 무작위성 제거 |
| Seed | 42 | 일관된 분류를 위한 고정 시드 |
| MaxOutputTokens | 65,536 | 잘림 방지를 위한 Gemini 최대값 |
| ResponseMIMEType | application/json | 구조화된 출력 강제 |
| ThinkingBudget | 0 | 동적 Thinking 오버헤드 비활성화 |
토큰 사용량 추적
비용 모니터링을 위한 분석별 실시간 토큰 추적:
go
type TokenUsage struct {
CandidatesTokens int32 // 출력 토큰
Model string // 모델 식별자
PromptTokens int32 // 입력 토큰
TotalTokens int32 // 입력 + 출력 합계
}GenerateContentResponse.UsageMetadata에서 추출analysis_id별 Phase 1/2 호출 전체 집계- 구조화된 로그 출력:
specview_token_usage
근거: Google AI Studio 사용량 통계는 약 1일 지연되어 커스텀 추출 없이는 실시간 리포지토리별 비용 추적 불가능.
청크 크기 진화
504 DEADLINE_EXCEEDED 오류 해결을 위한 점진적 축소:
| 반복 | 테스트/청크 | 결과 |
|---|---|---|
| 초기 | 10,000 | JSON 잘림 |
| v2 | 3,000 | 대규모 리포에서 여전히 504 오류 |
| v3 | 1,000 | 개선되었으나 간헐적 타임아웃 |
| 최종 | 500 | 안정적인 15-25초 처리 시간 |
프롬프트 엔지니어링
Phase 1 (분류):
- 신뢰도 점수화를 통한 분류 제약 조건
- 출력 형식을 위한 언어별 지침
- 분석 결과의 도메인 힌트 통합
Phase 2 (변환):
- "명세 표기법" 스타일: 동작이 아닌 완료 상태
- 예: "사용자가 성공적으로 인증됨" ("사용자가 인증할 수 있어야 함" 아님)
- 다운스트림 필터링을 위한 신뢰도 점수화
오류 처리 흐름
Phase 1 실패
├── 재시도 (지수 백오프로 최대 3회)
├── 연속 5회 실패 후 서킷 브레이커 작동
└── 작업 실패 표시, 부분 결과 없음
Phase 2 실패 (기능별)
├── 재시도 (최대 2회)
├── 폴백: 원본 테스트 이름 + 0.0 신뢰도
└── 다른 기능 처리 계속재시도 가능 오류 패턴:
go
retryablePatterns := []string{
"rate limit", "quota exceeded", "too many requests",
"service unavailable", "internal server error", "timeout",
"connection reset", "connection refused", "temporary failure",
}JSON 파싱 오류 처리: Gemini가 간헐적으로 잘린 JSON 응답 반환. JSON 파싱 오류는 RetryableError로 래핑하여 지수 백오프를 통한 자동 재시도 트리거.
데이터 흐름
┌─────────────────────────────────────────────────────────────────────┐
│ SpecView 생성 파이프라인 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────────────┐ ┌──────────────┐ │
│ │ Analysis │────▶│ Phase 1: 분류 │────▶│ Phase 2: │ │
│ │ 결과 │ │ (gemini-2.5-flash) │ │ 변환 │ │
│ │ + 힌트 │ │ │ │ (flash-lite) │ │
│ └─────────────┘ └─────────────────────┘ └──────┬───────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ PostgreSQL │ │
│ │ spec_documents │ │
│ │ └── spec_domains │ │
│ │ └── spec_features │ │
│ │ └── spec_behaviors │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘결과
긍정적
| 영역 | 이점 |
|---|---|
| 비용 효율성 | $0.10/1M 토큰의 Flash-Lite로 Phase 2 비용 60%+ 절감 |
| 신뢰성 | 서킷 브레이커와 폴백으로 완전한 실패 대신 저하된 운영 보장 |
| 확장성 | 청킹으로 예측 가능한 리소스로 모든 규모의 리포지토리 처리 |
| 캐시 통합 | Content-hash 캐싱으로 중복 처리 및 쿼터 소비 방지 |
| 관측 가능성 | 2단계 분리로 명확한 메트릭 경계 제공 |
| 일관성 | 앵커 전파로 도메인 명명 일관성 유지 |
부정적
| 영역 | 트레이드오프 |
|---|---|
| 지연 | 순차 청킹으로 리포지토리 크기에 비례한 처리 시간 증가 |
| 벤더 종속 | 깊은 Gemini 통합으로 제공자 변경 시 마이그레이션 노력 필요 |
| 지원 중단 위험 | Gemini 2.5 Flash 2026년 6월 지원 중단 예정 |
| 품질 한계 | 복잡한 테스트 계층 구조에서 Gemini 분류 정확도가 Claude보다 낮음 |
| 폴백 품질 | Phase 2 폴백(원본 테스트 이름)은 의미적 개선 미제공 |
| Thinking 비활성화 | 비용 최적화로 잠재적 품질 향상 교환 |
기술적 시사점
| 측면 | 시사점 |
|---|---|
| 스키마 | 계층적: spec_documents → spec_domains → spec_features → spec_behaviors |
| 캐시 키 | (content_hash, language, model_id)로 모델 버전 인식 캐싱 가능 |
| 타임아웃 설계 | 270초 Phase 1 타임아웃으로 Gemini 5분 서버 제한 이하 유지 |
| 동시성 모델 | 전역 공유 속도 제한기가 단계 간 조정 |
| 오류 복구 | 신뢰도 점수화로 저품질 변환의 다운스트림 필터링 가능 |
