최근 고객들에게 지식베이스에 대한 사업계획이 많습니다. 사내에 흩어진 지식을 모아 지식베이스를 구축하고, 사용자들이 닿을 수 있는 접점에 노출시켜, LLM을 통해 정돈된 지식을 제공받겠다는 것이죠.
하지만 문제는 그 다음입니다. 흩뿌려진 지식 조각과 고객 질의 사이의 유사도를 계산해 끌어온 파편들을 LLM에게 그대로 던지면, 정작 LLM이 받아드는 것은 알아들을 수 없는 말 뭉치입니다. 맥락은 이미 사라졌고, 조각을 이루는 단어들은 고객 내부에서만 통용되는 그들의 언어입니다.
많은 고객들은 이제 이 문제를 인지하고 있습니다. 누군가는 이미 막대한 사업비를 들여 이 쓴맛을 겪어봤기 때문에 후발주자들은 해법을 찾다가 "Ontology"에 도달하게 되었습니다. 최근 검토하는 고객들의 사업계획서에서도, 온톨로지에 대한 요건들을 어렵지 않게 발견합니다.
온톨로지라고 하는건 존재의 본질을 탐구한다는 형이상학의 세부 학제입니다만, 최근 Palantir가 급부상하며 컴퓨터과학에서의 온톨로지가 더 널리 퍼진 개념이 되었습니다. 의미를 풀어보자면 우리 주변의 존재와 의미, 합의를 컴퓨터가 다룰 수 있도록 정의한 모델이라고 저는 받아들였습니다.
지난 며칠간 이 "온톨로지"라는 개념을 직접 이해해보고자, 지식베이스라는 유즈케이스를 한번 구현해보기로 했습니다. IBM 내부에서도 저는 비슷한 어려움을 겪고 있습니다. 저희는 다양한 자료를 Box라는 서비스에 모아두는데, pptx, xlsx, csv, pdf, jpg, boxnote 등 형식이 제각각인 파일들이 정돈되지 않은 채 여기저기 흩어져 있습니다. 필요한 자료를 누군가 이미 정리해두었기를 기대하며 폴더를 뒤지고 여러 키워드로 검색해보지만, 결코 만만한 일이 아닙니다. 게다가 IBM은 115년이라는 긴 역사만큼이나 다양한 줄임말과 내부 용어를 자랑합니다. 입사 이후 새로운 용어를 들을 때마다 그 의미와 풀네임을 엑셀에 정리해왔지만, 활용도는 낮습니다.
대상은 Korea Technical Community 박스 폴더에 쌓여 있는 11,064개의 파일입니다. 저는 이 1만여 개의 파일을 대상으로 온톨로지를 구축해, 우리 내부의 언어와 과거의 기록에 접근할 수 있는 시스템을 만들어보기로 했습니다.
저는 아래의 몇가지를 고민하는걸로 시작을 했습니다.
- 의미의 관계를 어떻게 정의할 것인가?
- 검색을 어떻게 수행할 것인가?
- 정형 데이터와 비정형 데이터를 어떻게 정리할 것인가?
- 1만 개의 문서를 어떻게 요약할 것인가?
1. 의미의 관계를 어떻게 정의할 것인가
문제: 라벨과 속성, 노드와 관계의 경계
가장 먼저 부딪힌 질문은 "무엇을 노드로 두고, 무엇을 관계로 둘 것인가" 였습니다. 이 결정이 시스템의 골격을 잡습니다. 지나치게 잘게 쪼개면 그래프가 비대해지고 질의가 느려지지만, 너무 거칠게 묶으면 정작 답을 얻고 싶은 질문이 그래프 위에서 표현되지 않습니다.
결정: 다섯 종류의 노드와 여섯 가지의 관계
고민 끝에 다섯 종류의 노드와 여섯 가지의 관계로 출발해 보기로 했습니다.
mermaid 로드 중…
이어서 (1) 각 노드의 역할, (2) 라벨과 속성의 경계 문제, (3) 한 그룹이 여러 팀에 걸치는 경우의 처리. 세 가지를 차례로 살펴봅니다.
1-1. 각 노드의 역할
- Document: 박스 위의 파일 한 건. 이름, 경로, 수정일, 박스 URL, 추정 문서 종류(
Proposal,Presentation,MeetingMinutes...) 같은 메타데이터를 가집니다. - Organization: 고객사. 단일 회사일 수도 있고, 그룹, 계열사일 수도 있습니다 (
is_group속성으로 구분). - Topic: Solution Play, Practice, TXC 같은 IBM 내부의 비즈니스 주제.
- Product: Instana, watsonx.ai, API Connect 같은 제품.
Topic을 동시에 라벨로 가집니다 - 제품도 결국 주제의 한 종류이기 때문입니다. - Team: 2026년 IBM Korea 영업조직 (Enterprise / Strategy / Horizon-1 / Horizon-2 ...).
1-2. 라벨인가, 속성인가
여기서 가장 어려웠던 부분은 라벨과 속성의 경계였습니다. "이 영업팀은 노드인가 속성인가?" "고객이 IBM 외에 어느 파트너와 협업했는지는 관계인가, Document 의 속성인가?" 이런 질문들은 정답이 없습니다. 그저 어떤 질문에 답하고 싶은지 를 떠올리며 결정할 뿐입니다. "Horizon-1 팀이 담당하는 고객사 중 Instana 제안서가 있는 곳" 이라는 질문에 답하고 싶다면, 팀은 노드여야 하고 MANAGES 관계가 있어야 합니다.
1-3. 한 그룹이 여러 팀에 걸칠 때 - H 그룹 분할 사례
또 하나의 발견은 하나의 그룹이 여러 팀에 걸칠 수 있다는 점 이었습니다. 처음 모델링할 때 저는 한 대기업 그룹(이하 H 그룹)을 단일 노드로 두고 영업조직과 1:1로 매핑했습니다. 그러다 사용자(저 자신)가 "Horizon-2 의 Instana 자료" 를 검색했을 때 H 그룹의 손해보험 계열사 자료가 결과에 섞여 나오는 걸 보고, 모델이 현실을 반영하지 못한다는 걸 알아챘습니다. H 그룹 은 IBM 영업 입장에선 분할 어카운트 였습니다 (금융 부문은 Horizon-1, 제조/유통 부문은 Horizon-2). 그래서 H Group 아래에 H Finance Group 과 H Industrial 이라는 두 sub-group 을 두고, 각자 다른 팀이 관리하도록 모델을 고쳤습니다.
이 작은 사건은 저에게 한 가지를 가르쳐줬습니다. 온톨로지는 단순히 데이터의 구조가 아니라 도메인 합의의 표현 이라는 겁니다. 모델은 "이 회사는 우리에게 어떤 의미인가" 같은 합의를 반영해야 하고, 그 합의는 시간에 따라 바뀝니다.
이렇게 만든 그래프입니다. 아래 그림은 시스템 안의 Organizations, Groups, Teams 를 시각화한 것입니다. 가운데 노란 노드가 Horizon-2 팀이고, 거기서 여러 제조, 유통, 공기업 그룹들과 새로 분리한 H Industrial 같은 그룹들이 뻗어 나오는 모습이 보입니다. H Industrial 아래로는 H 그룹의 시스템 자회사 가, 그리고 H Group 의 다른 가지 (H Finance Group, H 그룹 생명보험) 는 (화면에는 잘려 있지만) 별도로 Horizon-1 쪽에 연결되어 있습니다.

