페이증권의 업무도우미 AI봇을 소개합니다! 근데 이제 춘식이를 곁들인

페이증권의 업무도우미 AI봇을 소개합니다! 근데 이제 춘식이를 곁들인

시작하며

안녕하세요. 카카오페이증권 DevOps 팀의 테라입니다.

본 글은 사내 지식저장소를 구축하기 위해 Amazon Bedrock과 슬랙봇을 통합하고 고도화한 경험을 공유하기 위해 작성되었습니다.

프로젝트 탄생 배경

본 프로젝트는 ‘siri’ 라는 이름으로 (Apple의 siri가 모티브인 것 맞습니다.), 카카오페이증권의 산재된 내부 정보들을 검색증강생성(RAG)으로 구현하여 LLM을 통해 쉽게 조회하고자 기획되었습니다. 또한 보안상 ChatGPT 등의 외부 AI 도구들을 사내에서 사용할 수 없었는데요, 그 대체를 만들고 싶기도 했습니다.

그러다가 좀 더 카카오스럽고 귀엽게, 춘식이를 곁들여 ‘춘시리’ 라는 이름으로 발전하게 되었습니다.

춘식이
춘식이

춘시리에게 원하는 것

초기 목표는 다음과 같았습니다.

  • AI 봇에게 내부 문서(Confluence)를 학습시킨다.
  • AI 봇은 학습된 내부 정보를 바탕으로 내 질문에 대답해 준다.
  • 필요한 경우, AI 봇은 내부 정보가 아닌 일반 LLM 답변도 제공한다. (ChatGPT 대체)

추가로 다음 기능까지 구현하고자 하였습니다.

  • 수동으로 추가 정보를 학습시키기
  • 깃헙 PR에서 코드 리뷰해 주기
  • 지라 티켓을 분석하고 요약해 주간 리포트, 장애 리포트 등을 자동으로 작성해 주기

또한 카카오 사내 커뮤니케이션 툴인 아지트(Agit)에 저장된 내용들도 학습하고 취합할 수 있기를 기대하였습니다.

춘시리 아키텍처

저희는 그전까지 AI를 활용한 개발 경험이 없기에, LLM, RAG, Embedding 개념 등을 처음으로 공부해 가며 시작하였습니다. 따라서 본격적으로 춘시리 아키텍처를 설명하기 전에, 춘시리를 구성하는 AI 관련 컴포넌트들에 대해 가볍게 설명하고 넘어가도록 하겠습니다.

LLM과 Amazon Bedrock

먼저 LM(Language Model, 언어 모델) 이란, 인간의 언어를 이해하고 생성하도록 훈련된 인공지능 모델입니다. 그리고 LLM(Large Language Model, 거대 언어 모델) 은 방대한 양의 데이터로 사전 학습된 초대형 딥러닝 언어 모델입니다.

설명만 들어도 느끼셨겠지만, LLM을 직접 개발하기 위해서는 상당히 많은 자원(인적 자원, 컴퓨팅 자원 등)과 전문 지식이 필요합니다. 저희는 데이터 사이언티스트가 아니고, AI 관련 지식이 부족하기 때문에 외부 상용 LLM 모델을 사용해야 합니다. 상용 LLM 모델의 대표적인 예로는 OpenAI의 GPT 시리즈와 Meta의 Llama 시리즈가 있습니다.

Amazon Bedrock(이하 베드락) 은 다양한 LLM 모델을 호스팅해주고, 쉽게 다른 서비스와 연동할 수 있는 API를 지원하는 AWS의 매니지드 서비스입니다. 즉, 베드락을 사용하는 경우 AWS API를 사용해 다양한 외부 LLM을 사용할 수 있으며, 모델을 직접 호스팅하고 관리할 필요가 없습니다. 게다가 AWS를 이미 사용하고 있는 경우, AWS 비용으로 같이 청구되므로 추가적인 빌링 프로세스가 생길 필요 또한 없습니다.

🔒 베드락의 경우, 입력 프롬프트와 답변 내역을 저장하거나 기록하지 않기 때문에 보안상 문제 되지 않았습니다.

