19 분 소요

Summary

Hermes Agent 를 Docker 로 띄우고 Codex OAuth 를 물린 뒤로, 이 친구한테 뭘 시킬까 계속 생각하고 있었어요. 그러다 나온 그림이 이겁니다.

내부 문서를 Hermes + Codex 로 읽혀서 신뢰도 높은 Q&A 를 무한 생성한다. 그걸 Redis 에 쌓아두고, 질문이 오면 거기서 찾아 즉답한다. 못 찾으면 Elasticsearch + EC2 GPU 로 RAG 를 돌려 답한다.

그럴듯하죠. 저도 처음엔 그럴듯해 보였어요. 부품도 다 정상이에요 — ES 하이브리드 검색, Redis 캐시, GPU 추론, LLM 기반 Q&A 합성. 하나하나는 전부 업계에서 쓰는 표준 패턴입니다.

그런데 이 셋을 이 순서로 엮으면 이상한 게 나옵니다. 짓기 전에 앉아서 한 번 뜯어봤고, 결론부터 말하면 원안 그대로는 10점 만점에 6점이었어요. 세 군데를 고치니 8.5점이 됐고요.

이 글은 그 채점표입니다. 코드는 2편부터 나오고, 여기서는 왜 그렇게 지으면 안 되는지만 봅니다.

💡 이 글에서 다루는 것

  • 원안의 전체 그림과 데이터 흐름
  • 채점 기준 — 아키텍처를 무엇으로 판정하나
  • 치명적 결함 ① “무한 생성”은 정보량 상한 때문에 성립하지 않는다
  • 치명적 결함 ② Redis 를 1차 답변자로 두면 정확도 상한을 캐시가 결정한다
  • 치명적 결함 ③ 신뢰도를 부탁하고 있고 검사하지 않는다
  • 구조적 결함 5개 — 벡터 이중화 · 모델 단차 · 휘발하는 본대 · 네트워크 경계 · 임베딩 미결정
  • 임베딩이 이 아키텍처의 진짜 단일 실패점인 이유, 그리고 로컬 GPU 로 부족할 때 외부로 가는 판단 기준
  • 로컬 임베딩 + 리랭커 절충안 — 문서를 밖으로 안 내보내면서 품질만 사오기
  • 비용 — GPU 상주가 맞는 선택인가
  • 보정안과 최종 점수, 그리고 남은 4편이 뭘 채우는지



1. 원안 — 어떤 그림이었나

먼저 뭘 지으려 했는지 정확히 적어둡니다. 채점은 이 표를 대상으로 합니다.

요소 어디에 무슨 일을 하나
Hermes + Codex 집 미니서버 (Docker) 내부 문서를 읽고 Q&A 를 무한 생성
Redis AWS 1차 답변자. 질문이 오면 여기서 먼저 찾음
Elasticsearch AWS Redis 가 못 찾을 때 원문 청크 검색
EC2 GPU AWS 검색 결과로 답변 생성 (RAG)

답변 경로는 두 갈래예요.

경로 조건 지연 답을 만든 주체
빠른 길 Redis hit 수십 ms 어제의 Codex (사전생성)
느린 길 Redis miss 수 초 지금의 EC2 GPU (실시간 RAG)

핵심 의도는 명확합니다. “자주 나올 질문은 미리 다 만들어두면 빠르잖아.” 그리고 이 의도 자체는 틀리지 않았어요. 실제로 프리컴퓨트(pre-compute)는 지연을 줄이는 정석입니다.

문제는 “미리 다 만들어둔다”의 ‘다’ 와, “Redis 가 1차 답변자”의 ‘1차’ 에 있습니다.



2. 채점 기준

아키텍처를 “좋다/나쁘다”로 감으로 말하면 논쟁만 남아요. 그래서 네 축으로 나눠서 봤습니다.

질문 배점
정합성 각 부품이 자기가 잘하는 일을 하고 있나 3
정확도 틀린 답이 나올 경로가 막혀 있나 3
수렴성 이 시스템은 끝나는가, 아니면 발산하는가 2
운영성 죽었을 때 되살릴 수 있나, 비용이 예측되나 2

원안 점수는 이렇게 나왔어요.

점수 한 줄 이유
정합성 1.5 / 3 벡터 인덱스가 두 벌 생김 + 임베딩 모델이 미정
정확도 1.0 / 3 오답이 캐시에 박히면 빠져나올 길이 없음
수렴성 1.5 / 2 “무한 생성”에 정지 조건이 없음
운영성 2.0 / 2 부품 자체는 다 흔한 것들, 되살리기 어렵진 않음
합계 6.0 / 10  

정합성에 적어둔 “임베딩 모델이 미정”이 처음엔 사소해 보였는데, 파고들수록 제일 컸어요. 그래서 7절을 통째로 거기에 씁니다.

이제 감점 사유를 하나씩 봅니다.



3. 치명적 결함 ① — “무한 생성”은 성립하지 않습니다

