Skip to content

feat: CRAG 개선 루프 순서 재정렬 (analysis → feedback)#10

Draft
dtype2100 wants to merge 2 commits into
masterfrom
cursor/pipeline-loop-order-3e09
Draft

feat: CRAG 개선 루프 순서 재정렬 (analysis → feedback)#10
dtype2100 wants to merge 2 commits into
masterfrom
cursor/pipeline-loop-order-3e09

Conversation

@dtype2100

Copy link
Copy Markdown
Owner

Summary

CRAG 파이프라인과 오프라인 eval 루프를 프로젝트에 맞는 개선 루프 순서로 재정렬했습니다.

analysis → verification → search → test → evaluation → verification → feedback

Runtime (LangGraph) 변경

이전: analyze → rewrite → retrieve → expand/rerank → generate → grounding → retry

이후:

Phase Node
Analysis analyze_query
Verification decide_rewriterewrite_query
Search hybrid_retrieve
Test test_retrieval (expand + rerank + relevance filter)
Evaluation generate_answerrun_judge (신규 연결)
Verification evaluate_grounding
Feedback retry_retrieval / retry_generation / retry_with_policy / finalize_*
  • run_judge가 그래프에 연결되지 않던 문제 수정
  • route_after_feedback로 judge + grounding 기반 피드백 라우팅 통합

Offline eval (make evals) 변경

scripts/run_evals.pyapp/core/improvement_loop.py의 phase 순서를 따릅니다.

  1. clarification (analysis)
  2. retrieval (search)
  3. pytest unit (test)
  4. answer (evaluation)
  5. judge (verification)
  6. feedback golden set (feedback) — 신규 run_feedback_eval.py

Testing

  • 78 tests passing
  • Golden set 5/5 passed
Open in Web Open in Cursor 

cursoragent and others added 2 commits July 8, 2026 16:07
- Split liveness/readiness health endpoints for Kubernetes and Docker
- Enable optional API key auth on data endpoints via API_KEY env var
- Add Prometheus metrics, request ID tracing, rate limiting, security headers
- Register global exception handlers with request_id in error responses
- Add GitHub Actions CI workflow and fix Makefile to use python -m
- Add Docker healthcheck and curl in Dockerfile
- Add integration tests for ops endpoints (73 tests passing)

Co-authored-by: jwlee <dtype2100@users.noreply.github.com>
…→feedback loop

- Define canonical improvement loop in app/core/improvement_loop.py
- Restructure LangGraph: wire run_judge, add test_retrieval phase
- Add unified route_after_feedback router with retry_retrieval/generation paths
- Reorder run_evals.py to match loop phases; add run_feedback_eval.py
- Add unit tests for feedback routing (78 tests passing)

Co-authored-by: jwlee <dtype2100@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cd0e207-6545-4346-aee5-76272974898a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/pipeline-loop-order-3e09

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces robust operational features to the RAG pipeline, including API key authentication, rate limiting, security headers, Prometheus metrics, and structured health/readiness probes. It also refactors the CRAG LangGraph pipeline to follow a structured 'improvement loop' with new nodes and feedback routing. Feedback on these changes focuses on critical performance and security improvements: addressing blocking synchronous calls in asynchronous endpoints, resolving potential memory leaks and proxy IP detection issues in the rate limiter, preventing timing attacks during API key verification, parallelizing dependency checks in health probes, and optimizing the RAG graph to bypass answer generation when no relevant contexts are retrieved.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread app/core/middleware.py
if self._limit <= 0 or request.url.path in _EXEMPT_PATHS:
return await call_next(request)

client_ip = request.client.host if request.client else "unknown"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

현재 구현은 request.client.host를 사용하여 클라이언트 IP를 가져오고 있습니다. 하지만 애플리케이션이 역방향 프록시(Nginx, Traefik, AWS ALB 등) 뒤에 배포되는 경우, 모든 요청의 IP가 프록시 서버의 IP로 인식되어 모든 사용자가 동일한 레이트 리밋 버킷을 공유하게 되는 문제가 발생할 수 있습니다. X-Forwarded-For 또는 X-Real-IP 헤더를 확인하여 실제 클라이언트 IP를 식별하도록 개선하는 것이 좋습니다.

Suggested change
client_ip = request.client.host if request.client else "unknown"
client_ip = request.headers.get("x-forwarded-for")
if client_ip:
client_ip = client_ip.split(",")[0].strip()
else:
client_ip = request.client.host if request.client else "unknown"

Comment thread app/core/middleware.py
def __init__(self, app, limit_per_minute: int) -> None:
super().__init__(app)
self._limit = max(limit_per_minute, 0)
self._windows: dict[str, deque[float]] = defaultdict(deque)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

self._windowsdefaultdict(deque)를 사용하여 메모리에 클라이언트 IP별 요청 타임스탬프를 저장합니다. 하지만 만료되거나 비활성화된 IP 항목을 삭제하는 정리(cleanup) 메커니즘이 없기 때문에, 시간이 지남에 따라 고유 IP가 누적되면서 메모리 누수(Memory Leak)가 발생하고 결국 OOM(Out Of Memory) 에러로 이어질 수 있습니다. 주기적으로 오래된 항목을 정리하거나, cachetools.TTLCache 또는 Redis와 같은 TTL 기반 저장소를 사용하는 것을 권장합니다.

Comment thread app/api/v1/ingest.py
Comment on lines 55 to 56
try:
count = ingest_documents(raw_docs)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