저희는 내부적으로 이미 AWS를 사용하고 있고, AI관련 지식도 부족하므로 매니지드 서비스인 베드락을 통해 다양한 LLM 모델을 사용해 보기로 결정하였습니다. 다만 아직 서울 리전에서는 베드락으로 사용할 수 있는 LLM 모델이 한정되어 있어서, 현재(2025.01.24) 기준 서울 리전 베드락에서 유일하게 사용할 수 있는 Anthropic의 Claude 모델 시리즈만을 사용해 보았습니다.

RAG (Retrieval-Augmented Generation, 검색증강생성)

RAG이란 LLM에 외부 지식 소스를 결합해 응답을 생성하는 방식을 뜻합니다. 따라서 일반적인 LLM 답변이 아니라, 특정 정보들(사내 문서 등)을 기반으로 한 답변을 원할 때 RAG 프로세스를 구현할 필요가 있습니다.

RAG을 구현하기 위해서는 LLM 모델, 텍스트 임베딩 모델, 벡터 DB가 필요합니다. LLM 외 나머지 두 요소는 다음 순서에서 설명하도록 하겠습니다. 이렇게 3가지 요소가 구성된 경우, RAG 프로세스의 작동 방식은 다음과 같습니다.

  1. 사용자의 질문을 입력받습니다.
  2. 입력받은 문자열을 임베딩 모델을 통해 벡터로 변환합니다.
  3. 변환된 벡터 데이터를 벡터 DB에서 조회합니다.
  4. 최초 질문과 검색된 정보가 LLM의 입력 데이터가 되어, 해당 데이터를 기반으로 LLM 모델이 답변합니다.

임베딩(Embedding)과 벡터(Vector)

임베딩은 텍스트(자연어)를 실수 형태의 벡터로 변환하는 과정, 또는 변환된 결과물(벡터 리스트) 모두를 의미합니다. 변환 시, 데이터 간 유사도가 기준이 됩니다. 예를 들어, ‘딸기’와 ‘사과’는 둘 다 과일이므로 유사한 벡터를 가지게 될 것입니다. 따라서 임베딩 된 벡터 간 거리를 계산하여 데이터 간 유사도를 측정할 수 있게 됩니다.

임베딩을 진행하기 위해서는 임베딩 모델이 필요하며, LLM 모델과 마찬가지로 베드락을 통해 외부 임베딩 모델을 사용하기로 하였습니다. 저희는 현재 기준 서울 리전 베드락에서 유일하게 지원되는 임베딩 모델인 아마존의 Titan 모델을 사용하였습니다.

벡터 DB(Vector Database)와 PGVector

벡터 DB는 벡터 형식의 데이터를 저장, 쿼리하고 유사성을 비교하기에 특화된 데이터베이스입니다. 일반 데이터베이스는 SQL 기반으로 정확히 일치하는 데이터를 검색한다면, 벡터DB는 유사성 검색을 통해 가장 비슷한 데이터를 찾습니다.

벡터 DB용 솔루션은 다양하지만, 저희는 가장 접근성이 좋은 PGVector를 골랐습니다. PGVector는 PostgreSQL용 확장 플러그인으로, 기존 PostgreSQL 생태계와 완전히 호환됩니다. 쉽고 빠르게 구축하기로는 매우 적합했으니, 추후 벡터DB를 선택할 때 참고하시기 바랍니다.

최종 아키텍처

춘시리 아키텍처
춘시리 아키텍처

🔒 춘시리를 구성하는 모든 컴포넌트는 연구개발망에 위치합니다.

춘시리는 두 파트로 나뉩니다.

  • 전처리 파트: 1시간마다 사내 문서를 스캔하고, 업데이트분을 벡터 DB에 저장합니다.

    1. 사내 문서(Confluence, 이하 위키)의 데이터를 가져와 데이터 청크를 생성합니다.
    2. S3에 백업된 데이터와 비교하여 업데이트된 부분만을 추출합니다.
    3. 베드락을 통해 임베딩 후 벡터DB에 업데이트합니다.
    4. 업데이트된 부분을 S3에 백업합니다. (최신화)
  • 서비스 파트: end-user(카카오페이증권 크루들)로부터 요청을 받고 처리합니다.

    1. 슬랙, 아지트, 깃헙으로부터 요청을 받습니다.
    2. (optional) RAG이 필요한 경우, 요청 내용을 임베딩 후 벡터 DB로부터 유사도 검색을 진행합니다.
    3. 유사도 검색 결과(optional)와 질문 내용을 합쳐 베드락 LLM에 질의합니다.
    4. LLM 답변을 요청자에게 전달합니다.