가장 먼저 걸린 게 이 단어였어요. 무한 생성.

정보량은 늘어나지 않습니다

직관적으로 먼저 말해볼게요. 사규 문서 100장을 놓고 Q&A 를 만든다고 합시다. 1,000개까지는 잘 나옵니다. 10,000개도 어떻게든 나와요. 그럼 1,000,000개는요?

문서 100장에 백만 개짜리 질문거리가 들어있지 않습니다. 어느 순간부터 모델은 “문서에 답이 있는 질문”을 다 써버리고, 문서에 답이 없는 질문을 만들기 시작합니다. 그리고 답이 없으니 답도 지어냅니다.

이걸 식으로 적으면 이렇게 돼요. 원문 문서 집합을 $D$, 생성한 Q&A 집합을 $A$ 라고 하면

\[H(A \mid D) = 0 \quad \Rightarrow \quad H(A) \le H(D)\]

쉽게 풀면 이렇습니다. $H$ 는 “엔트로피”라고 읽고, 쉽게 말해 정보의 양이에요. 물통에 담긴 물의 부피라고 생각하면 됩니다.

  • $H(D)$ — 원문 문서가 담고 있는 정보의 양. 물통의 물.
  • $H(A)$ — 우리가 만들어낸 Q&A 전체가 담고 있는 정보의 양.
  • $H(A \mid D)$ — “원문을 이미 다 알고 있을 때, Q&A 를 보면 추가로 알게 되는 정보의 양”. 세로 막대 $\mid$ 는 “~를 안다는 전제 하에”라고 읽어요.

Q&A 가 문서에만 근거해서 만들어졌다면, 문서를 이미 아는 사람에게 Q&A 는 새 정보를 하나도 주지 않아요. 그래서 $H(A \mid D) = 0$ 입니다. 그러면 자동으로 $H(A) \le H(D)$ 가 따라나와요 — 컵에 따른 물이 물통의 물보다 많을 수 없다는 얘기입니다.

여기서 중요한 건 대우(뒤집어 읽기) 예요.

Q&A 를 계속 만들어서 $H(A)$ 가 $H(D)$ 를 넘어섰다면, $H(A \mid D) = 0$ 이 깨진 것이다. 즉 문서에 없는 정보가 섞여 들어왔다는 뜻이다.

물통보다 컵에 물이 많다면, 누군가 수돗물을 더 부은 겁니다. 그 수돗물이 바로 환각이에요. 모델이 자기가 학습해서 알고 있는 일반 지식(parametric knowledge)이거나, 그냥 지어낸 말이거나.

무한히 생성하면 무한히 오염됩니다. 이건 프롬프트를 잘 써서 피할 수 있는 문제가 아니에요. 구조적으로 그렇게 됩니다.

그럼 언제 멈춰야 하나

“무한”을 버리고 포화(saturation) 로 바꿉니다. 새로 뽑은 질문 중 기존과 사실상 겹치지 않는 것의 비율을 보고 멈추는 거예요.

$k$ 번째 배치 $B_k$ 의 신규율(novelty rate) 을 이렇게 정의합니다.

\[r_k = \frac{\left\lvert\{\, q \in B_k \;:\; \max_{q' \in S_{k-1}} \cos(q, q') < \tau \,\}\right\rvert}{\lvert B_k \rvert}\]

쉽게 풀면 이렇습니다. 분수니까 한국어로 분모부터 읽을게요.

  • $\lvert B_k \rvert$ — 이번 배치에서 새로 만든 질문 개수. 세로 막대 두 개 $\lvert \cdots \rvert$ 는 “개수를 세라”는 뜻이에요.
  • 분자의 $S_{k-1}$ — 지금까지 쌓아둔 질문 전체(창고).
  • $\cos(q, q’)$ — 두 질문의 닮은 정도. “코사인 유사도”라고 읽고, 1에 가까울수록 같은 질문, 0에 가까울수록 딴 얘기예요.
  • $\max_{q’ \in S_{k-1}}$ — “맥스”라고 읽어요. 창고에 있는 질문들과 하나씩 다 비교해서 가장 닮은 것 하나의 점수를 고릅니다.
  • $\tau$ — “타우”. 우리가 정하는 기준선. 예를 들어 0.92.

⚠️ 여기서 0.92 라는 숫자는 보편 상수가 아닙니다. 어떤 임베딩 모델을 쓰느냐에 따라 “같은 질문”으로 볼 경계가 완전히 달라져요. 어떤 모델은 관련 없는 문장 쌍도 0.8 대가 예사고, 어떤 모델은 0.6 만 넘어도 사실상 같은 뜻입니다. 모델을 정한 다음 실제 분포를 재서 잡아야 하는 값이에요. 이 얘기는 7절에서 제대로 다룹니다.

그래서 분자는 “창고의 어떤 질문과도 0.92만큼은 안 닮은, 진짜 새 질문의 개수” 입니다. 이걸 이번 배치 전체 개수로 나눈 게 신규율 $r_k$ 예요.

