사내 LLM 플랫폼 0→1 구축
권한 거버넌스부터 챗봇 마켓플레이스까지, AWS Serverless 풀스택
TL;DR
현대캐피탈 임직원 전체가 쓰는 사내 LLM 챗봇 플랫폼을 AWS Serverless 풀스택으로 0→1 구축. AWS의 오픈소스 솔루션(GenAI LLM Chatbot) 위에 HC 전용 3가지 핵심 기능을 얹는 게 미션이었습니다:
- RAG 문서별 권한 모델 (조직/사용자/DRM 3축)
- AI Market (사내 챗봇 마켓플레이스 — 즐겨찾기·리뷰·승인·공유)
- MCP Tool 연동 (사내 도구 동적 등록)
3개월 안에 documents.py에만 26개 커밋을 쌓으며 RAG 권한 로직을 처음부터 끝까지 책임졌습니다.
Problem — 사내 ChatGPT는 왜 직접 만드는가
현대캐피탈은 이미 AWS 위에서 운영되는 회사. 임직원이 ChatGPT 같은 외부 SaaS를 쓰면 사내 데이터·금융 도메인 컴플라이언스가 부서지고, 그렇다고 외부 LLM을 막으면 생산성 격차가 벌어집니다.
해답은 사내 LLM 플랫폼. 다만 단순 챗봇이 아니라:
- AI 개발자가 사내 챗봇을 자기 손으로 만들고 공유할 수 있는 마켓
- 부서/사용자 단위로 다른 RAG 답변이 나오는 권한 거버넌스
- 사내 시스템·도구를 챗봇 도구로 동적 등록하는 확장성
이 세 가지가 기성 SaaS로는 풀 수 없는 부분. 그래서 직접 만들었습니다.
Approach — 오픈소스를 받아서 어디를 뜯어고쳤나
베이스: aws-samples/aws-genai-llm-chatbot (MIT-0 라이선스 AWS 참조 솔루션). AppSync GraphQL Subscription으로 LLM 스트리밍 응답을 받는 패턴이 이 솔루션의 핵심이라 그대로 활용.
HC 전용 3가지 기능은 처음부터 만들어야 했습니다:
1) RAG 문서별 권한 모델
가장 어려운 영역. 같은 RAG 코퍼스에서 사용자마다 다른 답이 나와야 합니다. 영업팀은 영업 매뉴얼만, 리스크팀은 리스크 정책만 — 동시에 부서장은 더 넓은 권한.
설계: DynamoDB Documents 테이블에 accessRules 필드 추가.
accessRules: {
allowAll: boolean,
organizations: string[], // 부서 단위
users: string[], // 개인 단위
drm: boolean // DRM 플래그
}
3축 권한 모델로 조직 기반 광역 정책과 예외 사용자 추가를 동시에 표현. allowAll: false일 때는 organizations 또는 users 중 하나 이상 필수 — 권한 누락 방지.
검증은 has_access(user, document) 함수로 API 호출 시점에 실행. 이 로직 하나에 26개 커밋을 쌓으며 엣지 케이스를 메웠습니다.
2) AI Market — Applications Single-Table Design
사용자 생성 챗봇의 메타데이터·즐겨찾기·리뷰·승인 상태를 한 테이블에서 다 다루기 위해 단일 테이블 디자인(STD):
PK SK
APP#{id} METADATA ← 챗봇 본체
APP#{id} BOOKMARK#{userId}#{appId} ← 즐겨찾기
APP#{id} REVIEW#{userId}#{appId} ← 리뷰
GSI 3개 (byUserId, byChatType, bySharedApp)로 내가 만든 챗봇, 카테고리별 마켓 목록, 공유된 챗봇 검색을 모두 단일 쿼리로.
승인 플로우: approveApplication() mutation으로 PENDING → APPROVED/REJECTED 상태 전이. 마켓 노출은 Cognito groups × Application.Roles 교집합으로 결정 — 부서별 마켓을 따로 들지 않아도 자연스럽게 분리.
3) MCP Tool 연동
Tools 테이블에 transport·URL·description을 저장 → ToolManager.get_mcp_tools()가 LangChain StructuredTool로 변환 → Bedrock 호출 시점에 동적으로 로드. 2024년 말 발표된 MCP 표준을 2025년 중반 사내 플랫폼에 일찍 도입한 케이스.
Architecture
사용자 → ClientVPN → AppSync (GraphQL/WebSocket)
├─→ send-query-resolver → SNS → SQS (LangChain/Multimodal 분기) → 추론 핸들러
│ ├─→ Bedrock Runtime (Claude/Nova)
│ └─→ SageMaker Endpoint (SLM)
│ ↓
│ Tool Manager (MCP/Function)
├─→ subscription-resolver (WebSocket) ← OutgoingMessage Lambda
└─→ proxyResolverFunction (REST-like routes)
├─→ APISessions (DDB)
├─→ APIApplications (DDB STD) ← AI Market
└─→ RagWorkspaces / RagDocuments (DDB)
↓
RAG 문서 배치 (SQS → Lambda → Step Functions)
↓
Isolate Subnet: OpenSearch / Aurora / Kendra
핵심 디자인 결정:
- WebSocket Subscription — 스트리밍 응답을 위해 REST가 아닌 GraphQL
- Isolate Subnet — 임베딩 저장소(OpenSearch/Aurora/Kendra)를 격리망에 격리
- 이중 추론 경로 — 일반 채팅은 Bedrock, 사내 호스팅 SLM은 SageMaker
Results
| 결과 | 상세 |
|---|---|
| 전사 출시 | 현대캐피탈 전 임직원 대상 운영 |
| RAG 권한 로직 | 3축(조직/사용자/DRM) accessRules 모델 설계 및 26 커밋으로 구현 |
| AI Market | Applications STD로 메타데이터/즐겨찾기/리뷰/승인까지 단일 테이블에서 처리. GSI 3개로 모든 access pattern 커버 |
| MCP 표준 도입 | 발표 6개월 만에 사내 플랫폼 도입, Bedrock 추론 파이프라인에 통합 |
| 본인 기여 | documents.py 26 / documents route 20 / applications.py 15 / ai_market.py 12 커밋 |
What I'd do differently — 권한을 OpenSearch 인덱스로 내렸을 것
현재 accessRules 검증은 API 계층(Lambda)에서 evaluation 합니다. 사용자가 RAG 쿼리를 던지면 OpenSearch가 전체 코퍼스에서 검색하고, Lambda에서 권한 없는 결과를 후필터로 제거하는 구조.
문제 두 가지:
- 불필요한 retrieval 비용 — 어차피 못 볼 문서까지 vector search에 포함
- 누락 위험 — 후필터가 깨지면 권한 없는 문서가 노출될 가능성
지금이라면 권한을 OpenSearch 인덱스의 메타데이터 필드로 내려서 쿼리 단계에 filter clause로 적용했을 것입니다. retrieval 단계에서 권한 없는 문서를 애초에 검색 결과에 포함하지 않는 구조가 더 안전하고 빠릅니다.
권한 모델은 데이터 계층까지 내려야 한다 — 짧은 3개월 프리랜서로 들어간 프로젝트에서 가장 크게 배운 것.
Role
프리랜서로 들어가 RAG 권한 모델 설계 + AI Market 백엔드 + MCP Tool 연동의 백엔드 풀스택을 담당. 코드 베이스는 AWS 오픈소스 솔루션 위에 올라갔고, HC 전용 기능 3개의 백엔드는 처음부터 만들었습니다.
대표 모듈 / 커밋:
src/lambda/python-sdk/python/genai_core/documents.py— RAG 권한 (26 커밋)src/lambda/chatbot-api/.../routes/ai_market.py— AI Market API (12 커밋)- 실제 커밋 메시지 예: "[BE] has_access 사용자 기준 권한 검증 로직 추가"