pile·
AI / ML·우아한형제들우아한형제들·

우아한공방의 새로운 동료, 시스템 맥락을 가진 챗봇서비스 개발기(feat. RAG)

디자인시스템 '우아한공방'의 문서와 코드베이스가 커지면서 같은 기본 질문이 Slack 서포트 채널에 반복해 들어왔다. MCP만으로는 비개발 직군의 진입 장벽이 높고 응답 품질·검색 범위·권한을 서비스 요구에 맞게 통제하기 어려웠다. Bedrock Knowledge Bases와 OpenSearch Serverless로 RAG 챗봇을 만들고, Retrieval 정확도와 Guardrail이 스트리밍을 끊는 문제를 풀어낸 기록이다.

핵심 포인트
  • 벡터 저장소로 AOSS를 고른 이유는 메타데이터 기반 필터링으로 컴포넌트 단위 검색 범위를 제어하기 위해서다.
  • "버튼" 질문에 IconButton까지 딸려오는 문제를 LLM으로 컴포넌트명을 추출한 뒤 메타데이터로 거르는 2단계 구조로 해결했다.
  • 메타데이터를 `|FilledButton|`처럼 구분자로 감싸 Button과 IconButton이 부분 일치로 함께 잡히는 것을 막았다.
  • Guardrail의 Output 검사가 정상 응답까지 차단하고 SSE 스트리밍을 끊어, Input만 검사하고 async 모드로 바꿨다.
  • 같은 RAG API를 MCP에도 연결해 웹 챗봇과 MCP가 동일한 Retrieval 결과와 정책을 공유하게 했다.
상세 정리
  • 문제의 성격: 단순 검색이 아니라 설계 의도와 컴포넌트 히스토리, 가이드와 코드베이스의 맥락까지 이해한 답변이 필요했다. "필요한 정보를 얼마나 빨리 찾는가" 자체가 디자인시스템의 사용자 경험이라는 인식이 출발점이다.
  • 스택 구성: RAG는 Bedrock Knowledge Bases, 벡터 저장소는 Amazon OpenSearch Serverless, 채팅 히스토리는 DynamoDB, 체인 구성은 LangChain으로 잡았다.
  • 응답 전달: 질문이 오면 문서를 검색하고 그 문맥과 함께 LLM에 넘긴 뒤, 결과를 SSE로 흘려보내 `data: {...}`와 `[DONE]`으로 종료를 알린다.
  • 시스템 프롬프트: 패키지 구조와 주요 컴포넌트 카테고리, 핵심 규칙, 답변 규칙을 주입해 응답 방향을 고정했다.
  • Retrieval 1차 문제: 질문을 그대로 임베딩해 검색하면 관련 없는 컴포넌트 문서가 섞여 들어왔다. Button을 물었는데 IconButton 문서가 함께 잡히는 식이다.
  • 컴포넌트 추출 단계: LLM에 질문을 먼저 넣어 관련 컴포넌트 이름 배열을 뽑고, 그 결과로 검색 범위를 좁혔다. 추가 LLM 호출이 붙어 레이턴시가 늘어나는 것이 대가다.
  • 구분자 트릭: 메타데이터를 `{"components": "|FilledButton|"}` 형태로 저장해 부분 문자열 매칭 사고를 막았다.
  • 체인 구성: RunnableSequence로 질문 분석 → 문서 검색 → 프롬프트 구성 → 응답 생성을 이어 붙였다.
  • topK 튜닝: 낮으면 토큰은 줄지만 맥락이 부족하고, 높으면 무관한 문서와 비용이 는다. 특정 컴포넌트 사용법은 낮게, 정책·히스토리 질문은 높게 질문 유형별로 동적 조정했다.
  • Guardrail 도입 배경: 개인 평가나 일반 상식처럼 의도와 무관한 질문이 들어오기 시작했다. 기본 필터에 더해 개인 관련 질의와 디자인시스템 무관 질의를 차단하는 정책을 추가했다.
  • Guardrail 부작용 ①: Output 검사가 켜지자 "이 이슈는 OO가 해결했어요"처럼 문서 맥락을 설명하는 정상 응답까지 개인 정보로 오판해 차단했다.
  • Guardrail 부작용 ②: Output 검사가 응답을 누적한 뒤 전달하는 방식이라 SSE 스트리밍이 끊겼다.
  • 해결 조합: Output 검사 정책을 끄고 Input만 검사하도록 바꾼 뒤, streamProcessingMode를 async로 켜고, LangChain의 ChatBedrockConverse 대신 AWS SDK의 ConverseStreamCommand를 직접 호출했다.
  • Storybook 통합 문제: Composition으로 여러 스토리북을 한 URL에 올리면 iframe이 둘로 갈려 챗봇이 한쪽에만 보였다. preview.tsx에서 최상단 document에 숨김 div를 만들고 createRoot로 위젯을 주입해 모든 스토리에서 일관되게 뜨게 했다.
  • 패키지화: 챗봇을 독립 npm 패키지로 분리해 비즈니스 로직과 UI를 갈랐다. 다른 팀도 자기 도메인 지식으로 같은 구조의 AI 서비스를 만들 수 있게 하는 것이 목적이다.
  • 클라이언트 스트리밍 처리: ReadableStream 리더로 받은 청크를 버퍼에 모아 개행 단위로 잘라내고, 마지막 미완성 조각을 버퍼에 남겨 다음 청크와 이어 붙이는 방식으로 글자 단위 타이핑을 구현했다.
  • 성과의 성격: 정량 수치는 공개하지 않았고, 반복 문의의 상당 부분을 챗봇이 흡수해 개발자가 시스템 설계에 집중하게 됐다는 정성적 정리에 그친다.
우아한형제들
우아한형제들 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. AI / ML·cloudflare-blogCloudflare Blog·

    AI 에이전트에는 컨테이너보다 경량 컴퓨트가 — @cloudflare/computer 소개

    Cloudflare가 AI 에이전트 런타임 패키지 @cloudflare/computer 얼리 프리뷰를 공개했다. 에이전트마다 컨테이너를 할당하는 방식이 수십억 동시 에이전트 규모로 확장되지 않는 문제를 해결하기 위해, 경량 isolate와 풀 Linux 컨테이너를 작업 복잡도에 따라 동적으로 선택하는 하이브리드 아키텍처를 제안한다.

    #agent-engineering#serverless#cloudflare-workers+2
  2. AI / ML·cloudflare-blogCloudflare Blog·

    더 작고 빠르고 안전하게: Kimi와 GLM 대규모 서빙 최적화

    Cloudflare Workers AI가 Moonshot의 Kimi K-시리즈와 Z.ai의 GLM처럼 메모리 제약이 큰 대형 MoE(Mixture-of-Experts) 모델을 효율적으로 서빙하기 위해 적용한 세 가지 최적화 기법을 다룬다. KV 캐시 양자화, 모델 가중치 압축, 공유 KV 캐시 무결성 검사를 계층적으로 적용해 처리량을 크게 높이고 비용을 낮췄다.

    #llm#inference-optimization#kv-cache+2