$r_k$ 가 0.9 라면 열 개 중 아홉 개가 새 질문이니 계속 캐도 됩니다. 0.05 라면 스무 개 뽑아야 하나 건지는 거니, 광맥이 끝난 겁니다. 거기서 멈춥니다.

그리고 진행률은 개수가 아니라 커버리지로 봅니다.

잘못된 지표 왜 나쁜가 대신 볼 것
생성한 Q&A 개수 같은 질문 백만 개도 백만 개 청크 커버리지 — 원문 조각 중 최소 1개 Q&A 가 인용한 비율
“오늘도 5만 개 생성” 오염 속도일 수도 있음 신규율 $r_k$ — 떨어지면 정지 신호

이 정지 조건과 커버리지 계산은 2편에서 코드로 만듭니다.



4. 치명적 결함 ② — Redis 를 1차 답변자로 두면

이게 제일 큰 문제였어요. 앞의 ①은 “낭비”지만, 이건 사고입니다.

hit 이면 아무것도 검증되지 않습니다

원안의 흐름을 다시 봅니다. 질문이 오면 Redis 에서 찾고, 찾으면 그대로 내보냅니다. ES 도 안 타고 LLM 도 안 탑니다.

그 말은 이겁니다.

사전생성 Q&A 하나가 틀렸다면, 그 오답은 50ms 만에, 근거 없이, 영원히 반복 출력된다.

RAG 의 안전장치가 뭐였는지 생각해보면 아찔해요. RAG 는 답을 만들 때마다 원문을 다시 찾아오고, 그 원문을 근거로 붙입니다. 사용자가 근거를 보고 “어 이거 아닌데”를 할 수 있어요. Redis hit 경로에는 그게 전부 없습니다. 어제 만든 답을 오늘 그대로 뱉는데, 어제 그게 맞았는지 확인할 방법이 경로 안에 없어요.

매칭 방식은 둘 다 함정입니다

그럼 Redis 에서 “찾는다”는 게 정확히 뭘까요. 두 가지밖에 없는데, 둘 다 문제가 있습니다.

방식 어떻게 결과
exact match 질문 문자열을 키로 hit rate 사실상 0. 자연어 질문은 매번 표현이 다름
semantic match 질문 임베딩으로 유사도 검색 hit 임계값이 곧 오답률

첫 번째는 그냥 안 됩니다. “연차 며칠이야?” 와 “연차 몇 일 쓸 수 있나요?” 는 다른 키예요. 캐시가 텅 빈 채로 놀게 됩니다.

두 번째가 진짜 문제예요. 유사도 0.85 로 hit 판정을 한다고 해봅시다. 이런 일이 벌어집니다.

들어온 질문 캐시에 있던 질문 유사도 판정 실제로는
육아휴직 급여는 얼마인가요 육아휴직 기간은 얼마인가요 0.89 hit → 즉답 완전히 다른 질문
퇴직금 중간정산 되나요 퇴직연금 중도인출 되나요 0.87 hit → 즉답 제도가 다름

임베딩은 “주제가 비슷한가”를 보지 “묻는 게 같은가” 를 보지 않아요. 급여와 기간, 퇴직금과 퇴직연금은 임베딩 공간에서 아주 가깝습니다. 그런데 답은 전혀 다르죠.

그리고 이 오답에는 “잘 모르겠는데요”의 흔적조차 없습니다. 확신에 차서, 빠르게, 근거 없이 나갑니다. 챗봇이 낼 수 있는 최악의 실패 모드예요.

처방 — 3단 게이트

hit 을 “답변 확정”이 아니라 “후보 제시” 로 강등시킵니다. 유사도 구간을 셋으로 자르는 거예요.

유사도 구간 처리 지연
$\ge 0.95$ 캐시 답 즉답 수십 ms 사실상 같은 질문. 여기만 진짜 캐시
$0.80 \sim 0.95$ RAG 로 보내되 그 Q&A 를 컨텍스트에 주입 수 초 힌트로는 쓸 만함. 확정은 안 함
$< 0.80$ 순수 RAG 수 초 캐시가 도움 안 됨

중간 구간이 핵심이에요. 버리지도 않고 믿지도 않습니다. “참고할 만한 비슷한 Q&A 가 있으니 같이 보고 판단해” 로 LLM 에 넘깁니다. 이러면 캐시가 정확도를 깎는 게 아니라 보태는 쪽으로 방향이 바뀝니다.

체감 지연은 어떻게 되냐고요? 실제로 0.95 이상 hit 은 반복 질문(FAQ 성)에서 잘 잡힙니다. 그리고 그게 트래픽의 대부분이에요. 빠른 길은 유지하되, 애매한 걸 빠른 길에 태우지 않는 것 이 요점입니다.

