0045: rag_core leaf 마이그레이션 계획 — embedding helpers + comparison_targets routing
0045: rag_core leaf 마이그레이션 계획 — embedding helpers + comparison_targets routing
- Status: accepted (completed by PR-G2 #847 + PR-G3 #861 + PR-G4 #872)
- Date: 2026-05-15
- Related: ADR 0001 · CLAUDE.md Repository map (rag_retrieval / rag_verifier / rag_answer / rag_query 분리) · issue #762
- Deciders: hskim
TL;DR
rag_retrieval이rag_core로의 late-import 두 곳을 가지고 있음 (embedding primitives + comparison_targets)- Plan A: embedding primitives 를 새 leaf
rag_embedding.py로 이동 - Plan B: comparison_targets 를
rag_query직접 import (이미 거기 있음)
2026-05-15 업데이트 (G4): 6개 ADR-0045 leaf 모듈 (
rag_query,rag_retrieval,rag_verifier,rag_answer,rag_embedding,rag_indexing) 모두rag_core로의 0 import 에지 — top-level + function-level. 이 불변량은tests/test_dependency_graph_invariance.py(issue #872) 가 회귀 테스트. back-edge 재도입하는 모든 미래 PR (function-body late-import 포함) 은 CI 실패.
배경
rag_core.py 가 1728 LOC. PR-H1a/b (issue #459 / #461) 와 PR-J1/J2/J3 (issue #465 / #468 / #478) 가 retrieval / verifier / answer / query 를 sibling leaf 모듈로 추출했지만 import 그래프가 아직 깨끗하지 않음: rag_retrieval.py 가 두 함수 내부에서 late-import 로 rag_core 에 reach back.
관찰된 late-import 인벤토리 (2026-05-15, branch main)
| Call site | Late-imported symbols |
|---|---|
rag_retrieval.py:168 |
comparison_targets_for_analysis |
rag_retrieval.py:490 |
DEFAULT_EMBEDDING_MODEL, DEFAULT_HASH_DIM, embed_texts, hashing_embeddings |
call site 2 × 심볼 5. 다른 3개 분리 모듈은 이미 top-level-clean:
rag_query.py,rag_verifier.py,rag_answer.py—rag_coreimport 0 (top-level + late)
CLAUDE.md 자체가 미완성 상태 인정:
“
rag_core.py는 여전히 orchestration + 많은 utilities → cycle 회피 위한 late-import (의존성 그래프 leaf 아님).”
본 ADR 은 cleanup 계획. 실제 코드 마이그레이션은 본 ADR 범위 외 — 별도 PR (GEF loop 의 G2, /Users/hskim/.claude/plans/gleaming-forging-dove.md 추적) 로 land.
왜 둘로 나누나, 하나가 아니라
5개 late-imported 심볼이 두 semantic 그룹으로 나뉨:
- Embedding primitives —
embed_texts,hashing_embeddings,_embed_with_openai,sentence_transformer_cache_available,huggingface_offline,expand_features,EmbeddingResult,DEFAULT_EMBEDDING_MODEL,DEFAULT_HASH_DIM.rag_retrieval.embed_query_for_index,rag_core인덱스 빌드,scripts/build_index.py— 3개 독립 consumer 가 사용. 검색 전용 아님. - Query-analysis 출력 reader —
comparison_targets_for_analysis는 PR-J3 추출의 일환으로 이미rag_query.py:397에 정의;rag_core는 re-export 만. 검색의 late-import 는 stale routing 결정, 누락된 home 아님.
다른 fix (새 leaf 모듈 vs import-source 변경) 라서 별도 PR 에 속함.
결정
Plan A: embedding primitives → 새 leaf 모듈 rag_embedding.py
rag_embedding.py 를 sibling leaf 로 생성, 기존 rag_text_processing.py / rag_metadata_processing.py 패턴 모델링. 이동:
- 상수:
DEFAULT_EMBEDDING_MODEL,DEFAULT_HASH_DIM - Dataclasses:
EmbeddingResult - 함수:
embed_texts,_embed_with_openai,sentence_transformer_cache_available,huggingface_offline,hashing_embeddings,expand_features
Consumer 업데이트:
rag_core.py— top-levelfrom rag_embedding import …; 다운스트림 코드 (tests, eval 스크립트) 안 깨지게 re-export alias 유지rag_retrieval.py— late-import 블록을 top-levelfrom rag_embedding import …로 교체scripts/build_index.py—from rag_core import DEFAULT_EMBEDDING_MODEL를from rag_embedding import …로 교체
Plan B: comparison_targets routing
rag_retrieval.py:168 에서 late-import 를 rag_query 의 top-level import 로 변경:
# old:
from rag_core import comparison_targets_for_analysis # inside function
# new (top-level):
from rag_query import comparison_targets_for_analysis
방향 안전성 검증 (2026-05-15):
$ grep -nE "^from rag_|^import rag_" rag_query.py
# (rag_retrieval 참조 없음)
rag_query 는 이미 leaf; rag_retrieval → rag_query 는 clean DAG 에지.
Sequencing
- Plan B 는 2줄 변경 (import 이동 1 + late-import 삭제 1). 같은 파일 (
rag_retrieval.py) 타겟이고 같은rag_coreback-edge 제거라 Plan A 와 같은 PR 묶음 허용. - Plan A + B 합쳐서 = G2 PR.
검토한 대안
(a) embedding primitives 를 rag_retrieval.py 로 이동
거부: scripts/build_index.py 는 검색 불필요하며 cross-encoder reranker, query expander 등의 import 비용 지불 안 해야 함. Embedding primitives 는 검색 전용 아님.
(b) rag_core 를 정식 home 으로 두고 late-import 를 accepted 로 문서화
거부: CLAUDE.md 가 이미 미완성 작업으로 호출. late-import 는 런타임에는 작동하지만 정적 분석 (IDE go-to-definition, mypy reachability) 패배시키고 신규 contributor 에게 아키텍처 부채 신호. senior-portfolio 가독성 비용.
(c) text/metadata helpers 도 분리하는 단일 big-bang 마이그레이션
거부: text/metadata helpers 는 이미 추출됨 (rag_text_processing.py, rag_metadata_processing.py). embedding 마이그레이션을 존재하지 않는 추가 분리와 묶으면 이득 없이 diff 부풀음. one concern per PR (CLAUDE.md).
(d) 실제 이동 대신 Python __all__ / re-export 사용
거부: re-export 는 cycle 못 깸; rag_core 가 여전히 함수 본체 소유. 전체 목적은 rag_core 를 얇게 만드는 것.
결과
이득
rag_retrieval.py가rag_core에 대해 진정한 leaf — back-edge 없음rag_core.py가 ~150 LOC 축소 (embedding 블록). GEF loop 가 명명한 ~600-LOC orchestration-only 목표로의 stepscripts/build_index.py가 embed 위해 rag_core 불필요 — 인덱스 빌드가 원칙적으로 full retrieval/answer 스택 로딩 없이 실행 가능- IDE / 정적 분석기가 진정한 의존성 그래프 보고
비용
- 유지할 새 모듈 파일 1개. 완화: 기존
rag_*_processing.py패턴 따르므로 온보딩 비용 0 에 가까움 rag_core의 re-export alias 는 그래프 깨끗함 관점에서 dead weight. eval 스크립트 안 깨지도록 첫 마이그레이션에서 의도적 유지; import site audit 후 제거 스케줄링 follow-up ADR 가능
미변경
- ADR 0001 naive-baseline 불변량:
embed_texts/hashing_embeddings시맨틱이 이동 후 byte-identical — G2 PR 은 순수 재배치, logic 변경 아님 - ADR 0003 답변 계약: 미터치 (답변 생성이 embed 안 함)
EMBEDDING_BACKENDenv 계약: 미변경 (env-var dispatch 는embed_texts내부 거주, as-is 이동)
본 ADR 범위 외
- embedding 너머 추가
rag_coreslim-down (ingestion 분리,_RunContext재배치) — GEF plan 의 G3 가 cover rag_core의 re-export shim 제거 — 무기한 연기. 외부 caller (tests/,scripts/,eval/,demo/,api/) 가 shim 의존; deprecate 는 leaf-migration 목표 너머의 별도 breaking 변경- pgvector / Qdrant adapter 구현 — GEF plan 의 F1/F2
Verification
본 ADR 은 plan-only. G2 (#459/#461) 마이그레이션 완료 후 검증 앵커 — rag_retrieval 의 rag_core back-edge 0 불변량은 회귀 테스트가 강제하고, rag_query 로 재배치된 함수는 존재로 확인:
G2 구현 PR 은 다음을 보여야 함:
make smoke통과 (EMBEDDING_BACKEND=hashing, hashing 경로 사용)bash scripts/test.sh통과 (full pytest)make real-eval-delta가 §5b parity 보임 (본 ADR 불변량은 bit-identical embeddings — 모든 델타는 버그)git grep -nE "^\s+from rag_core" rag_retrieval.py rag_query.py rag_verifier.py rag_answer.py가 0 라인 반환- ADR 0001
naive_baseline프리셋 golden 미변경