toC 플랫폼의 AI 채팅을 뒷받침하는 기술
요약
지식 공유 플랫폼 'Brain'에 도입된 CS AI 채팅의 기술적 구현 사례를 소개합니다. 서비스 사용법에 특화된 RAG 기반의 AI 채팅을 통해 97.9%의 답변 커버율을 달성했으며, 이를 통해 실제 고객 문의량을 약 67.7% 감소시키는 성과를 거두었습니다.
핵심 포인트
- RAG(Retrieval-Augmented Generation) 기술을 활용하여 서비스 사용법에 특화된 답변 제공
- ECS Service 분리 및 Fargate 활용을 통한 독립적인 채팅 인프라 구축
- ElastiCache 기반의 Rate Limiter를 통한 입력 횟수 및 빈도 제어
- SSE(Server-Sent Events)를 이용한 실시간 스트리밍 답변 구현
- 사용자 개인정보 보호를 위해 DB 직접 접근을 제한하는 단계적 스코프 설정
안녕하세요, 카와손입니다.
최근 지식 공유 플랫폼 「Brain」에 CS(Customer Service)를 담당하는 AI 채팅을 도입했습니다.
릴리스 후 47시간 동안의 이용 횟수는 238건.
AI가 답변할 수 있어야 했으나 답변하지 못한 것은 5건뿐.
**커버율이 97.9%**라는 결과가 나왔습니다.
(물론 집계 기간이나 집계 빈도에 따른 편차는 있지만, 속보로서의 수치입니다)
또한, 최종적으로 사람이 대응한 문의도 줄어들었습니다.
개략적으로 하루 9.3건이었던 문의가 하루 3건으로 줄어들어, 약 67.7% 감소했습니다.
이번에는 그 AI 채팅을 뒷받침하는 기술에 대해 소개하고자 합니다.
CS AI 채팅의 스코프
현시점에서 CS AI는 β(베타) 버전입니다. β 버전의 사양은 다음과 같습니다.
- 서비스 이용 방법에 관한 것에 특화하여 답변
- 사용자 개별 사안에 대해서는 답변하지 않음
2번의 경우, 아래와 같은 이유로 β 버전에서는 제외하기로 판단했습니다.
1. read only라고는 해도, AI가 db에 직접 접근할 필요가 있다.
2. 사용자 정보라고는 해도, db 안에는 사용자에게 보여주고 싶지 않은 데이터가 있다. 그것을 AI의 판단이나 메커니즘에 녹여내는 것은 고려해야 할 요소가 많다.
3. Brain의 기능은 매우 많으며, 사용법이나 사양에 관한 문의가 많다. 따라서 그것에 자동 응답하는 것만으로도 최소한의 가치(Minimum Value)를 낼 수 있다고 판단.
향후 전망으로서, 물론 모든 것을 커버하는 것을 염두에 두고 설계하고 있습니다.
이번에는 이 점을 스코프 외로 두고, 기술 사양을 기술하겠습니다.
아키텍처
인프라, 외부 스토리지, 외부 통신 등을 포함한 아키텍처의 전체 모습은 다음과 같습니다.
mermaid는 이쪽입니다.
사용자가 CS AI에 질문하면, ALB target group의 path-based routing에 의해 chat 전용 ECS Service로 요청을 분류합니다.
전용 ECS 서비스로 분리한 이유에 대해서는 후술하겠습니다.
chat 전용 ECS container에서는 mode 환경 변수를 task definition에 심어둠으로써, Fargate 상에서 chat 전용으로 동작하도록 하고 있습니다.
container 상에서 입력값의 유효성 검사(Validation)와 ElastiCache와 연동된 rate limiter를 통한 입력 횟수 및 빈도 평가를 수행합니다.
요청이 정당하다는 것이 확인된 단계에서 질문 횟수를 consume(소비)합니다.
이 단계에서 비로소 RAG(Retrieval-Augmented Generation)로서 선정하고 있는 외부 스토리지 및 외부 LLM(Large Language Model)과 통신하여 답변을 생성합니다.
이 탐색 로직에 대해서도 후술하겠습니다.
답변이 가능한 경우에는 chunk(청크)로서 본문을 반환하고, 마지막에 done 시그널을 반환합니다.
아래와 같은 이미지입니다.
data: {"type":"chunk","content":"답변 본문의 일부"}
data: {"type":"chunk","content":"본문의 나머지"}
data: {"type":"done"}
개별 문의, knowledge(지식) 부족, 외부 서비스 에러 등은 각각 error 또는 redirect와 전용 code를 response(응답)에 포함하는 형태로 반환합니다.
data: {
"type": "redirect",
"code": "knowledge_unavailable",
...
프론트엔드가 SSE(Server-Sent Events)를 읽어 들여, chunk를 받을 때마다 assistant 메시지에 추가하여 화면에 표시합니다.
redirect나 error의 경우에는 문의 양식 유도나 에러 표시로 분기합니다.
(redirect_url은 상수로 관리하고 있으며, 그 외의 값이 반환되지 않도록 제어하고 있습니다)
전송 후, 프론트는 남은 횟수를 재확인합니다. 이 요청 역시 마찬가지로 Chat ECS Service로 분류되며, 필요에 따라 UI 전환을 수행합니다.
기술 선정의 근거
이 구성을 실현함에 있어서의 기술 선정 포인트를 정리합니다.
크게 아래의 3가지 점입니다.
1. API의 ECS 서비스와 공존시킬 것인가
2. SSE인가 WebSocket인가
3. Pinecone인가 OpenAI Vector Store인가
1. API의 ECS Service에 공존시킬 것인가, 별도로 분리할 것인가
아래와 같은 이유로 전용 ECS Service를 분리하기로 판단했습니다.
- API의 HTTP 속성과 다름
- Auto Scaling (오토 스케일링) 조건이 다름
- 장애를 분리하고 싶음
...
API의 HTTP 속성과 다름
채팅은 SSE (Server-Sent Events)를 이용하기 때문에, API와 비교하여 상대적으로 긴 응답 시간을 허용합니다.
API가 요구하는 response (응답) 속도와 chat (채팅)이 요구하는 속도가 다르기 때문입니다.
하지만 ALB (Application Load Balancer)가 공통이기 때문에, 요청의 송신 대상에 따라 타임아웃 (timeout) 값을 변경하는 등의 제어는 할 수 없습니다. 따라서 성질이 다른 것을 별도의 ECS Service (ECS 서비스)로 분리하는 구성을 선정했습니다.
API용 ECS 서비스
기존 ALB의 idle timeout (유휴 타임아웃) 값에 맞춥니다. 이를 통해 기존의 response (응답)를 불필요하게 지연시키지 않도록 합니다.
채팅 전용 ECS 서비스
연결 자체는 길게 유지하되, 정기적으로 heartbeat (하트비트)를 계속 보냄으로써 idle timeout (유휴 타임아웃)을 회피합니다.
장애 분리와 오토 스케일링 조건의 차이
이 부분은 이해하기 쉬우므로 함께 다루겠습니다.
chat (채팅)이 고부하 상태가 되었다고 해서 API에 영향을 주고 싶지 않기 때문입니다.
chat (채팅)이 고부하가 되면 chat (채팅)에만 국한하여 auto scaling (오토 스케일링) 하는 것이 바람직한 형태입니다.
chat (채팅)이 액세스를 다 처리하지 못하더라도 API는 정상적으로 가동되기를 원하기 때문에, 이들을 완전히 분리하고자 Service (서비스) 단위로 분리하는 판단을 내렸습니다.
배포를 완전히 분리하고 싶음
chat (채팅)은 사용자의 입력값 범위가 매우 넓습니다.
따라서 예상치 못한 결함이 발생했을 때, 기존 API는 가능한 한 건드리지 않고 chat (채팅) 쪽만 deploy (배포)를 분리하고 싶다는 니즈가 있었습니다.
현재는 β (베타) 버전이라 이러한 니즈가 높다는 점도 있지만, 향후 AI가 발전함에 따라 interactive (대화형) 한 chat (채팅) 수요는 어느 정도 늘어날 것입니다.
이를 고려하여 별도의 ECS service (ECS 서비스)로 분리하는 판단을 내렸습니다.
2. SSE인가 WebSocket (웹소켓)인가
SSE를 선정한 이유는 단순합니다. 이번 chat (채팅)은 LLM (거대언어모델)으로부터의 streaming (스트리밍) 용도가 강하기 때문입니다.
사용자와 AI 간의 양방향 실시간 통신은 필요하지 않습니다.
하물며 하나의 통신으로 주고받을 필요, 즉 양방향 커넥션 (connection)을 유지할 필요도 없습니다.
AI로부터의 응답을 streaming (스트리밍) 하여 사용자에게 반환하는 측면이 강하기 때문에 SSE를 선정했습니다.
Pinecone (파인콘)인가 OpenAI Vector Store (OpenAI 벡터 스토어)인가
OpenAI Vector Store (OpenAI 벡터 스토어)를 채택했습니다.
vector DB (벡터 데이터베이스)의 tuning (튜닝)에 관한 유지보수 및 운용을 하고 싶지 않았던 점과, 다소 정성적인 이야기입니다만, OpenAI라는 거인의 어깨에 올라타 있으면 이 부분은 언젠가 최적화될 것이라고 판단했기 때문입니다.
Pinecone (파인콘)은 Vector DB (벡터 데이터베이스)로서의 검색성은 높게 평가합니다.
하지만 이번의 단순한 문서 검색에서는 Pinecone (파인콘)의 기동성을 활용하거나 유지보수 운용에 올리는 이점을 고려했을 때, 수지타산이 맞지 않는다고 판단했기 때문입니다.
탐색 로직과 응답 제어
탐색 로직의 전체 구성은 다음과 같습니다.
mermaid (머메이드)는 이쪽입니다.
전체를 보면 복잡해 보이지만, 하나씩 풀어보면 그렇지 않습니다.
먼저 사용자의 질문에 대해 검색 표현을 만드는 단계입니다. 의역이나 구어체 표현, 대화 이력 등을 통해 검색 쿼리 (query) 후보를 만듭니다.
그리고 정의된 최대 쿼리 수로 압축하여 주요 쿼리로 검색을 수행합니다.
정의된 최대 병렬도에 따라 OpenAI Vector Store (OpenAI 벡터 스토어)를 탐색합니다.
검색 결과가 중복되는 경우, 문자열 기반으로 rerank (리랭크) 하도록 구성했습니다. 히트 (hit) 수가 충분하지 않으면 남은 검색 쿼리를 실행합니다.
여기까지 해도 충분한 결과를 얻지 못한 경우에 한해 fallback (폴백) 검색을 수행하여, 넓게 후보를 훑어내는 식의 처리를 하고 있습니다.
검색 결과를 확정한 단계에서 문자열 기반으로 순위를 매깁니다. 요소는 다음과 같습니다.
1. 본문 일치
2. 파일명 일치
3. 정규화 일치
...
그 후, LLM (거대언어모델)에 의한 rerank (리랭크)가 필요한지 여부를 판정합니다.
후보가 매우 강력하거나, 반대로 후보가 적거나, 혹은 1위가 명확하게 강력한 경우에는 LLM의 rerank (리랭크)를 스킵 (skip) 합니다.
판단이 미묘한 경우에만 LLM으로 rerank (리랭크)를 수행합니다.
판단의 신뢰 점수 (confidence score)를 산출하고 있기 때문에, 임계값 (threshold)을 하회하는 경우에 한해서 다음 단계로 진행하는 흐름입니다.
최종적으로 순위를 확정하고, 답변 생성용 컨텍스트 (context)로 정비하도록 구성하고 있습니다.
코드베이스와 knowledge · OpenAI Vector Store의 동기화
코드베이스와 knowledge, 그리고 knowledge와 OpenAI Vector Store를 동기화하는 방식의 CD (지속적 배포)를 구축하고 있습니다.
API는 당연히 사양 (specification)을 포함하고 있지만, API의 변경에는 사용자에게 보여주고 싶지 않은 사양도 포함되어 있어, 오히려 프론트엔드 쪽이 사양 그 자체에 더 가깝습니다.
그렇기 때문에 프론트엔드는 프론트엔드만의 CI/CD를 통해 knowledge와 동기화하는 메커니즘을 구축했습니다.
knowledge는 OpenAI Vector Store를 이용하고 있으며, knowledge의 CI/CD를 통해 OpenAI Vector Store로의 반영을 수행합니다.
프론트엔드 브랜치를 release 브랜치에 머지 (merge)하는 타이밍에, knowledge 또한 그에 대응하는 release 브랜치에 대응하는 PR (Pull Request)을 생성하여, 대응 환경으로 승격 및 반영합니다.
이를 통해 front의 코드베이스 변경을 knowledge에 반영하고, 그 변경을 다시 OpenAI Vector Store에 반영하는 구성을 취하고 있습니다.
front의 업데이트가 우선시되므로, front의 feature 브랜치나 release 브랜치에 변경이 있으면 knowledge 리포지토리 (repository)가 OpenAI Vector Store와 동기화되는 형태를 취합니다.
(이 부분의 lock (잠금) 처리는 어떻게 되어 있는가 하는 이야기도 있습니다만, 수요가 있다면 해설하겠습니다. 현재는 정보 과다로 인해 생략합니다.)
knowledge의 관리 단위
1 topic = 1 파일로 구성하고 있습니다.
- 화면 단위의 knowledge
- 공통 사양 단위의 knowledge
각 파일은 yaml formatter (yaml 포맷터)를 탑재하고 있습니다.
일부 발췌합니다.
type: screen | common
id: 안정적인 식별자
category: 대분류
...
sync (동기화) 계열의 필드는 검색 쿼리 (query)에 직접적으로 작용하기보다는, 검색되는 knowledge를 코드 변경에 추종시키기 위한 메커니즘입니다.
category, route, tags, questions, related를 검색성에 강력하게 작용하는 필드로 채택하고 있습니다.
last_reviewed_at 에 대해서는, knowledge 또는 front 리포지토리에 변경이 있을 때 날짜만 업데이트하는 동작을 허용하고 있습니다.
이는 변경이 발생한 시점에, 최신 지식의 신선도가 유지되고 있다는 보증을 기록하기 위해서입니다.
Devin을 사용한 knowledge의 업데이트
AI만으로 해결되지 않고 조사가 필요해진 경우에는 사내 Slack (슬랙)으로 알림이 전송됩니다.
그곳에서 CS 담당자와 개발자가 필요에 따라 대화를 나눕니다.
Devin은 해당 채널에 들어와 있기 때문에, 문의가 해결된 후에 스레드 (thread)를 통해 Devin을 참여시킵니다.
Devin은 해당 스레드 내용을 해석하여, 범용적인 지식으로서 리포지토리에 반영 처리를 수행합니다.
과제와 요약
이번에 CS의 AI화를 통해 어느 정도 정량적인 성과를 거둘 수 있었습니다.
하지만 AI에게 db (데이터베이스)를 직접 다루게 하지 않는 이상, 역시 개별적이고 구체적인 조사에 관한 제약이 발생합니다.
현시점에서는 이는 CS 담당자와 개발 측이 커뮤니케이션을 통해 해결할 수밖에 없습니다.
AI에게 데이터베이스를 다루게 할 것인가에 대한 판단에는 다양한 고려 요소가 있습니다.
다만, 기존의 목적은 CS 담당자의 부하를 줄이는 것입니다.
앞으로는 비용 대비 효과 (ROI)의 문제입니다.
- 현시점에서 부하 경감이 충분하다면, 구현 단계로 옮길 필요는 없다
- 부하 경감이 불충분하다면, 다음 대책을 생각할 필요가 있다
AI에게 db를 다루게 하기 위해서는, 각종 가드레일 (guardrail) 및 권한 정비가 필요해집니다.
이러한 점들을 바탕으로 지속적으로 관찰하면서 다음 액션을 모색하는 것이 현시점에서의 최적해라고 생각합니다.
이번 기사가 유사한 과제를 안고 있는 기업에 도움이 되기를 바랍니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기