⚠️ 0.95 와 0.80 도 그대로 베껴 쓰면 안 되는 숫자입니다. 이 두 경계선은 임베딩 모델의 유사도 분포에 붙어 있는 값이에요. 모델을 바꾸면 같은 0.95 가 전혀 다른 의미가 됩니다. 즉 임베딩 모델을 고르는 일과 이 게이트를 잡는 일은 분리할 수 없어요. 7절의 주제입니다.

구간별 임계값 튜닝과 실제 색인 설계는 4편에서 다룹니다.



5. 치명적 결함 ③ — 신뢰도를 부탁만 하고 있습니다

원안에 “신뢰도 높은 질문과 답”이라고 적어놨는데, 다시 읽어보니 신뢰도를 만드는 부품이 그림에 없었어요.

생성만 있고 채점이 없습니다. 그럼 신뢰도는 어디서 오나요? 프롬프트에 “정확하게 답해줘”라고 쓰는 데서? 그건 부탁이지 보장이 아닙니다.

신뢰도를 얻는 방법 실제 효과
프롬프트에 “정확히 답해” 약간 도움. 보장 없음
좋은 모델 쓰기 도움. 여전히 보장 없음
생성물을 검사해서 떨어뜨리기 이것만이 보장

신뢰도는 부탁해서 얻는 속성이 아니라 걸러서 얻는 속성이에요. 그래서 파이프라인을 2단으로 나눕니다.

단계 하는 일 통과 못 하면
1단 생성 청크를 주고 Q&A 를 만들게 함
2단 검증 근거 청크만 주고 답의 주장이 전부 지지되는지 확인 폐기

2단이 핵심이에요. 검증할 때는 원래 질문을 만든 맥락을 다 지우고, 인용한 청크와 생성한 답만 놓고 봅니다. “이 답에 있는 문장 중 이 청크로 뒷받침되지 않는 게 하나라도 있나?” 를 묻는 거예요. 이걸 groundedness(그라운드니스) 라고 합니다.

여기서 통과 못 한 건 고치지 않고 버립니다. 고치려 들면 또 지어내거든요. 문서에 답이 없는 질문은 애초에 있으면 안 되는 질문이에요.

이 게이트를 어떻게 만들고, 자기가 만든 걸 자기가 채점하는 문제(self-preference bias)를 어떻게 피하는지는 3편에서 다룹니다.



6. 구조적 결함 다섯 가지

치명적이진 않은데, 그냥 두면 운영에서 계속 발목 잡을 것들입니다.

④ 벡터 인덱스가 두 벌 생깁니다

Redis 에서 시맨틱 매칭을 하려면 Redis Stack 의 벡터 인덱스가 필요합니다. 그런데 그건 ES 의 dense_vector 가 이미 하는 일이에요. 같은 임베딩 모델로 만든 벡터를 두 곳에 넣고 두 곳을 각각 관리하게 됩니다.

문제 구체적으로
이중 색인 문서 갱신 시 두 군데 다 갱신. 하나 빠지면 조용히 어긋남
정합성 붕괴 Redis 는 최신, ES 는 옛날 벡터. 디버깅 지옥
모델 교체 불가 임베딩 모델 바꾸면 두 벌 다 재색인

깔끔한 분리는 이렇습니다.

부품 맡는 일
Elasticsearch 원문 청크 검색 + 유사질문 매칭 (벡터는 여기만)
Redis 정규화한 질문 해시 → 답변. O(1) exact 캐시만

Redis 를 Redis 답게 쓰는 거예요. 그리고 이러면 ④가 사라지면서 ②의 3단 게이트도 ES 안에서 자연스럽게 구현됩니다.

⑤ 생성 모델과 서빙 모델이 다릅니다

사전생성은 Codex(GPT 급)가 하고, fallback RAG 는 EC2 GPU 에 올린 오픈 모델이 합니다. 문제는 사용자 눈에 이렇게 보인다는 거예요.

“얘는 어떤 건 진짜 잘 답하는데, 어떤 건 이상하게 답하네.”

hit 인지 miss 인지는 사용자가 모릅니다. 그냥 일관성 없는 챗봇으로 느껴져요. 품질이 낮은 게 아니라 들쭉날쭉한 게 문제입니다.

처방은 두 가지를 같이 씁니다.

처방 효과
답변 스타일 규격 강제 (길이·구조·출처 표기) 최소한 모양은 통일됨
사전생성 Q&A 를 서빙 모델의 few-shot 예시로 재사용 서빙 모델이 Codex 의 답변 스타일을 흉내내게 됨

두 번째는 덤이 있어요. 사전생성 Q&A 가 캐시일 뿐 아니라 학습 자산이 됩니다. 나중에 서빙 모델을 파인튜닝할 때 그대로 데이터셋이 돼요.

⑥ 본대가 휘발합니다

Redis 가 “1차 답변자”, 즉 본대라고 했는데 Redis 는 인메모리입니다.

