당신의 LLM 게이트웨이는 프롬프트를 개선할 수 있는 데이터를 버리고 있습니다
요약
Go 언어 기반의 LLM 게이트웨이인 'route-switch'를 소개합니다. 이 도구는 단순한 라우팅을 넘어, 프로덕션 환경의 호출 데이터를 캡처하고 이를 바탕으로 프롬프트를 자동으로 최적화하는 피드백 루프를 제공합니다.
핵심 포인트
- OpenAI 호환 API를 통해 다양한 LLM 제공자 간 라우팅 지원
- 모든 호출을 데이터셋으로 캡처하여 프롬프트 최적화에 활용
- MIPROv2 스타일의 베이지안 최적화 프로세스 적용
- 데이터셋 저장소(SQLite)와 분석 저장소(DuckDB)의 분리 설계
대부분의 LLM 게이트웨이는 라우팅(Routing) 단계에서 멈춥니다. 요청을 받아 제공자(Provider)를 선택하고, 전달한 뒤, 응답을 돌려줍니다. 유용하지만 정적입니다. 당신이 배포한 프롬프트는 영원히 그대로 실행되며, 이를 개선할 수 있는 유일한 사람은 파일을 여는 것을 기억하는 사람뿐입니다.
그동안 당신은 프롬프트를 개선할 수 있는 바로 그 데이터를 생성하고 있습니다. 모든 요청은 프로덕션 환경에서 당신의 프롬프트가 무엇을 만들어냈는지 보여주는 레이블이 지정된 예시(Labelled example)입니다. 대부분의 게이트웨이는 대시보드용으로 이를 로그(Log)에 남긴 뒤 버려버립니다.
route-switch는 이 루프를 완성하는 Go 언어 기반의 게이트웨이입니다. 이 도구는 하나의 OpenAI 호환 API를 통해 여러 제공자 간에 라우팅을 수행하고, 모든 호출을 데이터셋으로 캡처하며, 캡처된 트레이스(Traces)를 대상으로 MIPROv2 스타일의 최적화를 실행합니다. 즉, 즉시 교체 가능한 엔드포인트(Endpoint) 뒤에서 라우팅과 피드백 루프를 결합한 것입니다. MIT 라이선스이며, 리포지토리는 여기에 있습니다: https://github.com/Skelf-Research/route-switch
라우팅은 기본 사양일 뿐, 핵심은 루프입니다
이 게이트웨이는 OpenAI API를 사용하므로, 도입 방법은 베이스 URL(Base URL)을 변경하는 것뿐입니다. gollm 라이브러리를 통해 OpenAI, Anthropic, Google, Ollama, Cohere, Mistral 등을 가로질러 라우팅합니다. 이 부분은 이미 흔한 기능(Commodity)입니다.
차별화된 부분은 프롬프트를 상수가 아닌, 개선되는 대상으로 취급한다는 점입니다. 문서화된 루프는 다음 6단계로 구성됩니다:
- 캡처(Capture): 모든 호출은 프롬프트별 SQLite 데이터베이스에 저장됩니다.
- 부트스트랩(Bootstrap): 최근 기록을 보정 데이터(Calibration data)로 로드합니다.
- 생성(Generate): 제공자 호출을 통해 지시문 후보(Instruction candidates)를 생성합니다.
- 최적화(Optimize): 조합들에 대해 베이지안 최적화(Bayesian optimization)를 실행합니다.
- 평가(Evaluate): 구성된 평가 전략(Evaluation strategy)으로 데이터셋 행을 재실행합니다.
- 배포(Deploy): 최적화된 프롬프트로 게이트웨이를 업데이트합니다.
1단계는 다른 게이트웨이들이 건너뛰는 부분이며, 2단계부터 6단계까지를 가능하게 만드는 핵심적인 부분입니다.
저장소 분리는 흥미로운 설계 선택입니다
두 개의 저장소가 존재하며, 제가 처음 아키텍처를 읽었을 때 범했던 실수는 이 둘을 혼동한 것이었습니다.
**데이터셋 저장소 (dataset store)**는 SQLite이며, dataset.base_path 아래에 프롬프트당 하나의 데이터베이스가 생성됩니다. 여기에는 렌더링된 입력(input), 출력(output), 변수(variables), 비용(cost), 성공 플래그(success flags), 타임스탬프(timestamps)가 기록됩니다. 보관 기간은 dataset.max_records에 의해 제한됩니다. 이것이 학습 자료 (training material)입니다.
**분석 저장소 (analytics store)**는 DuckDB입니다. 요청 및 응답 요약, 지연 시간 (latency), 에러율 (error rates), 비용 (cost)을 수집하며, /status 및 /v1/system/analytics 엔드포인트 (endpoints)를 구동합니다. 이것이 대시보드 (dashboard)입니다.
액세스 패턴 (access patterns)을 이해하고 나면 이 둘을 분리하는 것이 타당함을 알 수 있습니다. 옵티마이저 (optimizer)는 하나의 프롬프트에 대해 개별 행을 다시 재생 (replay)해야 하는데, 이는 SQLite가 잘 처리할 수 있고 프롬프트별로 자연스럽게 파티셔닝 (partitioned)된 상태를 유지하는 포인트 룩업 (point-lookup) 워크로드 (workload)입니다. 반면 분석 (analytics)은 모든 데이터를 가로질러 집계 (aggregate)해야 하며, 이는 DuckDB가 존재하는 이유인 컬럼형 (columnar) 워크로드입니다. 하나의 저장소만 사용했다면 이 작업들 중 하나에서는 성능이 더 나빴을 것입니다.
실행하기
YAML에서 프로바이더 (providers)와 전략 (strategy)을 설정한 다음, 게이트웨이 (gateway)를 시작하세요:
model_providers:
openai:
api_key: "sk-..."
...
./route-switch --config config.yaml --gateway
이제 OpenAI 호환 클라이언트 (OpenAI-compatible client)를 해당 주소로 지정하세요:
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
...
기존의 SDK, 기존의 요청 형태 (request shape), 변경된 URL 하나만 있으면 됩니다. 이것이 바로 테스트 비용을 저렴하게 만드는 요소입니다.
프롬프트가 개선되었다는 것을 어떻게 알 수 있나요?
이것이 제가 가장 관심을 두는 질문입니다. 왜냐하면 "자동 프롬프트 최적화 (automatic prompt optimization)"는 보통 프로젝트들이 모호하게 얼버무리는 부분이기 때문입니다. Route-switch는 세 가지 구체적인 평가 전략 (evaluation strategies)을 제공합니다:
- 유사도 (Similarity): 기본값입니다. 토큰 중첩 (Token overlap) 및 길이 휴리스틱 (length heuristics)을 사용하며, 기본 임계값 (threshold)은 0.7입니다. 창의적이거나 긴 형식의 출력 (long-form output)을 위해 설계되었습니다.
- 정확한 일치 (Exact match): 공백을 제거하고, 이진 방식(pass or fail)으로 판정합니다. 결정론적인 답변 (deterministic answers) 및 유닛 테스트 (unit-test) 스타일의 검증을 위한 것입니다.
- 키워드 일치 (Keyword match): 미리 정의된 키워드를 허용하거나 예상 출력에서 키워드를 도출하며, 존재 여부에 따라 부분 점수를 부여합니다.
프롬프트별로 선택할 수 있으며, 옵티마이저의 조정 가능한 변수 (tunables)들은 마법이 아닌 실제 설정 (config)입니다.
mipro_v2:
num_candidates: 5
num_trials: 20
...
goptuna를 기반으로 하며, 당신이 선택한 전략에 의해 점수가 매겨지는 지시어 후보군 (instruction candidates)에 대한 베이지안 탐색 (Bayesian search)입니다. 최적화 (Optimization)는 원한다면 지속적으로 실행될 수도 있습니다. 게이트웨이에는 OptimizationEnabled와 OptimizationInterval에 의해 제어되는 백그라운드 옵티마이저 (background optimizer)가 있습니다.
이러한 구체성은 설령 당신이 이 게이트웨이를 실행하지 않더라도 훔쳐올 만한 가치가 있는 부분입니다. 점수 함수 (scoring function) 없이는 "내 프롬프트를 최적화해줘"라는 말은 아무런 의미가 없습니다. 세 가지 방식을 명시하고 당신이 선택할 수 있게 하는 것이 이 기능의 정직한 버전입니다.
적합하지 않은 경우
가치는 나중에 나타납니다 (The value is backloaded). 최적화는 보유한 트레이스 (traces)만큼만 성능을 발휘합니다. 첫날에는 최적화할 대상이 아무것도 없습니다. 만약 당신의 프롬프트가 일주일에 열 번 정도만 실행된다면, 이 루프는 파일을 직접 수정하는 사람을 이길 만큼 충분한 신호 (signal)를 수집하지 못할 수도 있습니다. 이 방식은 처리량이 많은 프롬프트에서 효과를 발휘합니다.
최적화는 토큰 비용을 발생시킵니다. 위의 설정을 문자 그대로 읽어보십시오: 20번의 시행 (trials), 5개의 후보 (candidates), 4개의 지시어 후보 (instruction candidates), 그리고 후보를 생성한다는 것은 프로바이더 (provider) 호출을 의미합니다. 이는 이득을 얻기 전, 인퍼런스 (inference) 비용 외에 추가로 발생하는 실제 지출입니다. 품질 향상이 이 비용을 상쇄하려면 충분한 요청 수에 걸쳐 배가되어야 합니다.
기본 평가기 (default evaluator)는 휴리스틱 (heuristic)입니다. 유사도 점수 산정 (Similarity scoring)은 토큰 중첩 (token overlap)과 길이 (length)를 기준으로 하며, 의미론적 판단 (semantic judgment)이 아닙니다. 긴 형식의 출력물에 대해서는 이것이 대략적인 대리 지표 (proxy)가 될 뿐이며, 점수는 높지만 읽기에는 더 나쁜 프롬프트로 최적화될 가능성이 분명히 존재합니다. 정확한 일치 (Exact match) 및 키워드 일치 (keyword match)는 더 날카롭지만, 작업에 확인 가능한 정답이 있는 경우에만 적용됩니다.
광고된 전략 중 하나는 아직 구현되지 않았습니다. README에는 최소 연결 (least-connections) 부하 분산 (load balancing)이 나열되어 있습니다. 하지만 internal/gateway/load_balancer.go에서 해당 케이스는 "연결 추적 (connection tracking)을 구현할 때까지는 당분간 모두를 동일하게 취급함"이라는 주석과 함께 라운드 로빈 (round-robin)을 반환합니다. 라운드 로빈 (round-robin), 가중 라운드 로빈 (weighted round-robin), 성능 기반 (performance-based) 및 랜덤 (random) 방식은 실제로 구현되어 있습니다. strategy: least_connections를 설정하고 무언가를 얻었다고 가정하기 전에 이 사실을 알아둘 가치가 있습니다.
이것은 당신의 핵심 경로 (critical path)에 있습니다. 모든 요청이 이를 통해 흐르며, 데이터셋과 분석 (analytics) 저장소는 이제 당신이 소유하게 된 상태입니다. 단순히 멀티 프로바이더 장애 조치 (failover)가 필요한 트래픽이 적은 앱이라면, 가벼운 프록시 (proxy)를 사용하는 것이 부담이 적습니다.
핵심 요약 (Takeaways)
- 라우팅 (Routing)은 범용적인 기술입니다. 프로덕션 트래픽을 프롬프트별 데이터셋으로 캡처하는 것이 하위의 모든 과정을 가능하게 만드는 핵심입니다.
- 액세스 패턴 (access pattern)에 따라 저장소를 분리하세요. 재생 (replay)을 위한 프롬프트당 SQLite, 집계 분석 (aggregate analytics)을 위한 DuckDB를 사용하십시오.
- 스코어링 함수 (scoring function)를 확인하지 않고 "자동 최적화 (automatic optimization)"를 절대 수용하지 마세요. 이름 없는 평가자 하나보다 이름이 명시된 세 명의 평가자가 더 낫습니다.
- 기능 테이블 (feature table)을 신뢰하기 전에 코드를 읽으세요. 제가 발견한 격차는 단 하나의 파일에서 비롯되었습니다.
리포지토리 (Repo)와 문서는 트리 내 documentation/docs에 있습니다:
https://github.com/Skelf-Research/route-switch
만약 프로덕션 환경에서 트레이스 기반 프롬프트 최적화 (trace-driven prompt optimization)를 실행해 본 적이 있다면, 저는 코드에서는 얻을 수 없었던 수치를 알고 싶습니다. 최적화된 프롬프트가 실제로 직접 수동으로 튜닝한 프롬프트를 이기기 전까지, 얼마나 많은 트레이스 (traces)를 캡처했습니까? 그 임계값 (threshold)이 비즈니스 케이스의 전부이며, 저는 아직 이를 공개한 사람을 보지 못했습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기