기존 검색은 벡터 유사도만으로 top_k(3개)를 바로 확정해서 프롬프트에 넣었다. 실무 RAG에서는 "벡터 유사도 top_k만으로 끝내지 않고, 더 넓게 붑은 후보를 리랭커로 재정렬해서 최종 top_k를 고르는" 2단계 검색이 거의 필수로 쓰인다 — 벡터 유사도는 의미적으로는 가깝지만 질문의 실제 의도와는 미묘하게 어긋나는 청크를 상위에 올리는 경우가 많은데, 리랭커(query-document 쌍을 직접 보고 관련도를 매기는 cross-encoder류 모델)가 이걸 상당히 교정해준다.

왜 로컬 cross-encoder(bge-reranker 등)가 아니라 Cohere API인가

bge-reranker-base처럼 널리 쓰이는 오픈소스 리랭커는 cross-encoder라 torch/transformers 런타임 위에서 모델을 직접 로드해야 한다. 그런데 이 서비스가 떠 있는 파드의 메모리 한도는 deployment.yaml 기준 384Mi이고, 뜨는 노드(k3s-worker2)도 GPU 없는 1 core/1.75GB 워커다. torch + transformers를 임포트하는 것만으로도 수백MB를 먹는 게 보통이라, 모델 가중치까지 얹으면 384Mi를 사실상 넘긴다.

이미 이 서비스는 임베딩/생성 둘 다 Gemini API 호출로 처리하고 있어서(사안 모델을 전혀 안 띄움), 같은 원칙을 리랭커에도 적용해 Cohere Rerank API(rerank-v3.5, 다국어 지원 — 한국어 포함)를 붙였다. 파드 메모리 부담이 거의 없고(HTTP 호출 하나 추가되는 정도), 기존 아키텍cc98와의 일관성도 유지된다.

구현

배포 쪽 변경 (k3s-msa-infrastructure 리포)

apps/threat-intel-ai-service/deployment.yaml에 COHERE_API_KEY 환경변수를 target-tracking-secrets Secret의 cohere-api-key 키에서 읽도록 추가했다. GEMINI_API_KEY와 다르게 optional: true를 붙였는데, 이 키는 아직 실제 클러스터 Secret에 없기 때문이다 — optional이 없으면 없는 키를 참조하는 순간 파드가 CreateContainerConfigError로 못 뜵다. Cohere API 키를 실제로 발급받아 넣기 전까지는 리랭커 없이(벡터 검색만으로) 그대로 동작한다.

kubectl -n c4i patch secret target-tracking-secrets \
  --type merge -p '{"stringData":{"cohere-api-key":"<발급받은 키>"}}'
kubectl -n c4i rollout restart deployment/threat-intel-ai-service

신경 써야 할 점 / 아직 검증 못 한 것

① score 필드의 의미가 리랭크 on/off에 따라 달라진다. 리랭크가 켜지면 이 score가 원래 Qdrant 코사인 유사도에서 Cohere relevance_score로 덤어쓰인다 — 둘 다 대략 0~1 범위지만 계산 방식이 다른 별개 지표다. 이 score는 /chat의 SSE sources 이벤트로 프론트엔드에 그대로 노출되므로, 나중에 프론트에서 이 값을 해석/필터링하는 로직을 추가한다면 이 점을 알고 있어야 한다.

② 실제 API 호출 검증은 못 했다. Cohere API 키가 없어서, rerank()가 실제 응답을 어떻게 파싱하는지는 설치 후 시그니처/스키마 검사로만 검증되었고(cohere 패키지를 .venv에 임시 설치해 ClientV2.rerank()의 실제 시그니처와 응답 스키마를 코드 작성 전에 직접 확인 후 삭제), 진짜 HTTP 요청-응답 왕복은 아직 안 해뷈다.

③ 리랭크가 실제로 답변 품질을 개선하는지는 숫자로 아직 못 재었다. eval/evaluate_rag.py(RAGAS faithfulness/answer_relevancy)는 /chat을 그대로 호출하므로 코드 수정 없이 리랭크 on/off 두 상태로 각각 돌려서 비교할 수 있다 — 실제 키가 들어간 뒤 해야 할 다음 작업으로 남겨둔다.

관련 문서