Aiden Kwak Pages

IBM Korea Ontology 구축기 (2)

RAG 챗 답변 품질 교정

2026-05-08 ·

1편에서 이야기하지 못한 부분이 있었습니다. 채팅 인터페이스에서 그래프 사실과 문서 청크를 함께 LLM 컨텍스트에 주입하는, Graph-augmented RAG의 구현 부분 이었습니다. 답변 품질이 너무 저조하여 이건 따로 기록을 해야겠다는 생각에서였는데, 우선 지금까지 확인한 내용들을 기록하려합니다.

저는 처음 채팅 인터페이스를 띄우고 단순한 질문 하나를 던졌습니다.

"APIC 제안이력"

답이 돌아왔습니다.

"A 생명은 AI 지식베이스(RAG) 구축 사업을 위해 IBM의 Instana, API Connect 및 watsonx Code Assistant 등 솔루션을 활용할 계획이며, APIC를 초기 제안 단계에 포함하는 것이 합리적인 접근이라고 생각한다. [4]"

한 문장이었습니다. 인용도 단 한 건. 인용 카드 네 장이 화면 아래에 떠 있었지만, 모두 score 0.016 으로 동일했습니다. 11,064개의 문서 위에 그래프와 BM25 와 임베딩을 다 깔아두었는데, 정작 챗은 의미 있는 답을 만들어내지 못했습니다. 또한 "APIC"라는 키워드에 편향된 답변을 내놓는 모습도 보입니다.

답변은 한문장이고 score는 모두 0.016임

우선 화면에 떠 있는 인용 카드들의 score 가 모두 0.016 으로 동일하다는 점에서 시작해보려합니다.

1. 점수 0.016 - RRF의 무력화

가장 먼저 눈에 들어온 신호는 인용 카드 네 장의 점수가 모두 0.016 이라는 점이었습니다. 1편에서 다뤘던 RRF (Reciprocal Rank Fusion) 의 공식을 다시 떠올려봅니다.

score(d) = Σ_i  1 / (k + rank_i(d))   # k = 60

두 retriever (BM25 와 벡터) 가 모두 한 청크를 hit 하면, 그 청크는 최소 1/(60+1) + 1/(60+1) ≈ 0.033 을 받습니다. 한 쪽만 hit 하고, 그쪽 retriever 의 1위였다면 정확히 1/(60+1) ≈ 0.0164. 네 건이 모두 0.016 이라는 건, BM25 가 본 청크와 벡터가 본 청크가 한 건도 겹치지 않았다 는 뜻입니다. 둘이 만나지 못하니 fusion 의 효과가 0입니다.

왜 만나지 못했을까. "APIC" 으로 BM25 를 돌리면 본문에 "APIC" 이라는 글자가 정확히 들어 있는 청크만 잡힙니다. 그런데 우리 코퍼스에서 가장 잘 정리된 제안서들 (예를 들어 B 저축은행_Open API시스템 구축(제안서) 같은 핵심 문서) 은 본문에 "IBM API Connect" 라고 적혀 있습니다. 풀네임으로요. BM25 는 그걸 다른 단어로 봅니다. 한편 벡터는 의미적으로 가까운 청크를 끌어왔지만, "A 생명 1차 미팅 회의록" 같은 인접 토픽의 잡음을 함께 데려왔습니다. 결국 두 retriever 는 각자 다른 풀에서 결과를 길어 올렸고, 그 둘이 한 번도 만나지 못했습니다.

증상: 0.016 일색의 점수표. 원인: 어휘의 사각지대. 열쇠: 검색 전 단계에서 어휘를 풀어주기.

2. 글로서리를 검색까지 끌어쓰기

1편에서 저는 글로서리를 만들었습니다. ibm_terms.ymlAPI Connect 가 canonical 로 들어 있고, 그 별칭으로 APIC, IBM API Connect 가 적혀 있습니다. 그런데 정작 BM25 쿼리는 그 사전을 한 번도 들여다보지 않고 사용자의 원문을 그대로 OpenSearch 에 넘기고 있었습니다. 글로서리는 LLM 의 시스템 프롬프트 에서만 안내문으로 등장했죠. "APIC 은 API Connect 와 같은 것입니다" 라는 문구가 모델에게 전달은 되지만, 그것은 이미 검색이 끝난 뒤의 일입니다. 검색이 빈약하면 컨텍스트가 빈약하고, 컨텍스트가 빈약하면 답도 빈약합니다.

다음과 같이 해결했습니다. BM25 호출 직전, 사용자의 쿼리를 글로서리로 한 번 통과시켜 별칭을 OR 로 합성합니다.

# 사용자 입력:  "APIC 제안이력"
# expand_query 통과 후: "APIC 제안이력 API Connect IBM API Connect"

