고객지원 챗봇은 LLM 애플리케이션 중에서 가장 현실적인 사례입니다. 이유는 간단합니다. 고객은 친절한 답변을 원하지만, 그보다 먼저 정확한 답변을 원합니다. 환불 정책을 잘못 안내하거나, 없는 할인 혜택을 만들어 내거나, 개인정보를 함부로 요구하는 챗봇은 제품 경험을 개선하기는커녕 회사를 위험하게 만듭니다.
그래서 고객지원 챗봇은 단순한 "대화형 UI"가 아닙니다. FAQ를 검색하고, 제품 문서를 읽고, 정책을 확인하고, 필요하면 주문 시스템이나 계정 시스템을 호출하고, 모르면 상담원에게 넘기는 작은 고객센터 운영 시스템입니다.
앞 장들에서 우리는 블로그 글 생성기, PDF 요약기, 책 기반 Q&A 챗봇을 만들었습니다. 이번 장에서는 그 흐름을 실제 고객지원 시나리오로 확장합니다. 핵심은 다음 한 문장입니다.
고객지원 챗봇은 RAG만으로도, Function Calling만으로도 충분하지 않다. 검색된 근거, 업무 시스템, 안전장치, 상담원 전환이 함께 설계되어야 한다.

고객지원 챗봇이 어려운 이유
FAQ 챗봇을 만든다고 하면 처음에는 쉬워 보입니다. "배송은 며칠 걸리나요?", "환불은 어떻게 하나요?", "비밀번호를 잊어버렸어요" 같은 질문에 답하면 될 것 같습니다. 실제로 초기 버전은 FAQ 몇 개를 프롬프트에 넣는 것만으로도 그럴듯하게 동작합니다.
문제는 고객 질문이 FAQ 문장처럼 들어오지 않는다는 데 있습니다.
고객은 "배송 기간은?"이라고 묻기도 하지만, "어제 샀는데 금요일 전까지 받을 수 있나요?", "제 주문 왜 아직도 준비 중이에요?", "선물인데 늦으면 취소할게요"라고 묻습니다. 같은 배송 문의라도 단순 정책 안내일 수도 있고, 특정 주문의 실시간 배송 조회일 수도 있고, 불만이 섞인 상담 요청일 수도 있습니다.
또한 고객지원에는 회사의 책임이 따라옵니다. LLM이 "아마 환불될 것 같습니다"라고 답하는 순간, 고객은 그것을 회사의 약속으로 받아들일 수 있습니다. 특히 환불, 결제, 개인정보, 보안, 장애 신고, 의료·금융·법률 성격이 섞인 문의에서는 더 조심해야 합니다.
그래서 고객지원 챗봇은 보통 세 가지 층으로 나누어 설계합니다.
지식 답변: FAQ, 제품 문서, 정책 문서를 검색해 답변한다.
업무 처리: 주문 조회, 예약 변경, 환불 접수, 계정 수정처럼 외부 시스템을 호출한다.
상담원 전환: 근거 부족, 정책 충돌, 민감 요청, 고객 불만 고조 상황을 사람에게 넘긴다.
RAG는 첫 번째 층을 담당합니다. Function Calling과 Tool Use는 두 번째 층을 담당합니다. 안전장치와 handoff 정책은 세 번째 층을 담당합니다.
전체 구조 그리기
고객지원 챗봇의 기본 아키텍처는 다음과 같습니다.
[고객]
↓
[채팅 UI]
↓
[API 서버]
↓
[의도 분류]
├─ 정보성 문의 ─→ [RAG 검색] ─→ [LLM 답변 생성]
├─ 상태 조회 ─→ [권한 확인] ─→ [외부 API 호출] ─→ [LLM 답변 생성]
├─ 업무 접수 ─→ [필수 정보 수집] ─→ [티켓/CRM 등록]
└─ 위험/불확실 ─→ [상담원 전환]
↓
[로그·평가·개선]여기서 중요한 점은 LLM이 모든 일을 직접 하지 않는다는 것입니다. LLM은 고객의 말을 이해하고 답변을 자연스럽게 만드는 데 강합니다. 하지만 실제 주문 상태를 알고 있거나, 환불을 승인하거나, 고객 정보를 수정할 권한은 없습니다. 그런 일은 반드시 애플리케이션 코드와 업무 시스템이 담당해야 합니다.
이 구조는 앞에서 배운 RAG와 Function Calling을 하나로 묶은 형태입니다. 사용자가 "반품 기간이 어떻게 되나요?"라고 물으면 정책 문서를 검색해 답합니다. 반면 "제 주문 반품 접수해 주세요"라고 말하면 주문 번호와 본인 확인이 필요하고, 이후 반품 API나 상담 티켓 시스템을 호출해야 합니다.
기업 환경에서는 지식 저장소로 FAISS 같은 인메모리 벡터 저장소, ChromaDB 같은 로컬·개발용 벡터 DB, OpenSearch나 Kendra 같은 관리형 검색 서비스를 조합할 수 있습니다. AWS의 RAG 사례에서도 Faiss, OpenSearch, Kendra 같은 저장소를 용도에 따라 함께 사용하는 구조가 소개됩니다.1 작은 프로젝트라면 ChromaDB나 FAISS로 시작하고, 문서량과 트래픽이 커지면 OpenSearch, Kendra, Elasticsearch, Milvus 같은 운영형 검색 시스템을 검토하면 됩니다.
지식 저장소 만들기
고객지원 챗봇의 첫 번째 재료는 지식입니다. 여기서 지식은 모델이 사전학습 때 배운 일반 지식이 아니라, 우리 회사가 고객에게 실제로 안내해야 하는 문서입니다.
고객지원용 지식 저장소에는 보통 다음 문서가 들어갑니다.
FAQ: 자주 묻는 질문과 표준 답변
제품 문서: 사용법, 설정 방법, 오류 코드, 호환성 정보
정책 문서: 배송, 교환, 반품, 환불, 보증, 개인정보 처리 방침
장애 공지: 알려진 문제, 점검 일정, 임시 해결 방법
내부 상담 가이드: 상담원이 고객에게 안내할 수 있는 승인된 문구
여기서 가장 중요한 원칙은 "최신 문서가 정답"이라는 점입니다. LLM의 기억이 아니라 회사의 최신 정책 문서가 기준이어야 합니다. RAG가 고객지원에 유용한 이유도 여기에 있습니다. RAG는 모델이 답을 만들기 전에 회사 문서에서 관련 근거를 찾아 컨텍스트로 넣어 주므로, 일반 LLM만 사용할 때보다 회사별 최신 정보에 맞춘 답변을 만들기 쉽습니다.2
문서를 저장할 때는 원문 전체를 그대로 벡터 DB에 넣지 않습니다. 먼저 작은 조각으로 나눕니다. 이것을 청킹이라고 했습니다. 고객지원 문서는 짧고 명확한 단위가 많기 때문에, "질문 하나와 답변 하나", "정책 조항 하나", "오류 코드 하나"를 하나의 chunk로 만드는 방식이 잘 맞습니다.
예를 들어 FAQ 원본이 다음과 같다고 합시다.
{
"id": "faq_return_001",
"category": "refund",
"title": "단순 변심 반품 기간",
"content": "상품 수령 후 7일 이내에는 단순 변심 반품을 신청할 수 있습니다. 단, 사용 흔적이 있거나 포장이 훼손된 경우 반품이 제한될 수 있습니다.",
"source": "refund_policy.md",
"updated_at": "2026-04-01"
}이 문서는 하나의 chunk로 저장하기에 적당합니다. 반면 긴 제품 매뉴얼은 제목 구조를 보존한 채 나누는 것이 좋습니다.
{
"id": "manual_device_a_wifi_003",
"category": "usage",
"product": "Device A",
"title": "Wi-Fi 재설정 방법",
"content": "Device A의 Wi-Fi를 재설정하려면 전원 버튼과 연결 버튼을 동시에 5초간 누릅니다. 상태 표시등이 파란색으로 깜박이면 앱에서 새 네트워크를 선택합니다.",
"source": "device_a_manual.pdf",
"page": 18,
"updated_at": "2026-03-15"
}좋은 chunk에는 본문뿐 아니라 메타데이터가 함께 있어야 합니다. 고객지원에서는 특히 category, product, policy_version, updated_at, source, page, locale 같은 필드가 중요합니다. 검색 결과가 여러 개 나왔을 때 최신 문서를 우선하거나, 사용자가 특정 제품을 언급했을 때 해당 제품 문서만 필터링할 수 있기 때문입니다.
인덱싱 파이프라인 구현하기
이제 문서를 읽고, chunk로 나누고, 임베딩한 뒤, 벡터 저장소에 넣는 파이프라인을 만들어 봅시다. 아래 코드는 특정 벡터 DB에 강하게 묶이지 않도록 단순화한 예시입니다.
# supportbot/indexing.py
from dataclasses import dataclass
from typing import Any
@dataclass
class SupportChunk:
id: str
text: str
metadata: dict[str, Any]
def make_faq_chunk(item: dict[str, Any]) -> SupportChunk:
text = f"""
[분류] {item.get('category', 'general')}
[제목] {item['title']}
[내용] {item['content']}
""".strip()
return SupportChunk(
id=item["id"],
text=text,
metadata={
"category": item.get("category"),
"source": item.get("source"),
"updated_at": item.get("updated_at"),
"product": item.get("product"),
},
)FAQ처럼 구조화된 데이터는 위처럼 쉽게 chunk로 만들 수 있습니다. PDF나 HTML 문서는 텍스트 추출 과정이 먼저 필요합니다. 실제 운영에서는 표, 제목, 페이지 번호, 링크가 사라지지 않도록 전처리하는 것이 중요합니다. 고객지원 답변에서는 "어느 문서의 어느 조항에 근거했는가"가 매우 중요하기 때문입니다.
임베딩과 저장은 다음과 같은 인터페이스로 생각할 수 있습니다.
# supportbot/vector_store.py
class VectorStore:
def add(self, chunks: list[SupportChunk]) -> None:
raise NotImplementedError
def search(
self,
query: str,
top_k: int = 5,
filters: dict | None = None,
) -> list[dict]:
raise NotImplementedError개발 단계에서는 ChromaDB나 FAISS로 이 인터페이스를 구현할 수 있습니다. 운영 단계에서는 OpenSearch, Elasticsearch, Kendra, Pinecone, Weaviate, Milvus 같은 시스템으로 바꿀 수 있습니다. 중요한 것은 애플리케이션 코드가 특정 저장소에 지나치게 의존하지 않도록 검색 인터페이스를 분리하는 것입니다.
검색 결과는 단순히 본문만 반환하지 말고, 점수와 출처를 함께 반환해야 합니다.
{
"text": "상품 수령 후 7일 이내에는 단순 변심 반품을 신청할 수 있습니다...",
"score": 0.82,
"metadata": {
"category": "refund",
"source": "refund_policy.md",
"updated_at": "2026-04-01"
}
}이 점수는 나중에 handoff 조건을 판단할 때도 사용됩니다. 예를 들어 최고 검색 점수가 너무 낮다면 "근거 문서를 찾지 못했다"고 판단하고 상담원 연결이나 추가 질문으로 전환할 수 있습니다.
고객 문의 의도 분류하기
고객지원 챗봇은 모든 질문을 같은 방식으로 처리하면 안 됩니다. "배송비는 얼마인가요?"와 "내 주문 어디쯤 왔나요?"는 모두 배송 관련 질문이지만 처리 방식이 다릅니다. 전자는 정책 문서로 답할 수 있고, 후자는 주문 시스템 조회가 필요합니다.
따라서 먼저 문의 의도를 분류합니다. 처음에는 규칙 기반으로 시작해도 충분합니다. 이후 데이터가 쌓이면 LLM 기반 분류나 작은 분류 모델을 사용할 수 있습니다.
의도는 너무 세분화하지 않는 것이 좋습니다. 처음에는 다음 정도면 충분합니다.
shipping_policy: 배송비, 배송 기간, 배송 가능 지역 같은 일반 정책order_status: 특정 주문의 배송 상태 조회refund_policy: 환불·반품·교환 정책 문의refund_request: 환불 또는 반품 접수 요청account: 로그인, 비밀번호, 계정 정보 변경pricing: 가격, 요금제, 할인, 세금계산서 문의usage: 제품 사용법, 설정, 오류 해결incident: 장애 신고, 버그 신고, 서비스 이상human: 상담원 연결 요청, 불만, 민감 문의
LLM을 이용해 의도 분류를 할 때는 자연어 답변이 아니라 구조화된 JSON으로 받는 것이 좋습니다.
{
"intent": "order_status",
"confidence": 0.91,
"entities": {
"order_id": "A12345"
},
"needs_auth": true,
"reason": "사용자가 특정 주문의 현재 배송 상태를 묻고 있음"
}이 분류 결과에 따라 다음 단계가 갈립니다.
# supportbot/router.py
RAG_INTENTS = {
"shipping_policy",
"refund_policy",
"pricing",
"usage",
}
TOOL_INTENTS = {
"order_status",
"refund_request",
"account",
"incident",
}
HANDOFF_INTENTS = {
"human",
}
def route(intent: str) -> str:
if intent in RAG_INTENTS:
return "rag"
if intent in TOOL_INTENTS:
return "tool"
if intent in HANDOFF_INTENTS:
return "handoff"
return "clarify"여기서 핵심은 "고객지원 자동화"를 "무조건 봇이 끝까지 답변하는 것"으로 오해하지 않는 것입니다. 좋은 챗봇은 답할 수 있는 것과 답하면 안 되는 것을 구분합니다.
RAG 답변 만들기
정보성 문의는 RAG로 처리합니다. 예를 들어 고객이 묻습니다.
단순 변심으로 반품하고 싶은데 언제까지 가능해요?
이 질문은 refund_policy로 분류됩니다. 그러면 벡터 검색으로 반품 정책 문서를 찾고, 검색된 근거를 LLM 컨텍스트에 넣어 답변합니다.
# supportbot/rag.py
MIN_SCORE = 0.55
def build_context(docs: list[dict]) -> str:
lines = []
for i, doc in enumerate(docs, start=1):
meta = doc["metadata"]
lines.append(
f"""
[문서 {i}]
출처: {meta.get('source')}
업데이트: {meta.get('updated_at')}
분류: {meta.get('category')}
내용:
{doc['text']}
""".strip()
)
return "nn".join(lines)
def should_answer(docs: list[dict]) -> bool:
if not docs:
return False
return docs[0].get("score", 0) >= MIN_SCORELLM에 전달하는 프롬프트는 다음처럼 구성할 수 있습니다.
너는 고객지원 상담 도우미다.
아래 제공된 근거 문서만 사용해 답변하라.
근거 문서에 없는 내용은 추측하지 말고, 추가 확인이 필요하다고 말하라.
정책 판단이 필요한 경우 단정하지 말고 상담원 연결을 안내하라.
[고객 질문]
{question}
[근거 문서]
{context}
[답변 규칙]
- 고객에게 친절하고 간결하게 답한다.
- 날짜, 금액, 조건은 근거 문서에 있는 경우에만 말한다.
- 필요한 경우 출처 이름을 함께 알려 준다.
- 고객이 다음에 할 수 있는 행동을 안내한다.이 프롬프트는 "친절함"보다 "근거 기반"을 먼저 둡니다. 고객지원 챗봇은 그럴듯한 말솜씨보다 확인 가능한 답변이 중요합니다.
응답 예시는 다음과 같습니다.
단순 변심 반품은 상품을 수령한 날로부터 7일 이내에 신청할 수 있습니다.
다만 사용 흔적이 있거나 포장이 훼손된 경우에는 반품이 제한될 수 있습니다.
근거: refund_policy.md, 2026-04-01 업데이트
반품 접수를 원하시면 주문 번호를 알려 주세요. 주문 상태를 확인한 뒤 접수 가능 여부를 안내해 드릴게요.이 답변은 세 가지 점에서 좋습니다. 근거 문서에 있는 내용만 말합니다. 조건을 함께 안내합니다. 다음 행동으로 자연스럽게 이어집니다.
도구 호출이 필요한 문의 분리하기
이번에는 고객이 이렇게 묻는다고 해 봅시다.
주문 A12345 지금 어디쯤 왔나요?
이 질문은 RAG만으로 답하면 안 됩니다. 배송 정책 문서에는 "일반적으로 2~3일 소요"라고 쓰여 있을 수 있지만, 고객이 묻는 것은 특정 주문의 실시간 상태입니다. 이때는 주문 조회 API를 호출해야 합니다.
도구는 LLM이 직접 실행하는 것이 아니라, 애플리케이션 코드가 실행합니다. LLM은 "어떤 도구를 어떤 인자로 호출해야 하는지"를 제안하고, 실제 호출 권한과 검증은 서버가 맡습니다.
예를 들어 주문 조회 도구의 스키마는 다음처럼 정의할 수 있습니다.
# supportbot/tools.py
from dataclasses import dataclass
@dataclass
class ToolResult:
ok: bool
data: dict | None = None
error: str | None = None
def get_order_status(user_id: str, order_id: str) -> ToolResult:
# 실제 서비스에서는 여기서 인증된 사용자와 주문 소유자를 확인한다.
order = fake_order_api_get(user_id=user_id, order_id=order_id)
if order is None:
return ToolResult(ok=False, error="ORDER_NOT_FOUND")
return ToolResult(
ok=True,
data={
"order_id": order["id"],
"status": order["status"],
"carrier": order.get("carrier"),
"tracking_number_masked": mask_tracking(order.get("tracking_number")),
"estimated_delivery": order.get("estimated_delivery"),
},
)여기서 user_id는 반드시 서버가 인증 세션에서 가져와야 합니다. 고객이 "나는 user_id가 77입니다"라고 말한다고 해서 그 값을 믿으면 안 됩니다. 고객지원 챗봇에서 도구 호출은 항상 권한 확인과 함께 설계해야 합니다.
도구 호출 결과를 받은 뒤 LLM에는 민감한 원본 데이터를 그대로 넣지 않는 편이 좋습니다. 송장 번호 전체, 전화번호, 주소, 결제 정보 같은 값은 마스킹하거나 필요한 일부만 전달합니다.
def mask_tracking(value: str | None) -> str | None:
if not value:
return None
if len(value) <= 4:
return ""
return "" + value[-4:]LLM은 도구 결과를 바탕으로 고객에게 자연스럽게 안내합니다.
주문 A12345는 현재 배송 중입니다.
택배사는 한빛택배이고, 송장 번호 끝자리는 9012입니다.
예상 도착일은 5월 14일입니다.
배송사 사정에 따라 도착 시간이 달라질 수 있습니다. 배송 지연이 2일 이상 발생하면 제가 상담원 연결을 도와드릴 수 있어요.이 답변은 RAG 답변과 다릅니다. 근거 문서가 아니라 업무 시스템의 최신 상태를 사용했습니다. 고객지원 챗봇은 이 둘을 명확히 구분해야 합니다.
정책 문의와 업무 요청을 함께 처리하기
현실의 고객 문의는 한 가지 의도만 갖지 않습니다.
어제 받은 상품이 불량인 것 같은데 교환 가능한가요? 가능하면 바로 접수해 주세요.
이 질문에는 두 가지 의도가 있습니다. 먼저 교환 정책을 안내해야 하고, 이어서 교환 접수를 진행해야 합니다. 이런 경우에는 한 번에 API를 호출하기보다 단계적으로 처리하는 것이 안전합니다.
먼저 RAG로 교환 정책을 확인합니다. 그런 다음 접수에 필요한 정보를 수집합니다. 예를 들어 제품 상태 사진, 주문 번호, 수령일, 불량 증상 등이 필요할 수 있습니다.
교환 가능 여부는 상품 상태와 수령일에 따라 달라집니다.
정책 문서에 따르면 제품 불량으로 의심되는 경우 수령 후 7일 이내에 교환 접수를 요청할 수 있습니다.
접수를 도와드리기 위해 다음 정보가 필요합니다.
- 주문 번호
- 불량 증상 설명
- 제품 상태를 확인할 수 있는 사진
주문 번호를 먼저 알려 주시겠어요?여기서 불릿 리스트는 필요한 정보가 3개 이상이므로 자연스럽게 사용할 수 있습니다. 고객에게 한꺼번에 너무 많은 정보를 요구하지 않으려면 대화 흐름에 따라 하나씩 물어보는 것도 좋습니다.
이후 고객이 주문 번호를 입력하고 로그인 상태가 확인되면 create_exchange_ticket 같은 도구를 호출합니다.
def create_exchange_ticket(
user_id: str,
order_id: str,
reason: str,
attachments: list[str],
) -> ToolResult:
if not is_order_owned_by(user_id, order_id):
return ToolResult(ok=False, error="PERMISSION_DENIED")
ticket = crm_create_ticket(
user_id=user_id,
order_id=order_id,
category="exchange",
reason=reason,
attachments=attachments,
)
return ToolResult(
ok=True,
data={"ticket_id": ticket["id"], "status": "submitted"},
)고객지원에서 도구 호출은 "편리함"이지만 동시에 "책임"입니다. 특히 환불 승인, 결제 취소, 개인정보 변경처럼 되돌리기 어려운 작업은 사용자의 명시적 확인을 받은 뒤 실행해야 합니다.
교환 접수를 진행할 수 있습니다.
주문 번호 A12345에 대해 "전원이 켜지지 않음" 사유로 교환 티켓을 생성할까요?
접수를 진행하려면 "네, 접수해 주세요"라고 답해 주세요.이런 확인 단계는 대화가 조금 길어지더라도 반드시 필요합니다.
상담원 전환 조건 정하기
좋은 고객지원 챗봇은 모든 질문에 답하려고 하지 않습니다. 오히려 멈출 줄 아는 챗봇이 좋은 챗봇입니다.
상담원 전환, 즉 handoff 조건은 미리 명확하게 정해야 합니다. 운영 중에 "이런 건 사람이 봐야 하는데 봇이 계속 답했네"라는 일이 반복되면 고객 신뢰가 떨어집니다.
다음 상황에서는 상담원 전환을 고려해야 합니다.
검색된 문서의 관련도 점수가 낮아 근거가 부족한 경우
서로 다른 정책 문서가 충돌하거나 최신 버전을 판단하기 어려운 경우
환불 승인, 보상, 법적 책임, 안전 문제처럼 회사의 공식 판단이 필요한 경우
고객이 화가 났거나 반복해서 불만을 표현하는 경우
개인정보, 결제 정보, 계정 보안처럼 민감한 정보가 포함된 경우
고객이 명시적으로 상담원 연결을 요청한 경우
handoff는 실패가 아닙니다. 오히려 고객지원 시스템의 정상적인 기능입니다. 챗봇이 처리할 수 있는 일은 빠르게 처리하고, 사람이 필요한 일은 필요한 정보와 함께 넘겨야 합니다.
상담원에게 넘길 때는 대화 요약과 검색된 문서, 도구 호출 결과를 함께 전달하면 좋습니다.
{
"ticket_type": "human_handoff",
"customer_id": "user_123",
"summary": "고객은 주문 A12345의 배송 지연으로 불만을 표시함. 일반 배송 정책은 안내했으나 실제 지연 보상 가능 여부는 상담원 판단 필요.",
"intent": "shipping_delay_complaint",
"retrieved_docs": ["shipping_policy.md", "delay_compensation.md"],
"tool_results": {
"order_status": "배송 중, 예상일 초과 2일"
},
"priority": "high"
}이렇게 넘기면 상담원은 처음부터 다시 묻지 않아도 됩니다. 고객 입장에서도 "봇과 한 대화가 사라졌다"는 느낌을 덜 받습니다.
시스템 메시지 설계하기
고객지원 챗봇의 시스템 메시지는 단순히 "친절하게 답하세요"로 끝나면 안 됩니다. 시스템 메시지는 챗봇의 업무 규정집입니다. 브랜드 톤, 답변 범위, 금지 표현, 개인정보 처리, 도구 호출 원칙, 상담원 전환 조건을 모두 포함해야 합니다.
예시는 다음과 같습니다.
너는 ACME Store의 고객지원 AI 상담원이다.
항상 친절하고 차분하며 전문적인 톤으로 답한다.
고객에게 책임을 돌리거나 비난하는 표현을 사용하지 않는다.
[지식 사용 규칙]
- 제공된 근거 문서와 도구 호출 결과를 우선 사용한다.
- 근거가 없는 정책, 가격, 환불 조건, 보상 기준을 추측하지 않는다.
- 문서가 부족하면 추가 확인이 필요하다고 말하고 상담원 연결 또는 티켓 생성을 제안한다.
[도구 사용 규칙]
- 주문 상태, 예약 변경, 환불 접수, 계정 변경은 외부 도구를 통해서만 처리한다.
- 도구 호출 전 필요한 정보와 권한 확인이 되었는지 확인한다.
- 결제 취소, 환불 접수, 계정 정보 변경은 고객의 명시적 확인 후 진행한다.
[개인정보 및 보안]
- 주민등록번호, 카드 전체 번호, 비밀번호, 인증번호를 요청하지 않는다.
- 사용자가 민감 정보를 입력하면 필요한 경우 즉시 삭제 또는 마스킹 안내를 한다.
- 로그나 답변에 불필요한 개인정보를 남기지 않는다.
[상담원 전환]
- 고객이 상담원을 요청하면 즉시 전환 절차를 안내한다.
- 정책 충돌, 법적 책임, 안전 문제, 반복 불만, 위협적 표현은 상담원에게 넘긴다.
[답변 형식]
- 먼저 결론을 말하고, 필요한 조건을 짧게 설명한다.
- 고객이 다음에 할 수 있는 행동을 안내한다.
- 모르는 것은 모른다고 말한다.이 시스템 메시지는 서비스의 성격에 맞게 바꿔야 합니다. 쇼핑몰, SaaS, 병원 예약, 금융 서비스, 교육 서비스는 금지 표현과 handoff 조건이 다릅니다. 특히 민감 산업일수록 "LLM이 해도 되는 말"과 "반드시 사람이 해야 하는 말"을 더 엄격하게 나누어야 합니다.
고객지원 프롬프트 설계에서는 RAG 문맥만 사용하도록 제한하고, 모르는 경우 지원팀 연결을 안내하며, 도구 사용 로직을 명확히 쓰는 방식이 자주 권장됩니다.3 이 원칙은 단순해 보이지만 운영 품질을 크게 좌우합니다.
대화 로그와 개인정보 다루기
고객지원 챗봇은 운영하면서 반드시 로그를 남겨야 합니다. 그래야 어떤 질문에서 실패했는지, 어떤 문서가 부족한지, 어떤 의도가 자주 들어오는지 알 수 있습니다. 하지만 고객지원 로그에는 개인정보가 섞이기 쉽습니다. 로그를 남기되, 불필요한 민감 정보는 남기지 않는 설계가 필요합니다.
로그에는 다음 정보를 남기면 좋습니다.
대화 ID, 사용자 ID의 익명화 값, 시간
분류된 의도와 신뢰도
검색된 문서 ID와 검색 점수
사용한 도구 이름과 성공·실패 여부
최종 답변 유형: RAG 답변, 도구 답변, 상담원 전환, 추가 질문
고객 피드백: 도움이 됨, 도움이 안 됨, 상담원 요청
반대로 다음 정보는 가능하면 저장하지 않거나 마스킹해야 합니다.
비밀번호, 인증번호, 카드 번호, 계좌 번호
주민등록번호, 여권번호 같은 고유 식별 정보
주소, 전화번호, 이메일 등 필요 이상으로 긴 원문 개인정보
고객이 실수로 입력한 민감한 건강·금융·법률 정보
간단한 마스킹 함수는 다음처럼 시작할 수 있습니다.
# supportbot/privacy.py
import re
PATTERNS = [
(re.compile(r"bd{3}-d{2}-d{5}b"), "[계좌번호_마스킹]"),
(re.compile(r"bd{3}-d{4}-d{4}b"), "[전화번호_마스킹]"),
(re.compile(r"bd{6}-d{7}b"), "[주민등록번호_마스킹]"),
(re.compile(r"bd{4}-d{4}-d{4}-d{4}b"), "[카드번호_마스킹]"),
]
def redact(text: str) -> str:
redacted = text
for pattern, replacement in PATTERNS:
redacted = pattern.sub(replacement, redacted)
return redacted실제 서비스에서는 정규식만으로 충분하지 않습니다. 국가별 전화번호 형식, 이메일, 주소, 주문번호, 내부 고객번호 등을 추가로 처리해야 합니다. 또한 로그 접근 권한, 보관 기간, 삭제 요청 처리도 함께 설계해야 합니다. RAG 챗봇은 외부 지식 저장소와 고객 데이터를 함께 다룰 수 있기 때문에 암호화, 접근 제어, 익명화 같은 개인정보 보호 장치가 중요합니다.4
간단한 FastAPI 서버 만들기
이제 전체 흐름을 하나의 API로 묶어 보겠습니다. 코드는 실제 서비스를 완성하기 위한 전체 구현이라기보다, 어떤 순서로 처리해야 하는지 보여 주는 뼈대입니다.
# app.py
from fastapi import FastAPI, Depends
from pydantic import BaseModel
app = FastAPI()
class ChatRequest(BaseModel):
message: str
conversation_id: str | None = None
class ChatResponse(BaseModel):
answer: str
mode: str
sources: list[dict] = []
handoff: bool = False
@app.post("/chat", response_model=ChatResponse)
def chat(req: ChatRequest, user=Depends(get_current_user)):
safe_message = redact(req.message)
classification = classify_intent(safe_message)
mode = route(classification.intent)
if mode == "rag":
docs = vector_store.search(
query=safe_message,
top_k=5,
filters={"category": classification.intent},
)
if not should_answer(docs):
ticket = create_handoff_ticket(
user_id=user.id,
message=safe_message,
reason="LOW_RETRIEVAL_CONFIDENCE",
)
log_event(req, classification, docs, mode="handoff")
return ChatResponse(
answer="관련 정책 문서를 충분히 찾지 못했습니다. 정확한 안내를 위해 상담원에게 연결해 드릴게요.",
mode="handoff",
handoff=True,
)
context = build_context(docs)
answer = generate_support_answer(
question=safe_message,
context=context,
user_profile={"locale": user.locale},
)
log_event(req, classification, docs, mode="rag")
return ChatResponse(
answer=answer,
mode="rag",
sources=[doc["metadata"] for doc in docs],
)
if mode == "tool":
result = handle_tool_flow(
user=user,
message=safe_message,
classification=classification,
)
log_event(req, classification, [], mode="tool", tool_result=result)
return result
if mode == "handoff":
create_handoff_ticket(
user_id=user.id,
message=safe_message,
reason="USER_REQUESTED_HUMAN",
)
log_event(req, classification, [], mode="handoff")
return ChatResponse(
answer="상담원 연결을 도와드릴게요. 지금까지의 대화 내용을 함께 전달하겠습니다.",
mode="handoff",
handoff=True,
)
return ChatResponse(
answer="정확히 도와드리기 위해 한 가지만 더 여쭤볼게요. 배송, 환불, 계정, 사용법 중 어떤 문의에 가까울까요?",
mode="clarify",
)이 서버에는 몇 가지 중요한 장치가 들어 있습니다. 먼저 사용자 메시지를 마스킹합니다. 다음으로 의도를 분류하고, RAG와 도구 호출과 상담원 전환을 분리합니다. 검색 신뢰도가 낮으면 답을 지어내지 않고 상담원에게 넘깁니다. 그리고 모든 흐름에서 로그를 남깁니다.
실제 서비스에서는 스트리밍 응답, 대화 상태 저장, 재시도, 타임아웃, 인증, 관리자 콘솔, 상담원 툴 연동이 추가됩니다. 하지만 기본 구조는 크게 달라지지 않습니다.
실패하는 질문을 개선 데이터로 바꾸기
고객지원 챗봇은 한 번 배포하고 끝나는 제품이 아닙니다. 오히려 배포 후부터 진짜 성능 개선이 시작됩니다. 고객은 문서 작성자가 예상하지 못한 방식으로 질문합니다. 검색은 가끔 엉뚱한 문서를 가져옵니다. 정책 문서는 시간이 지나며 바뀝니다. 이 모든 변화를 로그로 관찰하고 개선해야 합니다.
운영 로그에서 특히 봐야 할 것은 실패한 대화입니다.
예를 들어 "도움이 안 됨" 피드백이 많이 달린 질문을 모아 보면 다음 문제를 발견할 수 있습니다.
FAQ 문서에는 답이 있지만 고객 표현과 달라 검색이 잘 안 된다.
문서가 너무 길게 chunk되어 필요한 부분이 검색 결과 아래로 밀린다.
오래된 정책 문서와 최신 정책 문서가 함께 검색된다.
LLM이 근거 문서를 제대로 따르지 않고 일반적인 답을 섞는다.
실제로는 주문 API가 필요한 질문인데 RAG로만 처리했다.
이런 문제는 모델을 바꾸기 전에 문서와 검색 파이프라인을 고치는 것만으로도 많이 개선됩니다. FAQ 제목을 고객 표현에 맞게 보강하고, chunk 크기를 조정하고, 최신 문서 우선순위를 높이고, reranking을 추가하고, 의도 분류 규칙을 고치는 방식입니다.
일부 기업 사례에서는 고객지원 RAG 시스템에 응답 품질을 감시하는 guardrail과 LLM judge를 붙여 검색 정확성, 답변 정확성, 문맥 일치성 등을 평가합니다.5 모든 프로젝트가 처음부터 복잡한 평가 시스템을 갖출 필요는 없지만, 최소한 "어떤 답변이 왜 실패했는가"를 기록하는 구조는 반드시 필요합니다.
품질 지표 정하기
고객지원 챗봇의 품질은 "답변이 자연스러운가"만으로 평가할 수 없습니다. 고객지원의 목적은 문제 해결입니다. 그래서 운영 지표도 문제 해결 중심으로 잡아야 합니다.
대표적인 지표는 다음과 같습니다.
정확도: 답변이 정책과 실제 상태에 맞는가
근거 문서 일치율: 답변 내용이 검색된 문서에 의해 뒷받침되는가
해결률: 상담원 없이 고객 문제가 해결되었는가
상담원 전환율: 전체 대화 중 사람이 처리한 비율은 얼마인가
응답 지연 시간: 고객이 답변을 받기까지 얼마나 걸리는가
검색 실패율: 관련 문서를 찾지 못한 질문이 얼마나 되는가
고객 만족도: 도움이 됨/안 됨, 별점, 재문의율 등
이 지표들은 서로 균형을 봐야 합니다. 상담원 전환율을 무조건 낮추려고 하면 봇이 답하면 안 되는 질문까지 답할 수 있습니다. 반대로 안전하게만 운영하면 너무 많은 질문이 상담원에게 넘어가 자동화 효과가 떨어집니다. 목표는 "전환율 최소화"가 아니라 "정확하고 안전한 자동 해결률 최대화"입니다.
평가 데이터셋도 만들어야 합니다. 실제 고객 질문에서 개인정보를 제거한 뒤, 대표 질문과 정답 근거 문서를 묶습니다.
{
"question": "단순 변심 반품은 며칠 안에 해야 해요?",
"expected_intent": "refund_policy",
"expected_sources": ["refund_policy.md"],
"must_include": ["수령 후 7일 이내", "사용 흔적", "포장 훼손"],
"must_not_include": ["무조건 환불", "30일 이내"]
}이런 평가셋을 주기적으로 실행하면 문서가 바뀌거나 프롬프트를 수정했을 때 품질이 나빠졌는지 확인할 수 있습니다.
속도와 안정성 확보하기
고객지원 챗봇은 느리면 쓸모가 크게 줄어듭니다. 사용자는 상담 대기 시간을 줄이려고 챗봇을 쓰는데, 챗봇이 매번 10초씩 생각한다면 답답함만 커집니다.
속도를 개선하는 첫 번째 방법은 캐싱입니다. "배송비는 얼마인가요?", "영업시간은 어떻게 되나요?", "비밀번호를 잊어버렸어요" 같은 질문은 반복해서 들어옵니다. 검색 결과와 생성 답변을 일정 시간 캐싱하면 비용과 지연 시간을 줄일 수 있습니다.
두 번째는 비동기 처리입니다. 문서 인덱싱, 로그 분석, 평가 실행, 상담 티켓 후처리는 실시간 응답 경로에서 분리하는 것이 좋습니다. 고객에게 답하는 API는 가볍게 유지하고, 오래 걸리는 작업은 큐와 워커로 넘깁니다.
세 번째는 검색과 생성을 분리해 최적화하는 것입니다. 검색이 느리면 인덱스 구조와 필터를 개선해야 하고, 생성이 느리면 더 작은 모델을 쓰거나 답변 길이를 줄이거나 스트리밍을 적용할 수 있습니다. AWS의 Multi-RAG 사례에서도 여러 지식 저장소를 순차 검색하면 지연 시간이 늘어나므로 병렬 검색을 통해 지연을 줄이는 방식을 설명합니다.1
규모가 커지면 지역별 배포와 관리형 검색 서비스도 고려해야 합니다. 고객이 한국, 일본, 미국에 분산되어 있다면 가까운 리전에서 응답하도록 구성할 수 있습니다. 고객지원은 24시간 운영되는 경우가 많기 때문에 장애 대응, 재시도, rate limit, fallback 모델도 함께 설계해야 합니다.
작은 범위부터 시작하기
고객지원 챗봇을 처음 만들 때 가장 위험한 접근은 모든 문의를 한 번에 자동화하려는 것입니다. 배송, 환불, 계정, 장애, 결제, 법무, 보안까지 모두 처리하려고 하면 설계가 복잡해지고 위험도 커집니다.
처음에는 작게 시작하는 것이 좋습니다. 예를 들어 배송 정책과 반품 정책처럼 문서가 명확하고 반복 문의가 많은 영역을 먼저 자동화합니다. 그다음 주문 조회처럼 읽기 전용 API를 연결합니다. 이후 환불 접수나 예약 변경처럼 상태를 바꾸는 도구를 추가합니다. 마지막으로 상담원 전환과 평가 체계를 고도화합니다.
현실적인 도입 순서는 다음과 같습니다.
1단계: FAQ와 정책 문서 기반 RAG 답변
2단계: 주문 조회, 예약 조회 같은 읽기 전용 도구 연결
3단계: 반품 접수, 티켓 생성 같은 제한적 업무 처리
4단계: 로그 분석, 평가셋, 관리자 콘솔 구축
5단계: 다국어, 멀티 리전, 대규모 검색 인프라 확장
이 순서로 가면 실패해도 위험이 작습니다. 그리고 각 단계에서 실제 고객 질문을 보며 다음 자동화 대상을 정할 수 있습니다.
이번 장의 정리
고객지원 챗봇은 LLM 애플리케이션의 종합 실습입니다. RAG로 회사 문서를 검색하고, Function Calling으로 업무 시스템을 호출하고, 시스템 메시지로 안전장치를 세우고, 로그와 평가로 계속 개선해야 합니다.
핵심은 LLM에게 모든 책임을 맡기지 않는 것입니다. LLM은 고객의 말을 이해하고 자연스럽게 답하는 엔진입니다. 하지만 어떤 문서를 근거로 삼을지, 어떤 API를 호출할지, 언제 상담원에게 넘길지, 어떤 정보를 저장하지 말아야 할지는 애플리케이션이 정해야 합니다.
고객지원 챗봇을 잘 만들면 반복 문의를 줄이는 것 이상의 효과가 생깁니다. 고객이 무엇을 어려워하는지 보이고, 문서의 빈틈이 드러나고, 제품 개선의 단서가 쌓입니다. 챗봇은 단순한 비용 절감 도구가 아니라 고객의 목소리를 구조화하는 장치가 됩니다.
다음 장에서는 고객지원과는 조금 다른 방향의 애플리케이션을 만들어 봅니다. 이번에는 사용자의 글쓰기 과정을 돕는 AI 글쓰기 도우미입니다. 고객지원 챗봇이 정확성과 안전성을 우선했다면, 글쓰기 도우미는 사용자의 의도, 문체, 편집 흐름을 어떻게 다룰지가 핵심이 됩니다.
참고
1Multi-RAG와 Multi-Region LLM로 한국어 Chatbot 만들기
2What is a RAG chatbot? Benefits, challenges, and how to build one
3Customer Support Chatbot Prompt
4What is a RAG chatbot? Benefits, challenges, and how to build one
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.