GS리테일 AX 블로그

AI와 데이터로 고객의 생활 가치를 혁신하는 GS리테일 AX 이야기

카테고리 없음

[사내 지식 AI 만들기 ③-1부·대화] 사용자는 한 줄로 묻지 않았다 — 그런데 정확도 1.0으로도 부족했다 (RAG → AgenticRAG)

DarionKim 2026. 7. 10. 13:56

사서가 필요했다

2탄까지 — 지식은 통합 컬렉션에 잘 저장되고, 권한대로 안전하게 조회된다. 그런데 한 가지가 끈질기게 남았다.

"이 질문은 어느 KB를 봐야 하지?"

per-KB 시절엔 사람이 골라줬다. "그건 인사 KB에 물어보세요." 새 KB가 끝없이 추가되는 환경에서, 이 수동 선택은 곧 한계에 부딪혔다. 게다가 사용자는 한 줄로 묻지 않는다.

"지난번 그 정산 건, 점포 기준이랑 본사 기준 다르지? 그리고 그거 휴가에도 영향 있어?"

한 마디에 정산·점포운영·인사 세 지식이 얽힌다. 멀티턴, 멀티인텐트. 사람이 KB를 골라주는 고전 RAG로는 이 대화를 감당할 수 없다. 잘못 고르면 — 문서는 있는데 답을 못 한다.

이건 가정이 아니라 반복해서 겪은 패턴이었다. 같은 사용자가 부서 경계를 넘나드는 질문을 매번 다른 봇에 따로 물어야 했고, "이걸 왜 매번 나눠서 물어야 하나"라는 불만이 쌓였다. 지식은 잘 저장돼 있는데(1탄), 그걸 하나의 대화로 잇지 못하면 사용자에게는 여전히 흩어진 지식일 뿐이었다.

RAG → AgenticRAG

전환점은 이거였다. "어느 지식을 볼지"를 사람이 하드코딩하는 대신, 에이전트가 대화 맥락 + 사용자 권한 + 의도로 동적으로 고르게 했다.

🔍 여기서 "Agentic"의 범위: 에이전트가 무엇을 검색할지 스스로 정한다는 뜻이다 — 의도를 분해하고, 접근 가능한 KB scope를 고르고, 멀티인텐트를 라우팅한다. (검색 결과를 스스로 평가·재시도하는 더 무거운 _적응형 루프_는 지연·비용 트레이드오프가 있어 별개의 프론티어로, 신중히 단계적으로 켠다. 과장하지 않는다.)

멀티턴/멀티인텐트 질문
   → 의도 분해 (멀티 인텐트 감지)
   → 사용자 권한(2탄) 해석 → 접근 가능한 KB scope 자동 선택   ← 사람이 KB 안 고름
   → scope 내 통합 검색(1탄)
   → 필요하면 clarify ("어느 점포 기준이요?")
   → rerank → 근거 기반 답변

대화 스택:

LLM 라우팅   : LiteLLM Proxy (작업별 모델 분리 — 분류는 가볍게, 답변은 무겁게)
의도 분해    : LLM 기반 intent classifier (멀티인텐트 감지)
에이전트 조율: Supervisor 패턴 (역할별 에이전트에게 위임)
비동기 실행  : Temporal 워크플로 (에이전트 작업 상태 추적)
권한 신호    : 사용자 조직 기준 KB 접근범위 결정

한 명의 사서가 아니라 역할이 나뉜 여러 에이전트가 이 대화를 나눠 맡는다. 지식을 아는 에이전트, 그걸 조율하는 층, 실제로 화면을 조작하거나 알림을 보내는 에이전트 — 저마다 자기 몫을 하고, 서로에게 넘긴다. 그리고 누가 물었는지(조직)가 그 라우팅에 함께 반영된다 — 다만 "이 사람이 평소 자주 찾는 지식까지" 반영하는 개인화는 아직 다른 이야기다. (2부에서 왜 그런지 다룬다.)

사서 비유 — 사서 한 명에서, 사서 팀으로

예전 도서관에선 사서가 "303호 서가로 가세요"라고 위치를 알려줬다 — 사용자가 서가 번호를 알아야 했다(수동 KB 선택). 처음엔 사서 한 명(에이전트 하나)이 질문을 듣고 알아서 서가를 도는 정도로 충분해 보였다.

그런데 질문이 여러 전문 분야에 걸치자, 사서 한 명으로는 부족했다. 그래서 도서관을 사서 팀으로 다시 그렸다 — 각 분야를 잘 아는 사서들이 있고(아는 역할), 그 사서들 사이에서 "이 질문은 누구한테 먼저 물어야 하나"를 정리해주는 총괄 사서가 있고(조율하는 역할), 그리고 자료를 찾아주는 데서 그치지 않고 실제로 뭔가를 갖다주거나 대신 처리해야 할 때 움직이는 사람도 있다(실행하는 역할). 사용자는 여전히 한 명의 사서에게 물을 뿐이지만, 그 뒤에서는 팀이 협업한다.