벡터 retrieval 에는 일부러 적용하지 않았습니다. bge-m3 같은 다국어 임베딩은 "APIC" 과 "API Connect" 의 의미를 이미 가깝게 두기 때문에, 별칭을 덧붙이면 오히려 centroid 가 흐려집니다. BM25 의 어휘 사각지대만 보강하는 쪽이 깔끔했습니다.

다시 확인한 점수표에서 효과를 확인할 수 있었습니다.

BeforeAfter
점수 분포모두 0.016 (단일 retriever 1위만)0.041-0.063 (두 retriever 가 만남)
인용 다양성A 생명 관련 문서 1건B 저축은행 / C 증권 / D 증권 / E 은행 (해외) / API Connect SaaS 공식문서 등

두 retriever 가 만나기 시작했다는 신호가 점수 분포에서 그대로 보입니다.

3. 짧은 질문을 분해하기 - Query Decomposition as CoT

다음으로 손을 댄 것은 짧은 질문 자체였습니다. "APIC 제안이력" 은 사람에게는 명확하지만, 검색 시스템에게는 거의 두 단어짜리 신호입니다. 임베딩 공간에서 좁고, BM25 어휘에서도 두 토큰 뿐이죠.

좋은 답을 얻으려면 좋은 질문을 던지라고들하지만 매번 그러는게 쉬운 일이 아닙니다. 비개발자 현업인 고객들에게는 더한 일일 것입니다. 해결을 위해 떠올린건 우선 CoT입니다. CoT (Chain of Thought) 라고 하면 보통 답변 단계에서 모델이 단계적 추론을 거친다는 의미를 떠올립니다. 그런데 7B/9B 급 로컬 모델에서 답변용 CoT 의 효과는 제 경험에선 미미했습니다. 답변 latency 만 1.5-3 배 늘고 품질은 marginal합니다. 그럼 검색 전의 CoT라면 어떨까요? 즉 질문을 검색하기 좋은 형태로 분해하는 단계입니다.

mermaid 로드 중…

작은 LLM 한 번의 호출 (약 3초) 로 sub-query 2-4개를 만들고, 각각에 대해 Hybrid 검색을 돌립니다. 그리고 file_id 수준에서 RRF 를 한 번 더 - 여러 sub-query 에서 동시에 잡힌 문서가 자연스럽게 위로 올라옵니다. 이 단계는 listing 의도가 감지되거나, 질문이 14자 이하로 짧을 때만 트리거합니다. 대부분의 일반 질문에는 latency 페널티 없이 기존 single-query 경로를 그대로 탑니다.

이 단계가 끝났을 때, "APIC 제안이력" 은 10건 안팎의 다양한 고객사 제안 자료를 검색 결과에 끌어왔습니다. 이전엔 A 생명 관련 문서 하나만 인용에 걸리던 자리였습니다.

4. 온톨로지를 답변까지 흘려보내기

여기까지가 검색 의 보강이었다면, 다음은 1편에서 짠 그래프를 어떻게 챗 답변에 더 깊이 흘려보낼 것인가 였습니다. 솔직히 말하면 그래프는 검색 페이지에서만 활용되고 있었고, 챗에서는 메타 라벨 (고객: X, 제품: Y) 의 자리만 차지하고 있었습니다.

4-1. 자동 facet 추출

사용자가 "C 증권 오픈API 제안 이력" 이라고 쓰면, 시스템은 그 안에서 customer = C Securities 를 알아내야 합니다. 글로서리의 OrgEntry 마다 surface form (raw, canonical, aliases) 이 정의돼 있으니, 그것들을 단어 경계 매칭으로 한 바퀴 돌리면 됩니다.

질문: "C 증권 오픈API 제안 이력"
auto_facets: {"customer": ["C Securities"]}

이렇게 추출된 facet 을 어떻게 쓸지가 중요한 결정이었습니다. 두 선택지가 있었습니다.

  • Hard filter: WHERE 절로 검색 자체를 좁히기. 정확하지만 잘못 잡히면 결과가 통째로 사라집니다.
  • Soft boost: 점수에 ×1.30 곱하기. 잘못 잡혀도 다른 결과가 묻히지 않습니다.

저는 후자를 택했습니다. NER 기반 facet 추출은 잘못 매칭될 위험이 있고 (예: "F" 가 "F 증권" 인지 "F 시스템" 인지), hard filter 로 가면 그 한 번의 오인식이 사용자의 신뢰를 크게 깎습니다. soft boost 는 fail-soft 입니다 - 맞으면 더 좋은 답, 틀려도 평범한 답.

4-2. Graph supplement - Cypher 직격타

추출된 customer 또는 product 가 있으면, RAG 와 별도로 그래프에도 직접 쿼리를 던집니다.

