-
Agent Eval Pipeline: Swiss Cheese Grader 구현 리포트이코에코(Eco²)/Agent 2026. 2. 10. 02:29

https://github.com/eco2-team/backend/pull/545 

KST 기준 02.10 01:24 GitHub 서버 장애로 PR 지연 DATE: 2026-02-10
Author: Claude Code(Opus 4.6), mangowhoiscloud
Scope: apps/chat_worker/ — Eval Pipeline Phase 1+2+3+4
Status: ✅ Phase 4 완료 (Async Fire-and-Forget + 165 tests ALL PASS)
ADR: https://rooftopsnow.tistory.com/276
PRs: #548, #549 (feat/chat-eval-pipeline → develop)
E2E 검증 리포트(internal): docs/reports/eval-pipeline-e2e-verification-report.md
Related
# 문서 링크 ADR-1 Swiss Cheese Model for LLM Evaluation https://rooftopsnow.tistory.com/273 ADR-2 LLM-as-a-Judge 루브릭 설계: 정보이론 관점의 해상도 분석 https://rooftopsnow.tistory.com/274 ADR-3 Chat Eval Pipeline Integration Plan — Expert Review Loop https://rooftopsnow.tistory.com/275 ADR-4 ADR: Chat LangGraph Eval Pipeline https://rooftopsnow.tistory.com/276 Design docs/plans/chat-eval-pipeline-plan.md(v2.2)— Review docs/plans/chat-eval-pipeline-review.md— E2E Report docs/reports/eval-pipeline-e2e-verification-report.md— PRs feat(eval): Chat Eval Pipeline #548, #549
1. Executive Summary
Eco² 채팅 에이전트의 응답 품질을 자동 평가하는 Swiss Cheese 3-Tier Grader 파이프라인을 구현했습니다.
기존
feedback_node는 RAG 검색 품질(pre-generation)만 평가하고, 최종 응답 품질(post-generation)은 측정하지 않았습니다. 단일 LLM Judge에 의존하면 동일 편향이 반복되고, 모델/프롬프트 변경 시 채점 기준 이동(Calibration Drift)을 탐지할 수 없었습니다. 이를 해결하기 위해 3개 독립 계층이 서로 다른 차원을 평가하는 Swiss Cheese Model을 적용했습니다.L1 Code Grader ──→ 결정적 규칙 기반 (< 50ms, 무비용) L2 LLM Grader ──→ BARS 5축 루브릭 + Self-Consistency (1-5s) L3 Calibration ──→ CUSUM 통계적 드리프트 감지 (주기적) │ ▼ Score Aggregator → EvalResult (0-100 연속 점수 + S/A/B/C 등급)핵심 수치
구현 소스 파일 36개 (Phase 1+2: 24 + Phase 3: 11 + Phase 4: 1) BARS 루브릭 프롬프트 6개 테스트 165개 (단위 148 + 통합 17, 전수 통과) 총 코드 규모 ~6,000줄 설계 리뷰 점수 (R5) 99.8 / 100 구현 리뷰 점수 (R2) 97.1 / 100 사용자 체감 지연 0ms (async fire-and-forget) 평가 모델 GPT-5.2
2. 아키텍처 개요
2.1 Async Fire-and-Forget 아키텍처
변경 이유: 초기 구현에서 eval 서브그래프가 메인 그래프 내 answer → eval(블로킹) → END으로 동작하여, 스트리밍 완료 후 eval 실행 시간(~75ms+)만큼 딜레이가 발생했습니다. 업계 조사 결과 LangSmith/Langfuse 등 프로덕션 eval 도구들이 fire-and-forget 패턴을 채택하고 있어, eval 서브그래프를 메인 그래프에서 분리하고 asyncio.create_task로 비동기 실행하도록 전환했습니다.
┌─────────────────────────────────────────────────────────────────────────┐ │ Chat Eval Pipeline — Async Fire-and-Forget │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─ Main Chat Graph ─────────────────────────────────────────────────┐ │ │ │ │ │ │ │ START → intent → [vision?] → router ─┬→ waste ────┐ │ │ │ │ ├→ character ┤ │ │ │ │ ├→ location ─┤──→ answer ──┤ │ │ │ ├→ web_search┤ │ │ │ │ │ └→ general ──┘ │ │ │ │ │ │ │ │ │ │ END ◄─┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ answer_node → done(즉시) → eval(asyncio.create_task) │ │ │ │ │ eval_entry │ │ │ │ │ ┌────┴────┐ │ │ ▼ ▼ │ │ code_grader llm_grader │ │ (L1) (L2) │ │ └────┬────┘ │ │ ▼ │ │ eval_aggregator │ │ │ │ │ eval_decision │ │ │ │ │ calibration (L3) │ │ CUSUM drift │ │ │ │ │ 로그 + 저장 (모니터링 전용) │ │ │ │ 실행 모드: 비동기 fire-and-forget (사용자 지연 0ms) │ │ 용도: 품질 모니터링, Drift 감지 (재생성 없음) │ │ FAIL_OPEN: 평가 실패 시 B-grade(65.0) fallback, 응답 차단 없음 │ │ │ └─────────────────────────────────────────────────────────────────────────┘변경 전 vs 후:
변경 전: answer → eval(블로킹) → END ← 스트리밍 후 딜레이 발생 변경 후: answer → END(즉시) → eval(fire-and-forget) ← 사용자 지연 0ms변경된 파일 (Phase 4+ 아키텍처 전환):
파일 변경 이유 factory.pyEval 서브그래프를 메인 그래프에서 제거, answer → END복원. 메인 그래프에서 eval 서비스 의존 제거 (코드 그래프는 eval을 모른다)dependencies.pyget_eval_subgraph()신규 — eval 서비스 조립을 독립 캐시로 분리. ProcessChatCommand에 eval_subgraph 주입process_chat.py_run_eval_async()추가 — done 이벤트 발행 후asyncio.create_task로 비동기 실행. 실패해도 응답에 영향 없음 (FAIL_OPEN)2.2 Eval Subgraph 내부 구조

