Home 개인 비서 BM25 한국어 복합어 문제 — 형태소 분석기 없이 해결하기
Post
Cancel

개인 비서 BM25 한국어 복합어 문제 — 형태소 분석기 없이 해결하기

BM25를 붙이고 나서 “또리 생일”은 잘 찾는데, “내차가 뭐지?”는 여전히 못 찾는 문제가 생겼다.


증상

1
2
3
4
5
6
7
사용자: 내차는 기아 ev6야
봇: 💾 이렇게 기억할게요: _사용자의 차: 기아 EV6_ 맞나요?
사용자: 예
봇: ✅ 기억했습니다!

사용자: 내차가 뭐지?
봇: 사용자의 차량 정보에 대해서는 아직 기억해 둔 내용이 없습니다.

로그를 보면 search_memory가 3개 결과를 찾아서 에이전트에게 전달했다. 그런데도 “없다”고 답했다.

1
2
3
[tool] search_memory done count=3
[agent] ToolMessage content_len=80
reply: "사용자의 차량 정보에 대해서는 아직 기억해 둔 내용이 없습니다."

원인 — 한국어 복합어와 BM25 토크나이저

BM25는 토큰 단위로 매칭한다. 기존 토크나이저는 이렇게 동작한다.

1
2
def _tokenize(text: str) -> list[str]:
    return re.findall(r"[A-Za-z가-힣0-9]+", text.lower())

[가-힣]+ 패턴은 연속된 한글을 하나의 토큰으로 묶는다.

1
2
"내차가 뭐지?" → ["내차가", "뭐지"]
"사용자의 차: 기아 EV6" → ["사용자의", "차", "기아", "ev6"]

두 토큰 집합 사이에 겹치는 것이 없다. BM25 점수 = 0.

“내차가”는 “내(내 것)” + “차(자동차)” + “가(조사)”가 붙어있는 형태다. 형태소 분석기가 있어야 이를 분리할 수 있다. konlpy, kiwipiepy 같은 라이브러리가 이 역할을 하는데, C++ 의존성이 있어서 Docker 이미지가 무거워지고 설치 과정이 복잡하다.

벡터 검색도 마찬가지다. “내차가 뭐지?”와 “사용자의 차: 기아 EV6”는 의미적으로 관련있지만, 임베딩 모델이 이를 가깝게 배치하지 못했다.

결국 BM25도 0점, 벡터도 낮은 유사도라서 다른 관련 없는 문서들이 top-3를 차지하고 실제 기아 EV6 정보가 밀렸다.


해결 — 음절 단위 토큰 추가

형태소 분석기 없이 이 문제를 우회하는 방법은 한국어를 음절 단위로도 쪼개는 것이다.

1
2
3
4
5
6
def _tokenize(text: str) -> list[str]:
    # 기존: 연속된 한글을 하나의 토큰으로
    tokens = re.findall(r"[A-Za-z가-힣0-9]+", text.lower())
    # 추가: 한국어 음절을 개별 토큰으로도 추가
    korean_chars = re.findall(r"[가-힣]", text)
    return tokens + korean_chars

결과가 달라진다.

1
2
3
4
5
6
7
8
9
"내차가 뭐지?"
→ 단어: ["내차가", "뭐지"]
→ 음절: ["내", "차", "가", "뭐", "지"]
→ 최종: ["내차가", "뭐지", "내", "차", "가", "뭐", "지"]

"사용자의 차: 기아 EV6"
→ 단어: ["사용자의", "차", "기아", "ev6"]
→ 음절: ["사", "용", "자", "의", "차", "기", "아"]
→ 최종: ["사용자의", "차", "기아", "ev6", "사", "용", "자", "의", "차", "기", "아"]

“내차가”에서 분리된 개별 음절 “차”가 저장 문서의 “차”와 매칭된다.


음절 토큰의 노이즈 문제

음절 단위 토큰을 추가하면 모든 문서에 “사”, “이”, “의”, “는” 같은 단음절이 대량으로 생긴다. BM25에서 이런 토큰은 조사나 어미라서 쿼리와 문서 모두에 흔하게 나타난다.

BM25의 IDF(역문서빈도) 가중치가 이 문제를 어느 정도 완화한다. IDF는 모든 문서에 자주 나오는 토큰에 낮은 가중치를 준다. “이”, “의”, “는” 같은 음절이 모든 문서에 등장하면 IDF 가중치가 매우 낮아져서 점수에 거의 기여하지 못한다. “차”, “기아”, “ev6” 같이 특정 문서에만 나오는 토큰이 더 높은 가중치를 받아서 정확도가 유지된다.


형태소 분석기를 안 쓴 이유

kiwipiepy를 쓰면 “내차가” → [“내”, “차”, “가”] 정확히 분리할 수 있다. 그런데 Docker 이미지에 추가하면:

  • 이미지 크기 증가 (컴파일된 C++ 바이너리 포함)
  • 빌드 시간 증가
  • 플랫폼(arm64/amd64)에 따른 호환성 문제 가능

개인 비서에 저장되는 기억의 수가 수백 개 수준인 경우, 음절 분해로 충분히 원하는 결과를 얻을 수 있다. 정밀한 형태소 분석이 필요한 대규모 검색 시스템이라면 kiwipiepy 도입을 고려하는 게 맞다.


결과

1
2
3
4
"내차가 뭐지?" 쿼리
→ BM25에서 "차" 음절 매칭
→ "사용자의 차: 기아 EV6" 문서 상위 노출
→ 에이전트: "기아 EV6입니다."

작은 수정 하나로 해결됐다. 한국어 NLP에서 형태소 분석기 없이 키워드 검색을 구현할 때 음절 단위 분해는 좋은 트레이드오프다.

This post is licensed under CC BY 4.0 by the author.

개인 비서 기억 시스템 디버깅 — 고유명사 검색 실패, 데이터 오염, 확인 UX

Log Agent - RAG 기반 코드 검색에서 로컬 파일 직접 조회로 전환