사건 결과
인스턴스 재시작 본대 전멸
maxmemory-policy allkeys-lru 메모리 차면 조용히 오래된 Q&A부터 삭제
AOF/RDB 없이 운영 복구 불가

특히 두 번째가 무서워요. 에러도 안 나고, 로그도 안 남고, 그냥 어느 날부터 hit rate 이 떨어집니다.

원칙은 하나입니다.

Redis 는 언제나 파생물(derived)이다. 원본(source of truth)은 S3/디스크의 JSONL + ES 이고, Redis 는 그걸로 언제든 통째로 재구축 가능해야 한다.

이렇게 두면 Redis 가 통째로 날아가도 “재구축 스크립트 한 번 돌리면 되는 일”이 됩니다. 사고가 아니라 작업이 되는 거예요.

⑦ 집과 AWS 사이

Hermes 는 집 미니서버에 있고 Redis·ES 는 AWS 에 있습니다. 생성물을 집에서 AWS 로 밀어야 하는데, 여기서 하면 안 되는 게 하나 있어요.

Redis 를 인터넷에 직접 노출하지 마세요.

Redis 는 기본 설정이 무인증이고, 무인증 노출로 털린 사례가 역사적으로 가장 많은 데이터스토어 중 하나입니다. bind 0.0.0.0 에 보안그룹 열어두면 스캐너가 몇 시간 안에 찾아옵니다.

안전한 방향은 단방향 적재예요.

단계 무엇이 어디로
1 집 Hermes 가 생성물을 JSONL 로 로컬 디스크
2 검증 통과분만 S3 (IAM 사용자, PutObject 만)
3 AWS 쪽 로더가 읽어서 ES + Redis 에 적재

집에서 나가는 건 S3 PutObject 권한 하나뿐입니다. Redis 포트도, ES 포트도 집을 향해 열 필요가 없어요. 이 경계 설계는 5편에서 자세히 봅니다.

⑧ 임베딩 모델이 정해져 있지 않습니다

원안을 다시 읽어보면 “EC2 GPU 로 RAG” 라고만 되어 있고, 어떤 임베딩 모델을 쓸지가 없습니다. 처음엔 저도 구현 디테일이라 생각하고 넘겼는데, 뜯어볼수록 이게 제일 큰 구멍이었어요.

너무 커서 절을 따로 뺐습니다. 바로 다음 절입니다.



7. 임베딩 — 이 아키텍처의 진짜 단일 실패점

임베딩은 세 군데에서 동시에 쓰입니다

앞에서 세 개의 결함을 고쳤는데, 고친 처방들을 다시 보면 공통점이 있어요.

처방 안에서 쓰는 것
3절 신규율 $r_k$ 로 생성 정지 질문끼리의 코사인 유사도
4절 Redis 3단 게이트 질문↔캐시 질문의 코사인 유사도
ES dense 검색 질문↔청크의 코사인 유사도

전부 같은 임베딩 모델 위에 서 있습니다. 그래서 이렇게 됩니다.

임베딩이 약하면 세 개가 한꺼번에 무너집니다.

구체적으로 어떻게 무너지는지 보면 감이 옵니다.

실패 A — 다른 질문을 비슷하다고 볼 때

어디가 어떻게 무너지나
3절 정지 조건 새 질문인데 중복으로 처리 → 조기 정지, 커버리지에 구멍
4절 게이트 오답 즉답. 결함 ② 가 그대로 재발
ES 검색 엉뚱한 청크를 회수

실패 B — 같은 질문을 다르다고 볼 때

어디가 어떻게 무너지나
3절 정지 조건 중복이 안 걸러짐 → 무한 생성 재발
4절 게이트 hit rate 0. 캐시가 통째로 무용지물
ES 검색 있는 근거를 못 찾음

임베딩 품질이 이 시스템 전체의 상한이에요. 아무리 좋은 LLM 을 EC2 GPU 에 올려도, 검색이 엉뚱한 청크를 물어오면 LLM 은 그 엉뚱한 청크로 성실하게 답을 씁니다. RAG 에서 생성 모델은 검색이 준 것보다 좋은 답을 만들 수 없어요.

로컬 GPU 임베딩이 부족할 수 있는 지점

내부 문서는 대개 한국어고, 사규·규정·업무 매뉴얼 같은 문서예요. 이런 텍스트가 임베딩 모델에게 유독 까다로운 이유가 있습니다.

특성 왜 어려운가
한자어 명사구 중심 “중도인출” / “중간정산” 처럼 글자는 비슷한데 제도가 다른 쌍이 많음
조사·어미 변화 같은 질문이 표면형으로 크게 달라짐
사내 전문용어·약어 공개 코퍼스에 등장하지 않아 모델이 못 배움
공문서체 일상 대화체 질문과 문체 거리가 멀어 질문↔문서 매칭이 약해짐

특히 마지막이 은근히 큽니다. 사용자는 “연차 며칠 남았어?” 라고 묻는데 문서는 “연차 유급휴가는 계속근로기간에 따라 부여한다” 라고 쓰여 있어요. 영어 중심으로 학습된 모델은 이 간극을 잘 못 넘습니다.

그래서 로컬 GPU 에 다국어 오픈 임베딩 모델을 올렸을 때 충분할 수도 있고 아닐 수도 있습니다. 여기서 중요한 건 이겁니다.

“충분한지 아닌지는 짐작으로 알 수 없습니다. 재야 압니다. 그래서 설계에는 잴 수 있는 장치갈아끼울 수 있는 이음매가 먼저 들어가야 해요.”

선택지 세 가지

로컬로 부족하다는 게 확인됐을 때, 갈 수 있는 길은 셋입니다.

선택지 내부 문서가 밖으로 품질 재색인 비용 장애 반경
A. 로컬 GPU 임베딩 안 나감 모델 나름 전기값만 내 서버만
B. 외부 임베딩 API 전량 나감 대체로 상단 호출당 과금 외부 장애 = 검색 전면 중단
C. 로컬 임베딩 + 리랭커 후보 청크만 A 와 B 사이, B 에 근접 전기값 + 리랭킹 리랭커 죽어도 A 로 degrade

B 를 고를 때 반드시 알아야 할 함정이 두 개 있어요.

함정 1 — 임베딩은 색인 때와 질의 때 반드시 같은 모델이어야 합니다. 문서를 A 모델로 색인해놓고 질문을 B 모델로 임베딩하면 벡터 공간이 달라서 검색이 완전히 무너집니다. 그래서 외부 API 를 쓰면 질문이 들어올 때마다 매번 외부를 왕복해야 해요. miss 경로 지연에 왕복이 더해지고, 외부가 죽으면 검색이 통째로 멈춥니다. “평소엔 외부, 장애 때 로컬” 같은 폴백도 안 됩니다 — 그러려면 두 모델로 두 벌 색인을 유지해야 하거든요.

함정 2 — 외부 모델이 은퇴하면 전체 재색인이 강제됩니다. 임베딩 API 는 버전이 올라가고 구버전은 언젠가 내려갑니다. 그날 수백만 청크를 다시 임베딩해야 하고, 그 비용과 시간이 우리 일정이 아니라 공급자 일정에 묶여요.

여기에 이번 건의 성격이 하나 더 얹힙니다. 대상이 내부 문서예요. 전량을 외부 API 로 밀어 넣는 건, 순수하게 기술 판단만으로 결정할 문제가 아닙니다.

그래서 추천은 C — 로컬 임베딩 + 리랭커

C 가 이 구성에 제일 잘 맞습니다. 발상은 간단해요.

단계 누가 무엇을
1차 회수 로컬 임베딩 + BM25 정확도를 조금 포기하고 넉넉히 뽑음 (top-50)
2차 재정렬 리랭커 그 50개만 정밀하게 다시 줄 세워 top-5

왜 이게 통하냐면, 1차 회수와 2차 정렬에 요구되는 능력이 다르기 때문이에요.

  • 1차는 놓치지만 않으면 됩니다(재현율). 순서는 엉망이어도 정답이 50등 안에만 들면 돼요.
  • 2차는 50개만 보면 되니까 질문과 청크를 나란히 놓고 정독할 수 있습니다. 임베딩처럼 각자 따로 벡터로 압축해놓고 비교하는 게 아니라, 둘을 붙여서 읽는 거예요(cross-encoder). 그래서 훨씬 정확합니다.

임베딩이 약해서 생기는 손해의 대부분은 “정답이 뒤에 묻히는 것” 이지 “정답이 아예 없는 것” 이 아니에요. 그건 리랭커가 메울 수 있는 종류의 손해입니다.

C 의 이점을 정리하면 이렇습니다.

이점 설명
문서 전량이 밖으로 안 나감 리랭커에 넘어가는 건 질문 + 후보 청크 50개
재색인이 여전히 공짜 벡터는 로컬 모델이 만듦. 청킹 바꿔도 전기값만
우아한 성능 저하 리랭커가 죽어도 1차 회수 결과로 계속 서비스
리랭커도 로컬 가능 오픈 리랭커를 같은 EC2 GPU 에 얹으면 외부 의존 0

마지막 줄이 특히 좋아요. 리랭커는 임베딩과 달리 색인에 묶여 있지 않습니다. 질의 때만 쓰이니까 언제든 바꿔 끼울 수 있고, 심지어 껐다 켜도 색인이 멀쩡해요. 임베딩 모델을 바꾸면 전체를 다시 색인해야 하는 것과 완전히 대비됩니다.

그럼 외부 LLM 은 언제 쓰나

버리자는 게 아니에요. 쓰는 자리를 옮기자는 겁니다.

용도 외부 LLM 적합도 이유
Q&A 생성 (2편) 적합 배치. 지연 무관, 품질이 곧 자산. 원안의 Codex 자리
신뢰도 검증 (3편) 적합 배치. 채점은 생성보다 어려워서 좋은 모델값을 함
리랭킹 🔺 조건부 품질은 좋으나 질의마다 호출 → 지연·비용·장애 반경
임베딩 비권장 색인에 영구히 묶임. 함정 1·2 직격
서빙 생성 🔺 조건부 EC2 GPU 본선, 비용 대안으로 관리형

정리하면 이렇습니다.

배치에는 외부 좋은 모델을 아낌없이 쓰고, 질의 경로와 색인에는 내가 통제하는 로컬 모델을 둔다.

외부 모델의 품질은 이미 사전생성 Q&A 안에 응고되어 우리 쪽에 남습니다. 좋은 답을 배치로 사와서 자산으로 쌓아두고, 실시간 경로는 내가 소유한 것으로 돌리는 거예요. 이게 이 하이브리드 구성의 가장 좋은 쓸모입니다.

판단 기준 — 언제 로컬을 포기하나

말로만 “재보고 정하자”고 하면 안 정해지니까, 기준선을 못 박아둡니다. 3편에서 만들 골든셋으로 재현율을 재서 비교해요.

측정 결과 결정
로컬 + 리랭커가 외부 임베딩 대비 -3%p 이내 로컬 유지. 문서 반출 없음 + 재색인 공짜가 이김
-3 ~ -10%p 리랭커 모델부터 교체·튜닝. 그래도 안 되면 재검토
-10%p 초과 외부 임베딩 진지하게 검토. 단 함정 1·2 를 비용에 반영할 것

그리고 어느 쪽으로 가든, 설계에 이음매를 먼저 박아둡니다.

박아둘 것 이유
임베딩 호출을 인터페이스 뒤로 모델 교체가 한 파일 수정이 되게
벡터에 모델명·차원 태그 섞인 벡터를 검출 가능하게
임계값을 설정값으로 분리 모델 바꾸면 같이 다시 잡아야 하니까
재색인 스크립트를 1일차에 모델 교체는 “언젠가”가 아니라 반드시

마지막 줄이 이번 절의 결론이에요. 임베딩 모델은 언젠가 반드시 바뀝니다. 로컬로 시작했다가 부족해서 바꾸든, 더 좋은 게 나와서 바꾸든, 외부 모델이 은퇴해서 바꾸든요. 그러니 “어떤 모델이 최고냐”를 지금 맞히려 하지 말고, 바꾸는 게 싼 구조로 지어야 합니다.



8. 비용 — GPU 를 24시간 켜두는 게 맞나

숫자를 한 번 놓고 봅시다. GPU 인스턴스 온디맨드 요금은 리전·시점에 따라 다른데, g5.xlarge 기준 대략 시간당 $1 선입니다. 상주시키면

항목 대략
시간당 $1
하루 $24
$700 대

개인 프로젝트로는 꽤 부담스러운 금액이에요. 그런데 더 이상한 건 구조상 GPU 가 놀고 있다는 겁니다. Redis 가 1차 답변자면 GPU 는 miss 때만 일해요. hit rate 이 높을수록 GPU 가 노는데, 상주 비용은 그대로 나갑니다.

방식 월 비용 감각 트레이드
온디맨드 상주 $700 대 항상 준비됨. 대부분 놀고 있음
Spot + 오토스케일 상당히 절감 중단 가능성 → 재시도 로직 필요
생성 배치만 GPU, 서빙은 관리형 배치 시간만큼만 부품 하나 늘어남

세 번째가 이 구성엔 잘 맞습니다. 대량 Q&A 생성은 GPU 가 꼭 필요한 무거운 배치고, 서빙 fallback 은 간헐적이거든요. 성격이 정반대인 두 워크로드를 같은 상주 인스턴스에 얹을 이유가 없어요.

다만 7절 때문에 계산이 조금 달라집니다

앞 절에서 로컬 임베딩 + 리랭커를 골랐죠. 그런데 이 둘은 질의가 올 때마다 도는 물건이에요.

워크로드 언제 도나 GPU 필요도
Q&A 대량 생성 야간 배치 큼 (그러나 간헐)
질문 임베딩 질의마다 작음 (상시)
리랭킹 top-50 miss 마다 중간 (상시)
답변 생성 (fallback) miss 마다 큼 (간헐)

즉 “GPU 는 miss 때만 논다”던 앞의 진단이 절반만 맞았습니다. 임베딩과 리랭킹은 상주해야 해요. 대신 이 둘은 답변 생성용 LLM 보다 훨씬 가볍습니다.

그래서 현실적인 분리는 이렇게 됩니다.

무엇을 어디에
상시 (작은 GPU) 질문 임베딩 + 리랭커 작은 상주 인스턴스
간헐 (큰 GPU) 대량 생성 배치 Spot, 배치 시간만
간헐 (선택) fallback 답변 생성 위와 공유 또는 관리형