서비스 파트에서는 소소하지만 다양한 기능을 지원하고 있습니다. 플랫폼별 주요 기능들을 소개해 드린 후, 아키텍처를 구현한 코드에 관해 얘기하도록 하겠습니다.

슬랙춘시리 : 업무 도우미

슬랙에 서식하는 춘시리는 카카오페이증권의 업무 도우미입니다. 현재 제공되는 기능은 다음과 같습니다.

  1. 카카오페이증권 위키를 학습하여 대답해 주는 위키질문 기능
  2. 새로운 정보를 학습시키는 학습하기 기능
  3. ChatGPT를 대체하는 일반질문 기능
  4. 긴 스레드를 요약해 주는 요약하기 기능

위키질문은 최초 목표였던 위키 검색 기능입니다. 위키에 검색 기능이 있긴 하지만, 현재의 나에게 꼭 필요한 문서를 찾는 일은 사실 그렇게 쉽지 않습니다. 예를 들어, 신규 입사자가 VDI를 어디서 다운받을지 몰라 위키에 검색을 해 본다고 가정해 보겠습니다.

문서 시스템에 VDI 검색해 보기
문서 시스템에 VDI 검색해 보기

막상 VDI의 사용자 기준/어드민 기준 문서가 모두 섞여 있기 때문에 현재 나에게 필요한 문서를 찾는 데 시간이 걸리게 됩니다. 그리고 신규 입사자라면 그 시간이 훨씬 길어질 것입니다.

이번엔 춘시리에게 물어보겠습니다.

슬랙춘시리에게 VDI를 물어보기
슬랙춘시리에게 VDI를 물어보기

정확하게 내가 원하는 정보를 가져오는 것을 확인할 수 있습니다. 👍

그러나 사실 당연하게도, 사내의 모든 정보가 문서화 되어있진 않습니다. 따라서 아직 문서화 되어 있지 않은 정보를 춘시리에게 빠르게 학습시키기 위해, 학습하기 기능을 만들었습니다. 한 번, 카카오페이증권에서 시크릿 저장소로 사용하고 있는 Vault에 대한 권한을 얻기 위한 방법을 춘시리에게 문의해 보겠습니다.

슬랙춘시리에게 Vault 물어보기
슬랙춘시리에게 Vault 물어보기

맨 위의 링크는 사실 잘못된(예전에 사용하던) 정보입니다. 아무래도 문서 업데이트가 안 되어있을 수 있죠. 그렇다면 한 번 잘못된 정보를 정정해 보겠습니다.

슬랙춘시리에게 Vault 학습시키기
슬랙춘시리에게 Vault 학습시키기

동일한 질문을 다시 해보겠습니다.

슬랙춘시리에게 다시 Vault 물어보기
슬랙춘시리에게 다시 Vault 물어보기

위에서 학습한 내용을 그대로 흡수해 대답해 주는 춘시리를 볼 수 있습니다.

자세히 보신 분들은 아시겠지만, 질문 앞에 ‘위키/’ 또는 ‘학습/’ 이라는 커맨드 단어를 넣어줌으로써 특정 기능을 수행시키고 있습니다. 그리고 질문 앞에 아무 커맨드도 없는 경우엔 일반질문이 수행됩니다. 최초 목표가 위키질문이었는데 어째서 일반질문이 기본값으로 되었을까요?

춘시리 1차 공지
춘시리 1차 공지

위 캡쳐는 춘시리 최초 알파 오픈 시의 공지입니다. 알파 오픈 때만 해도 위키질문 기능만을 염두에 두고 있었으며, 추가기능으로 일반질문 사용할 수 있게 제공해 드렸었습니다. 그러나 막상 사용 후기들을 여쭤보니, 일반질문(당시엔 llm질문이라고 칭했습니다.)을 훨씬 많이, 잘 사용하고 있기 때문에 해당 기능이 기본값이면 좋겠다는 의견을 많이 듣게 되었습니다.

앞서 말했듯, 사내 보안 규정상 지금까지 ChatGPT 등의 외부 AI 도구를 사용하지 못했기 때문에 춘시리가 개발자분들의 업무 환경에 한 줄기 빛이 되었던 것입니다. 실제로 춘시리의 일반질문 기능을 통해 업무 도중 다양한 문제를 빠르게 해결할 수 있었습니다.