Graph Explorer 에서 특정 고객사 노드를 클릭하면 우측 패널에 산업, 문서수, 관련 자료가 뜬다.
2. 검색을 어떻게 수행할 것인가
문제: 한 시스템 안에 두 종류의 질문이 산다
지식베이스 위에서 검색은 단일한 문제가 아니었습니다. 어떤 질문이 발생할지 고민하다보니 사용자가 던지는 질문은 크게 두 종류로 나뉘었습니다.
| 질문 종류 | 예시 | 답의 형태 |
|---|---|---|
| 내용 검색 | "Instana 도입 사례" | 비슷한 내용을 담은 문서 청크 |
| 구조 검색 | "Horizon-1 팀이 관리하는 고객 중 Instana 제안서를 받은 곳" | 그래프 위 행(row)의 결합 |
생각해봅시다. 우리가 보통 떠올리는 검색은 검색어와 비슷한 문서청크를 찾아 결과를 반환하는 것입니다. 위 표의 첫번째 검색어가 그러한 사례일 것입니다. 하지만 "A팀이 관리하는 고객에 Instana를 제안했던 사례" 같은 질문은 어떨까요? 청크와 유사도를 검색하는 것만으로는 해결할 수 없습니다. 자세한건 아래에서 더 이야기하겠습니다.
결정: 두 경로를 모두 두되, 자동으로 라우팅한다
내용 검색은 BM25 + 벡터의 RRF 결합 으로, 구조 검색은 자연어 → Cypher 로 처리하기로 했습니다. 사용자가 모드를 일일이 선택하지 않아도 되도록, 챗 화면에서는 키워드 휴리스틱과 LLM 의도 분류로 어느 경로를 탈지 자동으로 결정합니다. 챗화면은 아직 기대만큼의 성능을 보여주지못해서 오늘은 검색화면만 소개드리겠습니다.
이어서 (1) 하이브리드 검색의 동작, (2) 벡터로 풀 수 없는 관계형 질문, (3) 두 경로의 운영 수치. 세 가지를 살펴봅니다.
2-1. 하이브리드 검색: BM25 와 벡터의 RRF 결합
BM25(키워드 매칭) 만으로는 의미가 같지만 단어가 다른 경우 ( "관측성" 와 "Observability" 같은 ) 를 잡지 못하고, 반대로 벡터 검색만으로는 APIC, RHEL 같은 약어의 정확한 일치가 약했습니다. 그래서 둘을 함께 돌리고 RRF(Reciprocal Rank Fusion) 로 점수를 결합 하는 방식을 택했습니다.
mermaid 로드 중…
예를 들어 "D 증권 Instana 제안" 이라고 검색하면, BM25 는 D 증권, Instana, 제안 같은 키워드가 정확히 등장한 청크를 끌어오고, 벡터는 의미상 가까운 청크 (영문 회사명, AIOps, PoC 범위 같은 표현들) 를 끌어옵니다. 두 결과를 RRF 로 합치면 둘 다 좋은 점수를 받은 청크가 자연스럽게 위로 올라옵니다. 각 결과 카드에는 BM25 rank 17, Vec rank 1 처럼 두 경로의 순위가 함께 표시되어, 어느 신호가 그 결과를 끌어올렸는지 한눈에 알 수 있도록 했습니다.

