룰베이스드 한국어 FAQ 챗봇 회고 — 형태소 분석과 매칭 임계값 튜닝
전문용어가 많은 특정 분야의 FAQ 챗봇을 만들었다. 코드는 공개할 수 없지만, 한국어 룰베이스드 챗봇을 만들면서 부딪힌 문제들과 결정들을 정리해본다.
어떤 챗봇이었나
특정 분야 — 전문용어가 많은 — FAQ를 답해주는 챗봇이었다. 사용자가 자연스럽게 질문을 던지면 매칭되는 FAQ의 답변을 돌려주는 형태. 관리자가 카테고리·FAQ·키워드·이력을 관리할 수 있는 어드민도 함께 만들었다.
LLM 호출 없이 룰베이스드로 가야 했던 이유는 두 가지였다.
- 응답의 일관성 — 같은 질문에는 같은 답이 나와야 한다. 관리자가 정리한 답변을 그대로 돌려주는 게 안전했다.
- 운영 비용 — 정답이 이미 정해진 FAQ에 매번 LLM을 거치는 건 비용 대비 효용이 낮다.
스택
- Spring 프레임워크 (전자정부표준프레임워크 위)
- Komoran — 한국어 형태소 분석기. 사용자 사전을 따로 운영해 전문용어를 인식시킴
- Apache Commons Text의 JaroWinkler — 단어 단위 유사도
- MyBatis — DAO 패턴, 프로시저 기반 PK 채번
매칭 엔진의 흐름
사용자 질문이 들어오면 대략 이런 단계로 처리한다.
- Komoran으로 형태소 분석 → 일반명사(NNG)·고유명사(NNP)·숫자(SN)·외국어(SL)만 추출
- 의문 종결 어미 같은 노이즈 토큰 제거
- FAQ 후보군을 메모리 캐시에서 조회 (없으면 DB)
- 후보 FAQ마다 F1 점수(precision·recall 조화평균)로 단어 단위 매칭
- 키워드(운영자 등록 동의어)가 사용자 질문에 그대로 포함되면 고정 점수 보정
- 길이별 동적 임계값으로 통과 판정
- 매칭 성공·실패와 관계없이 이력 로그 적재
핵심은 4·5·6이다. 시행착오가 여기서 가장 많이 나왔다.
1. “가요”를 명사로 본 형태소 분석기
“필요 서류는 뭔가요” — 미매칭
같은 키워드를 가진 FAQ가 분명히 있는데도 이런 질문이 자꾸 미매칭으로 떨어졌다. 처음엔 이해가 안 갔다.
로그를 까서 보니, Komoran이 “뭔가요”를 분석하면서 뭔(MM) 가요(NNP)로 끊고 있었다. 의문 종결 어미 “~요”가 NNP(고유명사)로 잘못 분류된 것. 그 결과 사용자 질문의 토큰 수가 부풀려졌고, F1 점수의 precision이 떨어져 임계값을 못 넘었다.
해결은 단순했다. 명사 추출 결과에서 의문 종결 어미 변형(가요, 나요, 에요, 예요)을 강제로 제거하는 stopword 처리. 현재 확인된 오분류는 “가요” 하나지만, 모델 변동 대비 안전망으로 같은 패턴 세 개를 함께 등록해 뒀다.
작은 문제처럼 보였지만, 한국어 챗봇이 형태소 분석기의 어떤 한계를 안고 있는지 가장 명확하게 가르쳐준 사건이었다. 이후로는 미매칭 케이스가 들어올 때마다 형태소 분석 결과부터 먼저 확인하는 습관이 생겼다.
2. F1 점수를 채택한 이유
처음엔 단순 매칭 카운트(사용자 토큰 중 몇 개가 FAQ 토큰과 일치하는가)로 점수를 매겼다. 문제는 두 방향에서 나타났다.
- 긴 질문에 너무 엄격해진다. 자유롭게 풀어 쓴 6+ 토큰 질문은 일부만 매칭돼도 충분한데, 매칭 비율이 낮아 떨어진다.
- 짧은 질문에 너무 관대해진다. 토큰 1~2개가 우연히 겹치면 점수가 부풀려진다.
F1 점수(recall과 precision의 조화평균)로 바꿨더니 두 방향이 모두 잡혔다. 사용자 토큰 매칭률(recall)과 FAQ 토큰 매칭률(precision) 중 어느 한쪽만 좋아서는 점수가 오르지 않고, 둘 다 균형 잡혀야 통과한다.
3. 길이별 동적 임계값
F1 점수를 써도 한 가지 임계값으로 모든 질문 길이를 커버할 수는 없었다. 자유 서술이 길어질수록 사용자 토큰 중 FAQ에 안 나오는 단어가 자연스럽게 늘기 때문이다(precision의 자연 감소).
그래서 임계값을 토큰 수에 따라 분기했다.
| 사용자 토큰 수 | 임계값 | 비고 |
|---|---|---|
| 0 | (무조건 거절) | 명사가 하나도 안 잡힌 질문 |
| 1 | 0.65 | 4자 이상 전문용어만 통과 |
| 2~5 | 0.70 | 기본값 |
| 6+ | 0.50 | precision 자연 감소 보정 |
1단어 가드(4자 이상 + 0.65)는 전문용어 도메인 특성에서 나왔다. “참여” 같은 짧은 일반 명사 한 단어만 던지면 거절해야 하지만, Komoran 사용자 사전에 등록해 둔 4자 이상의 다음절 합성어 전문용어는 한 단어만으로도 의도가 분명하니 통과시킨다.
4. 키워드 완전 포함 = 고정 점수
운영자가 FAQ에 동의어 키워드를 등록할 수 있게 했다. 같은 의미인데 표현이 다른 질문들을 잡기 위해서다. 처음엔 키워드도 일반 토큰처럼 F1 점수에 묻혀 있었는데, 운영자가 “이 단어가 들어오면 분명 이 FAQ”라고 명시 등록한 신호치고는 너무 약하게 반영됐다.
그래서 사용자 질문에 키워드가 단어 경계 단위로 그대로 포함되면 0.95라는 고정 점수를 부여하도록 바꿨다. FAQ 길이 페널티를 우회하는 강한 시그널로 취급하는 셈이다. 운영자가 한 줄 등록한 게 한 줄만큼 정확히 효과를 내게 됐다.
부가적 결정들
매칭 로직 외에도 자잘하게 신경 쓴 부분들.
- FAQ 캐시 무효화는 트랜잭션 커밋 후에. 커밋 전에 캐시를 비우면 다른 요청이 아직 커밋 안 된 stale 데이터를 다시 캐시에 채울 수 있다.
- 이력 적재는 매칭 성공·실패 모두. 운영자가 미매칭 케이스를 분석해서 FAQ를 보강할 수 있게.
- 이력 적재 실패는 메인 로직에 영향 없게 격리. 로그용 로직이 본 응답을 망치지 않게 try-catch로 감쌌다.
- 키워드 PK 일괄 채번. 다건 등록 시 시퀀스 프로시저를 N번 부르지 않도록 시퀀스 범위를 한 번에 받아온다.
회고
룰베이스드 챗봇은 LLM 챗봇보다 작아 보이지만, “한국어를 어떻게 토큰으로 끊을 것인가”부터 “어떤 점수를 어떤 임계값으로 자를 것인가”까지 결정할 게 많았다. 그리고 그 결정의 옳고 그름은 결국 실제 사용자의 질문 로그와 미매칭 케이스가 알려줬다.
배운 게 있다면, 테스트보다 로그가 진실에 가깝다는 것. 의도된 테스트 케이스로는 잡히지 않던 “가요” 사건도, 운영 로그에서 미매칭 패턴을 들여다보다 잡혔다. 룰베이스드 챗봇을 운영할 거라면 가장 먼저 신경 써야 할 건 좋은 매칭 알고리즘이 아니라 좋은 로그라는 생각이 들었다.