핵심은 "검색을 똑똑하게"이기 이전에, "무엇을 검색할지를 에이전트가 정한다"였다. 끝없이 자라는 KB를 사람이 일일이 관리하던 구조에서, 에이전트가 권한과 의도로 길을 찾는 구조로.

rerank — 후보를 찾은 다음, 순서를 다시 세운다

"무엇을 검색할지"는 에이전트가 정했다. 그런데 검색(1탄, dense+sparse hybrid)은 후보를 넓게 찾는 데 강할 뿐, 그 후보를 질문에 가장 잘 맞는 순서로 세우는 데는 약하다. 임베딩 검색(bi-encoder)은 질문과 문서를 각각 따로 벡터로 만들어 거리로 비교하기 때문에 빠르지만, 질문과 문서를 나란히 놓고 세밀하게 견주지는 못한다.

그래서 상위 후보에만 한 번 더 손을 댄다. 질문과 문서를 함께 입력해 정밀하게 관련도를 매기는 cross-encoder 기반 reranker를 상위 후보에만 돌린다 — 전체 후보에 돌리면 느리니까, 값싼 1차 검색으로 후보를 좁힌 다음 비싼 2차 정렬을 그 위에만 얹는 구조다. 상용 rerank API를 1순위로 쓰고, 막히면 자체 호스팅 cross-encoder로 넘어가는 이중화도 해뒀다.

순서를 정할 때도 모델 점수 하나만 보지 않는다. 모델 관련도 + 원래 검색 순위 + 출처 종류(공식 문서·웹·그래프·FAQ 별 가중치) + 필요하면 결과 다양성을 가중합해서 최종 순서를 만든다. FAQ로 등록된 답이 있으면 살짝 밀어주고, 비슷한 문서만 줄줄이 나오지 않도록 다양성 점수도 얹는다. "무엇을 볼지"는 에이전트가 고르고, "어떤 순서로 보여줄지"는 이 재정렬이 고른다 — 둘이 같이 맞아야 사용자가 실제로 원하는 답에 먼저 닿는다.

(이 "값싸고 넓게 + 비싸고 정밀하게" 이중 구조, 기억해 두면 2부에서 한 번 더 보게 된다 — 검색 말고 다른 layer에서.)

정확도 1.0인데, 왜 여전히 나쁜 경험이었나

여기서 또 한 번 배웠다. 어느 질문이 어느 지식 범위(KB)에 속하는지, API로 태그를 정확히 매기는 것 자체는 정확도 1.0까지도 낼 수 있었다. 그런데 그것만으론 부족했다.

사람은 한 번에 묻지 않는다. 되묻고, 앞서 한 말을 다시 끌어오고, 한 문장에 여러 의도를 섞는다. 지식 범위를 완벽하게 골라내는 것과, 그 반복·다의도 대화를 자연스럽게 받아내는 것은 다른 능력이었다. 태깅 정확도가 아무리 높아도, 대화 기능이 그 대화의 흐름을 못 따라가면 — 사용자에게 돌아오는 건 잘 만든 지식이 아니라, 뚝뚝 끊기는 나쁜 경험뿐이었다.

즉 1탄의 저장, 2탄의 권한이 아무리 정교해도, 이 위에서 사람과 자연스럽게 대화를 이어가는 능력이 없으면 그 지식은 사용자에게 닿지 않는다. 지식 범위를 고르는 일(라우팅)과 대화를 이어가는 일(대화 관리)은 같이 완성돼야 하는 한 쌍이었다.

심화 — reranker는 fallback 안에 fallback, 그리고 점수가 1.0에 붙어버린 사고

이 절은 깊다. 라우팅과 재정렬이 실제 코드에서 어떻게 방어되는지 궁금한 분을 위한 detail이다. 바쁘면 맺음으로 건너뛰어도 좋다.

① 코드 — 리랭커는 fallback 안에 fallback

본문에서 "상용 rerank API 1순위, 막히면 자체 호스팅 cross-encoder"라 했다. 실제 코드는 한 겹 더 깊다.

def _create_reranker():
    """우선순위: cohere(상용, LiteLLM 경유) > cross_encoder(자체) > 없음."""
    if reranker_type == "cohere" and os.getenv("LITELLM_API_BASE"):
        return CohereReranker()                    # 1순위: 상용 API
    if os.getenv("DISABLE_CROSS_ENCODER_RERANKER") != "true":
        adapter = CrossEncoderIRerankerAdapter()   # 2순위: 자체 호스팅
        loop.create_task(adapter.warmup())         # 모델 로드는 논블로킹
        return adapter
    logger.warning("리랭커 없음 — 벡터 유사도 순서만. 관련도 ~20%% 저하 예상.")
    return None                                    # 3순위: 없음(조용히 넘기지 않고 숫자 로깅)

눈여겨볼 두 가지: (1) 자체 모델 warmup을 loop.create_task논블로킹 발사 — 콜드 모델 로딩이 그 요청을 붙잡지 않는다. (2) "리랭커가 아예 없는" 최후 분기가 조용히 넘어가지 않고 "~20% 관련도 저하"라는 숫자를 로그에 남긴다. 그리고 자체 어댑터(CrossEncoderIRerankerAdapter) 안에는 또 3단 fallback이 있다 — 원격 자체호스팅 HTTP → 인프로세스 로컬 모델 → 원순위 기반 선형감쇠. fallback 안의 fallback.

