1. 배경: 결제로 이어지는 대화에서의 환각
KT 유선가입 에이전트처럼 실제 가입과 결제로 이어지는 상용 서비스에서, LLM이 틀린 정보를 말하는 것은 단순한 버그가 아닙니다. 요금이나 약정 조건을 잘못 안내하면 그대로 고객 분쟁이 됩니다.
다만 LLM은 모르면 모른다고 하지 않고 그럴듯하게 지어냅니다. 그리고 지어낸 답은 형식이 완벽해서 겉으로는 정상 응답과 구분되지 않습니다. 이것이 가장 위험한 부분이었습니다.
이 문제를 두 개의 프로젝트에서 각각 다른 방식으로 마주쳤고, 결국 같은 결론에 도달했습니다.
2. 문제 정의: 프롬프트 기반 제약의 한계
처음 접근은 누구나 하는 방식이었습니다. 프롬프트에 규칙을 씁니다.
- 주어진 데이터에 없는 내용은 절대 생성하지 마세요.
- 확실하지 않으면 모른다고 답하세요.
- 임의로 상품이나 조건을 추천하지 마세요.효과가 있었습니다. 대부분의 경우에는.
문제는 나머지였습니다. 예상하지 못한 질문이 들어오거나, 대화가 길어지거나, 데이터가 애매하게 비어 있을 때 모델이 규칙을 넘어갔습니다. 이에 따라 규칙을 하나 더 추가합니다. 그러면 또 다른 곳에서 샙니다.
어느 순간 프롬프트가 금지 조항 목록이 되어 있었습니다. 그런데도 "이제 안 샌다"고 말할 수 있는 근거가 없었습니다. 테스트를 통과했다는 것이 다음에도 통과한다는 보장이 아니었기 때문입니다.
3. 기술적 난점
근본적인 이유가 있었습니다. 프롬프트는 지시이지 제약이 아닙니다.
코드에서 private 필드는 외부에서 접근할 방법 자체가 없습니다. 하지만 프롬프트의 "하지 마세요"는 모델이 참고하는 강한 권고일 뿐입니다. 확률적으로 동작하는 시스템에게 확률적인 수단으로 확정적인 보장을 요구하고 있었던 것입니다.
여기에 더해, 만들던 구조 자체에 문있었습니다. 답변을 생성하는 모델이 원본 데이터에 전부 접근할 수 있었습니다. 조회한 목록 전체가 컨텍스트에 들어가 있으니, 모델 입장에서는 그중 임의로 골라 말하는 것이 물리적으로 가능했습니다. 확정되지 않은 항목을 확정된 것처럼 안내하는 사고가 여기서 나왔습니다.
4. 대안 검토
1안. 프롬프트를 더 정교하게 쓴다
규칙을 명확히 하고, few-shot 예시를 넣고, 출력 형식을 강제합니다.
- 장점: 당장 적용이 빠릅니다.
- 단점: 보장이 없습니다. 개선은 되지만 "안 납니다"고 말할 수 없습니다. 그리고 규칙이 늘수록 프롬프트가 길어져 토큰과 지연이 늘고, 규칙끼리 충돌하기 시작합니다.
2안. 생성한 답변을 사후 검증합니다
답변을 만든 뒤 다른 모델이나 규칙으로 검사해서 걸러냅니다.
- 장점: 최종 출력을 한 번 더 거릅니다.
- 단점: 검증에 또 비용과 지연이 듭니다. 그리고 검증기 자체가 LLM이면 그것도 틀릴 수 있습니다. 무엇보다 잘못된 답이 만들어지는 것 자체는 못 막고, 만들어진 뒤에 잡는 방식입니다.
3안. 지어낼 재료를 주지 않습니다
답변 생성 단계에 확정된 값만 전달하고, 원본 데이터에는 접근하지 못하게 합니다.
- 장점: 구조적으로 불가능해집니다. 존재하지 않는 정보를 생성하려면 참조할 데이터가 필요한데, 해당 데이터가 컨텍스트에 없습니다.
- 단점: 파이프라인을 여러 단계로 나눠야 하고, 무엇이 "확정"인지 정의하는 작업이 필요합니다.
5. 설계 결정 및 근거
3안을 골랐습니다. 판단의 기준은 이거였습니다. 모델의 선의에 기대는 대책은 상용 서비스에서 근거가 되지 못합니다.
답변 생성 노드의 격리
단일 LLM이 전부 처리하던 구조를 여러 단계로 쪼갰습니다. 상태 확인 → 주제 분류 → 처리 주체 선택 → 유형 세분화 → 답변 생성 순입니다.
여기서 핵심은 마지막 단계의 성격입니다. 답변 생성 노드는 확정된 상태값만 주입받습니다.
[ 이전 구조 ]
조회 결과 전체 ──────────────→ LLM ──→ 답변
(여기서 아무거나 고를 수 있음)
[ 바꾼 구조 ]
조회 결과 전체 → 판정·검증 로직 → 확정된 값만 → LLM ──→ 답변
(코드가 결정) (고를 여지가 없음)무엇을 추천할지, 어떤 조건이 적용되는지는 앞단의 비즈니스 로직이 코드로 결정합니다. LLM은 이미 확정된 것을 자연스러운 문장으로 바꾸는 역할만 합니다. 인가되지 않은 항목을 안내하려 해도 그 항목이 컨텍스트에 없습니다.
이 분리에는 예상 못 한 이득이 있었습니다. 정책 변경과 문구 변경이 분리됐습니다. 정책이 바뀌면 백엔드 코드만, 표현이 바뀌면 프롬프트만 고칩니다. 서로 영향을 주지 않으니 전체 회귀 검증 없이 당일 배포가 가능해졌습니다.
빈 응답이 나지 않게 폴백 체인을 깔았습니다
재료를 제한하면 새로운 문생깁니다. 줄 게 없을 때 대화가 끊깁니다.
이에 따라 단계적으로 내려가는 폴백 체인을 만들었습니다. 정확히 매칭되는 것을 먼저 찾고, 없으면 속성 기반으로 대안을 제시하고, 그것도 안 되면 상담 연결을, 마지막에는 지식 검색으로 넘깁니다. 매칭 정확도에 따라 "정확히 있음 / 부분적으로 있음 / 없음"을 구분해 각각 다르게 안내했습니다.
모른다고 말하되 끝내지는 않는 것이 목표였습니다. 환각을 막는 것과 사용자를 데드엔드에 두지 않는 것은 함께 설계해야 하는 문제였습니다.
참조 범위 제한: 검색 기반 시스템
RAG 파이프라인에서도 같은 원리를 적용했습니다. 수집한 데이터만 참조하도록 생성 단계를 제약했습니다. 그리고 이것이 실제로 동작하는지 대조 실험으로 확인했습니다.
같은 질문을 검색을 끄고 켜면서 비교했습니다.
| 질문 | 검색 OFF | 검색 ON |
|---|---|---|
| 서비스 센터 번호는? | 02-485-9311 ✗ | 1588-0000 ✓ |
| 배터리 용량은? | 1000mAh · 1시간 ✗ | 5200mAh · 4시간 ✓ |
검색을 끄면 모델은 그럴듯한 번호와 사양을 만들어냈습니다. 형식은 완벽하고 내용은 전부 틀렸습니다. 이 표를 만들고 나서야 "환각을 막았습니다"는 말에 근거가 생겼습니다.
6. 실험 결과
| 측정 항목 | 개선 전 | 개선 후 |
|---|---|---|
| 답변 성공률 | 78% (단일턴) | 98%+ (단일·멀티턴 평균) |
| 인가되지 않은 항목 안내 | 발생 | 구조적으로 차단 |
| 정책 변경 배포 | 전체 회귀 검증 필요 | 당일 무중단 배포 |
| 검색 추가 비용 | - | +0.02초 |
성공률을 78%에서 98%로 개선한 것은 단일 조치의 결과가 아닙니다. 실패한 케이스의 원인을 분류하고 → 파이프라인을 고치고 → 다시 평가하는 회귀 사이클을 정착시킨 결과입니다. 그리고 검증 범위를 단일 질문에서 멀티턴과 비선형 흐름 시나리오까지 넓혔습니다. 검증 범위를 넓히면 점수는 일시적으로 떨어지지만, 그게 실제 사용 환경에 더 가깝습니다.
7. 결론 및 시사점
신뢰성은 지시가 아니라 구조에서 나옵니다. "하지 마세요"라고 쓰는 것과 "할 수 없게" 만드는 것은 다른 종류의 보장입니다. 이후로는 환각 대책을 검토할 때 "이 모델이 지어내려면 무엇이 필요한가, 이를 뺄 수 있는가" 를 먼저 묻습니다. 프롬프트 개선은 그 다음입니다.
LLM에게 맡길 일과 맡기지 않을 일을 나눠야 합니다. LLM의 역할을 "의도 파악과 문장 생성"으로 좁혔습니다. 값을 비교하고 조건을 따지는 일은 코드가, 사고로 직결되는 판단은 규칙이 맡았습니다. 이는 LLM을 불신해서가 아니라, 확정성이 필요한 곳에는 확정적인 수단을 쓰는 게 맞다고 봤기 때문입니다.
막았다는 말에는 근거가 필요합니다. 대조 실험 표를 만들기 전까지 "환각이 줄어든 것 같습니다"고밖에 말할 수 없었습니다. 켜고 끈 결과를 나란히 놓고서야 무엇을 얼마나 막았는지 설명할 수 있었습니다. 정성적인 개선도 비교 실험으로 만들면 근거가 됩니다.