MATCH (d:Document)
WHERE EXISTS { MATCH (d)-[:FOR_CLIENT]->(o:Organization)
               WHERE o.canonical_name IN $customers }
RETURN d.file_id, d.name, d.box_url, d.doc_type, d.summary, d.modified_at
ORDER BY coalesce(d.modified_at, '') DESC
LIMIT 8

이 결과를 RAG 검색 결과와 file_id 단위로 합칩니다 (중복 제거). 텍스트 검색이 본문 어휘 차이로 놓친 문서가 그래프 traversal 로 끌려옵니다. 실제로 "C 증권 오픈API 제안 이력" 에서, 텍스트 검색만으로는 v0.30 2차리뷰EIM 별도 버전 이 빠져 있었는데, graph supplement 가 두 건을 모두 데려왔습니다.

4-3. Graph neighbors 확장

1편의 _graph_neighbors 함수는 top hits 의 customer/product 에 대해 MEMBER_OF / MANAGES / FOR_CLIENT / MENTIONS 4개 관계만 보고 있었습니다. 12개의 fact 를 LLM 프롬프트에 주입했죠. 충분치 않았습니다. 그래서 세 가지를 추가했습니다.

  • Customer × doc_type 분포: "C 증권 문서 분포 - Presentation 12, MeetingMinutes 8, TechnicalDoc 3". 이 한 줄로 모델은 이 고객사가 어떤 단계에 있는지 (제안 단계 / 실행 단계 / 운영 단계) 를 알 수 있습니다.
  • Customer × Topic (-[:ABOUT]->(:Topic) 관계 traversal): "C 증권 주요 주제 - 오픈API (4), 마이데이터 (3), AI Platform (2)".
  • Top 문서의 partner / event 메타 (FROM_PARTNER, PRESENTED_AT): 본문에 안 나오지만 그래프엔 있는 정보.

5. 답변 형식의 함정 - 모델이 1번 항목에서 멈춘다

검색, 컨텍스트는 모두 준비됐는데, 그래도 답변이 한 줄이었습니다.

"1. C 증권은 IBM API Connect 기반 AI Platform 구축을 위한 제안서를 여러 차례 제출했습니다.[2][3][5][6][7][11][12]"

답변 끝. num_predict = 900 이었으니 토큰 한도에 걸린 것도 아니었습니다. 모델이 자기 의지로 stop 토큰을 뱉은 것입니다. 한참 들여다본 끝에 원인을 찾았습니다. listing 모드에서 쓰던 시스템 프롬프트의 첫 규칙이 문제였던 것으로 보입니다.

1. 도입 한 줄 요약 (한 문장).
2. 이어서 표 또는 불릿 목록으로 컨텍스트의 모든 관련 [번호] 항목을 빠짐없이 정리.
...

7B/9B 급 로컬 모델은 instruction 을 너무 엄격히 해석합니다. 1번 규칙 "한 문장" 을 정확히 한 문장으로 출력하고, 인용 [n][n][n] 까지 박은 뒤, 자기 할 일을 다 했다고 판단해 stop 합니다. 2번 "표 형식" 으로 진도가 안 나갑니다. 더 큰 모델이라면 문맥 전체를 보고 이건 아직 형식이 끝난 게 아니구나 를 알아챘을 텐데, 작은 모델은 규칙별 충실도가 높은 대신 규칙 사이의 의도까지 추론하지는 못합니다.

One-shot prompt로 수정하고 아래 프롬프트와 함께 세부 규칙들을 몇가지 추가했습니다.

질문이 이력/사례/목록형 입니다. 답변은 반드시 아래 출력 형식을 따르세요.
도입 문장, 서론, 요약 문단 금지. 곧장 `## 결과` 헤더로 시작.

### 출력 형식
## 결과 (총 N건)
- [n] 고객사 / 제품 / 문서명 - 한 줄 핵심 설명
- [m] 고객사 / 제품 / 문서명 - 한 줄 핵심 설명

### 예시
질문: "watsonx 도입 사례"
출력:
## 결과 (총 2건)
- [1] G 은행 / watsonx.ai / G은행_AI플랫폼_제안서.pptx - 챗봇 고도화 wx.ai 검토
- [2] H 금융 / watsonx Orchestrate / H금융_wxO_PoC.pptx - 업무 자동화 PoC
단계답변 길이항목 수
1번 도입 규칙 함정120자 (한 문장)0건
헤더 강제 후1,534자9건 (cut-off)
num_predict ↑ + 80자 cap1,144자12건 완결

CoT 적용 후

도입부에 결과 요약 한 줄이 붙고, 그 아래로 B 저축은행, C 증권, D 증권, E 은행 (해외) 같은 고객사들의 제안 자료가 줄지어 나옵니다.

Back toProject 목록