② 영리한 선택 — 점수 정규화: 절대 clamp vs 배치 상대 stretch

싼 1차 점수(벡터 유사도)와 비싼 2차 점수(cross-encoder 관련도)를 가중합할 때, 두 점수의 스케일이 다르다(코사인은 대략 01, rerank logit은 -1010 같은 무한대역). 우리가 고른 방식은 각 점수를 절대 기준 0~1로 clamp하는 것. 기각한 대안:

  • 정규화 없이 그냥 합산 — 스케일이 달라 무의미.
  • 배치 안에서 min-max로 늘리기(stretch) — 이게 함정이었다(④ 참조).
  • 순위-위치 가중(1등=1.0, 2등=0.95…) — 점수 크기를 버려서, 톱1이 "진짜 좋아서"인지 "그 배치에서 제일 덜 나빠서"인지 구분 못 함.

③ 숫자, 정직하게 — 둘은 실측, 하나는 아직

  • 점수 포화 fix: golden-question 평가에서 리랭커 톱1 점수가 천장에 붙어 있었다 — 실제 관련도와 무관하게 톱1이 ≥0.94인 비율이 89.6%. 배치 min-max 정규화가 그 배치의 최고점을 항상 1.0으로 만든 탓. 목표를 "톱1 ≥0.94 비율 <50%"로 잡고 고쳤다.
  • KB scope 확장 A/B(프로덕션 데이터, 골든 26문항): 좁은 기본 scope vs 권한 내 넓힌 scope → 도메인 질문 근거율(grounded@0.5)이 3/22(13%)에서 10/22(45%)로, 대조군 회귀 0.
  • intent classifier 정확도: 하드 넘버 아직 없음(목표 ≥80% top-1, 8문항 PoC만) — 정직하게 "측정 인프라는 설계했으나 아직 대규모 미가동".

④ 우리가 처음엔 틀렸다 — flag 켜니 "이 시스템 뭐야?"가 깨졌다

KB scope를 넓히는 건 명백히 좋아 보였다(③의 A/B가 증명). 그런데 프로덕션에서 flag를 켜자, 그동안 항상 맞히던 기본 질문 — "이 시스템 뭐야?" — 이 즉시 회귀했다. 격리된 A/B 경로에선 멀쩡했는데, 라이브 end-to-end 경로에서만 깨졌다.

원인은 ②의 점수 포화와 같은 버그 계열이었다: 명시적 KB 리스트가 없을 때만 도는 내부 "KB 선택" 스텝이, min/max 기반 정규화로 아무 상관없는 대형 KB에 인공적 천장 점수를 줘서 톱으로 올려버린 것. 좁혀진 후보 집합에서 "제일 top인 하나"가 실제 관련도와 무관하게 1.0을 받는, 바로 그 실패.

고친 순서가 교훈이다: (1) 같은 날 flag를 한 줄로 되돌려 롤백 → (2) 다음날 격리로 원인 규명(명시적 KB 리스트로 넘기면 정상, "필터 없이 파이프라인이 알아서 고르게" 하면 회귀) → (3) 넓힌 scope를 항상 명시적 KB 리스트로 넘겨 그 버그 경로를 고치는 대신 우회해 재출시. 이 사고는 지금 라이브 코드의 docstring(_resolve_accessible_default_kb_filter)에 그대로 적혀 있다 — 코드가 자기 과거 회귀를 기억한다.

교훈: 격리된 실험에서 좋았다고 라이브에서도 좋은 게 아니다. "이 경로에서만 도는 스텝"이 실험 경로엔 없고 라이브 경로엔 있으면, 그 차이가 정확히 회귀가 숨는 자리다.

맺음 — 그리고 이 반전이 남긴 질문

이제 에이전트가 알아서 지식을 찾는다. 멀티턴으로 얽힌 질문도, 권한 안에서, 근거를 모아 답한다. 그런데 정확도 1.0짜리 라우팅으로도 사용자는 여전히 나쁜 경험을 겪었다 — 그 이유를 따라가면 질문이 하나 남는다.

"검색이 맞았는가"와 "이 사람에게 답이 됐는가"는 다른 질문이었다. 그리고 2탄이 심어놓은 또 다른 숙제 — 이 사람이 _평소 자주 찾는 지식_은 여전히 못 알아본다는 것 — 도 그대로 남아있었다.

이 두 질문에 어떻게 답을 하려 했는지는 3탄-2부 — RAG Score와 Answer Readiness는 다른 질문이다에서.


시리즈: 1탄 저장 · 2탄 권한 · 3탄-1부 대화 (이 글) · 3탄-2부 대화 · 4탄 채널 · 번외 수집


김헌기 Darion · AX본부 > AI데이터부문 > AI혁신지원팀

AI 및 공통 Tech 기반의 기술 표준화 업무를 수행하고 있습니다. 동료들과 함께 기술 문화 만들기와 낯선 기술자와의 인연의 시작에 관심이 많습니다.