ingest 엔드포인트는 async def로 정의되어 있지만, 내부적으로 동기식 블로킹 I/O 작업인 ingest_documents(raw_docs)를 직접 호출하고 있습니다. 이로 인해 해당 작업이 실행되는 동안 FastAPI의 단일 이벤트 루프가 완전히 차단(block)되어 다른 모든 요청 처리가 대기하게 됩니다. 이를 방지하기 위해 asyncio.to_thread를 사용하여 별도의 스레드 풀에서 실행하도록 수정해야 합니다.

Suggested change
try:
count = ingest_documents(raw_docs)
try:
import asyncio
count = await asyncio.to_thread(ingest_documents, raw_docs)

Comment thread app/api/v1/ingest.py
Comment on lines 87 to 88
try:
results = search_documents(req.query, top_k=req.top_k)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

semantic_search 엔드포인트 역시 async def 내부에서 동기식 블로킹 함수인 search_documents를 직접 호출하여 이벤트 루프를 차단합니다. asyncio.to_thread를 사용하여 비동기적으로 실행하도록 개선해야 합니다.

Suggested change
try:
results = search_documents(req.query, top_k=req.top_k)
try:
import asyncio
results = await asyncio.to_thread(search_documents, req.query, top_k=req.top_k)

Comment thread app/api/v1/chat.py

@router.post("/query", response_model=ChatResponse, tags=["rag"])
async def rag_query(req: ChatRequest) -> ChatResponse:
async def rag_query(req: ChatRequest, _: None = Depends(RequireAPIKey)) -> ChatResponse:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

rag_query 엔드포인트는 async def로 선언되어 있지만, 내부적으로 무거운 LLM 호출과 벡터 검색을 수행하는 동기식 블로킹 함수인 run_chat을 직접 호출하고 있습니다. 이로 인해 RAG 파이프라인이 실행되는 수 초 동안 FastAPI의 이벤트 루프가 완전히 차단되어 동시 요청 처리 성능이 극도로 저하됩니다. 이 엔드포인트를 동기식 def rag_query로 변경하여 FastAPI가 스레드 풀에서 실행하도록 하거나, run_chat 호출부를 asyncio.to_thread로 감싸서 비동기적으로 실행해야 합니다.

Comment thread app/api/v1/health.py
Comment on lines +21 to +23
vector = await check_vectorstore()
redis = await check_redis()
llm = await check_llm()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

health 엔드포인트에서 check_vectorstore(), check_redis(), check_llm() 비동기 함수들을 순차적으로 await 호출하고 있습니다. 이로 인해 전체 헬스체크 응답 시간이 각 체크 시간의 합만큼 늘어나게 됩니다. 이 함수들은 서로 독립적이므로 asyncio.gather를 사용하여 병렬로 실행하면 응답 속도를 크게 개선할 수 있습니다.

    import asyncio
    vector, redis, llm = await asyncio.gather(
        check_vectorstore(),
        check_redis(),
        check_llm(),
    )

Comment thread app/api/v1/health.py
Comment on lines +48 to +50
vector = await check_vectorstore()
redis = await check_redis()
llm = await check_llm()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

readiness 엔드포인트에서도 마찬가지로 의존성 체크를 순차적으로 실행하고 있습니다. asyncio.gather를 사용하여 병렬로 실행하도록 개선하는 것을 권장합니다.

Suggested change
vector = await check_vectorstore()
redis = await check_redis()
llm = await check_llm()
import asyncio
vector, redis, llm = await asyncio.gather(
check_vectorstore(),
check_redis(),
check_llm(),
)

Comment thread app/core/security.py
if not settings.auth_enabled:
return

if api_key != settings.api_key:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

api_key != settings.api_key와 같은 일반적인 문자열 비교 연산은 실행 시간이 문자열의 일치하는 길이에 따라 달라지므로 **타이밍 공격(Timing Attack)**에 취약할 수 있습니다. 보안에 민감한 API 키 검증에는 일정한 시간 동안 비교를 수행하는 secrets.compare_digest를 사용하는 것이 안전합니다.

Suggested change
if api_key != settings.api_key:
import secrets
if not secrets.compare_digest(api_key, settings.api_key):

Comment thread app/core/middleware.py
response.headers.setdefault("X-Content-Type-Options", "nosniff")
response.headers.setdefault("X-Frame-Options", "DENY")
response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
response.headers.setdefault("X-XSS-Protection", "1; mode=block")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

X-XSS-Protection 헤더의 1; mode=block 설정은 현대적인 브라우저에서는 더 이상 지원되지 않거나 오히려 새로운 클라이언트 사이드 취약점을 유발할 수 있어 권장되지 않습니다. 현대적인 보안 표준에 따라 이 헤더를 0으로 설정하여 비활성화하고, 대신 강력한 Content Security Policy(CSP)를 적용하는 것을 권장합니다.

Suggested change
response.headers.setdefault("X-XSS-Protection", "1; mode=block")
response.headers.setdefault("X-XSS-Protection", "0")

Comment thread app/graphs/crag/nodes.py
Comment on lines +137 to +143
filtered = filter_relevant(contexts)
logger.info(
"Test phase: %d → %d contexts passed relevance filter",
len(contexts),
len(filtered),
)
return {"expanded_contexts": filtered}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

test_retrieval 단계에서 검색된 문서들이 관련성 필터(filter_relevant)를 통과하지 못해 expanded_contexts가 빈 리스트가 되는 경우에도, 그래프는 그대로 다음 단계인 generate_answerrun_judge로 진행됩니다. 이로 인해 컨텍스트가 없는 상태에서 무의미한 답변 생성 및 평가를 위해 고비용의 LLM 호출이 연이어 발생하게 됩니다. 관련 문서가 전혀 없는 경우, 답변 생성을 건너뛰고 즉시 쿼리 재작성 및 재검색(feedback/retry) 단계로 분기할 수 있도록 조건부 라우팅을 추가하는 것을 고려해 보세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants