이 시리즈는 Databricks 환경에서 구축했던 항공/운송/물류 분야의 RAG Agent 구축 프로젝트 관련 내용을 정리하는 시리즈입니다. 요구사항과 관련하여 Databricks의 어떤 기술을 사용해 이를 충족하였으며, 수행 중 발생한 문제 상황을 어떻게 해결했는지 정리합니다.
실제 고객사에서 수행한 프로젝트와는 별도의 내용으로, 기술적인 내용만을 정리합니다.
한국어는 어근에 조사, 접사, 어미가 결합하는 교착어 특성을 지니며, 따라서 공백 기반의 단순 토큰화만으로는 키워드 검색(BM25)의 검색 일치율이 크게 저하됩니다. 이에 FTS(Full-Text Search) 앞단에 토크나이저를 두어 검색 일치율을 제고하도록 하며, 이 프로젝트에서는 Kiwi 형태소 분석기를 기반으로 설계된 전용 토크나이징 모듈 kiwi_tokenizer.py를 사용합니다.

| 구분 | 인덱싱 시점(tokenize_for_index) | 쿼리 검색 시점(tokenize_and_expand_for_query) |
|---|---|---|
| 목적 | Delta Table 원본 청크의 핵심 명사 색인화 | 사용자 자연어 질문의 의도 분석 및 검색어 확장 |
| 오타 교정 | 비활성화(인덱스 생성 성능 보존) | 활성화(교정 가중치 임계값: 2.5) |
| 동의어 확장 | 비활성화(문서 빈도 왜곡 방지 및 IDF 보존) | 활성화(Aho-Corasick 오토마톤 기반 OR 확장) |
| 사용자 사전 | 활성화(동일 품사 매칭 보장) | 활성화(동일 품사 매칭 보장) |
형태소 분해 결과의 통일성을 제고하고 검색 누락을 방지하기 위해 정규화 및 형태 복원 알고리즘이 동작합니다.
_merge_derived_nouns)한국어 구문 분석 중 명사 추출 시 접미사나 어미가 결합되면서 단어가 여러 토큰으로 과도하게 쪼개지는 문제를 방지합니다.
VV, VA, XSV, XSA, VX 태그) 뒤에 명사형 전성어미(ETN: ㅁ, 음, 기 등)가 이어지는 패턴을 감지하여 하나의 일반명사(NNG) 형태로 합성합니다.알리(VV) + ㅁ(ETN) 알림(NNG))NNG, NNP 태그) 뒤에 명사파생 접미사(XSN)가 연속으로 출현할 경우 이를 결합합니다.할인(NNG) + 권(XSN) 할인권(NNG))t.start)부터 마지막 접미사 토큰의 끝 위치(last.start + last.len)까지의 원문 텍스트 범위를 파이썬 슬라이싱으로 직접 잘라내어 표면형을 복원합니다.| 대상 유형 | 원천 형태소 구조 | 병합 후 형태소 | 예시 | 트레이드오프 및 설계 정책 |
|---|---|---|---|---|
| 전성명사 | 용언 어간(STEM) + 명사형 어미(ETN) | 일반명사 (NNG) | 알리(VV) + ㅁ(ETN) = 알림 | 일반 용언의 명사형 변형(예: "그가 옴")까지 병합될 위험이 있으나, BM25 특성상 희소도가 낮아 노이즈 전파가 미미하며 색인/검색 수렴 효과가 더 큽니다. |
| 파생명사 | 명사(NNG/NNP) + 파생접미사(XSN) | 일반명사 (NNG) | 할인(NNG) + 권(XSN) = 할인권 | "할인권"을 단일 1토큰으로 병합 색인하므로 "할인" 단독 질의 시 정확 매칭이 어려워질 수 있으나, 접미사 단독 색인의 무가치함을 제거하고 부분 일치는 하이브리드의 dense 채널이 보완합니다. |
NNG) / 고유명사 (NNP) / 외국어 (SL) / 숫자 (SN) / 한자 (SH)이 장에서는 사용자 사전, 동의어 사전, 불용어 사전의 파일 규격과 이들의 형상 관리 계획, 그리고 에이전트 내부에 커스텀 구현된 Aho-Corasick 기반 동의어 확장 알고리즘의 동작 원리를 기술합니다.
운영자는 세 가지 유형의 사전을 준비하여 RAG 시스템에 공급할 수 있으며, 각 사전 파일은 다음과 같은 정밀한 확장자 및 컬럼 포맷 요건을 충족해야 합니다.
user_dict.tsv (UTF-8, Tab 구분자 사용)표면형 \t 품사태그 \t 가중치스코어 (예: RAG\tNNP\t5.0)변이형 \t 원형/품사태그 \t 가중치스코어 (예: 수화물\t수하물/NNG\t-3.0)복합명사 \t 어휘1/태그1 + 어휘2/태그2 + ... \t 가중치스코어 (예: 드림패스\t드림/NNP + 패스/NNG\t-3.0)add_user_word API의 인자 규격과 다르게, TSV 파일 구조상 변이형 매핑을 4번째 열로 추가하여 구성(수화물\tNNG\t-3.0\t수하물)할 경우 load_user_dictionary 파서가 ValueError를 발생시키며 중단되므로 반드시 2번째 열에 슬래시 표기법을 적용해야 합니다.synonym_dict.csv (UTF-8, Comma 구분자 사용)수하물,짐,가방,캐리어)=> 기호를 사용합니다. (예: RAG => 검색증강생성)SYN_MAX_TOKENS) 이하로 작성을 제한합니다.stopwords_dict.csv (UTF-8, 단일 열 구조)# 기호로 시작하는 행은 파싱 단계에서 주석으로 간주하여 자동 생략됩니다.일반 한국어 코퍼스로 학습된 Kiwi 분석기가 항공/객실 도메인 특화 어휘를 자의적으로 분해하지 않도록 형태소 점수 가산 방식을 정의합니다. 가중치 점수는 형태소 분석 후보군 경쟁 시 해당 후보군에 가산되는 LM 점수값입니다.
| 분류 항목 | 품사 태그 | 가중치 가산값 | 적용 예시 | 목적 및 세부 규칙 |
|---|---|---|---|---|
| 신규 고유명사 및 약어 | 고유명사 (NNP) | +5.0 | ULD, WTO, RAG | 기본 언어 모델 분해 후보보다 무조건 최우선 채택되도록 보장 |
| 의존명사 획득 승격 | 일반명사 (NNG) | +3.0 | 킬로그램, 시간 | 의존명사(NNB) 태그는 색인 화이트리스트 탈락 대상이므로 명사 태깅 강제 |
| 변이형 및 표기 정규화 | 일반명사 (NNG) | -3.0 (원형 바인딩) | 수화물 (원형: 수하물) | 변이형 후보의 선택 순위를 강제로 낮추고 원형 단어 수렴 유도 |
Databricks Vector Search 및 Kiwi 형태소 분석기 라이브러리는 자체적인 동의어 확장 매커니즘을 내장하고 있지 않습니다. 본 시스템은 이 제약을 해소하기 위해 파이썬 C-Extension 기반의 다중 패턴 매칭 라이브러리인 pyahocorasick 패키지를 채택하여 검색 런타임에 동의어 확장을 직접 구동합니다.
pyahocorasick) 특징KEY_SEQUENCE 정수 맵 탐색: 문자열 기반 매칭(KEY_STRING)을 수행하면 단어 일부분만 겹쳐도 트리거되는 경계 오류(예: '수하물'이 '수하물함'을 확장)가 발생합니다. 본 모듈은 토큰 단위 경계 격리를 위해 KEY_SEQUENCE 정수 시퀀스 매칭 모드를 활용합니다. Kiwi 분석기로 토큰화된 모든 형태소를 고유 정수 ID(tok2id)로 매핑하고, 동의어 구를 tuple[int, ...] 형태의 ID 시퀀스로 아호코라식 트리에 등록 및 검색합니다.동의어 확장은 입력 질의 토큰들에 대해 다음 3단계 연산을 거쳐 BM25 쿼리 문자열로 가공됩니다.
[입력 쿼리 토큰화 결과] -> [위탁, 수하물, 분실, 규정] (각 토큰 정수 ID 변환 완료)
↓
[1단계: Aho-Corasick 탐색] -> 모든 매칭 구간 탐색: (0, 1)= "위탁 수하물" (2토큰) / (1,)= "수하물" (1토큰) 중복 매칭
↓
[2단계: 최장 비중첩 선택] -> i=0 에서 가장 긴 2토큰 "위탁 수하물" 채택 후 인덱스 +2 점프 (i=2 이동)
"수하물" 은 겹치는 구간이므로 무시 및 흡수 (이중 확장 방지)
↓
[3단계: 토큰화 및 중복 제거] -> 확장 그룹 ['위탁 수하물', '체크인 수하물'] 전개 -> [위탁, 수하물, 체크인, 수하물]
Set 자료형 적용으로 "수하물" 중복 제거 -> [위탁, 수하물, 체크인, 분실, 규정] 반환
automaton.iter(seq) API를 호출하여 질의 토큰 ID 배열 전체를 1회 스캔하고, 겹치는 모든 동의어 매칭 후보군 목록을 수집합니다.ln > best[st][1])만을 채택하고, 매칭된 길이만큼 루프 인덱스(i += step)를 건너뛰어 토큰을 소비합니다."위탁 수하물 분실"이라는 질의에서 "위탁 수하물" 그룹이 정상 매칭되었을 때, 그 하부의 "수하물" 토큰이 단독으로 동작해 "짐", "가방" 등으로 동의어 오확장을 일으키는 문제를 차단합니다.동의어 사전의 키 빌드 경로를 검색/인덱싱과 일치시켜, 키와 질의의 토큰 모양이 일치하지 않아 동의어 매칭이 유실되는 현상을 예방합니다.

_extract_tokens 기반)을 적용해 키를 빌드합니다.kiwi_key_tokens)로 키를 구제합니다.대용량 스파크 연산과 서빙 엔드포인트의 Cold-start 지연 시간을 통제하기 위해 적용된 런타임 최적화 기법입니다.
resolve_dictionary_dirs)__file__)를 기준으로 상대 경로 역추적을 수행하여, MLflow 모델 패키지 내부 아티팩트 볼륨(artifacts/user_dict 등)이 존재하는지 판별하고 존재할 경우 동봉된 사전 경로로 환경을 자동 오버라이드합니다.get_tokenizer)_tokenizer_instance를 사용해 인스턴스를 캐싱하며, 다수의 파티션을 처리하는 Spark 워크 노드 혹은 Model Serving 컨테이너 인스턴스당 단 1회만 초기화가 보장되도록 싱글턴 생성 제어를 완수합니다.get_tokenize_udf)pandas_udf) 구조를 제공합니다.load_synonyms=False 옵션을 인가하여 메모리 사용량을 최소화합니다.# kiwi_tokenizer.py - stems 및 XSN 접미사 결합 처리
def _merge_derived_nouns(self, text: str, tokens) -> list:
noun_tags = {"NNG", "NNP"}
out, i, n = [], 0, len(tokens)
while i < n:
t = tokens[i]
j = None
# 전성명사: 어간 + ETN (예: 알림)
if i + 1 < n and t.tag in self.stems and tokens[i + 1].tag == "ETN":
j = i + 1
# 파생명사: 명사 + XSN
elif t.tag in noun_tags:
j = i
if j is not None:
k = j + 1
while k < n and tokens[k].tag == "XSN":
k += 1
if k > j + 1 or j > i:
last = tokens[k - 1]
# 원문 text 슬라이싱을 통한 원래 표면형 복원
out.append((text[t.start : last.start + last.len], "NNG"))
pointer = k
i = k
continue
out.append((t.form, t.tag))
i += 1
return out
# kiwi_tokenizer.py - Aho-Corasick 매칭 및 중복 제거
def tokenize_and_expand_for_query(self, query: str) -> str:
extracted_keywords = self._extract_tokens(query, self.query_tokenize_opts, lowercase=False)
if not extracted_keywords:
return ""
best = {}
if len(self.automaton):
seq = tuple(self.tok2id.get(t.lower(), _UNK_ID) for t in extracted_keywords)
for end, (grp, ln) in self.automaton.iter(seq):
st = end - ln + 1
# 동일 시작 위치(st)에 대해 최장 길이 매칭만 채택
if st not in best or ln > best[st][1]:
best[st] = (grp, ln)
out = []
seen = set()
i = 0
n = len(extracted_keywords)
while i < n:
if i in best:
grp, step = best[i]
else:
grp, step = [extracted_keywords[i].lower()], 1
for phrase in grp:
for tk in phrase.split():
if tk not in seen:
seen.add(tk)
out.append(tk)
i += step
return " ".join(out)