LangGraph
SendAPI로 L1/L2/L3를 병렬 팬아웃 실행합니다.route_to_graders()는 상태를 보고 실제로 실행할 노드만Send리스트에 포함합니다:code_grader: 항상 포함llm_grader:llm_grader_enabled == True일 때만calibration_check:should_run_calibration == True일 때만
3. 아키텍처 의사결정: Async Fire-and-Forget 전환
3.1 문제
초기 구현에서 eval 서브그래프가 메인 그래프에 동기적으로 포함되어
answer → eval(블로킹) → END흐름으로 인해 스트리밍 후 딜레이가 발생했습니다.3.2 업계 조사
블로킹 Gate 높음 완전 지원 OpenAI Agents SDK (output guardrail) 청크 인라인 검증 낮음 중간 차단 NeMo Guardrails, Guardrails AI Fire-and-Forget 0 불가 LangSmith Online Evals, Langfuse 주요 발견:
- OpenAI Agents SDK: output guardrail은 항상 블로킹, 스트리밍 중 output guardrail 미지원 (GitHub #495)
- LangSmith / Langfuse: 비동기 fire-and-forget이 기본, 샘플링(5%)으로 비용 관리
- 코딩 에이전트(Claude Code, Copilot, Cursor): post-generation eval 파이프라인은 제품 리서치 및 개발 단계에서 반영
3.3 결정
Async fire-and-forget 채택 (재생성 없음, 모니터링 전용)
- Eval Pipeline 목적을 품질 모니터링 + Drift 감지로 한정
- 재생성은 데이터 축적 후 C grade 빈도 기반으로 재검토
- 프론트엔드 수정 불필요, 사용자 체감 지연 0ms
3.4 재생성(Regeneration) 미채택 이유
- UX 문제: 텍스트 스트리밍이 이미 사용자에게 표시된 상태에서 "이 응답은 품질 미달입니다, 재생성합니다"는 불쾌한 경험
- 업계 사례: LangSmith/Langfuse는 fire-and-forget, Claude Code/Copilot은 post-gen eval이 제품에 포함 X (리서치/디벨롭용 파이프라인 존재, Anthropic Eval의 경우 현 이코에코 Swiss Cheese 모델의 레퍼런스)
- 비용: 재생성은 LLM 호출을 2배로 증가시키며, C등급 빈도가 낮으면 ROI가 맞지 않음
- 모니터링 우선: 데이터 축적 후 C등급 빈도가 높으면 모델/프롬프트 개선으로 근본 해결
4. Clean Architecture 계층 설계
4.1 의존 방향
┌──────────────────────────────────────────────────────────────────┐ │ Dependency Rule │ │ 의존은 반드시 안쪽(Domain)으로만 향한다 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Infrastructure ──▶ Application ──▶ Domain ◀── (순수 Python) │ │ (Adapters) (UseCases) (Entities) │ │ │ │ Infrastructure는 Application Port를 implements │ │ Application은 Domain Service를 uses │ │ │ └──────────────────────────────────────────────────────────────────┘4.2 계층별 구성 요소
Domain Layer — 순수 비즈니스 규칙, 외부 의존 없음
컴포넌트 파일 역할 EvalGradedomain/enums/eval_grade.pyS/A/B/C 등급 Enum, 경계 매핑 AxisScoredomain/value_objects/axis_score.pyBARS 단축 축 VO (frozen) ContinuousScoredomain/value_objects/continuous_score.py0-100 연속 점수 VO CalibrationSampledomain/value_objects/calibration_sample.py보정 샘플 VO EvalScoringServicedomain/services/eval_scoring.py비대칭 가중 합산 로직 Domain Exceptions domain/exceptions/eval_exceptions.py검증 예외 Application Layer — UseCase 오케스트레이션, Port 정의
컴포넌트 파일 역할 EvalConfigapplication/dto/eval_config.pyFeature Flag + 실행 설정 EvalResultapplication/dto/eval_result.py통합 평가 결과 (frozen) BARSEvaluatorapplication/ports/eval/bars_evaluator.pyLLM 평가 Protocol EvalResultCommandGatewayapplication/ports/eval/...결과 저장 Protocol EvalResultQueryGatewayapplication/ports/eval/...결과 조회 Protocol CalibrationDataGatewayapplication/ports/eval/...보정 데이터 Protocol CodeGraderServiceapplication/services/eval/code_grader.pyL1 결정적 평가 LLMGraderServiceapplication/services/eval/llm_grader.pyL2 BARS 평가 ScoreAggregatorServiceapplication/services/eval/score_aggregator.py다층 결과 통합 CalibrationMonitorServiceapplication/services/eval/calibration_monitor.pyL3 CUSUM 드리프트 EvaluateResponseCommandapplication/commands/evaluate_response_command.py3-Tier 오케스트레이터 Infrastructure Layer — 외부 연동 어댑터
컴포넌트 파일 역할 OpenAIBARSEvaluatorinfrastructure/llm/evaluators/bars_evaluator.pyBARSEvaluator 구현체 Pydantic Schemas infrastructure/llm/evaluators/schemas.pyStructured Output 스키마 BARS Prompts (6개) infrastructure/assets/prompts/evaluation/루브릭 앵커 텍스트 eval_node.pyinfrastructure/orchestration/langgraph/nodes/ChatState→EvalState 매핑 eval_graph_factory.pyinfrastructure/orchestration/langgraph/서브그래프 빌더 RedisEvalCounterinfrastructure/persistence/eval/redis_eval_counter.pyRedis INCR 글로벌 요청 카운터 RedisEvalResultAdapterinfrastructure/persistence/eval/redis_eval_result_adapter.pyRedis L2 Hot Storage PostgresEvalResultAdapterinfrastructure/persistence/eval/postgres_eval_result_adapter.pyPostgreSQL L3 Cold Storage CompositeEvalCommandGatewayinfrastructure/persistence/eval/composite_eval_gateway.pyRedis+PG 동시 저장, PG non-blocking CompositeEvalQueryGatewayinfrastructure/persistence/eval/composite_eval_gateway.pyRedis-first, PG fallback JsonCalibrationDataAdapterinfrastructure/persistence/eval/json_calibration_adapter.pyJSON Calibration Set 로더 + 메모리 캐시 Calibration Fixture infrastructure/assets/data/calibration_set.json8 샘플 × 6 Intent, v1.0 V005 Migration migrations/V005__create_eval_schema.sqlchat.eval_results + chat.calibration_drift_log
5. 3-Tier Grader 상세
5.1 L1: Code Grader — 결정적 규칙 기반
비용 0, 지연 < 50ms. 6개의 직교 슬라이스로 응답의 구조적 품질을 검사합니다.
슬라이스 가중치 검사 대상 기준 예시 format_compliance0.15 마크다운 구조, 괄호 매칭 깨진 코드블록 감지 length_check0.15 토큰 수 50 ≤ tokens ≤ 2,000 language_consistency0.15 한국어 비율 ≥ 80% hallucination_keywords0.25 금지 표현 블록리스트 "100% 안전", "아무렇게나 버려도" 등 14개 citation_presence0.15 출처 표기 패턴 "환경부", "출처:", "※" 등 intent_answer_alignment0.15 Intent별 필수 섹션 waste: "분리배출", "방법", "주의" 출력:
CodeGraderResult(scores, passed, details, overall_score)5.2 L2: LLM Grader — BARS 5축 루브릭
BARS(Behaviorally Anchored Rating Scale) 5축을 LLM-as-Judge로 채점합니다.
5축 및 가중치 (ADR-2 기반):Faithfulness 0.30 RAG 컨텍스트 대비 사실 충실도 Relevance 0.25 질문에 대한 관련성 Completeness 0.20 필요 정보의 완결성 Safety 0.15 안전성 (위험/오해 소지) Communication 0.10 소통 품질 (명확성, 자연스러움) waste/bulk_waste Intent에는
HAZARDOUS_WEIGHTS가 적용되어 Safety 가중치가 상향됩니다.편향 완화 전략 (ADR-2 기반):
편향 유형 완화 전략 구현 Position Bias 5축 루브릭 블록 순서를 매 호출마다 셔플 bars_evaluator.pyVerbosity Bias RULERS #6: "길이-품질 비상관" 명시 bars_system.txtScore Instability Self-Consistency (아래 참조) llm_grader.pySelf-Consistency 전략:
BARS 1-5점 중 경계 점수(2, 4)는 등급 변동 리스크가 큽니다. 이 구간에서 추가 N회 독립 평가 후 중앙값을 채택합니다.원본 평가: faithfulness=4 (경계 점수!) 추가 3회: [3, 4, 3] 전체 집합: [4, 3, 4, 3] → 중앙값 = 3 채택5.3 L3: Calibration Monitor — CUSUM 드리프트 감지
Two-Sided CUSUM(Cumulative Sum) 알고리즘으로 채점 기준 이동을 통계적으로 감지합니다.
S⁺(i) = max(0, S⁺(i-1) + (xᵢ − μ₀ − k)) 상향 이동 S⁻(i) = max(0, S⁻(i-1) + (μ₀ − xᵢ − k)) 하향 이동μ₀ 3.0 BARS 5점 척도 기대 평균 k (slack) 0.5 허용 변동폭 h (critical) 4.0 ~3σ 수준 — CRITICAL 경보 h × 0.6 2.4 ~2σ 수준 — WARNING 경고 5.4 Score Aggregator — 다층 결과 통합
L1(Code) + L2(LLM) 결과를 단일
EvalResult로 합산합니다.S 90-100 최고 품질 A 75-89 양호 B 55-74 보통 (FAIL_OPEN 기본값) C 0-54 미달 (로깅, 재생성 없음)
6. 비용 제어 및 안전 장치
6.1 다단계 비용 가드레일
요청 도착 │ ▼ eval_sample_rate ── 랜덤 샘플링 (기본 100%) │ └── 제외 시 → B등급 반환 (FAIL_OPEN) ▼ L1 Code Grader ──── 무비용, 항상 실행 │ ▼ get_daily_cost() ── 일일 비용 체크 (< $50 USD) │ └── 초과 시 → L1-only 모드 (Degraded) ▼ L2 LLM Grader ──── BARS 평가 실행 │ ▼ request_counter ─── N번째 요청마다 L3 실행 (기본 100회) │ ▼ eval_decision ──── 로깅 + 저장 (모니터링 전용, 재생성 없음)6.2 FAIL_OPEN 정책 (ADR-1 기반)
평가 파이프라인의 장애가 서비스에 영향을 주어서는 안 됩니다.
실패 지점 동작 등급 eval_entry 예외 _entry_failed=True→ short-circuitB L2 LLM 타임아웃/예외 빈 dict 반환 → L1-only 모드 - L3 Calibration 예외 calibration_status=None- Aggregation 예외 EvalResult.failed(reason)B 결과 저장 예외 로그만 남김 (non-blocking) - 전체 eval task 예외 _run_eval_asynccatch → 경고 로그만- fire-and-forget이므로 eval 전체가 실패해도 사용자 응답은 이미 전달 완료된 상태입니다.
6.3 실행 모드 3종
모드 L1 L2 L3 응답 영향 용도 sync동기 동기 (5s timeout) — 없음 (fire-and-forget) 실시간 품질 추적 async동기 비동기 (30s) 비동기 없음 Production 모니터링 shadow비동기 비동기 (60s) 비동기 없음 A/B 테스트, 로그만 모든 모드에서 "응답 영향: 없음"으로 통일. fire-and-forget 아키텍처 전환으로 sync 모드에서도 재생성이 발생하지 않습니다.
7. EvalConfig Feature Flags
EvalConfigDTO가 전체 파이프라인의 동작을 제어합니다.설정 기본값 역할 enable_eval_pipelineTrueEval Pipeline 활성화 여부 eval_mode"async"실행 모드 (sync/async/shadow) eval_sample_rate1.0평가 샘플링 비율 (0.0-1.0) eval_llm_grader_enabledTrueL2 LLM Grader 활성화 eval_regeneration_enabledFalseC등급 재생성 (비활성, 미사용) eval_model"gpt-5.2"평가용 LLM 모델 eval_temperature0.1평가 LLM temperature eval_max_tokens1000평가 LLM max tokens eval_self_consistency_runs3Self-Consistency 추가 평가 횟수 eval_cusum_check_interval100N번째 요청마다 Calibration 실행 eval_cost_budget_daily_usd50.0일일 평가 비용 상한 (USD)
8. 파일 구조
migrations/ └── V005__create_eval_schema.sql # ★ Phase 3 신규 apps/chat_worker/ │ ├── domain/ │ ├── enums/ │ │ └── eval_grade.py # EvalGrade (S/A/B/C) 90줄 │ ├── value_objects/ │ │ ├── axis_score.py # AxisScore (frozen VO) 72줄 │ │ ├── continuous_score.py # ContinuousScore (frozen) 65줄 │ │ └── calibration_sample.py # CalibrationSample (frozen) 80줄 │ ├── services/ │ │ └── eval_scoring.py # EvalScoringService 84줄 │ └── exceptions/ │ └── eval_exceptions.py # Domain 예외 47줄 │ ├── application/ │ ├── dto/ │ │ ├── eval_config.py # EvalConfig 51줄 │ │ └── eval_result.py # EvalResult (frozen) 178줄 │ ├── ports/eval/ │ │ ├── bars_evaluator.py # BARSEvaluator Protocol 84줄 │ │ ├── eval_result_command_gateway.py # 결과 저장 Protocol 50줄 │ │ ├── eval_result_query_gateway.py # 결과 조회 Protocol 66줄 │ │ └── calibration_data_gateway.py # 보정 데이터 Protocol 70줄 │ ├── services/eval/ │ │ ├── code_grader.py # L1 Code Grader 498줄 ★ │ │ ├── llm_grader.py # L2 LLM Grader 203줄 │ │ ├── score_aggregator.py # Score Aggregator 179줄 │ │ └── calibration_monitor.py # L3 Calibration 291줄 │ └── commands/ │ ├── evaluate_response_command.py # 3-Tier Orchestrator 305줄 │ └── process_chat.py # ★ Phase 4+: eval_subgraph 주입 + _run_eval_async() │ ├── infrastructure/ │ ├── llm/evaluators/ │ │ ├── schemas.py # Pydantic Schemas 72줄 │ │ └── bars_evaluator.py # OpenAIBARSEvaluator 251줄 │ ├── assets/prompts/evaluation/ │ │ ├── bars_system.txt # System Prompt 21줄 │ │ ├── bars_faithfulness.txt # Faithfulness 앵커 28줄 │ │ ├── bars_relevance.txt # Relevance 앵커 28줄 │ │ ├── bars_completeness.txt # Completeness 앵커 29줄 │ │ ├── bars_safety.txt # Safety 앵커 30줄 │ │ └── bars_communication.txt # Communication 앵커 29줄 │ ├── persistence/ # ★ Phase 3 신규 │ │ └── eval/ │ │ ├── __init__.py # 패키지 exports │ │ ├── redis_eval_counter.py # Global Request Counter 79줄 │ │ ├── redis_eval_result_adapter.py # Redis L2 Hot Storage 115줄 │ │ ├── postgres_eval_result_adapter.py # PostgreSQL L3 Cold 119줄 │ │ ├── composite_eval_gateway.py # Composite Gateway 140줄 │ │ └── json_calibration_adapter.py # JSON Calibration Loader 114줄 │ ├── assets/data/ │ │ └── calibration_set.json # Calibration Fixture (8샘플) 325줄 │ └── orchestration/langgraph/ │ ├── factory.py # ★ Phase 4+: eval 서브그래프 제거, answer → END 복원 │ ├── nodes/eval_node.py # Entry Adapter + 구조화 로깅 135줄 │ └── eval_graph_factory.py # Subgraph Builder + 노드별 로깅 633줄 ★ │ ├── setup/ # ★ Phase 3+4 수정 │ ├── config.py # +14 eval 환경변수 필드 (PG DSN 추가) │ └── dependencies.py # ★ Phase 4+: get_eval_subgraph() 독립 캐시 + ProcessChatCommand 주입 │ ├── tests/unit/ # 17개 파일, 148개 테스트 ├── domain/ │ ├── enums/test_eval_grade.py 14 tests │ ├── services/test_eval_scoring.py 9 tests │ └── value_objects/ │ ├── test_axis_score.py 11 tests │ ├── test_calibration_sample.py 6 tests │ └── test_continuous_score.py 10 tests ├── application/ │ ├── commands/eval/test_evaluate_response.py 8 tests │ └── services/eval/ │ ├── test_code_grader.py 15 tests │ ├── test_llm_grader.py 6 tests │ ├── test_score_aggregator.py 7 tests │ └── test_calibration_monitor.py 6 tests └── infrastructure/ ├── llm/evaluators/test_bars_evaluator.py 6 tests ├── orchestration/langgraph/ │ ├── nodes/test_eval_node.py 5 tests │ └── test_eval_subgraph_keys.py 5 tests └── persistence/eval/ ├── test_redis_eval_counter.py 9 tests ├── test_redis_eval_result_adapter.py 8 tests ├── test_composite_eval_gateway.py 15 tests └── test_json_calibration_adapter.py 8 tests │ └── tests/integration/eval/ # 12개 테스트 └── test_eval_wiring.py # DI wiring + counter + recalibrate stub
9. 테스트 결과
9.1 pytest 실행 결과
$ .venv/bin/python -m pytest apps/chat_worker/tests/ -m eval_unit -v 165 passed in 4.32s ✅9.2 테스트 분포
Domain 5 50 EvalGrade, AxisScore, ContinuousScore, CalibrationSample, EvalScoringService Application 5 42 CodeGrader, LLMGrader, ScoreAggregator, CalibrationMonitor, EvaluateResponseCommand Infrastructure (Phase 1+2) 3 16 OpenAIBARSEvaluator, eval_node, EvalState↔ChatState 키 정합성 Infrastructure (Phase 3) 4 40 RedisEvalCounter, RedisEvalResultAdapter, CompositeEvalGateway, JsonCalibrationAdapter Integration (Phase 3+4) 1 17 DI wiring, counter injection, recalibrate stub, gateway assembly, PG pool wiring 합계 18 165 — 9.3 정적 분석
black (포매팅) ✅ All clean ruff (린트) ✅ All checks passed
10. 전문가 리뷰 결과
10.1 설계안 리뷰 (5 Round)
R1 67 60 75 72 73 69.4 R2 90 85 92 88 91 89.2 R3 96 93 97 95 96 95.4 R4 99 98 99 99 99 98.8 R5 100 100 100 99 100 99.8 ✅ 10.2 구현체 리뷰 (2 Round)
R1 82 80 85 87 82 83.2 R2 97.5 94 100 97 97 97.1 ✅ 10.3 R1→R2 주요 개선 사항
# R1 지적사항 R2 수정 내용 1 route_after_eval클로저에서EvalConfig미참조Config 파라미터 바인딩으로 수정 2 CUSUM h=5.0은 과도 h=4.0 ( 3σ), warning=2.4 (2σ)로 조정3 비용 가드레일 미구현 get_daily_cost()+eval_cost_budget_daily_usd추가4 eval_sample_rate미구현execute()최상단에 샘플링 게이트 추가5 Position Bias 미완화 5축 루브릭 순서 셔플 + RULERS #6 verbosity 비상관 6 EvalResultmutablefrozen=True적용, calibration_status를aggregate()파라미터로 변경7 EvalResult.failed()continuous_score=0.065.0 (B등급 중간값)으로 수정 8 eval_node에서 EvalConfig re-export (계층 누수) __all__에서 제거, 직접 import로 변경9 eval_unit 마커 누락 (11개 파일) 전수 적용 10 except 블록 이중 예외 state.get("job_id")호출 제거
11. 설계 결정 요약
3-Tier Swiss Cheese Model 단일 Grader는 동일 편향의 사각지대 반복 ADR-1 BARS 5점 척도 (1-5) 7점은 정보량 +0.36비트 대비 신뢰도 하락, 3점은 해상도 부족 ADR-2 비대칭 가중치 Faithfulness(0.30)는 환경 도메인에서 안전 직결 ADR-2 Self-Consistency (경계 점수만) 전수 재평가는 비용 3-5배, 경계만 하면 ~1.4배 ADR-2 CUSUM (h=4.0, k=0.5) Shewhart보다 미세 변화 탐지에 유리 ADR-4 FAIL_OPEN (실패 시 B등급) 평가 장애가 서비스에 영향 안 줌 ADR-1 Async Fire-and-Forget 스트리밍 후 딜레이 해소, 업계 표준 (LangSmith/Langfuse) Section 3 재생성 미채택 UX 문제 + 업계 사례 (Claude Code/Copilot 미지원) Section 3.4 GPT-5.2 평가 모델 BARS 5축 채점 정확도/일관성 향상 — Protocol-Based Ports 구조적 서브타이핑으로 테스트 용이 Clean Architecture Skill Send API 병렬 팬아웃 L1/L2/L3 독립 실행으로 지연시간 최소화 ADR-4 5-Round 설계 리뷰 69.4→99.8점 점진적 개선으로 설계 품질 보장 ADR-3
12. SDK Structured Output 점검
BARS Evaluator의 LLM 호출이 SDK 레벨 Structured Output을 사용하는지 점검한 결과입니다.
12.1 호출 체인
OpenAIBARSEvaluator._call_structured(schema=BARSEvalOutput) → LLMClientPort.generate_structured(response_schema=schema) → 어댑터별 SDK-level API 호출12.2 어댑터별 검증 결과
어댑터 SDK 레벨 메커니즘 검증 파일:라인 OpenAI YES 1차: Agents SDK output_type=schema→ 2차: Responses API{type: "json_schema", strict: True}openai_client.py:205,250Gemini YES response_mime_type="application/json"+response_schema=schemagemini_client.py:361LangChain YES beta.chat.completions.parse(response_format=schema)langchain_runnable_wrapper.py:35812.3 결론
갭 없음. 설계안 §3.2.2의 "SDK-level Structured Output 보장" 요구사항을 정확히 충족합니다.
13. Phase별 구현 히스토리
13.1 Phase 1+2: Domain + Application + Infrastructure
- Domain Layer (EvalGrade, Value Objects, EvalScoringService)
- Application Layer (DTOs, Ports, Services, Command)
- Infrastructure (BARS evaluator, rubric prompts, eval subgraph)
- 108개 단위 테스트
13.2 Phase 3: Gateway Adapters + DI Wiring
카테고리 파일 역할 Migration V005__create_eval_schema.sqlchat.eval_results + chat.calibration_drift_log DDL Fixture calibration_set.json8 샘플 × 6 Intent, v1.0-2026-02-10 Counter redis_eval_counter.pyRedis INCR pipeline (INCR + EXPIRE, TTL=2d) Redis Adapter redis_eval_result_adapter.pyL2 Hot Storage (LPUSH, LTRIM, INCRBYFLOAT) PG Adapter postgres_eval_result_adapter.pyL3 Cold Storage (asyncpg pool 주입) Composite composite_eval_gateway.pyRedis+PG 동시 저장, Redis-first 조회, PG non-blocking Calibration json_calibration_adapter.pyJSON→CalibrationSample 변환 + 메모리 캐시 Tests 52개 신규 테스트 adapter 단위 + DI 통합 13.3 Phase 4: Config + PG DI + SSE + Logging + Skill
# 파일 변경 내용 1 setup/config.pyenable_eval_pipeline기본값True+ PG DSN 필드 3개 추가2 setup/dependencies.pyget_eval_pg_pool(),close_eval_pg_pool(),_get_eval_pg_adapter()+ gateway PG adapter 주입3 events/redis_progress_notifier.pySTAGE_ORDER에 "eval": 17추가4 services/progress_tracker.pyPHASE_PROGRESS eval(90,98), NODE_TO_PHASE, NODE_MESSAGES 추가5 commands/process_chat.pydone 이벤트 result에 eval 결과 포함 (초기 구현) 6 nodes/eval_node.pyeval_entry 구조화 로깅 (intent, answer_len, should_calibrate) 7 eval_graph_factory.pycode_grader, llm_grader, aggregator, decision 노드별 결과 로깅 8 .claude/skills/eval-feedback-loop/SKILL.md5-expert 피드백 루프 스킬 신규 생성 13.4 Phase 4+: Async Fire-and-Forget 전환
변경 이유: 스트리밍 후 딜레이 해소. 업계 조사 결과 LangSmith/Langfuse=fire-and-forget, Claude Code/Copilot=post-gen eval 없음.
# 파일 변경 내용 변경 이유 1 factory.pyEval 서브그래프를 메인 그래프에서 제거, answer → END복원메인 그래프의 eval 블로킹 제거, 관심사 분리 2 dependencies.pyget_eval_subgraph()독립 캐시 +ProcessChatCommand에 eval_subgraph 주입eval 서비스 조립을 메인 그래프와 분리 3 process_chat.py_run_eval_async()추가, done 후asyncio.create_task사용자 지연 0ms 보장 4 eval_config.pyeval_model: "gpt-4o-mini"→"gpt-5.2"평가 정확도/일관성 향상 핵심 코드 변경 (
process_chat.py:366-372):# 6. Eval Pipeline (비동기 fire-and-forget) # done 이벤트 발행 후 실행 → 사용자 체감 지연 없음 if self._eval_subgraph is not None: asyncio.create_task( self._run_eval_async(result, log_ctx), name=f"eval-{request.job_id}", )핵심 코드 변경 (
factory.py:642-649):# Eval Pipeline은 비동기 fire-and-forget으로 분리 (process_chat.py에서 실행) # 그래프에서는 항상 answer → END graph.add_edge("answer", END) if eval_config is not None and eval_config.enable_eval_pipeline: logger.info( "Eval pipeline enabled (async fire-and-forget, mode=%s)", eval_config.eval_mode, )
14. Known Limitations
14.1 해결된 제한사항
Phase 제한사항 해결 Phase 1+2 → 3 Gateway 어댑터 미구현 ✅ CompositeEvalGateway Phase 1+2 → 3 DI Wiring 미완성 ✅ 5개 팩토리 함수 Phase 1+2 → 3 Calibration 트리거가 stopgap ✅ RedisEvalCounter Phase 1+2 → 3 Integration Test 미작성 ✅ 17개 DI wiring 테스트 Phase 3 → 4 asyncpg Pool 미주입 ✅ get_eval_pg_pool()Phase 3 → 4 Composite Gateway PG wiring 미완성 ✅ _get_eval_pg_adapter()Phase 4 → 4+ 스트리밍 후 딜레이 ✅ Async fire-and-forget 14.2 현재 제한사항
# 제한사항 영향 해결 계획 1 recalibrate()stubHITL 인프라 미구축 — 경고 로그만 남김 Phase 5+: HITL 재교정 파이프라인 2 pyproject.toml 마커 미등록 eval_unit,eval_regression등 pytest 마커가 공식 등록 안 됨Phase 5 3 E2E 통합 테스트 미작성 실제 Redis/PG 컨테이너 기반 검증 없음 Phase 5+
15. Next Steps (Phase 5+)
- HITL Recalibrate 구현: Calibration Set 재채점 → Baseline 갱신 → Version bump
- pyproject.toml 마커 등록:
eval_unit,eval_regression,eval_capability - E2E 통합 테스트: 실제 Redis/PG 컨테이너 기반 어댑터 검증
- Grafana 대시보드: eval 메트릭 (grade 분포, cost, drift 상태) 시각화
- A/B Test 인프라: shadow 모드로 새 프롬프트/모델 비교 파이프라인
- C등급 빈도 모니터링: 데이터 축적 후 재생성 필요성 재검토
Appendix A: EvalState TypedDict
class EvalState(TypedDict): # ── Input (eval_entry에서 매핑) ── query: str intent: str answer: str rag_context: str | list[str] | None conversation_history: list[dict] feedback_result: dict | None # ── Config (eval_entry에서 주입) ── llm_grader_enabled: bool should_run_calibration: bool eval_retry_count: int # ── Grader 결과 채널 (Annotated + reducer) ── code_grader_result: Annotated[dict | None, priority_preemptive_reducer] llm_grader_result: Annotated[dict | None, priority_preemptive_reducer] calibration_result: Annotated[dict | None, priority_preemptive_reducer] # ── Aggregated Output (ChatState로 전파) ── eval_result: dict | None eval_grade: str | None eval_continuous_score: float | None eval_needs_regeneration: bool eval_improvement_hints: list[str] # ── Internal ── _entry_failed: bool _prev_eval_score: float | NoneAppendix B: ChatState Eval Keys (Layer 8)
# infrastructure/orchestration/langgraph/state.py class ChatState(TypedDict): # ... (기존 Layer 1-7 필드) ... # ── Layer 8: Eval Pipeline ── eval_result: dict[str, Any] | None eval_grade: str | None eval_continuous_score: float | None eval_needs_regeneration: bool eval_retry_count: int eval_improvement_hints: list[str] _prev_eval_score: float | NoneAppendix C: Async Fire-and-Forget 실행 흐름
# process_chat.py — 실행 순서 async def execute(self, request): # 1. 파이프라인 실행 (스트리밍) result = await self._execute_streaming(...) # 2. 토큰 스트림 완료 await self._progress_notifier.finalize_token_stream(...) # 3. done 이벤트 발행 (사용자에게 완료 전달) await self._progress_notifier.notify_stage(stage="done", ...) # 4. Eval Pipeline (비동기 fire-and-forget) # done 이벤트 발행 후 실행 → 사용자 체감 지연 없음 if self._eval_subgraph is not None: asyncio.create_task( self._run_eval_async(result, log_ctx), name=f"eval-{request.job_id}", ) # 5. 즉시 응답 반환 (eval 완료를 기다리지 않음) return ProcessChatResponse(status="completed", ...)
Service
이코에코
frontend.dev.growbin.app
'이코에코(Eco²) > Agent' 카테고리의 다른 글
이코에코(Eco²) LLM Precision 종합 리포트 (0) 2026.02.26 PgBouncer 검토 및 Redis Checkpoint Sync 비교 (1) 2026.01.27 이코에코(Eco²) Agent: LangSmith Telemetry 토큰 추적 보강 (0) 2026.01.27 이코에코(Eco²) Agent: SSE Shard 기반 Redis Pub/Sub 연결 최적화 (1) 2026.01.25 이코에코(Eco²) Agent: OpenAI Agents SDK 전환 및 LLM Client 보강, E2E 검증 (1) 2026.01.25