같은 LLM 답변에 대해 두 번 비용을 지불하는 것을 멈추다 (그리고 임베딩만으로는 프롬프트를 중복 제거할 수 없는 이유)
요약
LLM 호출 비용 중복 지불 문제와 API 요청 실패 시 발생하는 앱 타임아웃 문제를 해결하기 위해, 작성자는 Rust 기반의 `tokio-prompt-orchestrator` 서버를 개발했습니다. 이 서버는 기존 OpenAI/Anthropic 클라이언트 앞에 배치하여 같은 질문에 대한 모델 재호출을 방지하고, 회로 차단기 및 지출 한도 관리 기능을 제공합니다.
핵심 포인트
- Rust 기반의 `tokio-prompt-orchestrator`를 사용하여 LLM 호출 비용 중복 지불 문제를 해결할 수 있습니다.
- 이 서버는 기존 OpenAI/Anthropic SDK 변경 없이 프록시 형태로 쉽게 통합 가능합니다.
- 단순 유사성 캐싱을 넘어, 의미적 일치(semantic match)를 고려한 고급 중복 제거 기능을 제공합니다.
- 회로 차단기 및 지출 한도 관리 등 안정적인 시스템 운영에 필요한 기능들을 포함하고 있습니다.
저는 같은 LLM 답변에 대해 두 번씩 돈을 지불하고 있었습니다. 에이전트가 재시도하거나, 사용자가 더블 클릭하거나, 두 명의 작업자가 1초 간격으로 동일한 질문을 하면, 각각이 청구서 상에서 새로운 모델 호출로 기록됩니다. 저를 괴롭히던 또 다른 점은 다음과 같습니다. 제공업체(provider)에 문제가 있는 10분 동안 요청들이 실패하는 것이 아니라 쌓이다가 전체 앱에 타임아웃이 연쇄적으로 발생한다는 것입니다.
그래서 저는 제 코드와 모델 사이에 작은 Rust 서버를 두었습니다. 이것은 tokio-prompt-orchestrator라고 불리며, MIT 라이선스이며, 기존의 OpenAI 또는 Anthropic 클라이언트 앞에 아무것도 변경하지 않고 배치할 수 있습니다.
이미 가지고 있는 것 앞에 배치하기
시작하고, 클라이언트의 기본 URL을 이 서버를 가리키도록 설정합니다:
export OPENAI_BASE_URL=http://127.0.0.1:8080/v1
기존 코드는 계속 작동하며, 이제 같은 질문이 두 번 들어와도 두 번째 모델 호출을 하는 대신 중복 제거(dedup) 창에서 답변을 받습니다:
curl -s -i localhost:8080/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Summarize this ticket"}]}'
x-orchestrator-dedup: cached
또한 회로 차단기(circuit breaker) (제공업체가 다운되었을 때 OpenAI의 오류 형식으로 503 반환), 타임아웃, 지출 한도(hit 시 429 반환), 그리고 데드 레터 큐를 제공합니다. Anthropic의 POST /v1/messages 엔드포인트 역시 동일한 처리를 받으므로 공식 Anthropic SDK도 변경 없이 작동합니다.
키가 없어도 모든 것을 시도해 볼 수 있습니다: orchestrator --provider echo는 사용자의 프롬프트로 답변합니다.
흥미로운 부분:
| Pair | 유사도 (Similarity) | 답변 재사용 (Answer reused) |
|---|---|---|
| "What is the capital of France?" / "Which city is the capital of France?" | 0.959 | yes |
| ... | ||
| "Convert 10 miles to kilometers"와 "Convert 10 kilometers to miles"는 0.99의 점수를 받는데, 이는 제가 시도했던 실제 패러프레이즈보다 높은 수치입니다. 순수한 유사성 캐시(pure-similarity cache)라면 한 사용자에게 반대 질문에 대한 답변을 기꺼이 제공할 것입니다. |
따라서 의미적 일치(semantic match) 역시 동일한 숫자와 같은 순서로 공유되는 단어를 유지해야 합니다. 이는 몇 번의 추가적인 모델 호출 비용이 발생합니다 (단어 순서를 변경하는 재작성된 "how do I reverse a list in Python"은 새로운 호출을 받습니다). 하지만 이 방식은 다른 사람의 답변 쪽으로 기울기보다는 두 번째 호출 쪽으로 기울게 만듭니다. 만약 직접 LLM 캐시를 구축하고 있다면, 임계값(threshold)을 낮추기 전에 실제 트래픽에서 히트율을 확인해 보시길 권합니다.
orchestrator --semantic-dedup은 임베더(embedder)를 로컬에서 실행합니다 (fastembed, BGE-small. 128 MB의 일회성 다운로드로 API 키가 필요 없습니다).
추가: 자체 문서로부터 답변 가져오기 (Also: answer from your own docs)
orchestrator --docs ./my-docs는 tantivy(BM25)를 사용하여 마크다운 및 텍스트 폴더를 인덱싱하고, 각 질문 앞에 가장 적합한 구절들을 배치합니다. 검색에 실패하거나 2초 이상 걸리면, 컨텍스트 없이 프롬프트가 진행됩니다: 검색은 요청을 절대 놓치지 않습니다 (retrieval never drops a request).
사용해 보기 (Try it)
- Linux: 의존성 없이 한 줄 설치로 가능
- Windows: 단일 .exe 파일
- macOS 또는 소스에서:
cargo install --locked tokio-prompt-orchestrator --features web-api,tantivy - 또는 Rust 라이브러리로
Repo: https://github.com/Mattbusel/tokio-prompt-orchestrator
Site (실제 실행 과정을 단계별로 재현): https://tokio-prompt-orchestrator.vercel.app/
특히 프로덕션 환경에서 의미적 캐싱(semantic caching)을 운영하는 분들의 의견이 듣고 싶습니다: 어떤 임계값을 사용하시나요, 그리고 어떤 잘못된 히트(false hits)를 경험하셨는지 궁금합니다?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기