춘시리 일반질문예시
춘시리 일반질문예시

질문을 조금 대충 하더라도 적절한 답변을 찾아주는 춘시리입니다.

이 외에도, 길고 긴 논의 스레드를 중요 키워드만 발라내서 요약해 주는 요약하기 기능이 존재합니다. (의외로 반응이 가장 핫했습니다.) 예를 들어, 논의하느라 댓글이 100개가 넘어간 스레드가 생겼다고 가정해 보겠습니다. 벌써부터 머리가 아득해지는 것 같지만…

춘시리와 함께라면 더 이상 두렵지 않습니다. 춘시리에게 요약을 부탁해 보겠습니다.

춘시리의 스레드 요약
춘시리의 스레드 요약

주요 내용만 깔끔하게 요약된 것을 확인할 수 있습니다. 만약 다른 크루들이 활발하게 대화하고 있는 스레드에 직접적으로 춘시리 요약 메시지를 남기기 민망하다면, 원격으로도 요약 요청이 가능합니다.

원격 스레드 요약
원격 스레드 요약

요약 커맨드 뒤에 특정 스레드의 링크를 입력하면, 해당 스레드의 내용을 읽어 요약해 줍니다.

🔒 보안을 위해 요청한 유저가 해당 채널에 속해있는지를 검사한 후 요약을 진행합니다.

아지트춘시리 : 세미 데브옵스 엔지니어

아지트의 춘시리는 세미 데브옵스 엔지니어입니다. 데브옵스 팀에 문의가 들어오면 위키 내용을 기반으로 1차 답변을 해줍니다. 실제로 유용했던 사례를 몇 가지 보여드리도록 하겠습니다.

첫 번째 사례는, 한 개발자분이 신규 카프카 컨슈머를 개발하던 중 카프카 클라이언트 에러가 발생하여 제보를 주셨던 경우입니다. 문의 내용에는 카프카 정보와 사용 코드, 그리고 에러 메시지를 같이 첨부해 주셨습니다.

아지트 1차 대응: Kafka
아지트 1차 대응: Kafka

춘시리의 답변입니다. 실제로 인증 메커니즘이 활성화되지 않은 카프카에 인증을 시도했던 사례 였고, 춘시리 덕분에 더 빠르게 문제를 파악할 수 있었습니다.

두 번째 사례로는, 카카오 클라우드 인프라 리소스 확인 문의였습니다. 카카오 클라우드에 구성된 특정 서브넷과 인스턴스가 미사용 중인 것 같은데, 확인해달라는 문의가 들어왔습니다.

아지트 1차대응: 인프라 자원 확인
아지트 1차대응: 인프라 자원 확인

내부 문서를 검색할 수 있는 춘시리는 해당 자원들이 테스트용 자원이라는 것을 곧바로 파악해서 알려주었고, 그 덕에 빠르게 조치할 수 있었습니다.

깃헙춘시리 : PR 리뷰봇

동료가 PR을 올렸고, 내가 리뷰해 줘야 하는데, PR diff가 꽤 크다고 가정해 보겠습니다. 이런 경우는 동료의 코드를 파악하는 데 상당한 시간을 소요하게 됩니다. 여기서, 슬랙춘시리의 요약 기능처럼 코드 변경점을 요약해 주고, 간단한 리뷰라도 춘시리가 해준다면 얼마나 좋을까요?

해서 만들어 보았습니다. 춘시리의 리뷰 몇 가지를 소개해 드리겠습니다.

깃헙 코드리뷰1
깃헙 코드리뷰1

이는 실제 춘시리 코드에 대한 리뷰입니다. 어떤 파일의 몇 번째 라인에서 어떤 변경점이 일어났는지를 요약해 주며, 마무리로 춘시리의 의견까지 남겨져 있습니다.

이번엔 지적을 받아보기 위해 일부러 메모리 누수가 일어날 코드를 짜서 올려보았습니다.

깃헙 코드리뷰2
깃헙 코드리뷰2

잘 혼내주는 것(?)을 확인할 수 있습니다.

현재는 1. PR이 새로 열리거나 2. 열린 PR에 새 커밋이 올라오거나 3. 닫힌 PR이 다시 열리는 이벤트마다 춘시리 리뷰가 호출되고 있습니다. 따라서 한 PR 내에 춘시리 리뷰가 상당히 많이 달릴 수 있는데요, PR이 더러워지는 것을 방지하기 위해 춘시리의 이전 리뷰는 Minimize 처리를 해주고 있습니다.

