RAG 반복 개발의 고통
프로덕션 RAG 시스템을 구축해보신 분이라면 이 사이클을 잘 아실 겁니다. 파라미터 조정, 재인덱싱, 재평가라는 사이클을 무수히 많이 반복하죠. 이 과정은 며칠에서 몇주가 걸리기도 합니다.
이 글은 제가 지난 3월 29일 진행된 🦞 Ralphthon Seoul #2에서 만든 실제 프로젝트 self-rag 에 Ralph Loop과 Arize Skills를 결합해 Claude Code에게 모든 것을 맡겼을 때 무슨 일이 생겼는지에 대한 이야기입니다.
8시간 동안 Loop를 돌린 결과는 다음과 같습니다.
Recall@5가 39%에서 75%로 36% 상승했습니다. 그리고 루프를 닫으며 클코는 여기서 더 올리는 방법들도 가이드해주었습니다. 아마 2-3일 더 돌려놓으면 90%도 꿈은 아닐 거 같다는 생각이 들었습니다. (그래서 지금 돌리고 있습니다.)
Loop가 도는 동안 Claude Code는 인간의 개입없이 Arize에서 진행된 평가 결과에 기반해서 자동으로 LangGraph 에이전트 코드와 Index 기법을 수정했습니다.
CLAUDE.md의 자기개선 루프
Ralph의 각 이터레이션이 하는 일은 CLAUDE.md로 제어됩니다. 핵심은 세 가지입니다: 매 스토리 완료 후 평가, 실패 분석 후 동적 백로그 확장, 그리고 지표 기반 종료 조건.
## 자기개선 루프 (CRITICAL)
스토리를 passes: true로 표시한 후 반드시:
1. scripts/run_experiment.py로 Arize 실험 실행 (recall@1, @5, @10 측정)
2. 실패 패턴 분석: 어떤 쿼리가 실패하는가? 인덱스 문제인가, 에이전트 문제인가?
3. Recall@5 < 80%이면, 실패를 분석하고 prd.json에 새 스토리 추가:
- 인덱스 개선 (청킹, 매핑, 임베딩) → index/ 디렉토리
- 에이전트 개선 (쿼리 확장, 리랭킹, 스코어링) → agent/ 디렉토리
## Stop Condition
- Recall@5 > 80% AND 새 스토리 없음 → <promise>COMPLETE</promise> 출력 후 종료
- Recall@5 ≤ 80% OR 새 스토리 추가됨 → 다음 이터레이션 계속
이 설계 덕분에 Ralph는 한 번 실행하면 목표 달성까지 자율적으로 동작합니다. eval 결과에서 새 스토리를 생성해 PRD를 키우기 때문입니다. 각 이터레이션은 구현 → 평가 → 반성 → 백로그 확장 → 반복의 사이클을 돕니다.
참고로, Ralph는 모든 의미 있는 코드 변경을 즉시 커밋합니다. 여러 변경 사항을 하나의 큰 커밋으로 묶지 않습니다 — 이터레이션 간에 작업이 손실되지 않도록 하기 위함입니다.
Blue/Green 인덱스 패턴
RAG 시스템을 반복 개선할 때 가장 까다로운 부분은 인덱스 변경이 파괴적이라는 것입니다 — index/index.py를 실행하면 인덱스가 완전히 재생성됩니다. 부분 재인덱싱은 scripts/reindex.py로 가능하지만(누락된 패시지만 추가), 청킹 전략이나 매핑이 바뀌면 전체 재인덱싱이 필요합니다.
OpenSearch의 Blue/Green 배포 패턴으로 해결했습니다:
에이전트는 항상
self-ragalias를 통해 쿼리합니다 — 하드코딩된 인덱스명은 절대 사용하지 않습니다Ralph가 인덱싱 전략을 개선하면 새 버전 인덱스를 생성합니다 (
self_ralph_v1,self_ralph_v2, ...)새 인덱스에서
scripts/run_experiment.py로 Arize 실험을 실행한 후, 성능이 개선됐을 때만 alias를 원자적으로 교체합니다이전 인덱스는 즉시 롤백 가능하도록 유지합니다
client.indices.update_aliases(body={
"actions": [
{"remove": {"index": "*", "alias": "ralphton"}},
{"add": {"index": "self_ralph_v12", "alias": "ralphton"}}
]
})
아침이 됐을 때 Ralph는 11개의 인덱스 버전을 만들어뒀습니다. 여러 개가 베이스라인보다 나빴고 프로모션되지 않았습니다. Alias는 항상 가장 성능이 좋은 버전을 가리켰습니다.
아키텍처: 두 노드의 단순함이 핵심입니다
LangGraph 에이전트는 의도적으로 단순하게 설계하였습니다. 그래프 자체는 StateGraph(State)에 단 두 개의 노드만 존재합니다.
retrieve —
OpenSearchRetriever가text-embedding-3-large(1024 dims)로 async kNN 검색을 수행하고, top-k 문서를 반환합니다call_model — 검색된 컨텍스트를 RAG 프롬프트에 넣고, GPT-4o-mini가 답변을 생성합니다
State는 messages (대화)와 docs (검색된 문서)를 가진 dataclass입니다.
이 단순한 구조가 자기개선 루프에 이상적이었습니다 — Ralph가 인덱스 쪽(index/)과 에이전트 쪽(agent/)을 독립적으로 개선할 수 있었기 때문입니다. 청킹 전략을 바꾸려면 index/ 디렉토리만, 쿼리 확장이나 리랭킹을 추가하려면 agent/ 디렉토리만 수정하면 됩니다.
트레이싱은 src/agent/instrumentation.py에서 arize.otel.register + LangChainInstrumentor를 통해 Arize OTel 트레이싱을 등록합니다. 이것이 매 실험의 트레이스를 Arize로 자동 전송하는 기반이 됩니다.
Arize가 결정적인 차이를 만든 이유
제로 설정 평가를 위한 Arize Skills
Arize는 최근 Arize Skills를 출시했습니다 — 코딩 에이전트에게 Arize 워크플로우에 대한 네이티브 지식을 부여하는 사전 구축된 명령어 세트입니다. Claude Code 내에서 아래 명령어로 한번에 설치할 수 있습니다:
claude /plugin marketplace add Arize-ai/arize-skills
claude /plugin install arize-skills@Arize-ai-arize-skills
이를 통해 Claude Code는 평가의 생성과 평가결과를 skill을 통해 접근할 수 있게 됩니다. 그리고 이렇게 Arize Skills로 읽어온 최신 평가 결과는 Claude Code에 의해 리뷰되어 새로운 개선점을 도출하는 데에 사용됩니다.
17번의 이터레이션에서 일관된 실험
모든 평가가 동일한 Arize 실험 러너 — recall_at_1, recall_at_5, recall_at_10 — 를 사용했기 때문에, 17개의 실험 전체에서 결과가 직접 비교 가능했습니다. 코드베이스가 변해도 평가 드리프트가 없었습니다.
Arize UI에서 진행 상황이 실시간으로 보였습니다:
39% ██░░░░░░░░░░░░░ 베이스라인
52% ██████░░░░░░░░░ 400자 청크 재인덱싱 (+13pp)
56% ████████░░░░░░░ RRF/BM25 가중치 튜닝 (+4pp)
58% ████████░░░░░░░ HyDE 멀티 시그널 (+2pp)
63% ██████████░░░░░ 멀티쿼리 변형 (+5pp)
75% ███████████████ 2단계 GPT-4o 리랭킹 (+12pp)
80% ████████████████ 목표
각 실험은 legal-rag-bench 데이터셋에 결과로 기록됐습니다. 75%에 도달했을 때 어떤 변화가 어떤 점프를 이끌었는지 정확히 추적할 수 있었습니다.
Ralph가 실제로 발견한 것들
최종 점수보다 더 가치 있는 결과는 각 이터레이션이 쌓아올린 progress.txt의 인사이트들이었습니다.
성능 향상에 가장 큰 임팩트를 주었던 개선 사항은 청크 사이즈였습니다. 1000 토큰 → 400 토큰으로의 청크 전환만으로 +13pp 개선을 얻었습니다. 2번째 루프 실행 중에 발견됐고, 이후 모든 것의 기반이 됐습니다. 법률 문서 특성상 조항 단위의 짧은 청크가 훨씬 효과적이었습니다.
HyDE는 두 방향으로 작동합니다. 가상 문서 임베딩을 kNN 신호(+2pp)와 BM25 쿼리 신호(+4pp) 양쪽으로 활용하는 것은 예상치 못한 발견이었습니다. BM25 신호가 오히려 더 강했습니다 — 법률 용어의 정확한 키워드 매칭이 중요하기 때문으로 보입니다.
신호 희석은 실재했습니다. RRF 신호를 ~6개 이상 추가했더니 성능이 저하되는 것을 확인하였습니다. Ralph는 10개 이상의 신호로 한 번 실험했다가 recall이 떨어지는 것을 확인했습니다. 이후 이터레이션은 보수적이었습니다.
Cross-encoder 리랭킹은 기대 이하였습니다. ms-marco 모델은 상당한 레이턴시와 함께 +1pp만 추가했습니다. 두 번의 실험 후 사용을 중단했습니다.
LLM 리랭킹이 cross-encoder를 앞섰습니다. 도메인 특화 GPT-4o-mini 프롬프트가 범용 모델을 큰 차이로 앞섰습니다. 이미 call_model 노드에서 GPT-4o-mini를 사용하고 있었기에 리랭킹에도 같은 모델을 활용한 것은 자연스러운 선택이었습니다.
이 중 어느 것도 원래 PRD에 없었습니다. Ralph가 모두 발견하고, progress.txt에 문서화하고, 다음 스토리를 더 똑똑하게 생성하는 데 활용했습니다.
직접 재현하기
사전 준비
시작하기 전에 다음이 필요합니다:
Arize 계정 — arize.com에서 가입합니다 (무료 티어 사용 가능)
Python 3.10+ 및 uv
OpenSearch 인스턴스 (kNN 활성화)
API 키 — OpenAI, Arize, OpenSearch 인증 정보
Claude Code —
npm install -g @anthropic-ai/claude-code로 설치
Arize AX CLI와 Skills도 설치합니다:
# Arize AX CLI 설치 및 설정
pip install arize-ax-cli
ax config set --space-id <your-space-id> --api-key <your-arize-api-key>
# Claude Code에 Arize Skills 플러그인 설치
claude /plugin marketplace add Arize-ai/arize-skills
claude /plugin install arize-skills@Arize-ai-arize-skills
Step 1: 레포 클론 및 의존성 설치
git clone <repo-url> && cd self-rag
# Agent 의존성
cd agent && uv sync --dev && cd ..
# Index pipeline 의존성
cd index && uv sync --dev && cd ..
환경 변수를 설정합니다:
cp agent/.env.example agent/.env
cp index/.env.example index/.env
# 두 .env 파일에 OpenAI, Arize, OpenSearch 인증 정보 입력
Step 2: QA 데이터셋 업로드 및 인덱싱
Claude Code 안에서 arize-skills를 사용해 isaacus/legal-rag-bench의 qa 스플릿을 Arize 데이터셋으로 업로드합니다. Arize UI 내에서 직접 CSV 업로드를 해도 괜찮습니다. 이 데이터셋이 자기개선 루프의 평가 기준이 됩니다.
그 다음 코퍼스를 인덱싱합니다:
cd index && python index.py
Step 3: Ralph 시작
터미널에서 Claude Code를 열고 "run ralph"라고 입력합니다:
claude
> run ralph
이게 전부입니다. Ralph가 LangGraph Agent를 실행하고, 스토리를 실행하고, Arize 실험을 돌리고, 실패를 분석하고, 새 스토리를 추가하고, Recall@5 > 80%를 달성할 때까지 계속합니다. 자러 가시면 됩니다.
핵심 교훈
지표 기반 Stop Condition이 자율성의 열쇠입니다. Ralph가 "Recall@5 > 80% 달성"을 종료 조건으로 삼기 때문에, eval 결과에서 새 스토리를 생성하고 실험과 개선을 반복합니다. 한 번 실행하면 목표 달성까지 자율적으로 돌아갑니다.
동적 PRD 확장이 핵심 메커니즘입니다. 자기개선 루프는 마법이 아닙니다 — eval → 분석 → prd.json에 추가 → 계속. 이 단순한 추가가 유한한 태스크 목록을 적응형 개선 엔진으로 바꿉니다.
Arize Skills가 평가를 쉽게 만듭니다. 일관되고 제로 설정 평가 없이는 하룻밤에 17개의 실험을 실행할 수 없습니다. Skills 덕분에 Ralph는 계측 방법이나 평가 방법을 알아낼 필요 없이 scripts/run_experiment.py를 호출하면 됩니다.
Blue/Green 인덱싱이 두려움 없는 실험을 가능하게 합니다. Alias 기반 롤백 덕분에 Ralph는 공격적인 인덱스 변경을 시도할 수 있었습니다. 여러 실험이 오히려 나빴지만 아무것도 망가지지 않았습니다.
모노레포 구조가 독립적 개선을 가능하게 합니다. index/와 agent/가 분리되어 있어 Ralph가 인덱싱 전략과 에이전트 로직을 독립적으로 실험할 수 있었습니다. 한쪽을 바꿔도 다른 쪽이 깨지지 않습니다.
progress.txt가 코드보다 더 가치 있습니다. 문서화된 인사이트 — 무엇이 작동했고, 무엇이 안 됐고, 왜인지 — 가 17번의 이터레이션에 걸쳐 누적되며 하룻밤 실행의 가장 중요한 산출물이 됐습니다.
다음 단계
목표까지 5pp가 남아있습니다. Ralph의 최종 분석은 두 가지 개선점을 식별했습니다:
도메인 특화 법률 임베딩이 필요한 ~15개 쿼리
추가 리랭킹으로 해결 가능한 ~10 개 근접 미스.
다음 실행은 이번이 멈춘 곳에서 시작될 것입니다 — 같은 progress.txt, 같은 인덱스 버전, Ralph가 식별한 특정 실패 패턴을 타겟으로 한 새 스토리들. scripts/reindex.py로 누락된 패시지만 추가 인덱싱하는 것도 다음 이터레이션의 유력한 전략입니다.
이 실험이 의미하는 바는 간단합니다.
"Ground Truth만 있으면 어떤 시스템이든 최적화할 수 있다."
이것은 올해초부터 이야기된 Closing the loop을 작은 단위에서 수행해본 것입니다. 인간의 개입이 전혀 없이 스스로 개선되는 에이전트.. 올해는 바로 그 시작이라는 것을 이번 실험을 계기로 직접 체감할 수 있었습니다. 실험에 사용한 코드를 공개하였으니 직접 수행해보시고 여러분의 데이터셋으로 마음껏 실험하고 결과를 공유해주시기 바랍니다.
Github: https://github.com/seanlee10/self-rag
감사합니다.