Hybrid 모드 - BM25 와 bge-m3 벡터의 RRF 결합. 우측에 두 경로의 순위가 함께 표시된다.
2-2. 그런데, 어떤 질문은 의미가 아니라 관계를 묻습니다
하이브리드 검색을 붙이고 그래프가 어떤 질문에서 활용될지를 고민하니, 곧 막히는 질문 유형을 발견했습니다.
"호라이즌1에서 제안한 Instana 자료"
이 질문을 벡터 검색에게 던지면 어떻게 될까요. 임베딩 공간에서는 호라이즌1 이라는 단어가 어떤 문서 청크 와 가까울지를 따집니다. 그런데 우리 문서 안에는 "호라이즌1에서 이 회사를 담당합니다" 같은 문장이 거의 등장하지 않습니다. 영업조직 매핑은 문서 본문이 아니라 별도의 메타데이터(정형 데이터) 에 있기 때문입니다. 벡터는 유사도 를 보지만, 이 질문은 유사도가 아니라 그래프 위의 경로 를 묻고 있습니다.
mermaid 로드 중…
이 경로를 따라 행을 모아야만 답이 나옵니다. 벡터로는 안 됩니다. BM25 도 마찬가지입니다 (같은 문서에 호라이즌 과 Instana 가 동시에 등장할 일이 거의 없으니까요.)
그래서 두 번째 경로로 자연어 → Cypher 를 두기로 했습니다. 사용자의 자연어 질문을 코딩에 강한 작은 LLM(qwen2.5-coder:7b)이 Cypher 쿼리로 변환하고, Neo4j 가 그것을 실행해 그래프 위의 행을 그대로 돌려줍니다. 위 질문은 다음 같은 Cypher 로 옮겨집니다.
MATCH (:Team {canonical_name:'Horizon-1'})-[:MANAGES]->(group:Organization) MATCH (group)<-[:MEMBER_OF*0..2]-(o:Organization) MATCH (d:Document)-[:FOR_CLIENT]->(o) MATCH (d)-[:MENTIONS]->(:Product {canonical_name:'Instana'}) RETURN DISTINCT o.canonical_name AS 고객, d.name AS 파일, d.doc_type AS 유형, d.modified_at AS 수정일, d.box_url AS URL ORDER BY d.modified_at DESC LIMIT 30
벡터가 비슷한 글을 찾는 도구라면, Cypher 는 그래프를 따라 걷는 도구라고 볼수있겠습니다. 둘은 풀고 있는 문제의 종류가 다릅니다. 결과는 30개의 행 (Horizon-1 이 담당하는 H Finance Group, S 생명보험, HF 금융 그룹, M 자산운용 생명 같은 금융 어카운트들의 Instana 자료) 이 깔끔하게 돌아옵니다.