깃헙 코드리뷰3
깃헙 코드리뷰3

📌 뒤에서 설명하겠지만, 저희는 베드락에 가드레일을 적용해 사용하고 있습니다. 그리고 춘시리 리뷰는 PR의 diff를 모두 긁어와서 베드락에 prompt에 같이 넘겨주는 방식으로 진행됩니다.

-> PR의 diff가 너무 큰 경우, 아래 에러가 가드레일단 에서 발생했었습니다.

An error occurred (ThrottlingException) when calling the InvokeModel operation (reached max retries: 4): Too many requests sent to ApplyGuardrail: On-demand ApplyGuardrail content filter policy text units per second limit exceeded.

이는 On-demand ApplyGuardrail Content filter policy text units per second 라는 이름의 AWS Quota에 걸린 것으로, 해당 Quota를 조절하거나 Stream API를 사용하는 방법으로 해결하실 수 있습니다.

추가 기능 : 주간보고 자동화

으레 많은 회사에서 그렇듯, 카카오페이증권에서도 매주 주간 보고가 진행됩니다. 이전에는 매주 크루들이 직접 티켓을 정리하고 리더들이 다시 요약해야 했었습니다.

그러나 이젠 춘시리가 업무 티켓들을 취합, 분석 후 자동으로 요약을 해줄 수 있게 되었습니다. 먼저 크루별 티켓은 다음 사진과 같이, 슬랙에서 각 크루들이 직접 상호작용하며 정리할 수 있으며 춘시리의 요약이 자동으로 생성됩니다.

주간 보고 자동화1
주간 보고 자동화1

팀 내의 모든 크루들이 티켓 정리를 마무리하면, 팀 전체의 업무 내역 또한 요약됩니다. 이때, 각 리더는 티켓 정리 완료. 아지트로 발사! 🚀 버튼을 통해 요약된 내용을 손쉽게 아지트로 포스팅할 수 있게 됩니다.

주간 보고 자동화2
주간 보고 자동화2

이를 통해 주간 보고 작성 과정이 훨씬 효율적으로 개선되어, 많은 크루들의 시간과 노력을 절감할 수 있게 되었습니다.

구현한 코드

이제 이 기능들을 구현한 코드에 관해서도 이야기해 보겠습니다. 우선 언어로는, AI 관련 라이브러리 생태계와 레퍼런스가 풍부하고 저희에게도 익숙한 Python을 선택하였습니다. 여기에 AWS의 서비스인 베드락을 사용하기 때문에 AWS Python SDK인 boto3를 사용하게 되었습니다.

boto3에서 제공하는 베드락 모델 호출 함수로는 invoke_modelconverse 가 있습니다. (invoke_model_with_response_stream, converse_stream은 앞 함수들의 스트리밍 처리 버전일 뿐이므로 생략하도록 하겠습니다.)

invoke_model 함수는 이름 그대로, 베드락에서 제공하는 모델을 호출 하는 함수입니다. 따라서 모든 작업은 invoke_model 함수만으로도 구현이 가능하며, 가장 기본이 되는 함수라고 볼 수 있습니다. 그러나, 이 함수를 사용하는 경우 사용하려는 모델에 따라 API호출 방식이 달라집니다.

예를 들어, 저희가 사용한 Anthropic의 Claude 모델의 경우는 다음과 같은 형식을 가지고 호출할 수 있습니다. (공식 문서 참조)

import boto3, json

client = boto3.client("bedrock-runtime", region_name="ap-northeast-2")

model_id = "anthropic.claude-3-haiku-20240307-v1:0"

user_message = "주식 잘하는 법을 알려줘"
prompt = "네 이름은 춘시리야. 묻는 말에 친절하고 예의 있게 답해줘.\n\n" + user_message

native_request = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 512,
    "temperature": 0.5,
    "messages": [
        {
            "role": "user",
            "content": [{"type": "text", "text": prompt}],
        }
    ],
}

response = client.invoke_model(modelId=model_id, body=json.dumps(native_request))

Amazon의 Titan 모델을 호출하기 위해선 다음과 같은 형식을 사용해야 합니다. (공식 문서 참조)