큰 GPU 를 24시간 켜두는 대신, 작은 걸 상주시키고 큰 걸 필요할 때만 부르는 구조예요. 원안의 “GPU 하나에 다 얹기”보다 훨씬 싸고, 무엇보다 각 층을 따로 늘릴 수 있습니다.

마침 Bedrock 챗봇 시리즈에서 관리형 쪽은 이미 정리해뒀으니, 서빙만 그쪽으로 빼는 선택지도 열려 있습니다. 다만 이번 시리즈는 목표대로 EC2 GPU 를 본선으로 두고 짓고, 비용 최적화는 5편에서 따로 다룰게요.



9. 보정안과 최종 점수

결함들을 고친 그림은 이렇게 됩니다.

원안 보정안
Q&A 무한 생성 신규율 기반 포화 정지 + 커버리지로 진행률 관리
Redis 1차 답변자 (hit = 확정) 3단 게이트 (즉답 / 힌트 주입 / 순수 RAG)
생성만 있음 생성 → groundedness 검증 → 통과분만 적재
Redis + ES 벡터 이중화 벡터는 ES 에만, Redis 는 exact 캐시
Redis 가 본대 Redis 는 파생물, 원본은 S3 JSONL + ES
집↔AWS 직결 S3 경유 단방향 적재
임베딩 미결정 로컬 임베딩 + 리랭커, 골든셋으로 측정 후 교체 가능한 이음매
GPU 하나에 다 얹기 작은 상주 GPU(임베딩·리랭커) + 큰 간헐 GPU(생성 배치)

다시 채점하면

원안 보정안 무엇이 바뀌었나
정합성 1.5 2.5 벡터 단일화, 부품별 역할 분리, 임베딩 이음매
정확도 1.0 2.5 검증 게이트 + 3단 게이트 + 리랭커
수렴성 1.5 2.0 정지 조건이 생김
운영성 2.0 1.5 부품이 늘어 복잡도는 오히려 증가
합계 6.0 8.5  

⚠️ 8.5 는 조건부 점수입니다. 7절의 임베딩 측정을 실제로 돌려서 통과했을 때의 점수예요. 재보지 않고 넘어가면 3절·4절의 처방이 숫자만 그럴듯한 채로 작동하지 않습니다. 이 시리즈에서 유일하게 “생략하면 나머지가 다 무의미해지는” 단계입니다.

그리고 8.5 가 끝이 아닙니다. 이 설계를 실제로 굴려보면 “잊지 마세요”로 남겨둔 자리가 몇 군데 드러나는데, 그걸 구조로 바꿔서 6편에서 9.4 까지 올립니다. 거기서 10점을 왜 못 사는지도 같이 적어요.

운영성이 떨어진 게 눈에 띄죠. 정직하게 적어둡니다. 검증 게이트와 3단 게이트는 공짜가 아니에요 — 관리할 파이프라인 단계가 늘고, 튜닝할 임계값이 생기고, 모니터링할 지표가 늘어납니다.

그럼에도 8.5 인 이유는 명확합니다. 원안의 6점은 “복잡도가 낮아서” 받은 점수가 아니라 “틀린 답을 막을 방법이 없어서” 깎인 점수였거든요. 챗봇에서 그건 타협할 수 있는 축이 아닙니다.

판정: 조건부 통과. 지어도 됩니다.



10. 남은 5편이 채우는 것

이 글은 설계도의 빨간 줄만 그었어요. 실제로 짓는 건 다음부터입니다.

채우는 결함 다루는 것
2편 Hermes + Codex 생성 파이프라인. 청킹, 질문 생성, 중복 억제, 커버리지 기반 정지
3편 ③⑧ groundedness 검증, 폐기 기준, 골든셋으로 임베딩 재기, 회귀 테스트
4편 ②④⑧ ES BM25+dense 하이브리드, RRF, 리랭커, Redis 3단 게이트, 캐시 무효화
5편 ⑤⑥⑦ S3 단방향 적재, 재구축 가능한 Redis, GPU 층 분리와 비용, 모니터링·롤백
6편 ②의 잔여 + 운영성 2차 채점. 즉답 게이트 이중화, 근거 생존 확인, 임계값 6→1, 골든셋 델타 갱신 → 9.4점

마지막으로 이번 검증에서 크게 배운 두 가지를 적어두고 마칠게요.

하나. 부품이 다 표준이어도 엮는 순서가 틀리면 시스템은 틀립니다. 특히 캐시를 검증 앞에 두면, 그 캐시는 성능 최적화가 아니라 오답 증폭기가 됩니다.

둘. 이 시스템의 상한은 LLM 이 아니라 임베딩이 정합니다. 그런데 임베딩은 미리 맞힐 수 없고 재야 알 수 있는 것이라, 좋은 모델을 고르는 것보다 바꾸기 쉽게 짓는 게 먼저입니다.

“빠른 답”과 “맞는 답” 중에 뭘 먼저 놓을지의 문제였고, 원안은 순서가 뒤집혀 있었어요. 2편에서 이어갑니다.