자연어가 Cypher 로 변환되고, Neo4j 가 그래프 위 행을 직접 반환한다. H Finance Group 이 Horizon-1 산하로 정확히 분류되어 H 그룹의 손해보험 자회사 자료가 함께 따라온 모습이 보인다.
여기서 두 가지 실용적인 안전장치가 필요했습니다. 하나는 몇 가지 keyword 휴리스틱과 LLM 의 의도 분류로 어느 경로를 탈 지 자동 라우팅 하는 것 - 사용자가 모드를 일일이 고를 필요 없도록. 다른 하나는 LLM 이 만들어낸 Cypher 가 항상 옳지는 않다는 사실을 인정하고, 재시도와 도메인-특화 few-shot 예시 로 정확도를 끌어올리는 것이었습니다. 실제로 처음에는 LLM 이 노드 패턴 안에 OR 를 끼워 넣는다거나 MEMBERS_OF (실제 관계명은 MEMBER_OF) 같은 오타를 만들어 재시도가 자주 발생했습니다. 도메인 사전(아래에서 다룰 별칭 사전)과 시스템 프롬프트의 SCHEMA STRICTNESS 절을 보강하면서 점차 1회 시도 성공률이 올라갔습니다.
2-3. 운영 수치: 왜 NL→Cypher 가 디폴트가 아닌가
세 경로의 응답 시간은 본질적으로 다릅니다.
| 경로 | 평균 레이턴시 | 비고 |
|---|---|---|
| BM25 단독 | 약 50 ms | OpenSearch 단일 호출 |
| Hybrid (BM25 + 벡터 + RRF) | 약 150-300 ms | 두 호출 병렬 + 점수 결합 |
| 자연어 → Cypher | 약 5-10 s | LLM 추론 + (필요시) 재시도 |
사용자가 가장 자주 던지는 질문은 의미 검색입니다. 그래서 디폴트는 Hybrid 로 두고, NL→Cypher 는 사용자가 명시적으로 선택하거나 챗에서 라우터가 그래프 의도 라고 판단했을 때만 탑니다. 200 ms 와 10 초의 차이는 사용자 경험상 큰 격차이고, 대부분의 질문은 Hybrid 로도 충분히 답이 됩니다.
3. 정형 데이터와 비정형 데이터를 어떻게 정리할 것인가
문제: 정형과 비정형이 한 시스템에서 교차한다
지식베이스의 본질적인 어려움은 정형과 비정형이 한 시스템 안에서 교차한다 는 점이라고 저는 결론지었습니다.
- 정형: 영업조직, 고객사 그룹, 계열사, 제품 카탈로그, 별칭 사전. 이건 사람이 관리해야 하는 큐레이션 영역 입니다. 자동화되면 오히려 모델이 망가집니다 (H 그룹 분할 같은 미묘한 결정을 LLM 이 알아서 내릴 수는 없습니다).
- 비정형: pptx 슬라이드, pdf 본문, boxnote 노트. 형식이 제각각이고, 내부 약어로 가득합니다. 사람이 일일이 정리할 수 없는 양(11,064개의 파일) 이라 자동 파이프라인이 필요합니다.
결정: 정형은 사람이 큐레이션하는 YAML, 비정형은 12단계 파이프라인
정형 데이터는 사람이 읽고 고치기 쉬운 YAML 글로서리(organizations.yml, ibm_terms.yml, products.yml, topics.yml) 로 두었습니다. 비정형 데이터는 파싱 → 청킹 → 임베딩 → 엔티티 추출 → 정규화 → Neo4j 적재의 파이프라인을 거칩니다.
이어서 (1) 글로서리 큐레이션 루프, (2) 한국어 처리 디테일, (3) 비정형 파이프라인, (4) 표기 다양성 문제를 살펴봅니다.
3-1. 글로서리 큐레이션 루프: 모델이 모르는 건 사람이 가르친다
글로서리는 완성된 사전 이 아니라 자라나는 사전입니다. 시스템을 돌리다 보면 LLM 이 추출한 엔티티 중에 글로서리에 없는 이름이 계속 등장합니다. 새 고객, 새 약어, 새 제품. 이 raw 문자열들은 별도의 unknown_terms.log 에 쌓입니다.
mermaid 로드 중…
이 루프가 중요한 이유는 두 가지입니다.
첫째, LLM 의 환각이 그래프를 오염시키지 않습니다. 엔티티 추출 LLM이 "Hanwha Insurence" (오타) 같은 이름을 만들어내도, 그것이 글로서리의 별칭 매칭을 통과하지 못하면 그래프에 노드로 만들어지지 않습니다. 대신 unknown_terms.log 에 기록되어 사람의 판단을 기다립니다. 그래프는 항상 사람이 인정한 합의 위에서만 자랍니다.
둘째, 시스템이 천천히 똑똑해집니다. 새 고객이 늘면 그 별칭들이 추가되고, 다음 파이프라인 실행부터는 자동으로 인식됩니다. "한국 건설" 같은 단어가 처음에는 미지의 단어였다가, 한 번 등록하면 다음부터는 Korea Construction canonical 노드로 정규화됩니다.
자동화된 사전 학습이 아닌 사람-기계 협력 루프 인 점이 중요합니다. LLM이 대량의 텍스트에서 후보를 발견 하고, 사람이 그 중 무엇이 진짜 새로운 의미인지 결정합니다.
3-2. 한국어 처리: Nori, bge-m3
한국어 지식베이스는 영어용 컴포넌트를 그대로 갖다 쓸 수 없습니다. 세 가지 지점에서 한국어 특화가 필요했습니다.
OpenSearch BM25 - Nori 토크나이저. 한국어는 교착어입니다. "한화손해보험에서" 라는 단어는 영어식 공백 분할로는 한 덩어리지만, 형태소로 쪼개면 한화 + 손해보험 + 에서 입니다. Nori 토크나이저는 한국어 형태소 분석기로, 인덱싱 단계에서 어미, 조사를 분리해 검색 정확도를 끌어올립니다. Nori 없이 BM25 를 돌리면 "한화손해보험" 으로 검색했을 때 "한화" 만 등장하는 문서를 못 찾는 일이 생깁니다.
임베딩 - bge-m3. 다국어 임베딩 모델 중에서도 한국어 성능이 잘 알려져 있고, 1024 차원으로 가볍습니다. Ollama 로 띄워두고 같은 머신에서 사용합니다.
3-3. 비정형 파이프라인
파이프라인은 다음과 같이 흐릅니다.
mermaid 로드 중…
스캔 PDF 와 이미지 슬라이드처럼 텍스트 추출 자체가 실패 하는 경우엔 별도 OCR 보정 단계로 우회합니다. 이런 케이스는 의외로 많아서 (약 4%의 문서가 OCR 없이는 본문을 못 읽었습니다) 처음 파이프라인에 두지 않으면 시스템 커버리지에 큰 구멍이 생겼을 겁니다.
3-4. 표기의 다양성 (같은 회사가 세 가지 이름으로 등장)
가장 자주 마주치는 문제는 표기의 다양성 입니다. 같은 회사가 한 문서에선 "한화", 다른 문서에선 "Hanwha Group", 또 다른 곳에선 "한화그룹주식회사" 로 등장합니다. LLM 추출만으로는 이걸 같은 엔티티로 묶지 못합니다. 그래서 추출된 raw 문자열을 글로서리의 별칭(aliases) 사전과 대조해 정규화된 노드(canonical_name)에 연결합니다. 3-1에서 말한 큐레이션 루프와 정확히 같은 메커니즘입니다.
4. 1만 개의 문서를 어떻게 요약할 것인가
문제: 청크 검색만으로는 답이 충분치 않다
지금까지 다룬 검색 경로는 모두 청크 단위 입니다. 500 토큰짜리 조각 하나가 검색 단위죠. 그런데 사용자가 "이 문서가 무슨 내용이야?" 같은 문서 단위 질문을 던지면, 청크만으로는 답이 너무 좁습니다. 청크 하나는 한 페이지의 한 문단일 뿐이니까요.
또 다른 문제는 LLM 컨텍스트 비용입니다. 한 문서의 모든 청크를 한 번에 LLM 에 던지면 길이가 폭발하고, 답변 품질도 오히려 떨어집니다 (긴 컨텍스트의 lost in the middle 현상).
결정: 문서 단위 요약을 미리 생성해 노드 속성으로 둔다
각 Document 노드에 summary 속성을 두고, 파이프라인에 요약 단계 를 추가했습니다. 모든 청크를 모아 작은 LLM 으로 한 줄에서 한 단락 분량 요약을 만들어 미리 저장해 두면, 챗에서 문서 단위 질문이 들어왔을 때 이 요약을 컨텍스트로 활용할 수 있습니다.
요약 모델은 비용 때문에 로컬에서 돌릴 수 있는 작은 모델 을 골랐습니다 - qwen2.5:7b.
이어서 (1) 청크 단위 vs 문서 단위 요약의 차이, (2) 작은 모델의 한자 오염 문제와 그 한계, (3) 운영적 비용을 살펴봅니다.
4-1. 청크 vs 문서 단위 - 검색은 좁게, 요약은 넓게
청크 단위 검색과 문서 단위 요약은 보완 관계입니다.
| 단위 | 강점 | 약점 |
|---|---|---|
| 청크 (500 토큰) | 정확한 위치 검색, 구체적 인용 가능 | 큰 그림 못 보여줌 |
| 문서 (요약 1단락) | 한눈에 무슨 내용인지 파악 | 세부 사항 손실 |
챗 인터페이스에서는 두 단위를 함께 사용합니다. 검색은 청크로 하되, 검색 결과의 각 문서에 대해 그 문서의 요약을 함께 LLM 컨텍스트에 넣어줍니다. 이렇게 하면 LLM 이 청크의 지엽적 내용 과 문서의 전체 맥락 을 동시에 볼 수 있습니다.
4-2. 한자 오염: 작은 모델의 한계
요약 단계에서 가장 골치 아팠던 문제는 한자 오염 이었습니다. qwen2.5:7b 같은 중국 출신 모델은 학습 데이터의 상당 부분이 중국어입니다. 한국어로 요약을 시켜도, CJK 권에서 공유되는 단어(예를 들어 戰略, 構造, 分析)를 한국어 전략, 구조, 분석 대신 한자로 출력하는 일이 자주 발생합니다.
처음에는 프롬프트로 막을 수 있을 거라고 생각했습니다. v1, v2, v3 ... v4 까지 프롬프트를 다듬었습니다.
당신은 한국어로만 답합니다. 한자(漢字), 일본어, 중국어를 절대 출력하지 마세요. 모든 단어는 한글 또는 영어 알파벳으로만 표기하세요. 한자가 등장하면 즉시 한국어로 바꿔쓰세요.
이런 식의 강한 제약을 걸어도, 작은 모델은 토큰 단위에서 결국 한자를 흘리고 맙니다. 어떤 단어는 학습 분포상 한자 토큰의 확률이 한글 토큰보다 높게 자리잡고 있어, 프롬프트가 가용 가능한 confidence 범위 안에서 그 분포를 완전히 뒤집지 못합니다. 모델 크기가 커질수록 한자 오염은 줄지만 (gemma2:9b 는 거의 없습니다), 7B 급에서는 본질적인 한계가 있다고 결론지었습니다.
현재는 후처리 로 한자가 등장한 줄을 감지해 다시 요약하거나, 한자가 1% 이상 등장한 문서는 요약 실패로 표시하고 큰 모델로 재시도합니다. 100% 깨끗하진 않지만, "이 시스템은 작은 로컬 모델로도 돌아간다" 는 제약을 지키기 위한 타협입니다.
4-3. 운영적 비용: 로컬 7B 모델로 1만 개를 요약하는 데 걸리는 시간
11,064개의 문서를 qwen2.5:7b 로 요약하면 한 문서당 약 7초가 걸립니다.(M4에서 실행) 21시간 정도 소요되었고, GPU 메모리 대역폭이 병목이라 병렬처리는 소용없었습니다.
마치며
며칠간의 작업을 통해 제가 가장 크게 느낀 것은, 온톨로지는 특정 기술이 아니라 "합의를 데이터로 옮기는 작업" 이라는 사실입니다. 어느 수준까지 분류할 것인지, 어떤 관계를 노드로 둘 것인지, 같은 이름이 등장했을 때 같은 것으로 볼지, 이 모든 결정은 도메인을 아는 사람이 내려야 합니다. LLM 이 자동으로 풀어주지 않습니다. 다만 LLM 은, 그렇게 사람이 만든 합의 위에서, 사용자의 자연어와 그래프 사이의 통역사로 일할 뿐입니다.
지금 제가 만든 시스템은 11,064개의 문서, 122,468개의 청크, 6,520개의 요약, 149개의 조직, 62개의 제품, 6개의 팀으로 채워져 있습니다. 하이브리드 검색, 자연어 → Cypher, RAG 채팅의 세 모드가 돌아가고, 도메인 정확도를 평가하기 위한 10가지 시나리오 테스트도 붙여뒀습니다.
다음 글에서 다룰 주제는 셋입니다.
- 답을 어떻게 만들 것인가: 채팅 인터페이스에서 그래프 사실(Neo4j 노드의 속성)과 문서 청크의 내용을 LLM 컨텍스트에 함께 주입하는 Graph-augmented RAG 의 구현, 그리고 그 답변 품질이 아직 기대만큼 나오지 않는 이유.
- 평가의 어려움: "이게 정말 정답인지를 어떻게 알 수 있는가." 정답이 정의되지 않은 영역에서 시스템 정확도를 측정하는 시도.
- 운영의 어려움: 환각, 별칭 큐레이션, OCR 실패, 파이프라인 안정화 등 첫 며칠을 만들고 나서야 보이기 시작한 잡일들.
부록: 전체 기술 스택과 역할
참고용으로, 시스템에서 사용한 컴포넌트와 각자의 역할을 정리해 둡니다.
데이터 저장소
| 컴포넌트 | 역할 |
|---|---|
| Neo4j | 지식 그래프 본체. Document, Organization, Topic, Product, Team 노드와 그 사이의 관계, 그리고 Chunk 노드와 임베딩 벡터를 모두 보관. 벡터 인덱스로 kNN 검색까지 한 곳에서 처리. |
| OpenSearch | BM25 키워드 검색 인덱스. Nori 토크나이저로 한국어 형태소 분석에 활용했음. |
| Parquet (로컬 파일) | 파이프라인 중간 산출물 저장. inventory.parquet, chunks.parquet, chunks_with_embeddings.parquet, doc_summaries.parquet 등. Pandas 로 읽고 쓰면서 단계별 체크포인트 역할. |
LLM (모두 로컬, Ollama 위에서 동작)
| 모델 | 용도 | 선택 이유 |
|---|---|---|
| bge-m3 (임베딩) | 청크의 1024차원 벡터 생성 | 다국어, 한국어 강함, 가벼움. 외부 API 미사용(보안). |
| qwen2.5-coder:7b | 자연어 → Cypher 변환 | 코드 생성 능력 강함, 작은 크기로 충분히 빠른 추론. |
| qwen2.5:7b | 문서 단위 요약 | 한국어 출력 가능, 7B로 1만건 요약 가능한 가벼운 옵션. (한자 오염 한계 있음) |
| gemma2:9b | 챗 RAG 답변 생성 | 한국어 품질, CJK 청결성이 7B 모델보다 안정적. 9B여서 속도와 품질의 절충점. |
검색, 랭킹
| 컴포넌트 | 역할 |
|---|---|
| RRF (Reciprocal Rank Fusion) | BM25 결과(Top-30)와 벡터 결과(Top-30)를 점수 결합. score = Σ 1/(60 + rank). |
| 자연어 → Cypher 라우터 | 질문에 키워드 휴리스틱(예: "관리하는", "팀별", "그룹") 매칭으로 그래프 의도를 감지해 NL→Cypher 경로로 분기. |
| 재시도 + few-shot | LLM 이 만든 Cypher 가 실패하면 에러 메시지를 다시 LLM 에 던져 보정. 도메인 특화 few-shot 12종으로 정확도 확보. |
비정형 데이터 처리
| 컴포넌트 | 역할 |
|---|---|
| python-pptx, pdfplumber, PyMuPDF | pptx, pdf, boxnote 본문 텍스트 추출. |
| Tesseract OCR | 스캔 PDF, 이미지 슬라이드처럼 텍스트 추출이 실패한 4% 문서의 보정. PyMuPDF 로 페이지를 이미지 렌더링한 뒤 Tesseract 로 OCR. |
| Chunking (500 토큰) | 토큰 기준 슬라이딩 윈도우로 청크 분할. 의미 단위 보존을 위해 문장 경계 우선. |
정형 데이터 (Glossary)
| 파일 | 내용 |
|---|---|
organizations.yml | 고객사, 그룹, 계열사 정의. canonical_name, aliases, parent_group, industry, type. |
ibm_terms.yml | IBM 내부 용어, 영업조직(Team) 정의. Horizon-1/2 같은 팀 구조와 member_accounts. |
products.yml | IBM 및 파트너 제품 카탈로그. canonical_name, aliases, vendor, category. |
topics.yml | Solution Play, Practice 같은 비즈니스 주제. |