import boto3, json

# 주의 - Titan 텍스트 모델은 아직 서울 리전에서 사용할 수 없으며, 단순 예시 코드입니다.
client = boto3.client("bedrock-runtime", region_name="ap-northeast-2")

model_id = "amazon.titan-text-premier-v1:0"

user_message = "주식 잘하는 법을 알려줘"
prompt = "네 이름은 춘시리야. 묻는 말에 친절하고 예의 있게 답해줘.\n\n" + user_message

native_request = {
    "inputText": prompt,
    "textGenerationConfig": {
        "maxTokenCount": 512,
        "temperature": 0.5,
    },
}

response = client.invoke_model(modelId=model_id, body=json.dumps(native_request))

즉, 사용하려는 모델 공급사별 API 호출 방식이 달라지므로 다양한 모델을 사용하기 위해선 다양한 호출 방식을 익혀야 하는데, 상당히 번거롭고 비효율적입니다. 이런 문제를 해소하기 위해 AWS에서는 2024년 5월에 Converse API를 출시하였습니다.

요약하자면, converse 함수는 결국 invoke_model을 한 단계 더 추상화한 함수로, 어떤 공급사의 모델을 사용하더라도 일관된 API를 사용할 수 있게 됩니다.

다시 한번 Claude와 Titan 모델을 호출해 보겠습니다. 이번엔 converse를 사용하도록요! (참조 - Claude / Titan)

import boto3

##### Anthropic의 Claude 모델 사용 예시 #####
client = boto3.client("bedrock-runtime", region_name="ap-northeast-2")

model_id = "anthropic.claude-3-haiku-20240307-v1:0"

user_message = "주식 잘 하는 법을 알려줘"
prompt = "네 이름은 춘시리야. 묻는 말에 친절하고 예의 있게 답해줘."

conversation = [
    {
        "role": "user",
        "content": [{"text": user_message}],
    }
]

response = client.converse(
    modelId=model_id,
    system = [{'text': system_prompt}],
    messages=conversation,
    inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9},
)

response_text = response["output"]["message"]["content"][0]["text"]


##### Amazon의 Titan 모델 사용 예시 #####
client = boto3.client("bedrock-runtime", region_name="ap-northeast-2")

model_id = "amazon.titan-text-premier-v1:0"

user_message = "주식 잘하는 법을 알려줘"
prompt = "네 이름은 춘시리야. 묻는 말에 친절하고 예의 있게 답해줘."

conversation = [
    {
        "role": "user",
        "content": [{"text": user_message}],
    }
]

response = client.converse(
    modelId=model_id,
    system = [{'text': system_prompt}],
    messages=conversation,
    inferenceConfig={"maxTokens": 512, "temperature": 0.5, "topP": 0.9},
)

response_text = response["output"]["message"]["content"][0]["text"]

정확히 동일한 코드에서 model_id 인자값만 변경된 것을 확인할 수 있습니다. 따라서 저희는 권장되기도 하고, 인터페이스가 일관되어 추후 모델 변경에도 용이할 converse를 사용하였습니다.

그러나 여기서 잠깐 ✋!

LangChain이라는, LLM을 쉽게 사용할 수 있게 해주는 오픈 소스 프레임워크가 있습니다. LangChain에서는 다양한 LLM 들을 API를 통해 호출하는 것뿐만 아니라 외부 데이터 또는 타 시스템과의 상호작용하는 기능까지 지원합니다. (즉, RAG에 필요한 임베딩, 벡터 스토어 등을 쉽게 구현할 수 있게 됩니다.)

따라서, RAG을 구현해야 하는 내부 문서 기반 질문을 구현할 땐, 이 LangChain의 ChatBedrockConverse 클래스를 사용하는 것이 더 빠를 것이라 기대하였습니다. langchain의 다양한 모듈들을 활용해 RAG을 구현한 코드는 다음과 같습니다.

import boto3
from langchain_postgres import PGVector
from langchain_aws.embeddings.bedrock import BedrockEmbeddings
from langchain_aws import ChatBedrockConverse
from langchain_core.prompts import PromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

client = boto3.client("bedrock-runtime", region_name="ap-northeast-2")

