2026. 8. 16. 15:04ㆍPROJECT/Toy Project
[LLM Router #4] 학습 목표, 요구사항, Harness 회고
1. 시작하며
앞선 세 글에서는 LLM Router 프로젝트를 시작하기 전에 무엇을 배우고, 무엇을 만들고, 어떤 방식으로 AI와 개발할지를 정리했다.
프로젝트를 시작할 때 가장 중요하게 생각했던 원칙은 이전 프로젝트들과 크게 다르지 않았다.
Baseline
↓
문제 재현
↓
측정 / Evaluation
↓
원인 분석
↓
후보 비교
↓
Human Gate
↓
구현
↓
재평가
그리고 이번에도:
측정하지 않은 개선을 개선이라고 쓰지 않는다.
사용하지 않은 기술을 효과가 없었다고 쓰지 않는다.
를 기준으로 프로젝트를 진행했다.
Phase 9까지 진행한 지금은 처음 작성한 학습 목표와 요구사항, 그리고 Harness 전략이 실제 Repository에서 어느 정도 유지됐는지 돌아볼 수 있게 됐다.
결론부터 말하면 조금 복잡하다.
Harness 전략은 대체로 잘 지켰다.
하지만 프로젝트를 시작할 때 블로그에서 그렸던 LLM Router와 실제 Repository에서 구현한 LLM Router는 처음부터 학습의 중심축이 달랐다.
이번 글에서는 이 차이를 숨기지 않고 그대로 정리해보려고 한다.
2. 최종 구조부터 보면
실제 프로젝트의 최종 구조는 생각보다 단순하게 남았다.
POST /api/v1/chat
↓
Baseline Router
↓
configured default model
↓
model-small
↓
Gemini generateContent
↓
answer / model / provider
Model Catalog에는 두 개의 Model을 두었다.
model-small
→ gemini-2.5-flash
model-large
→ gemini-3.5-flash
Provider
→ GEMINI
즉 최종적으로:
여러 Provider를 동적으로 선택하는 Gateway
가 아니라:
하나의 Provider 안에서
두 Model을 비교하고
Router의 선택을 Evaluation하는 구조
가 됐다.
처음부터 의도한 최종 구조는 아니었다.
하지만 현재 Repository의 REQUIREMENTS.md, ROADMAP.md, DESIGN.md 기준으로 보면 이 구조는 이상한 결과도 아니다.
오히려 #3에서 정리했던:
Phase에 도달했다
≠
특정 기술을 반드시 도입한다
는 원칙을 그대로 따른 결과에 가깝다.
3. 실제로 무엇을 측정했나
이번 프로젝트에서는 Direct Model Evaluation과 Router Evaluation을 분리했다.
Direct Model Evaluation
7 Case
×
2 Model
그리고:
Router Evaluation
7 Case
를 별도로 실행했다.
최종 Checklist 결과는:
model-small
6 / 7 PASS
model-large
6 / 7 PASS
였다.
Router 경로에서는 명확한 정책 위반으로 분류한 ROUTING_FAILURE는 없었다.
다만 하나의 후보는 남았다.
simple-003
JSON 형식 요구
↓
Baseline Default Model
↓
형식 조건 실패
그리고 Reasoning Case 2건에서는:
HTTP 429
가 발생했다.
이 부분은 Routing 실패나 Answer Quality 실패로 섞지 않고 Provider Failure로 별도 분리했다.
이 프로젝트에서 실제로 관찰한 것은 결국:
Routing Decision
Answer Quality
Model Latency
End-to-End Latency
Token Usage
Estimated Cost
Provider Failure
였다.
4. 처음 계획과 실제 프로젝트는 같은 프로젝트였나?
여기가 이번 회고에서 가장 크게 남은 부분이다.
#1과 #2에서 그렸던 프로젝트는 사실 Multi-LLM Gateway에 더 가까웠다.
당시 핵심 질문은 이런 것이었다.
여러 Provider API를 어디까지 공통화할 수 있을까?
Streaming과 Non-streaming은 어떻게 다를까?
Timeout은 어느 정도가 적절할까?
어떤 Error를 Retry해야 할까?
Retry 횟수가 늘어나면
Success Rate / Latency / Provider Call 수는 어떻게 달라질까?
같은 Provider Retry와
다른 Provider Failover는 어떤 차이가 있을까?
Circuit Breaker는 언제 필요한가?
Routing 전략에 따라
Cost와 Latency는 어떻게 달라질까?
즉 흐름을 줄이면:
블로그 #1 / #2
Multi-LLM Gateway
↓
Provider 장애
↓
Streaming / Timeout / Retry / Failover
↓
Cost / Latency / Stability
쪽이었다.
그런데 실제 Repository의 중심은 달라졌다.
Repository
Request
↓
Model 선택
↓
Quality / Cost / Latency 비교
↓
Routing Decision Evaluation
즉 실제로는:
요청 특성에 따라 어떤 Model을 선택하는 것이 적절한가?
를 더 많이 다뤘다.
둘 다 LLM Router라는 이름 안에 들어갈 수는 있다.
하지만 학습 대상으로 보면 같은 프로젝트라고 하기 어렵다.
5. 학습 목표 #1을 기준으로 보면
#1의 최종 목표는 대략 다음과 같았다.
여러 LLM Provider를 하나의 Gateway 뒤에서 일관된 Interface로 사용하고, Latency·Error·Rate Limit·Timeout을 재현한 뒤 Retry·Failover·Circuit Breaker·Routing 전략의 Trade-off를 측정값으로 설명한다.
실제 Repository에서 할 수 있게 된 설명은 조금 다르다.
어떤 요청에서 Routing 결과가 적절했는지, 실패가 발생했다면 어느 Layer의 문제인지 분리하고, 현재 Dataset에서 왜 Baseline Policy를 유지했는지 Quality·Cost·Latency 결과를 기준으로 설명한다.
정리하면:
| 학습 목표 | 실제 결과 |
|---|---|
| Multi-Provider 추상화 | Gemini Client 하나. Provider 공통화 경계를 실제 두 Provider로 검증하지 못함 |
| Unified Request / Response | {message} → {answer, model, provider} 수준의 최소 Baseline |
| Streaming / TTFT | 미구현 |
| Timeout | HTTP Read Timeout과 Error Mapping은 존재. Trade-off 실험은 없음 |
| Retry | 미도입 |
| Failover | 미도입 |
| Circuit Breaker | 미구현 |
| Rate Limit | 실제 429를 관찰하고 RATE_LIMIT으로 분리 |
| Provider Health | 미구현 |
| Routing 전략 비교 | BASELINE_DEFAULT만 사용 |
| Cost | 측정했지만 Runtime Routing 변수로 사용하지 않음 |
| Token | Evaluation / Log에서 추적 |
| Failure Simulator | Mockito Test Double 수준. 반복 가능한 Failure Scenario는 없음 |
| p50 / p95 / p99 / TTFT | 집계 Benchmark 없음 |
| Answer Quality | 오히려 실제 Repository에서 가장 중요한 평가 축 중 하나가 됨 |
이 표만 보면 #1의 학습 목표를 모두 달성했다고 말할 수는 없다.
특히:
Streaming
Retry
Failover
Circuit Breaker
Multi-Provider
Tail Latency
는 실제로 실험하지 못했다.
6. 그래도 답할 수 있게 된 질문도 있다
모든 목표를 놓친 것은 아니다.
몇 가지 질문에는 실제 결과를 가지고 답할 수 있게 됐다.
가장 큰 Model을 사용하는 것이 항상 좋은가?
현재 Dataset에서는 그렇지 않았다.
model-small
6 / 7 PASS
model-large
6 / 7 PASS
Quality Checklist 기준으로 두 Model의 차이는 크지 않았다.
반면 Cost는 큰 차이가 났다.
model-large Cost
≈ model-small의 13.4배
즉 현재 Dataset 기준에서는:
더 비싼 Model
=
더 좋은 결과
라고 말할 근거가 없었다.
Cost를 실제로 측정했는가?
측정했다.
Input Token
Output Token
Estimated Cost
를 Evaluation 결과에서 비교했다.
다만:
Cost-aware Routing
까지 구현한 것은 아니다.
Cost를 관찰 가능한 Metric으로 만든 것과
Cost를 Router의 선택 기준으로 사용한 것은 구분해야 한다.
429가 발생하면 어떻게 되는가?
현재는:
Provider 429
↓
RATE_LIMIT
↓
Failure 반환
이다.
자동 Retry도 없고 Fallback도 없다.
실제 Failure를 관찰했지만
그것만으로 바로 Fallback을 추가하지 않았다.
이 부분은 #3에서 정한 원칙과는 잘 맞았다.
7. 반대로 답하지 못한 질문도 명확하다
다음 질문들은 이번 Repository만으로는 답할 수 없다.
Provider API를 어디까지 공통화할 수 있는가?
Streaming과 Non-streaming 장애 처리는 어떻게 다른가?
Timeout은 몇 초가 적절한가?
어떤 Error를 Retry해야 하는가?
Retry 횟수와
Success Rate / Latency / Provider Call 수는 어떤 관계인가?
같은 Provider Retry와
다른 Provider Failover 중 무엇이 나은가?
Circuit Breaker가 필요한 조건은 무엇인가?
Streaming 중간에 Failover할 수 있는가?
Fixed Routing보다 나은 최종 Routing 전략이 있는가?
이 질문에 답하려면 최소한:
두 번째 Provider
Failure Simulator
반복 가능한 Delay / 429 / 500 Scenario
Streaming
Provider Call Count
Tail Latency
가 필요하다.
이번 프로젝트에는 없다.
따라서:
Multi-Provider Gateway의 안정성과 Resilience까지 검증했다.
라고 쓰면 과장이다.
8. 요구사항 #2와 Repository의 요구사항도 달랐다
이번 프로젝트에는 사실상 두 개의 요구사항이 존재했다.
첫 번째는 블로그 #2에서 작성한 요구사항이다.
Multi-Provider Gateway
Provider Adapter
Streaming
Provider Simulator
Retry
Failover
Circuit Breaker
Benchmark
두 번째는 실제 Repository의:
docs/REQUIREMENTS.md
다.
이쪽은:
Model Catalog
Baseline Router
Routing Decision
Evaluation Dataset
Quality / Cost / Latency
Failure Classification
에 더 가까웠다.
실제 구현은 Repository의 요구사항을 따라갔다.
문제는:
블로그 #2
≠
Repository REQUIREMENTS
상태가 프로젝트 초반부터 존재했다는 것이다.
9. 같은 Baseline이라는 말도 기준에 따라 달라졌다
블로그 #2에서 생각한 Baseline 완료 조건은 꽤 넓었다.
Non-streaming Chat API
잘못된 Request 거절
Unified Response
최소 1 Provider
Provider Adapter
공통 Error
Request ID / Provider Call ID
Call Latency / E2E Latency
Token
Provider Simulator
Success Rate
p50 / p95 / p99
Provider Call Count
실제 Repository Baseline은 다음 정도였다.
Chat API
Validation
Gemini 호출
공통 ErrorCode
requestId
Model Latency
End-to-End Latency
Token
Estimated Cost
Evaluation Dataset
빠진 것을 줄이면:
Provider Call ID
Provider Simulator
Success Rate 집계
p50 / p95 / p99
두 번째 Provider Adapter
다.
그래서:
블로그 #2 기준
→ Gateway Baseline 미완료
이고:
Repository ROADMAP 기준
→ Routing Baseline 완료
→ Phase 9까지 진행
이다.
이번 프로젝트에서 가장 위험했던 지점은 바로 이런 완료 정의의 이중화였다.
10. Repository REQUIREMENTS 안에서는 비교적 정직했다
실제 Repository의 요구사항만 기준으로 보면 결과는 조금 다르다.
Experiment 009 기준으로 대부분의 Baseline 기능은 구현됐다.
충족에 가까웠던 항목:
FR-01 ~ 08
FR-10 ~ 13
FR-15
FR-17
FR-18
비기능 요구사항도:
NFR-01 ~ 03
NFR-05 ~ 07
NFR-09 ~ 13
정도는 실제 구현과 Evaluation으로 확인했다.
반면 일부는 부분 충족이었다.
FR-14
Allowed Model / Max Cost / Max Latency 같은
Expected Condition이 Dataset에 충분히 들어가지 않음
FR-16
Quality Checklist는 있지만
사실 검증까지 하지는 않음
NFR-04
Generation Parameter / 실행 시각 기록이 약함
NFR-14
Provider가 하나라 Provider 종속성 비교가 부족함
명확하게 미충족인 것도 있었다.
FR-09
Capability 기반 후보 제외
NFR-08
Capability 안전성
그리고:
Fallback
Routing 전략 변경
Cascade
는 실패 근거가 충분하지 않아 적용하지 않았다.
이 부분은 요구사항을 억지로 성공 처리하지 않고 그대로 남겼다는 점에서 Harness 원칙을 지킨 결과라고 본다.
11. Harness #3는 실제로 얼마나 지켰나
학습 목표보다 Harness 전략은 실제 Repository와 훨씬 잘 맞았다.
#3에서 정했던 핵심 원칙을 하나씩 보면 대부분 실제 개발 과정에 남아 있다.
| #3 원칙 | 실제 |
|---|---|
| 기술 사용 ≠ 성공 | Semantic Routing / Cascade / Fallback을 넣지 않아도 완료로 판단 |
| Repository를 기억으로 사용 | REQUIREMENTS / DESIGN / ROADMAP / TASKS / Experiment / ADR 유지 |
| 문서 역할 분리 | DESIGN에 미래 구조를 현재 Architecture처럼 넣지 않음 |
| ROADMAP = 문제 확인 순서 | Phase 이름을 기술 도입 목록으로 만들지 않음 |
| Baseline First | Default Model만 선택하는 Router부터 시작 |
| 문제 재현 후 해결 | 429를 보기 전에 Fallback을 미리 넣지 않음 |
| Test ≠ Evaluation | ./gradlew test와 Evaluation Runner를 분리 |
| Dataset 보호 | simple-003 Expected를 구현에 맞춰 완화하지 않음 |
| Expected Model 하나만 정답으로 두지 않음 | Checklist 조건을 이용 |
| Failure Layer 분리 | Routing / Model Quality / Provider Failure 분리 |
| Quality / Cost / Latency 분리 | 하나의 종합 Score로 합치지 않음 |
| Experiment 이름은 문제 중심 | routing-failure-analysis 같은 이름 사용 |
| 사용 안 함 ≠ 효과 없음 | 비교하지 않은 기술은 효과 없다고 쓰지 않음 |
| Git = Experiment History | Phase별 Experiment Commit 유지 |
| Rule ≠ Skill | Skill을 억지로 만들지 않음 |
| Graph / Multi-Agent 기본값 아님 | 사용하지 않음 |
이 부분은 이번 프로젝트에서 가장 잘 지킨 축이다.
12. Baseline을 단순하게 유지한 것은 괜찮았나
최종 Router는 끝까지:
BASELINE_DEFAULT
에 가까운 구조로 남았다.
처음만 보면:
LLM Router 프로젝트인데
Routing 전략을 안 바꿨다?
라고 생각할 수도 있다.
하지만 실제 Dataset을 보면 단순하지 않다.
Direct Model Evaluation에서:
small 6 / 7
large 6 / 7
이었다.
그리고 명확한 정책 위반 Routing Failure도 없었다.
즉 현재 Dataset만 놓고 보면:
Semantic Routing
LLM Routing
Cascade
를 추가해야 할 강한 근거가 나오지 않았다.
따라서:
복잡한 Router를 만들지 않았다
는 실패라기보다 #3의 개발 원칙에 따른 정상 결과라고 볼 수 있다.
다만 여기서 중요한 표현이 있다.
Semantic Routing이 효과가 없어서 사용하지 않았다.
가 아니다.
현재 Dataset과 Failure Case에서는 Semantic Routing 도입을 정당화할 근거가 충분하지 않았다.
가 더 정확하다.
13. 이름만 보고 Large Model이 더 좋다고 판단하지 않았다
이번 프로젝트에서 꽤 의미 있었던 부분이다.
처음에는 자연스럽게:
Simple Request
→ Small Model
Reasoning Request
→ Large Model
같은 구조를 예상할 수 있다.
하지만 실제 Evaluation에서는:
small 6 / 7
large 6 / 7
이었다.
두 Model의 Checklist 차이는 사실상 simple-003의 JSON 형식 Case 정도였다.
반면 비용은 큰 차이가 있었다.
large
≈ small 대비 13.4배 Cost
따라서:
Reasoning
=
무조건 Large
같은 Rule을 바로 넣지 않았다.
처음 세운 가설보다 실제 Dataset을 우선했다는 점에서 이번 Harness가 제대로 작동한 부분이다.
14. 429도 Routing Failure로 만들지 않았다
Reasoning Request 두 건에서는 Router 경로에서 HTTP 429가 발생했다.
결과만 보면:
Reasoning Case 실패
다.
하지만 이를 그대로:
Large Model 선택 실패
또는
Router Quality 실패
로 처리하지 않았다.
실제 Failure Layer는:
Request
↓
Routing
↓
Model 선택
↓
Provider 호출
↓
HTTP 429
였다.
따라서:
PROVIDER_FAILURE
로 분리했다.
ContentOps Agent에서:
Retrieval Failure
≠
Generation Failure
를 분리했던 것과 같은 방식이다.
이번 프로젝트에서는:
Routing Failure
≠
Provider Failure
를 구분했다.
15. Dataset을 Router에 맞게 고치지 않은 것도 중요했다
simple-003은 끝까지 실패 Case 후보로 남았다.
Router나 Model이 현재 Expected를 만족하지 못한다고 해서:
Expected 형식을 완화하거나
Case를 삭제하거나
정답을 현재 출력에 맞추는 방식
으로 Metric을 높이지 않았다.
이전 ContentOps Agent에서도:
retrieval-010
tool-007
tool-008
같은 실패를 Dataset에 남겼던 것과 같은 방식이다.
이번에도:
Evaluation Dataset이 구현을 따라가지 않게 한다.
는 원칙을 지켰다.
16. 반대로 Harness에서도 못 지킨 부분이 있다
Harness를 잘 지켰다고 해서 완벽했던 것은 아니다.
오히려 반복해서 아쉬움으로 남은 부분도 있었다.
16.1 README를 또 후반에 많이 채웠다
#3에서는 이전 프로젝트 회고를 반영해:
README
=
Phase 1부터 관리
하려고 했다.
하지만 실제로는:
실행 방법
curl
Postman
Evaluation 방법
등이 프로젝트 후반에 많이 보강됐다.
즉 README를:
Repository 진입점 Harness
로 보겠다는 생각은 맞았지만 실제 습관은 완전히 바뀌지 않았다.
다음 프로젝트에서는 Baseline Commit 시점에 실행 가능한 README가 이미 있어야 한다.
16.2 ADR도 결정 시점보다 늦게 작성했다
실제 개발 중에는:
Provider 변경
Large Model 변경
Baseline Policy 유지
같은 결정이 있었다.
하지만 ADR 일부는 Phase 9에 가서 정리했다.
docs/adr/
├── 001-use-gemini-provider.md
├── 002-use-gemini-3.5-flash-as-large.md
└── 003-keep-baseline-default.md
판단 과정은 Experiment에 남아 있었지만:
Human Gate
↓
Decision
↓
ADR
가 바로 이어지지는 않았다.
ADR을 판단 과정의 기록으로 사용하려면 결정 직후 작성하는 것이 더 맞다.
16.3 Failure Injection을 통제하지 못했다
이번 프로젝트에서 관찰한 외부 Failure는 꽤 있었다.
OpenAI
credit_balance_exhausted
gemini-2.5-pro
404
gemini-3.5-flash
503
Gemini
429 quota
문제는 모두 실제 외부 상태에 의존했다는 것이다.
오늘 발생한 429를:
내일 같은 조건으로
똑같이 재현
할 수 있다는 보장이 없다.
Mockito를 이용한 429 / 502 / 504 Test는 있었지만 이것은:
Error Mapping 경로 확인
에 가깝다.
다음과 같은 Experiment Harness는 아니었다.
FIXED_DELAY
FAIL_THEN_SUCCESS
INTERMITTENT_FAILURE
연속 500
429 비율 조절
그래서:
Retry 전후 p99
Failover 전후 Success Rate
Circuit Breaker Open 중 Latency
를 같은 조건으로 비교할 수 없었다.
17. Gateway 학습을 하려면 Simulator가 먼저였어야 했다
이번 회고에서 가장 크게 바뀐 생각 중 하나다.
처음에는 실제 Provider를 여러 개 붙이면 Multi-Provider를 배울 수 있다고 생각하기 쉽다.
하지만 실제로는 Provider 상태 자체가 실험 변수가 되어버렸다.
OpenAI
↓
Credit 429
Gemini Model
↓
404
다른 Gemini Model
↓
503 / 429
이런 상태에서는:
Provider 차이
Model 차이
장애 차이
Routing Policy 차이
가 한꺼번에 섞일 수 있다.
Gateway / Resilience 자체를 학습하려면 오히려 live Provider보다 먼저:
Simulator Provider A
Simulator Provider B
를 동일 Interface 뒤에 두는 편이 나았을 것 같다.
예를 들어:
NORMAL
FIXED_DELAY
ALWAYS_500
ALWAYS_429
FAIL_N_TIMES_THEN_SUCCESS
같은 Scenario를 직접 통제했다면 Retry나 Failover를 훨씬 명확하게 비교할 수 있었을 것이다.
18. Streaming이 빠진 것은 꽤 큰 범위 누락이다
#1과 #2에서는 Streaming도 중요한 학습 대상으로 잡았다.
특히 Streaming은 단순히:
응답을 조금씩 내려준다.
정도의 차이가 아니다.
예를 들어:
첫 Token 이전 Timeout
Streaming 중간 Failure
Client Disconnect
일부 응답 전달 후 Failover 가능 여부
같은 문제가 생긴다.
하지만 실제 Repository에는 Streaming 구현과 Experiment가 없다.
따라서 이번 프로젝트에서:
Non-streaming과 Streaming의 장애 처리 차이를 이해했다.
라고 말할 수는 없다.
이 부분은 학습 목표 기준으로 명확하게 미착수다.
19. Latency도 측정은 했지만 Gateway Metric으로 보지는 못했다
현재 Evaluation에는:
modelLatencyMs
endToEndLatencyMs
가 있다.
그래서 Case 단위로는 Model별 Latency를 비교할 수 있다.
하지만 #1에서 보고 싶었던 것은 조금 달랐다.
p50
p95
p99
TTFT
Timeout Threshold
Success Rate
처럼 Gateway의 Tail Latency와 안정성에 가까운 Metric이었다.
이번 프로젝트에서는 이런 집계 Benchmark까지 가지 않았다.
따라서:
Latency를 측정했다
는 맞지만:
Gateway Latency Trade-off를 검증했다
라고 말하기에는 부족하다.
20. Client Request와 Provider Call도 분리하지 못했다
#2에서 중요하게 봤던 개념 중 하나가:
Client Request
≠
Provider Call
이었다.
현재는 Retry나 Failover가 없기 때문에:
1 Client Request
=
1 Provider Call
이다.
그래서 큰 문제가 보이지 않는다.
하지만 Retry가 추가되면:
1 Client Request
↓
Provider Call #1
↓ 실패
Provider Call #2
↓ 실패
Provider Call #3
↓ 성공
처럼 달라진다.
이때 Provider Call 수는 바로:
Cost
Latency
Rate Limit
과 연결된다.
현재는 requestId는 있지만 별도의 providerCallId와 Call Count를 Runtime 관측 구조로 만들지 않았다.
Gateway / Resilience를 이어간다면 가장 먼저 보강해야 할 부분이다.
21. Evaluation Dataset도 목적이 섞였다
현재 Dataset은:
Simple 3
General 2
Reasoning 2
구조다.
그리고 Quality는:
Keyword
형식 조건
Checklist
등을 이용해 평가했다.
이 구조는 Routing Quality를 확인하는 Dataset으로는 의미가 있었다.
하지만 처음 #1에서 생각했던 Gateway Benchmark는 조금 달랐다.
예를 들면:
짧은 응답
긴 응답
긴 Input
JSON 생성
긴 Output
같은 Case에서:
Success Rate
Latency
Token
Provider Call Count
를 보는 쪽에 가깝다.
지금 돌아보면 두 Dataset을 처음부터 분리하는 편이 더 좋았을 것 같다.
Gateway Benchmark Dataset
→ Short / Long / Stream / Delay
→ Success Rate / Latency / Token
Routing Quality Dataset
→ Simple / General / Reasoning / Capability
→ Quality / Cost / Model Selection
하나의 Dataset으로 두 학습 목표를 동시에 잡으려다 실제로는 Routing Quality 쪽으로 더 기울었다.
22. 이번 프로젝트에서 가장 큰 실수는 기술이 아니었다
처음에는 회고를 하면서:
Multi-Provider를 안 넣었다.
Streaming을 못 했다.
Retry를 안 했다.
같은 점이 가장 큰 부족함처럼 보였다.
하지만 정리하면서 생각이 조금 달라졌다.
가장 큰 문제는 기술 구현량이 아니었다.
블로그 #1 / #2
→ Gateway / Resilience
Repository REQUIREMENTS
→ Routing Quality
처럼 프로젝트의 성공 기준이 두 개 존재했던 것이 더 큰 문제였다.
Harness의 목적 중 하나는:
AI와 개발자가 같은 프로젝트 상태와 같은 성공 기준을 공유하도록 만드는 것
이었다.
그런데 이번에는 Repository 안에서는 일관성을 유지했지만,
Repository 밖에 이미 작성한 블로그 글과 Source of Truth가 달라졌다.
즉 Harness를 Repository 안에서만 잘 관리하고
프로젝트 정의 자체의 변경은 놓쳤다.
23. 다음에는 Scope 변경 자체도 ADR로 남겨야겠다
프로젝트를 하다 보면 처음 계획과 실제 학습 방향이 달라질 수 있다.
그 자체는 문제라고 생각하지 않는다.
오히려 실험을 하다 보면 자연스럽게 더 중요한 문제가 보일 수 있다.
문제는 그 변화가 기록되지 않는 것이다.
이번에는:
Gateway / Resilience 학습
↓
Routing Quality Evaluation
으로 중심이 이동했지만
이를 명시적인 Scope 변경으로 남기지 않았다.
다음에는 이런 변화가 생기면:
현재 목표
변경하려는 목표
왜 변경하는가
기존 요구사항 중 무엇을 포기하는가
새 성공 기준은 무엇인가
를 Human Gate에서 확인하고 ADR로 남기는 편이 맞겠다.
예:
ADR
Scope change:
Multi-Provider Gateway
→ Model Routing Evaluation
그러면 회고 시점에:
어느 기준으로 프로젝트 성공을 판단해야 하지?
를 다시 고르지 않아도 된다.
24. 다음 프로젝트에서 Harness를 어떻게 바꿀까
이번 회고를 기준으로 몇 가지 규칙은 더 명확하게 가져갈 수 있을 것 같다.
1. 블로그와 REQUIREMENTS의 첫 문장을 맞춘다
Blog Goal
=
REQUIREMENTS Project Goal
둘이 달라지면 개발을 시작하지 않는다.
2. Scope가 달라지면 ADR을 남긴다
기존 Scope
↓
변경 이유
↓
새 Scope
↓
포기한 목표
↓
새 성공 기준
을 기록한다.
3. README는 Baseline Commit에 이미 있어야 한다
Baseline 완료
↓
README 실행 가능
상태를 완료 조건에 넣는다.
4. Human Gate 직후 ADR을 작성한다
Failure
↓
Human Gate
↓
Decision
↓
ADR
↓
Implementation
순서를 유지한다.
5. 외부 Failure보다 Simulator를 먼저 만든다
Resilience를 학습하는 프로젝트라면:
live Failure
를 기다리지 않는다.
Simulator
↓
통제 가능한 Failure
↓
Before / After
를 먼저 만든다.
6. Evaluation 반복이 세 번째가 되면 Skill 후보를 본다
이번에도:
Evaluation 실행
결과 집계
Failure 분류
Experiment Markdown 작성
이 여러 번 반복됐다.
다음에는 세 번째 정도 반복되는 순간:
Skill로 빼면 실제로 이득인가?
를 확인해볼 생각이다.
25. 그래서 이번 프로젝트는 실패였나?
그렇게 보지는 않는다.
다만 무엇을 성공으로 보는지 범위를 정확하게 나눠야 한다.
Multi-Provider Gateway / Resilience 관점
부족했다.
Provider 1개
Streaming 없음
Simulator 없음
Retry 없음
Failover 없음
Circuit Breaker 없음
Routing 전략 비교 없음
이기 때문이다.
Model Routing Evaluation 관점
의미 있는 결과가 있었다.
측정 가능한 Baseline을 만들었다.
두 Model을 동일 Dataset으로 비교했다.
Large Model이 이름만큼 압도적이라고 가정하지 않았다.
Quality / Cost / Latency를 분리했다.
429를 Routing Failure와 분리했다.
실패 Dataset을 구현에 맞춰 고치지 않았다.
복잡한 Routing을 넣지 않은 이유를 결과로 남겼다.
Harness 관점
대체로 잘 작동했다.
하지만:
README 작성 시점
ADR 작성 시점
Failure Simulator 부재
Skill 미추출
Blog와 Repository의 SoT 불일치
는 다음 프로젝트에서 보완해야 한다.
26. 정리
LLM Router 프로젝트를 시작할 때는 꽤 많은 것을 해볼 생각이었다.
Multi-Provider
Streaming
Retry
Failover
Circuit Breaker
Routing
Cost
Latency
하지만 실제 Repository에서 가장 많이 다룬 것은:
Request
↓
Model Selection
↓
Quality / Cost / Latency
↓
Routing Evaluation
이었다.
그 결과:
Semantic Routing을 구현했다.
Cascade를 구현했다.
Fallback을 구현했다.
같은 결과는 남지 않았다.
대신 현재 Dataset 기준으로는:
small 6 / 7 PASS
large 6 / 7 PASS
명확한 정책 위반 ROUTING_FAILURE 0
Provider Failure 429 별도 분리
large Cost ≈ small의 13.4배
라는 결과를 가지고 왜 Baseline Policy를 유지했는지 설명할 수 있게 됐다.
이 부분은 #3에서 세운 Harness 원칙과 잘 맞는다.
하지만 동시에:
Multi-Provider Gateway를 검증했다.
Streaming 장애를 다뤘다.
Retry / Failover Trade-off를 비교했다.
Circuit Breaker 필요성을 확인했다.
라고는 말할 수 없다.
이번 프로젝트의 가장 큰 교훈은 결국 기술 하나가 아니었다.
Harness가 Repository 안에서 잘 작동하더라도, 처음 작성한 목표와 Repository의 Source of Truth가 서로 다른 프로젝트를 가리키면 성공 기준 자체가 흔들릴 수 있다.
다음 프로젝트에서는 프로젝트 시작 시점에:
Blog Goal
=
REQUIREMENTS
=
ROADMAP
부터 맞춰놓으려고 한다.
그리고 Scope가 달라진다면 그 변화 자체를 Human Gate와 ADR로 남긴다.
결국 이번에도 남은 원칙은 같다.
기술을 사용했다
≠
문제를 해결했다
코드가 동작한다
≠
품질이 좋다
변경했다
≠
개선됐다
처음 계획했다
≠
끝까지 그 계획이 정답이다
AI에게 구현 속도는 계속 맡길 수 있다.
하지만:
무엇을 배우려고 하는지, 무엇을 성공이라고 판단할지, 그리고 그 기준이 중간에 바뀌었는지까지 개발자가 통제해야 한다.
이번 LLM Router에서는 그 부분까지 Harness의 범위라는 것을 새로 배웠다.
'PROJECT > Toy Project' 카테고리의 다른 글
| [Cloud Ops Lab #2] 요구사항 정의 (0) | 2026.08.16 |
|---|---|
| [Cloud Ops Lab #1] 프로젝트 소개와 학습 목표 (0) | 2026.08.16 |
| [LLM Router #3] 바이브 코딩과 Harness 전략 (0) | 2026.08.16 |
| [LLM Router #2] 요구사항 정의 (0) | 2026.08.16 |
| [LLM Router #1] 프로젝트 소개와 학습 목표 (0) | 2026.08.16 |