한 사용자가 우리의 번역 플러그인이 입력값에 상관없이 모든 요청에 대해 동일한 캐시 결과를 반환한다고 보고했습니다. 조사 결과 더 심각한 사실을 발견했습니다. 플랫폼 전체의 시맨틱 캐시 히트(hit) 중 95%가 가짜 양성(false positive)이었습니다. 199개의 서로 다른 번역 요청, 198개의 고유한 요청 본문이 있었지만, 단 하나의 캐시된 응답이 모든 요청에 제공되었습니다.
장기적인 에이전트 상태와 프로덕션 요청 처리에 관심이 있다면, 이 포스트는 AI 에이전트가 메모리를 계속 잃어버리는 이유, 단일 API 키 챗봇 가이드, 그리고 AI API 속도 제한(rate limiting) 가이드와 함께 읽으면 좋습니다.
버그 리포트
리포트는 간단했습니다. "시맨틱 캐시를 비활성화했는데도 모든 번역이 동일한 결과를 반환합니다."
세 개의 요청 ID, 세 개의 서로 다른 번역 세그먼트, 하지만 동일한 캐시 응답. 요청 본문의 크기는 1,564바이트에서 8,676바이트 사이였습니다. 캐시된 응답 ID는 모두 동일했습니다: chatcmpl-DG6J03nhdvcF7Ek0C8rJkjh7lN9pF.
첫 번째 의심: 사용자의 캐시 설정이 적용되지 않고 있었습니다. 이는 별도의 데이터 소스 동기화 버그로 밝혀졌습니다(관리자 패널은 한 테이블에 쓰고, API gateway는 다른 테이블에서 읽는 문제). 하지만 이를 수정하는 것은 문제의 절반만 해결한 것이었습니다. 캐시가 활성화되어 정상적으로 작동하더라도, 시맨틱 캐시는 절대 일치해서는 안 될 요청들을 매칭하고 있었습니다.
프로덕션 데이터
우리는 ClickHouse에서 24시간 동안의 캐시 히트 데이터를 추출했습니다. 수치는 처참했습니다.
| 모델 | 총 요청 수 | 캐시 히트 | 고유 요청 수 | 고유 응답 수 | 히트율 |
|---|---|---|---|---|---|
| gpt-4.1-nano | 200 | 199 | 198 | 1 | 99.5% |
| glm-4.6-thinking | 100 | 38 | 13 | 1 | 38% |
| gpt-5-nano | 31 | 29 | 28 | 2 | 93.5% |
| gpt-oss-120b | 18 | 17 | 17 | 1 | 94.4% |
| qwen3-vl-flash | 17 | 16 | 16 | 1 | 94.1% |
198개의 고유한 번역 요청이 모두 동일한 단일 캐시 응답을 반환하고 있었습니다. 이것은 캐시가 아닙니다. 상수를 반환하는 고장 난 함수와 다름없습니다.
영향을 받은 모든 모델은 두 가지 공통점을 공유했습니다. 모든 요청이 단일 사용자로부터 발생했으며, 모두 가변적인 사용자 콘텐츠와 함께 고정된 시스템 프롬프트 템플릿을 사용했다는 점입니다.
구조화된 입력에서 임베딩이 실패하는 이유
번역 플러그인은 다음과 같은 요청을 보냅니다.
System: "Act as a translation API. Output a single raw JSON object only.
Input: {"targetLanguage":"<lang>","title":"...","segments":[...]}"
User: {"targetLanguage":"zh","title":"Product Page",
"description":"Translate product descriptions",
"tone":"formal",
"segments":[{"text":"actual varying content here"}]}
시스템 프롬프트는 모든 요청에서 동일합니다. 사용자 메시지는 targetLanguage, title, description, tone이 고정된 JSON 객체입니다. 오직 segments[].text만 변경됩니다.
우리의 시맨틱 캐시가 임베딩을 위해 텍스트를 추출할 때, 시스템 프롬프트와 사용자 메시지를 연결합니다. 고정된 템플릿이 텍스트의 약 80%를 차지합니다. 임베딩 모델(all-mpnet-base-v2, 768 dimensions)은 이를 벡터로 압축하는데, 이때 템플릿 구조가 지배적인 영향을 미칩니다. 실제 번역 콘텐츠는 벡터 값에 거의 영향을 주지 못합니다.
결과: "translate 'Hello world'"와 "translate 'The quarterly financial report shows a 15% increase in revenue'" 사이의 코사인 유사도(cosine similarity)가 0.95를 초과합니다. 우리의 임계값(threshold)은 0.95입니다. 모든 번역 요청이 첫 번째 캐시된 항목과 일치하게 됩니다.
로그를 분석한 결과, 이 문제가 발생하는 세 가지 패턴을 발견했습니다.
번역 플러그인이 가장 심각한 사례였습니다. 고정된 JSON 키와 값들이 실제 번역 세그먼트를 압도했습니다. gpt-4.1-nano와 gpt-5-nano 모두 이 문제에 해당했습니다.
문맥 요약 어시스턴트도 비슷한 문제를 겪었습니다. 시스템 프롬프트가 너무 길어서 사용자 콘텐츠(5KB에서 47KB 사이)가 임베딩에서 거의 드러나지 않았습니다. 이 때문에 glm-4.6-thinking이 모든 대화에 대해 동일한 요약을 반환하게 되었습니다.
세 번째 패턴은 더 미묘했습니다. gpt-oss-120b와 qwen3-vl-flash의 경우, 모든 요청의 처음 500자가 바이트 단위로 동일했습니다. 가변적인 콘텐츠는 그 뒤에 나왔지만, 임베딩은 이미 공유된 접두사(prefix)에 의해 지배되었습니다.
연구 결과가 말해주는 것
이것은 새로운 문제가 아닙니다. 최근 논문들이 이를 수치화했습니다.
UC Berkeley의 vCache 프로젝트는 올바른 캐시 히트와 잘못된 캐시 히트가 "매우 겹치는 유사도 분포"를 가진다는 것을 발견했습니다. 최적의 임계값은 캐시된 항목에 따라 0.71에서 1.0까지 다양합니다. 단일 수치는 작동하지 않습니다. 그들의 해결책은 캐시 항목당 별도의 임계값을 학습하는 것이었으며, 이를 통해 히트율을 두 배로 높이면서 오류율을 6배 줄였습니다. (vCache, 2025)
쿼리 유형을 섞으면 상황은 더 나빠집니다. 카테고리 인식 캐싱 연구에 따르면, 0.80 임계값은 코드 쿼리(sort_ascending vs sort_descending)에서 15%의 가짜 매칭을 생성하는 반면, 동일한 임계값이 대화형 쿼리에서는 유효한 패러프레이징을 놓칩니다. 하나의 임계값으로 두 가지 실패 모드가 발생하는 것입니다. (Category-Aware Semantic Caching, 2025)
은행권에서도 이 문제를 겪었습니다. InfoQ 사례 연구에 따르면, RAG 시스템에서 "이번 달 대출금을 미룰 수 있나요"가 "대출금을 연체하면 어떻게 되나요"와 88.7%의 유사도로 일치했습니다. 의도는 다르지만 동일한 캐시 답변이 나간 것입니다. 그들은 99%의 가짜 양성률에서 시작하여 3.8%까지 낮추기 위해 네 차례의 최적화 과정을 거쳐야 했습니다. (InfoQ Banking Case Study, 2025)
더 근본적인 문제: 임베딩은 두 프롬프트가 의미적으로 유사한지를 측정할 뿐, 동일한 응답이 두 프롬프트 모두에 답이 될 수 있는지를 측정하지 않습니다. 그 간극에서 가짜 캐시 히트가 발생합니다. (Efficient Prompt Caching via Embedding Similarity, 2024)
우리가 찾은 모든 논문이 동의하는 한 가지는 임베딩 유사도만으로는 충분하지 않다는 것입니다. 검증 레이어가 필요합니다.
두 단계의 해결책
우리는 두 가지 방어책을 구축했습니다. 첫 번째는 임베딩 전 템플릿 노이즈를 제거하는 것이고, 두 번째는 매칭 후 히트를 검증하는 것입니다.
레이어 2: 임베딩을 위한 콘텐츠 추출
임베딩을 생성하기 전에 이제 구조화된 입력(JSON)을 감지하고 의미 있는 가변 콘텐츠만 추출합니다.
로직:
- 메시지 콘텐츠가
{또는[로 시작하는지 확인합니다. - JSON으로 파싱되면 모든 문자열 리프(leaf) 값을 재귀적으로 수집합니다.
- 짧은 값(20자 이하)은
"zh","formal","Product Page"와 같은 설정 필드일 가능성이 높으므로 필터링합니다. - 추출된 텍스트가 너무 짧거나 비어 있으면 원본 텍스트를 사용합니다.
function extractContentForEmbedding(text: string): string {
const extracted = tryExtractJsonContent(text);
return extracted && extracted.length > 20 ? extracted : text;
}
이 방식은 시스템 프롬프트와 사용자 메시지 모두에 적용됩니다. 번역 플러그인의 경우, 임베딩은 이제 2KB의 JSON 덩어리 대신 "Hello world"를 나타냅니다. 요약 어시스턴트의 경우, 템플릿 래퍼에서 실제 대화 내용을 끌어냅니다.
20자 임계값은 경험적으로 선택되었습니다.
"zh"(2자): 필터링됨. 설정값."formal"(6자): 필터링됨. 설정값."Product Page"(12자): 필터링됨. 템플릿 필드."Translate product descriptions"(31자): 유지됨. 의미 있는 콘텐츠."The quarterly financial report..."(40자 이상): 유지됨. 실제 번역 콘텐츠.
레이어 3: 핑거프린트(Fingerprint) 검증
시맨틱 캐시 히트가 발생한 후, 현재 요청의 추출된 텍스트 해시(hash)를 캐시된 항목에 저장된 해시와 비교합니다. 일치하지 않으면 히트를 거부합니다.
// 캐시 쓰기 시
entry.metadata.textHash = fnv1aHash(extractedText);
// 캐시 읽기 시, 유사도 매칭을 찾은 후
if (entry.metadata.textHash !== undefined) {
if (entry.metadata.textHash !== fnv1aHash(currentExtractedText)) {
// 가짜 양성: 의미적으로는 유사하지만 콘텐츠가 다름
metrics.recordFingerprintRejection();
return null;
}
}
해시는 원본 입력이 아닌 추출된 텍스트(레이어 2 이후)를 사용합니다. 템플릿 래퍼는 다르지만 실제 콘텐츠가 동일한 두 요청은 여전히 매칭됩니다. 콘텐츠가 다르면 해시가 달라져 거부됩니다.
textHash가 없는 이전 캐시 항목은 검증을 건너뜁니다(하위 호환성). 이들은 TTL을 통해 자연스럽게 만료됩니다.
해시에는 FNV-1a(32비트)를 사용합니다. 빠르고 결정론적이며, 약 40억 분의 1의 충돌률은 단일 캐시 히트를 확인하는 데 충분합니다.
왜 그냥 임계값을 높이지 않나요?
우리의 임계값은 이미 0.95입니다. 이를 높이는 것은 도움이 되지 않습니다. 문제는 구조적으로 유사한 입력이 실제 콘텐츠와 상관없이 0.95 이상의 유사도 점수를 생성한다는 것입니다.
vCache의 데이터가 이를 뒷받침합니다. 올바른 히트와 잘못된 히트의 유사도 분포가 너무 많이 겹쳐서 단일 컷오프(cutoff)로는 이들을 분리할 수 없습니다. 임계값을 0.99로 높이면 템플릿 비중이 높은 요청의 가짜 양성을 제거하지 못한 채, 패러프레이징에 대한 정당한 캐시 히트만 죽이게 됩니다.
입력을 수정하고 출력을 검증하십시오. 임계값을 만지작거리지 마십시오.
결과
두 레이어를 모두 배포한 결과:
| 지표 | 이전 | 이후 |
|---|---|---|
| gpt-4.1-nano 가짜 양성 | 198/199 | 0 |
| 전체 캐시 히트 중 가짜 양성 비율 | ~95% | <5% |
| 정당한 캐시 히트율 | 변화 없음 | 변화 없음 |
| 요청당 추가된 지연 시간(latency) | 0 | <1ms (JSON 파싱 + FNV 해시) |
레이어 2만으로도 번역 플러그인 문제는 해결되었을 것입니다. 레이어 3은 JSON 추출이 콘텐츠를 완전히 분리하지 못하는 경우나 JSON이 아닌 구조화된 입력을 위한 안전망 역할을 합니다.
핵심 요약
프로덕션 환경에서 시맨틱 캐시를 운영하고 있다면:
응답 다양성을 모니터링하세요. 모델의 캐시 히트율이 100%인데 고유 응답이 1개뿐이라면 가짜 양성 문제가 있는 것입니다. 쿼리 예시:
SELECT model, uniqExact(substring(response_body, 1, 200)) as unique_responses, count() as total FROM request_logs WHERE cache_hit = true GROUP BY model.구조화된 입력은 단순한 임베딩을 무력화합니다. 고정된 템플릿(JSON API, 시스템 프롬프트 래퍼, 양식 채우기 작업 등)이 포함된 모든 요청은 인위적으로 높은 유사도 점수를 생성합니다. 임베딩 전에 전처리를 수행하세요.
검증 레이어는 선택이 아닌 필수입니다. 연구 문헌에 나오는 모든 프로덕션 시맨틱 캐시에는 검증 레이어가 있습니다. 가벼운 해시 체크, 크로스 인코더(cross-encoder) 리랭커, 또는 전체 LLM 검증 호출 중 무엇을 사용할지는 지연 시간 예산에 따라 선택하세요.
전역 임계값은 해결책이 아닌 타협안입니다. 쿼리 유형마다 서로 다른 임계값이 필요합니다. 카테고리별 또는 항목별 임계값을 적용할 수 없다면, 최소한 입력 전처리를 추가하여 카테고리 간 임베딩 품질을 정규화하세요.
시맨틱 캐싱은 LLM API 비용을 30-70% 절감할 수 있습니다. 하지만 입력 전처리와 히트 검증 없이는 잘못된 답변을 제공하면서 성능이 개선되었다고 착각하게 될 뿐입니다.
LemonData는 캐싱, 라우팅 및 비용 최적화 기능이 내장된 300개 이상의 AI 모델에 대한 통합 액세스를 제공합니다. $1 크레딧으로 무료로 시작해 보세요.