user_message = "주식 잘하는 법을 알려줘"
prompt_template = """
네 이름은 춘시리야. 묻는 말에 친절하고 예의 있게 답해줘."

<context>
{context}
</context>

<question>
{question}
</question>

답변:
"""
prompt = PromptTemplate.from_template(prompt_template)

# 임베딩 모델 선언
embeddings = BedrockEmbeddings(
    client = client,
    model_id = "amazon.titan-embed-text-v2:0",
)

# 벡터디비 선언 (PGVector)
vectordb = PGVector(
    embeddings = embeddings,
    collection_name = "vector_store",
    connection = f"postgresql://{user}:{passwd}@{host}:{port}/{dbname}",
    use_jsonb = True,
)

# LLM 모델 선언
llm = ChatBedrockConverse(
    model = "anthropic.claude-3-haiku-20240307-v1:0",
    temperature = 0.5,
    max_tokens = 2048,
)

# 검색된 문서들을 하나의 문자열로 합쳐주는 함수
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# RAG 체인 구성
rag_chain = (
    {
        "context": vectordb.as_retriever() | format_docs,
        "question": RunnablePassthrough() # rag_chain.invoke 시 전달받는 문자열을 그대로 question으로 사용
    }
    | prompt
    | llm
    | StrOutputParser()
)

# RAG 호출
response = rag_chain.invoke(user_message)

(위 코드 샘플에서는 생략했지만, 실제로는 프롬프트에 history 태그도 추가하여 위키질문 시 스레드 내 질의응답 내역을 포함하게 하였습니다.)

다시 정리를 해보자면 다음과 같습니다.

  • RAG을 구현해야 하는 내부 문서 질문 기능 (슬랙의 위키질문 + 아지트춘시리) : ChatBedrockConverse 사용
  • 그 외 LLM 기능 : converse 사용

📖 사실 ChatBedrockConverse 만으로 모든 기능을 구현할 수 있겠지만, 개발 당시엔 자세한 차이들을 잘 몰랐어서 😅 ChatBedrockConverse & converse를 분기 처리하여 사용하도록 해두었습니다.

🚨 보안 이슈와 예외 처리 🚨

증권사에서 AI를 도입하며 가장 어려웠던 측면은 아무래도 보안이었습니다. 베드락(또는 AI)을 사용하는 데에 있어 보안 이슈가 없는지, 보안 이슈가 발생할 여지는 없는지를 꼼꼼하게 체크해주신 카카오페이증권의 침해대응팀, 정보보안팀에 우선 감사의 말씀을 전하고 시작하겠습니다. 🙇‍♀️

보안팀에서는 2가지 문제를 체크해 주셨습니다.

  1. 베드락의 입력값 / 출력값에 대한 제어 필요 (민감정보, 개인정보 등의 필터링을 위함)
  2. 내부 문서(위키)에 민감정보, 개인정보 등이 적혀있는 경우 춘시리가 읽지 말아야 함 (예외 처리)

1번의 경우, 베드락의 Guardrails (이하 가드레일) 서비스를 통해 쉽게 해결되었습니다. 보안팀에서 직접 가드레일의 버전을 관리하고, 춘시리에서는 반드시 가드레일을 통해 베드락을 이용하도록 개발하였습니다.

가드레일을 적용한 후 테스트를 위해, 가상의 인물인 ‘김민우’씨의 개인정보를 위키에 작성해 두고 춘시리에게 질의해 보았습니다.

춘시리가 민감 정보를 감지한 경우
춘시리가 민감 정보를 감지한 경우

민감정보가 포함된 답변을 하는 대신, 가드레일에 정의된 블락 메시지를 보내주는 것을 확인할 수 있었습니다.

2번의 경우는 사실 보안 관점뿐 아니라, 필요 없는 정보를 필터링하기 위해서도 이미 고민 중인 문제였습니다. 내부 문서엔 Deprecate 된 이전 정보들이 남아있기도 하고, 개인 스페이스에 잡다한 걸 적어두는 경우도 있기 때문입니다.

-> 예외 처리할 문서에 ‘exclude’ 레이블을 걸면 전처리 파트에서 제외하도록 개발하였습니다.

그러나, 예외 처리가 필요한 모든 문서를 찾아 ‘exclude’ 레이블을 다는 것은 쉽지 않습니다. 따라서 춘시리가 검색한 문서 내부에 민감정보, 개인정보 등이 있는 경우 별도 보안팀 슬랙 채널로 메시지를 보내주도록 설정하였습니다.

