
LangChain이 오래된 RAG 데이터로 환각을 일으키는 것을 방지하고 토큰 소모를 50% 줄여주는 로컬호스트 프록시를 구축했습니다
요약
RAG 파이프라인에서 오래된 데이터로 인한 환각 문제를 해결하기 위해 KU-Gateway라는 로컬 프록시를 제안합니다. 이 도구는 컨텍스트의 신선도를 점수화하여 노후된 정보를 필터링함으로써 토큰 소모를 줄이고 답변의 정확도를 높입니다.
핵심 포인트
- RAG 시스템의 고질적인 문제인 데이터 신선도(freshness) 문제 해결
- OpenAI 호환 API를 통해 기존 클라이언트에 쉽게 통합 가능
- Knowledge Universe API를 활용한 컨텍스트 신선도 점수 산정
- 불필요한 노후 컨텍스트 제거를 통한 토큰 비용 50% 절감
아무도 해결하지 못한 문제
제가 작업했던 모든 RAG (Retrieval-Augmented Generation) 파이프라인에는 동일한 사각지대가 있었습니다. 문서가 검색 인덱스(retrieval index)에 들어가는 순간, LLM (Large Language Model)은 이를 영원히 절대적인 진리로 취급했습니다. 더 이상 사용되지 않는 API에 대한 2022년 블로그 포스트, 8개월 전에 해결된 GitHub 이슈, 혹은 이후에 내용이 반박된 arXiv 논문 등은 프롬프트(prompt)에 도달할 때쯤이면 "이 정보는 오래되었을 수 있음"이라는 플래그(flag)를 달고 있지 않습니다. 모델은 그 정보가 최신인 것처럼 아주 자신 있게 추론해 버립니다.
이것은 검색(retrieval)의 문제가 아닙니다. 이것은 _신선도(freshness)_의 문제이며, 표준 LLM 애플리케이션 스택 중 그 무엇도 이 문제를 책임지지 않습니다. 그래서 저는 **KU-Gateway**를 구축했습니다. 이는 모델 호출 앞에 위치하는 OpenAI 호환 프록시(proxy)입니다. 이 프록시는 임베딩된 컨텍스트(embedded context)의 모든 조각에 대해 신선도 점수를 매기고, 단 하나의 토큰이라도 상위(upstream)로 전송되기 전에 신뢰하기에 너무 노후화된 것은 모두 제거합니다.
구조
KU-Gateway는 통합하기 의도적으로 매우 단순합니다. 이는 OpenAI와 동일한 /v1/chat/completions 규약을 사용하는 FastAPI 서비스이므로, 기존의 어떤 클라이언트에서도 한 줄의 코드 변경만으로 도입할 수 있습니다:
from openai import OpenAI
client = OpenAI(
...
이 익숙한 인터페이스 뒤에서, 모든 요청은 4단계 파이프라인을 거칩니다:
Your App ──▶ KU-Gateway (:8000) ──▶ LLM Provider (OpenAI / Anthropic / Gemini / local)
│
├─▶ extract <context> blocks from the request
...
점수 산정 자체는 Knowledge Universe API에 위임되며, 이 API는 decay_score (0 = 신선함, 1 = 완전히 노후됨)를 반환합니다. 또한 knowledge_velocity 레이블 (frozen, slow, moderate, fast, 또는 hypersonic 등)과 더 최신의 지식과 명백히 모순되는 콘텐츠에 대한 conflict_detected 플래그를 제공합니다. KU-Gateway의 유일한 역할은 사용자가 고민할 필요가 없도록 해당 점수를 '유지'할지 '삭제'할지에 대한 깔끔한 결정으로 변환하는 것입니다.
추출: 애초에 컨텍스트를 찾는 방법
요청은 "이 부분은 검색된 컨텍스트이고, 이 부분은 사용자의 실제 질문입니다"라고 미리 태그가 붙은 상태로 도착하지 않습니다. KU-Gateway는 메시지 내에서 <context>...</context> 태그로 감싸진 모든 내용을 점수 산정 대상(fair game)으로 간주합니다.
def extract_context_chunks(messages: List[Dict[str, Any]]) -> List[ContextChunk]:
chunks = []
context_pattern = r"<context>(.*?)</context>"
...
추출할 내용이 없다면 요청은 그대로 통과됩니다. 이러한 설정은 단순한 대화 단계에서 API 호출을 낭비하는 것을 완전히 방지합니다. 코드베이스의 이 부분은 확실히 v1 구현체입니다. 현재는 중첩되지 않은 평면적인(flat) 컨텍스트 블록을 가정하고 있습니다. 중첩된 태그나 구조화된 JSON 블롭(JSON blobs)과 같이 더 정교한 임베딩 방식(embedding schemes)을 사용하려면 더 유능한 파서(parser)가 필요할 것입니다. 해당 업그레이드는 이미 로드맵에 포함되어 있습니다.
필터링(Filtering): 실제 관문
청크들이 점수와 함께 돌아오면, 결정 로직은 놀라울 정도로 단순합니다. 청크의 감쇠 점수(decay score)가 해당 소스에 대해 설정된 특정 임계값(threshold)보다 낮으면 해당 청크는 살아남습니다.
def filter_chunks(self, chunks, results):
fresh_chunks, fresh_results, blocked_chunks = [], [], []
result_map = {r.chunk_id: r for r in results}
...
언급할 만한 두 가지 설계 결정 사항은 다음과 같습니다:
-
소스별 임계값 (Per-source thresholds).
arxiv콘텐츠가 노후화되는 방식은 Stack Overflow 답변과 다르기 때문에, 도메인 전반에 걸쳐 단일한 전역 임계값 (global cutoff)을 적용하는 것은 의미가 없습니다.KU_SOURCE_THRESHOLDS를 사용하면{"arxiv": 0.7, "github": 0.3}와 같이 설정할 수 있으며, 게이트웨이는 청크 (chunk)별로 이를 준수합니다. -
의도적인 페일 오픈 (Fail-open, deliberately). 만약 평가기 (evaluator)가 청크에 대해 결과를 전혀 생성할 수 없는 경우 (점수 산정 실패가 아닌, 처리되지 않은 예외 발생 시), 해당 청크는 조용히 삭제되는 대신 그대로 유지됩니다. 인프라 문제로 인해 페일 클로즈 (fail closed)되는 게이트웨이는 운영 환경에서 앱을 조용히 망가뜨리는 게이트웨이입니다. 만약 KU API 자체에는 접근 가능하지만 오류가 발생하는 경우에는, 대신 중립적인
decay_score=0.5, confidence=0.0을 반환합니다. 이는 별도의 예외 처리를 거치지 않고 일반적인 임계값 비교 과정을 거치게 됩니다.
터미널에서의 실제 모습
이것은 가상의 파이프라인 (pipeline)이 아닙니다. 실행 중인 게이트웨이에 대한 실제 요청이며, 수정되지 않은 터미널 출력 결과입니다:
해당 요청은 5개의 컨텍스트 청크 (context chunks)와 42개의 원래 토큰 (original tokens)과 함께 들어왔습니다. 4개의 청크가 각각 0.80, 0.88, 0.73, 0.54의 감쇠 점수 (decay scores)로 차단되었으며, 결과적으로 하나의 신선한 청크와 21개의 깨끗한 페이로드 (payload) 토큰만 남았습니다. 해당 요청 토큰의 50%는 순수하게 노후된 데이터였으며, 모델이 이를 확인하기도 전에 제거되었습니다. 차단된 청크들의 평균 감쇠 (average decay)는 0.74였습니다. 이것이 로그 한 줄에 담긴 핵심 가치 제안 (value proposition)입니다.
모든 요청은 자동으로 이러한 처리를 거치며, 동일한 수치들을 HTTP를 통해 조회할 수 있습니다:
curl http://localhost:8000/v1/telemetry
{
"requests": 12,
"total_tokens_saved": 340,
...
고백: 스타트업 배너는 데드 코드 (dead code)였습니다
이런 일은 자신이 만든 것에 대한 기억을 믿는 대신, 실제로 호출 그래프 (call graph)를 추적할 때만 드러나곤 합니다. telemetry.py에는 API 키 등급 감지, 감쇠 임계값 (decay thresholds), 지원되는 소스 개수 등을 다루는 완전히 구현된 print_startup() 메서드가 있었습니다. 심지어 멋진 Rich 렌더링 패널까지 사용하고 있었죠. 하지만 main.py에서 이를 호출한 적이 전혀 없었습니다. @app.on_event("startup") 핸들러는 아예 없었고, 세션 요약을 출력하는 종료 훅 (shutdown hook)만 존재했습니다. 배너는 초기 커밋 이후로 완전히 작성된 채로, 전혀 도달할 수 없는 상태로 그 자리에 놓여 있었던 것입니다.
해결책은 간단했습니다:
@app.on_event("startup")
async def startup_event():
if settings.telemetry_enabled:
...
이 내용을 포함하는 이유는 이것이 단 두 줄짜리 수정이라서 대단하기 때문이 아니라, "내가 문서화한 기능이 실제로 실행되는가"라는 질문이 "코드가 컴파일되는가"라는 질문보다 더 유용한 질문임이 드러났기 때문입니다. README를 작성하기 전에 자신의 프로젝트에 대해 스스로 던져볼 가치가 있는 질문입니다.
설정 (Configuration) 요약
모든 것은 .env에서 로드되는 pydantic-settings를 통해 환경 변수 (environment-variable) 기반으로 작동합니다. 필수 항목은 다음과 같습니다:
| 변수 (Variable) | 기본값 (Default) | 역할 |
|---|---|---|
KU_API_KEY | (필수) | Knowledge Universe 키, 반드시 ku_로 시작해야 함 |
| ... |
전체 참조는 repo README에서 확인할 수 있습니다.
배포 (Shipping it)
Dockerfile과 docker-compose.yml이 포함되어 있어, 모의(mock) KU API 및 모의 LLM 에코 서버와 함께 게이트웨이를 실행할 수 있습니다. 이 설정은 실제 자격 증명 없이 기능을 테스트할 때 진정으로 유용합니다:
make docker-up
그리고 실제 클러스터의 경우, kubernetes/ 디렉토리에 kubectl apply -f로 즉시 사용할 수 있는 Deployment, Service, Ingress, ConfigMap, Secret 매니페스트 (manifests)가 준비되어 있습니다.
아직 완료되지 않은 사항
여러분이 직접 고생하며 알아내기 전에 차라리 제가 미리 말씀드리는 편이 낫겠습니다:
-
Rate limiting(속도 제한) 및 auth(인증) 미들웨어는 스텁(stub) 상태입니다.
KU_RATE_LIMIT은 아직 아무런 동작을 하지 않습니다. 미들웨어 클래스들은 존재하지만 모든 요청을 그대로 통과시킵니다. -
BYOK(Bring Your Own Key) vault 지원은 스캐폴딩(scaffolded)만 되어 있으며, 구축되지 않았습니다.
KU_VAULT_ENABLED는 아무런 영향을 미치지 않습니다. -
YAML 설정이 연결되지 않았습니다. 저장소에
.ku-gateway.yaml.example파일이 있어 설정 형식을 설명하고 있지만, 코드가 실제로 이를 읽지는 않습니다. 현재로서는 환경 변수(Environment variables)가 유일한 실제 입력값입니다. -
메시지 재구성(Message reconstruction)은 의도적으로 단순하게 설계되었습니다. 평면적인
<context>블록은 잘 처리하지만, 깊게 중첩되거나 구조화된 컨텍스트 임베딩(context embedding)을 처리하려면 더 많은 작업이 필요합니다.
이 중 그 어떤 것도 요청을 받고, 오래된 컨텍스트를 제거하여, 깨끗한 요청을 내보내는 핵심 유스케이스(use case)를 방해하지는 않습니다. 하지만 여러분이 운영 환경(production)에서 스텁을 발견하게 만들기보다는, 베타 버전이라는 점을 미리 솔직하게 말씀드리고 싶습니다.
사용해 보기
git clone https://github.com/VLSiddarth/KU-Gateway.git
cd KU-Gateway
python -m venv venv && source venv/bin/activate
...
이 프로젝트는 MIT 라이선스를 따르며, 이슈(issues), PR(Pull Requests), 또는 단순히 해당 도메인에서 데이터 노후화 임계값(decay thresholds)이 잘못되었다는 의견을 주시는 것을 진심으로 환영합니다. 만약 여러분이 검색된 문서(retrieved documents)를 LLM에 전달하는 무언가를 만들고 있으면서, 단 한 번도 "이 데이터가 실제로 얼마나 오래된 거지?"라고 자문해 본 적이 없다면, 바로 그 지점이 이 도구가 채워줄 수 있는 공백입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기