춘시리가 민감 정보를 감지한 경우 보안팀이 받는 슬랙
춘시리가 민감 정보를 감지한 경우 보안팀이 받는 슬랙

보안춘시리 모듈은 rag_chainvectordb.as_retriever() | format_docs 단계에서 호출되어야 합니다.

-> format_docs 함수에서 보안춘시리 모듈을 호출하도록 수정하는 방식으로 구현하였습니다.

그리고 혹시나 오해할까 봐 덧붙이자면, 이는 민감정보를 검색한 사람을 잡아내기 위한 것은 절대 아니며, 😅 민감정보가 존재하는 문서를 보안팀이 체크해 예외 처리를 하거나 해당 민감정보를 삭제하는 등의 조치를 빠르게 하기 위한 장치입니다.

이상으로 지금까지 만든 춘시리에 대한 내용을 마치고, 추후 업데이트하려고 생각 중인 기능들을 소개해 드리겠습니다.

내일의 춘시리

앞으로 다음과 같이 춘시리를 발전시키고자 합니다.

  1. LLM / 벡터 DB 다양화
    • 사용할 수 있는 LLM 모델과 벡터 DB를 다양화할 생각입니다.
    • 베드락의 서울 리전 모델 출시를 기다리기보단, Ollama를 내부에 설치하여 다양한 오픈소스 LLM들을 제공할 예정입니다.
    • 베드락의 Knowledge Bases 기능도 활용해 볼 예정입니다.
  2. 개인화
    • 슬랙과 깃헙에서, 각 개인 또는 팀별 원하는 커스텀 조건을 반영할 수 있게끔 발전시키려고 합니다.
    • 여기서 커스텀 조건은, 슬랙 트리거 명령어 / LLM & 벡터 DB 종류 / 깃헙 코드리뷰 트리거 조건 등등이 있습니다.
  3. 챗봇 고급화
    • 다양한 페르소나를 정의하고, Multi Agent 아키텍처를 설계해 보려고 합니다.
    • 또한 일부 전문적인 분야에서는 파인튜닝도 적용해 볼 예정입니다.

다 할 수 있을진 모르겠지만, 춘시리의 발전으로 인해 카카오페이증권 크루들의 업무 생산성이 더 효율화되길 기대합니다.

마치며

입사하고 가장 처음 맡게 된 정식 프로젝트였는데요, 재미도 있고 춘식이도 귀여워서 생각 이상으로 애정을 쏟게 되었습니다. (한동안은 제 자신이 춘시리인 것 같았습니다.)

그리고 재미있었던 이유는, 1. 처음 해보는 거라 신기한데 2. 의외로 쉽기까지 해서 였던 것 같습니다.

어렴풋하게 AI 애플리케이션을 개발하는 것은 전문가들만이 가능할 것이라고 믿어왔는데, 베드락 & langchain과 같이 쉽게 추상화된 인터페이스들이 많아 비전문가도 충분히 개발할 수 있다는 사실을 몸소 체험하게 되었습니다. 춘시리가 단순한 기능만을 제공하기 때문도 있겠지만, 실제로 전체 개발 기간 동안 LLM 모듈 쪽 작업은 그리 길지 않았습니다. 오히려 슬랙 메시지 포맷팅과 아지트 API 분석, 그리고 리팩토링에 시간이 더 걸렸던 것 같습니다.

아직 부족한 게 많아 블로그를 써도 될까 고민도 했지만, AI 개발을 처음 하는 데브옵스 엔지니어도 베드락의 도움으로 비교적 짧은 기간 내에 챗봇을 개발할 수 있었다는 점에 의의를 두고 작성하게 되었습니다. 그리고 ‘어? 의외로 쉽게 만들어지네?‘라는 생각이 들며 AI 기술에 대한 관심 자체가 많이 높아졌습니다. 가능하다면 학습도 직접 시켜보고 싶구요. 앞으로도 이렇게 다양한 AI 기술들을 접목해 춘시리의 성능을 높이고, 다양한 업무 분야를 도울 기능도 제공해보려고 합니다.

이상으로 글을 마치겠습니다. 긴 글 읽어주셔서 감사합니다! :)

참고자료

terra.bite
terra.bite

카카오페이증권 DevOps팀 테라입니다. 뭐든 일단 해보는 것을 좋아합니다.