본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
AI Generated
2025. 12. 18. · 54 Views
사내 문서 Q&A 시스템 실전 구축 가이드
AWS Bedrock Knowledge Base를 활용하여 사내 문서를 효율적으로 검색하고 질문에 답하는 시스템을 구축하는 실전 프로젝트입니다. RAG 아키텍처와 Streamlit UI를 통해 완성도 높은 Q&A 서비스를 만들어봅니다.
목차
1. 프로젝트 요구사항 정의
어느 날 김개발 씨는 팀장님으로부터 흥미로운 프로젝트를 제안받았습니다. "우리 회사 문서가 너무 많아서 필요한 정보를 찾기가 힘들어요.
AI로 질문하면 답을 찾아주는 시스템을 만들어볼 수 있을까요?" 김개발 씨는 AWS Bedrock을 활용하면 가능하다는 것을 알고 있었습니다.
프로젝트 요구사항 정의는 시스템을 구축하기 전에 반드시 거쳐야 하는 첫 단계입니다. 마치 집을 짓기 전에 설계도를 그리는 것처럼, 무엇을 만들지 명확히 해야 합니다.
이 단계에서는 사용자가 원하는 기능, 처리해야 할 문서 종류, 그리고 기술 스택을 결정합니다.
다음 코드를 살펴봅시다.
# 프로젝트 요구사항 정의 예시
requirements = {
"목표": "사내 문서를 자연어로 검색하고 AI가 답변하는 시스템",
"입력_문서": ["PDF", "DOCX", "TXT", "MD"],
"기술_스택": {
"AI_서비스": "AWS Bedrock Knowledge Base",
"스토리지": "AWS S3",
"AI_모델": "Claude 3 Sonnet",
"UI_프레임워크": "Streamlit"
},
"핵심_기능": [
"자연어 질문 처리",
"문맥 기반 답변 생성",
"출처 문서 표시"
]
}
# 예상 사용자 질문 예시
example_questions = [
"휴가 신청 절차는 어떻게 되나요?",
"AWS 계정 접근 권한은 어떻게 받나요?",
"코드 리뷰 가이드라인을 알려주세요"
]
김개발 씨는 프로젝트를 시작하기 전에 먼저 노트에 필요한 것들을 정리하기 시작했습니다. "일단 무엇을 만들어야 하는지 정확히 알아야겠어요." 팀장님과의 대화를 다시 떠올려봅니다.
회사에는 인사 규정, 개발 가이드라인, 보안 정책, 장비 신청 절차 등 수많은 문서가 있습니다. 하지만 이 문서들이 여기저기 흩어져 있어서 직원들이 필요한 정보를 찾기가 쉽지 않습니다.
김개발 씨는 시니어 개발자 박선배 님을 찾아갔습니다. "선배님, 이런 시스템을 만들려면 어떻게 시작해야 할까요?" 박선배 님은 웃으며 답했습니다.
"좋은 질문이에요. 먼저 요구사항을 명확히 정의해야 합니다.
무엇을 만들지 모르고 코딩부터 하면 나중에 전부 다시 만들어야 할 수도 있거든요." 요구사항 정의란 무엇일까요? 쉽게 비유하자면, 요구사항 정의는 마치 여행 계획을 세우는 것과 같습니다. 어디로 갈지, 무엇을 준비할지, 어떤 경로로 갈지 미리 정하는 것입니다.
이처럼 프로젝트도 목표 지점과 가는 방법을 먼저 정해야 합니다. 김개발 씨는 먼저 핵심 기능을 정리했습니다.
사용자가 자연어로 질문을 입력하면, 시스템이 관련 문서를 찾아서 이해하기 쉬운 답변을 생성해야 합니다. 또한 답변의 출처가 되는 문서도 함께 보여주어야 신뢰성이 높아집니다.
다음으로 기술 스택을 선택해야 했습니다. 박선배 님은 AWS Bedrock Knowledge Base를 추천했습니다.
"이건 AWS에서 제공하는 RAG 서비스예요. 문서를 자동으로 분석하고 벡터로 변환해서 검색할 수 있게 해줍니다." RAG란 Retrieval Augmented Generation의 약자입니다.
한마디로 문서에서 관련 내용을 찾아서 AI 답변을 생성하는 방식입니다. 마치 도서관 사서가 질문을 받으면 관련 책을 찾아 읽고 요약해주는 것처럼 작동합니다.
김개발 씨는 처리해야 할 문서 형식도 정리했습니다. 회사에는 PDF, Word 문서, 텍스트 파일, 마크다운 파일 등 다양한 형식의 문서가 있습니다.
다행히 Bedrock Knowledge Base는 이런 다양한 형식을 모두 지원합니다. "UI는 어떻게 만들까요?" 김개발 씨가 물었습니다.
박선배 님은 Streamlit을 추천했습니다. "Streamlit은 Python으로 빠르게 웹 UI를 만들 수 있는 프레임워크예요.
복잡한 프론트엔드 개발 없이도 깔끔한 인터페이스를 만들 수 있습니다." 김개발 씨는 예상 사용자 질문도 몇 가지 적어보았습니다. "휴가 신청은 어떻게 하나요?", "AWS 계정 권한은 어떻게 받나요?", "코드 리뷰 가이드라인이 궁금해요" 같은 질문들입니다.
이런 예시 질문들은 나중에 시스템을 테스트할 때 유용하게 쓰입니다. 마지막으로 성능 목표도 정했습니다.
질문에 대한 답변은 5초 이내에 생성되어야 하고, 답변의 정확도는 최소 80% 이상이어야 합니다. 또한 하루에 수백 명이 동시에 사용해도 문제없어야 합니다.
박선배 님은 만족스럽게 고개를 끄덕였습니다. "좋아요.
이 정도면 프로젝트를 시작할 준비가 된 것 같네요. 요구사항이 명확하면 개발 과정에서 길을 잃지 않을 수 있습니다." 김개발 씨는 정리한 요구사항을 문서로 작성했습니다.
이 문서는 프로젝트가 진행되는 동안 나침반 역할을 해줄 것입니다. 요구사항 정의를 제대로 하면 나중에 "아, 이것도 필요했는데"라며 다시 만드는 일을 줄일 수 있습니다.
시간을 들여서라도 첫 단계를 탄탄히 하는 것이 중요합니다.
실전 팁
💡 - 실제 사용자와 인터뷰하여 어떤 질문을 가장 많이 하는지 파악하세요
- 기술 스택을 선택할 때는 팀의 기술 수준과 AWS 예산도 고려해야 합니다
- 예상 질문 리스트는 나중에 테스트 케이스로 활용할 수 있습니다
2. 문서 수집 및 S3 업로드
요구사항 정의를 마친 김개발 씨는 이제 실제 문서를 모아야 했습니다. "일단 어디에 어떤 문서들이 있는지부터 찾아봐야겠어요." 회사 곳곳에 흩어진 문서를 한곳에 모으는 작업이 시작되었습니다.
문서 수집 및 S3 업로드는 AI가 학습할 데이터를 준비하는 단계입니다. 마치 요리를 하기 전에 재료를 손질하는 것처럼, 문서를 정리하고 클라우드 스토리지에 업로드해야 합니다.
AWS S3에 문서를 체계적으로 저장하면 Bedrock Knowledge Base가 자동으로 읽어들여 벡터화할 수 있습니다.
다음 코드를 살펴봅시다.
import boto3
import os
from pathlib import Path
# S3 클라이언트 생성
s3_client = boto3.client('s3', region_name='ap-northeast-2')
bucket_name = 'company-docs-kb'
def upload_documents_to_s3(local_folder, s3_prefix='documents/'):
"""로컬 폴더의 문서를 S3에 업로드"""
uploaded_files = []
# 로컬 폴더의 모든 파일 탐색
for file_path in Path(local_folder).rglob('*'):
if file_path.is_file() and file_path.suffix in ['.pdf', '.docx', '.txt', '.md']:
# S3 키 생성 (폴더 구조 유지)
relative_path = file_path.relative_to(local_folder)
s3_key = f"{s3_prefix}{relative_path}"
# 파일 업로드
print(f"업로드 중: {file_path.name} -> {s3_key}")
s3_client.upload_file(str(file_path), bucket_name, s3_key)
uploaded_files.append(s3_key)
return uploaded_files
# 실행 예시
files = upload_documents_to_s3('./company_documents')
print(f"총 {len(files)}개 파일 업로드 완료")
김개발 씨는 먼저 회사 내 문서가 어디에 있는지 파악하기 시작했습니다. 인사팀의 공유 드라이브에는 인사 규정이, 개발팀 위키에는 기술 문서가, 그리고 각 부서마다 로컬 폴더에 문서가 산재해 있었습니다.
"이걸 어떻게 다 모으지?" 김개발 씨는 한숨을 쉬었습니다. 박선배 님이 조언했습니다.
"먼저 각 부서에 요청해서 중요한 문서들을 받아보세요. 그리고 S3 버킷에 체계적으로 정리해서 올리면 됩니다." S3란 무엇일까요? S3는 AWS Simple Storage Service의 약자입니다.
쉽게 말해 클라우드 저장소입니다. 마치 구글 드라이브나 드롭박스처럼 파일을 업로드하고 저장할 수 있는 공간입니다.
하지만 S3는 프로그래밍으로 쉽게 접근할 수 있고, 다른 AWS 서비스와 연동이 잘 된다는 장점이 있습니다. 김개발 씨는 먼저 S3 버킷을 하나 만들었습니다.
버킷 이름은 'company-docs-kb'로 정했습니다. 버킷은 S3에서 파일을 담는 최상위 컨테이너입니다.
마치 커다란 폴더라고 생각하면 됩니다. 다음으로 문서를 수집했습니다.
인사팀에서는 휴가 규정, 출장 규정, 복지 안내 문서를 보내주었습니다. 개발팀에서는 코딩 컨벤션, API 문서, 배포 가이드를 받았습니다.
총무팀에서는 장비 신청 절차, 회의실 예약 방법 등을 공유해주었습니다. 문서 형식도 다양했습니다.
PDF 파일도 있고, Word 문서도 있고, 마크다운 파일도 있었습니다. "다행히 Bedrock Knowledge Base는 이런 형식을 모두 지원해요." 김개발 씨는 안도했습니다.
이제 이 문서들을 S3에 업로드해야 했습니다. 손으로 하나씩 올릴 수도 있지만, 문서가 수백 개라면 시간이 너무 오래 걸립니다.
김개발 씨는 boto3라는 Python 라이브러리를 사용하기로 했습니다. boto3는 AWS 서비스를 Python에서 제어할 수 있게 해주는 공식 SDK입니다.
마치 리모컨으로 TV를 조종하듯, 코드로 AWS를 조작할 수 있습니다. 위의 코드를 살펴봅시다.
먼저 boto3.client('s3')로 S3 클라이언트를 생성합니다. 이것이 AWS S3와 통신할 수 있는 도구입니다.
upload_documents_to_s3 함수는 로컬 폴더를 순회하면서 PDF, DOCX, TXT, MD 파일을 찾습니다. Path.rglob('*')는 하위 폴더까지 모두 탐색하는 강력한 기능입니다.
각 파일을 찾으면 s3_client.upload_file()로 S3에 업로드합니다. 중요한 것은 S3 키입니다.
S3 키는 버킷 내에서 파일의 경로를 의미합니다. 예를 들어 'documents/hr/vacation_policy.pdf'처럼 폴더 구조를 유지하면 나중에 관리하기 편합니다.
김개발 씨는 로컬에 모아둔 문서 폴더를 함수에 넣고 실행했습니다. 콘솔에는 "업로드 중: vacation_policy.pdf -> documents/hr/vacation_policy.pdf"라는 메시지가 계속 출력되었습니다.
몇 분 후, "총 247개 파일 업로드 완료"라는 메시지가 나타났습니다. 김개발 씨는 AWS 콘솔에 접속해서 S3 버킷을 확인했습니다.
모든 문서가 깔끔하게 정리되어 있었습니다. 하지만 한 가지 문제가 있었습니다.
일부 Word 문서가 한글 2002 버전으로 저장되어 있어서 Bedrock이 읽지 못할 수도 있습니다. 박선배 님은 "그런 문서는 PDF로 변환해서 다시 올리세요"라고 조언했습니다.
김개발 씨는 문제가 되는 문서들을 PDF로 변환하고 다시 업로드했습니다. 이제 모든 문서가 표준 형식으로 S3에 저장되었습니다.
S3에 문서를 올릴 때는 폴더 구조를 잘 설계하는 것이 중요합니다. 부서별로 분류할 수도 있고, 문서 종류별로 분류할 수도 있습니다.
나중에 특정 부서의 문서만 검색하고 싶을 때 유용합니다.
실전 팁
💡 - S3 버킷 이름은 전 세계에서 유일해야 하므로 회사명이나 프로젝트명을 포함하세요
- 대용량 파일은 멀티파트 업로드를 사용하면 안정적입니다
- 문서를 업로드하기 전에 민감한 개인정보가 포함되어 있지 않은지 확인하세요
3. Knowledge Base 구성
문서를 S3에 모두 올린 김개발 씨는 이제 본격적으로 AI 시스템을 만들 차례였습니다. "Bedrock Knowledge Base를 설정하면 자동으로 문서를 분석해준다고 했지?" 드디어 핵심 기능을 구축할 시간입니다.
Knowledge Base 구성은 업로드한 문서를 AI가 이해할 수 있는 형태로 변환하는 단계입니다. 마치 책의 색인을 만드는 것처럼, 문서의 내용을 벡터로 변환하여 빠르게 검색할 수 있게 합니다.
Bedrock Knowledge Base는 이 과정을 자동화하고 OpenSearch Serverless를 통해 벡터를 저장합니다.
다음 코드를 살펴봅시다.
import boto3
import json
bedrock_agent = boto3.client('bedrock-agent', region_name='ap-northeast-2')
def create_knowledge_base():
"""Knowledge Base 생성"""
response = bedrock_agent.create_knowledge_base(
name='company-docs-kb',
description='사내 문서 Q&A 시스템용 Knowledge Base',
roleArn='arn:aws:iam::123456789012:role/BedrockKBRole',
knowledgeBaseConfiguration={
'type': 'VECTOR',
'vectorKnowledgeBaseConfiguration': {
'embeddingModelArn': 'arn:aws:bedrock:ap-northeast-2::foundation-model/amazon.titan-embed-text-v1'
}
},
storageConfiguration={
'type': 'OPENSEARCH_SERVERLESS',
'opensearchServerlessConfiguration': {
'collectionArn': 'arn:aws:aoss:ap-northeast-2:123456789012:collection/kb-collection',
'vectorIndexName': 'company-docs-index',
'fieldMapping': {
'vectorField': 'embedding',
'textField': 'text',
'metadataField': 'metadata'
}
}
}
)
kb_id = response['knowledgeBase']['knowledgeBaseId']
print(f"Knowledge Base 생성 완료: {kb_id}")
return kb_id
# 데이터 소스 연결 (S3 버킷)
def create_data_source(kb_id, bucket_name):
"""S3 데이터 소스를 Knowledge Base에 연결"""
response = bedrock_agent.create_data_source(
knowledgeBaseId=kb_id,
name='company-docs-source',
dataSourceConfiguration={
'type': 'S3',
's3Configuration': {
'bucketArn': f'arn:aws:s3:::{bucket_name}',
'inclusionPrefixes': ['documents/']
}
}
)
ds_id = response['dataSource']['dataSourceId']
print(f"데이터 소스 연결 완료: {ds_id}")
return ds_id
김개발 씨는 AWS Bedrock 콘솔을 열고 Knowledge Base 메뉴를 찾았습니다. "여기서 설정하면 되는구나." 하지만 설정 항목이 많아서 어떻게 해야 할지 고민이 되었습니다.
박선배 님이 옆에 와서 설명했습니다. "Knowledge Base는 크게 세 가지 요소로 구성돼요.
임베딩 모델, 벡터 저장소, 그리고 데이터 소스입니다." 임베딩 모델이란 무엇일까요? 임베딩 모델은 텍스트를 숫자로 변환하는 AI 모델입니다. 예를 들어 "휴가 신청 방법"이라는 문장을 [0.23, -0.45, 0.67, ...] 같은 숫자 배열로 바꿉니다.
이렇게 변환된 숫자를 벡터라고 부릅니다. 왜 숫자로 바꿀까요?
마치 GPS 좌표처럼, 비슷한 의미의 문장은 비슷한 좌표를 가지게 됩니다. "휴가 신청"과 "연차 내는 법"은 의미가 비슷하므로 벡터 공간에서 가까운 위치에 있습니다.
이렇게 하면 검색이 매우 빨라집니다. 김개발 씨는 Amazon Titan Embed 모델을 선택했습니다.
이 모델은 한국어도 잘 이해하고, 비용도 저렴합니다. 다음은 벡터 저장소입니다.
변환된 벡터를 어디에 저장할까요? Bedrock Knowledge Base는 OpenSearch Serverless를 사용합니다.
OpenSearch는 Elasticsearch 기반의 검색 엔진입니다. "Serverless"라는 단어가 붙은 이유는 서버 관리를 AWS가 알아서 해주기 때문입니다.
김개발 씨는 먼저 OpenSearch Serverless 컬렉션을 만들었습니다. 컬렉션 이름은 'kb-collection'으로 정했습니다.
그리고 인덱스 이름은 'company-docs-index'로 설정했습니다. 인덱스는 데이터베이스의 테이블과 비슷한 개념입니다.
마지막으로 데이터 소스를 연결해야 합니다. 데이터 소스는 문서가 저장된 위치를 의미합니다.
김개발 씨는 앞서 만든 S3 버킷을 데이터 소스로 지정했습니다. 위의 코드를 살펴봅시다.
create_knowledge_base 함수는 Knowledge Base를 프로그래밍 방식으로 생성합니다. knowledgeBaseConfiguration에서 타입을 'VECTOR'로 지정하고, 임베딩 모델로 Titan Embed를 선택합니다.
storageConfiguration에서는 OpenSearch Serverless를 벡터 저장소로 지정합니다. fieldMapping은 중요합니다.
벡터는 'embedding' 필드에, 원본 텍스트는 'text' 필드에, 메타데이터는 'metadata' 필드에 저장하겠다는 의미입니다. create_data_source 함수는 S3 버킷을 데이터 소스로 연결합니다.
inclusionPrefixes를 'documents/'로 설정하면 해당 폴더의 파일만 읽어들입니다. 김개발 씨는 코드를 실행했습니다.
"Knowledge Base 생성 완료: kb-abc123"이라는 메시지가 나타났습니다. 이제 Knowledge Base ID를 받았으니 나중에 사용할 수 있습니다.
하지만 아직 끝이 아닙니다. 문서를 실제로 벡터로 변환하려면 동기화 작업이 필요합니다.
박선배 님이 설명했습니다. "지금은 Knowledge Base만 만든 거고, 실제로 S3 문서를 읽어서 벡터화하려면 동기화를 실행해야 해요." 김개발 씨는 콘솔에서 "Sync" 버튼을 눌렀습니다.
진행 상황을 보여주는 프로그레스 바가 나타났습니다. 247개의 문서를 처리하는 데 약 10분이 걸렸습니다.
동기화가 완료되면 각 문서가 작은 청크로 나뉘어집니다. 보통 한 문서가 여러 개의 청크로 분할됩니다.
각 청크는 독립적으로 벡터화되어 인덱스에 저장됩니다. 이렇게 하면 긴 문서에서도 정확한 부분을 찾을 수 있습니다.
김개발 씨는 OpenSearch 대시보드를 열어 확인했습니다. 'company-docs-index'에 수천 개의 벡터가 저장되어 있었습니다.
"드디어 AI가 검색할 수 있는 형태가 되었네요!" Knowledge Base 구성은 복잡해 보이지만, 한 번 설정하면 나중에 문서를 추가해도 동기화만 다시 실행하면 됩니다.
실전 팁
💡 - IAM 역할(Role)을 미리 만들어서 Bedrock이 S3와 OpenSearch에 접근할 수 있게 권한을 부여하세요
- 동기화는 시간이 걸리므로 문서가 많다면 야간에 실행하는 것이 좋습니다
- 청크 크기는 기본값을 사용하되, 필요하면 조정할 수 있습니다
4. 검색 및 응답 테스트
Knowledge Base 설정을 마친 김개발 씨는 이제 진짜로 작동하는지 테스트해볼 시간이었습니다. "휴가 신청 방법을 물어보면 제대로 답변할까?" 손에 땀을 쥐고 첫 번째 질문을 입력했습니다.
검색 및 응답 테스트는 구축한 시스템이 실제로 정확한 답변을 제공하는지 확인하는 단계입니다. 마치 새로 만든 자동차를 시운전하는 것처럼, 다양한 질문을 던져보고 답변의 품질을 평가합니다.
Bedrock의 RetrieveAndGenerate API를 사용하여 문서를 검색하고 AI 답변을 생성합니다.
다음 코드를 살펴봅시다.
import boto3
import json
bedrock_agent_runtime = boto3.client('bedrock-agent-runtime', region_name='ap-northeast-2')
def query_knowledge_base(question, kb_id, model_arn):
"""Knowledge Base에 질문하고 답변 받기"""
response = bedrock_agent_runtime.retrieve_and_generate(
input={
'text': question
},
retrieveAndGenerateConfiguration={
'type': 'KNOWLEDGE_BASE',
'knowledgeBaseConfiguration': {
'knowledgeBaseId': kb_id,
'modelArn': model_arn, # Claude 3 Sonnet
'retrievalConfiguration': {
'vectorSearchConfiguration': {
'numberOfResults': 5 # 상위 5개 청크 검색
}
}
}
}
)
# 답변과 출처 추출
answer = response['output']['text']
sources = []
for citation in response.get('citations', []):
for reference in citation.get('retrievedReferences', []):
sources.append({
'uri': reference['location']['s3Location']['uri'],
'content': reference['content']['text'][:200] # 앞부분만
})
return answer, sources
# 테스트 실행
kb_id = 'kb-abc123'
model_arn = 'arn:aws:bedrock:ap-northeast-2::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0'
question = "휴가 신청은 어떻게 하나요?"
answer, sources = query_knowledge_base(question, kb_id, model_arn)
print(f"질문: {question}")
print(f"답변: {answer}")
print(f"\n출처:")
for idx, source in enumerate(sources, 1):
print(f"{idx}. {source['uri']}")
김개발 씨는 설레는 마음으로 첫 번째 테스트 질문을 준비했습니다. "휴가 신청은 어떻게 하나요?" 이 질문에 대한 답변은 인사 규정 문서에 있을 것입니다.
코드를 실행하자 몇 초 후 답변이 나타났습니다. "휴가 신청은 사내 인트라넷의 '전자결재' 메뉴에서 진행할 수 있습니다.
휴가 신청서를 작성하고 직속 상사의 승인을 받으면 됩니다. 연차는 최소 3일 전에 신청해야 하며..." 김개발 씨는 감탄했습니다.
"와, 정확한 답변이네요!" 더 놀라운 것은 답변 아래에 출처 문서가 표시된다는 것이었습니다. "s3://company-docs-kb/documents/hr/vacation_policy.pdf" 실제로 그 문서에서 정보를 가져온 것입니다.
박선배 님이 물었습니다. "어떻게 작동하는지 궁금하지 않아요?" RetrieveAndGenerate API는 어떻게 작동할까요? 이 API는 두 단계로 작동합니다.
첫 번째는 Retrieve, 즉 검색 단계입니다. 사용자의 질문을 벡터로 변환하고, Knowledge Base에서 비슷한 벡터를 가진 청크를 찾습니다.
위의 코드에서 numberOfResults: 5는 가장 관련성 높은 5개의 청크를 가져오라는 의미입니다. 두 번째는 Generate, 즉 생성 단계입니다.
검색된 청크들을 Claude 3 Sonnet 모델에게 전달하고, 자연스러운 문장으로 답변을 생성하게 합니다. Claude는 검색된 내용만을 기반으로 답변하므로 환각 현상이 줄어듭니다.
김개발 씨는 더 어려운 질문을 던져보았습니다. "AWS 계정 접근 권한은 어떻게 받나요?" 이번에도 정확한 답변이 돌아왔습니다.
"AWS 계정 권한은 IT 팀에 Jira 티켓을 생성하여 요청할 수 있습니다. 티켓에는 필요한 서비스, 권한 수준, 사용 목적을 명시해야 합니다..." 모든 답변에는 출처가 함께 표시되었습니다.
이것은 매우 중요한 기능입니다. 사용자는 답변이 어디에서 왔는지 확인할 수 있고, 더 자세한 정보가 필요하면 원본 문서를 직접 읽을 수 있습니다.
하지만 완벽하지는 않았습니다. 김개발 씨가 "점심 메뉴 추천해줘"라고 물어보자, 시스템은 "관련 정보를 찾을 수 없습니다"라고 답했습니다.
이것은 좋은 신호입니다. 문서에 없는 내용을 지어내지 않는다는 의미이기 때문입니다.
김개발 씨는 다양한 질문을 던져보았습니다. "코드 리뷰 가이드라인", "회의실 예약 방법", "VPN 접속 방법" 등 실제 직원들이 자주 물어볼 만한 질문들입니다.
대부분 정확한 답변이 돌아왔습니다. 때로는 답변이 너무 길거나 짧을 때도 있었습니다.
박선배 님은 "그럴 때는 프롬프트를 조정하면 돼요. 예를 들어 '간단히 요약해서 답변해주세요'라고 추가할 수 있습니다"라고 조언했습니다.
위의 코드에서 citations 부분을 보면 답변의 근거가 되는 문서 조각들을 확인할 수 있습니다. 각 citation에는 S3 URI와 실제 텍스트 내용이 포함됩니다.
이것을 UI에 표시하면 사용자가 신뢰할 수 있는 답변이 됩니다. 김개발 씨는 테스트 결과를 정리했습니다.
50개의 테스트 질문 중 47개가 정확한 답변을 받았습니다. 정확도 94퍼센트입니다.
목표였던 80퍼센트를 훨씬 넘었습니다. 남은 3개의 질문은 왜 실패했을까요?
분석해보니 문서 자체에 정보가 없거나 애매하게 작성되어 있었습니다. 김개발 씨는 해당 부서에 문서 개선을 요청했습니다.
테스트를 통해 시스템의 강점과 약점을 파악할 수 있습니다. 이제 실제 사용자를 위한 UI를 만들 준비가 되었습니다.
실전 팁
💡 - 다양한 유형의 질문을 준비하여 엣지 케이스를 테스트하세요
- 답변 품질이 낮으면 검색되는 청크 수를 늘려보세요 (5개 -> 10개)
- 출처 표시는 사용자 신뢰도를 높이는 핵심 기능입니다
5. Streamlit UI 개발
백엔드가 완성되었으니 이제 사용자가 쉽게 사용할 수 있는 화면이 필요했습니다. "명령줄로만 쓰면 다른 직원들이 못 써요." 김개발 씨는 웹 인터페이스를 만들기로 했습니다.
Streamlit UI 개발은 사용자 친화적인 웹 인터페이스를 빠르게 구축하는 단계입니다. 마치 멋진 외관을 입히는 것처럼, Python 코드만으로 깔끔한 웹 페이지를 만들 수 있습니다.
Streamlit은 데이터 과학자와 AI 개발자가 복잡한 프론트엔드 개발 없이도 인터랙티브한 앱을 만들 수 있게 해줍니다.
다음 코드를 살펴봅시다.
import streamlit as st
import boto3
from datetime import datetime
# 페이지 설정
st.set_page_config(page_title="사내 문서 Q&A", page_icon="📚", layout="wide")
# 세션 상태 초기화
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
# Bedrock 클라이언트
bedrock_runtime = boto3.client('bedrock-agent-runtime', region_name='ap-northeast-2')
KB_ID = 'kb-abc123'
MODEL_ARN = 'arn:aws:bedrock:ap-northeast-2::foundation-model/anthropic.claude-3-sonnet-20240229-v1:0'
def get_answer(question):
"""Knowledge Base에 질문하고 답변 받기"""
response = bedrock_runtime.retrieve_and_generate(
input={'text': question},
retrieveAndGenerateConfiguration={
'type': 'KNOWLEDGE_BASE',
'knowledgeBaseConfiguration': {
'knowledgeBaseId': KB_ID,
'modelArn': MODEL_ARN
}
}
)
return response['output']['text'], response.get('citations', [])
# UI 구성
st.title("📚 사내 문서 Q&A 시스템")
st.markdown("궁금한 사항을 질문하면 AI가 사내 문서에서 답변을 찾아드립니다.")
# 채팅 히스토리 표시
for chat in st.session_state.chat_history:
with st.chat_message(chat['role']):
st.write(chat['content'])
if 'sources' in chat:
with st.expander("📄 출처 보기"):
for src in chat['sources']:
st.caption(src)
# 질문 입력
if question := st.chat_input("질문을 입력하세요"):
# 사용자 질문 표시
with st.chat_message("user"):
st.write(question)
st.session_state.chat_history.append({'role': 'user', 'content': question})
# AI 답변 생성
with st.chat_message("assistant"):
with st.spinner("답변을 생성하고 있습니다..."):
answer, citations = get_answer(question)
st.write(answer)
# 출처 표시
if citations:
sources = [c['retrievedReferences'][0]['location']['s3Location']['uri']
for c in citations if c.get('retrievedReferences')]
with st.expander("📄 출처 보기"):
for src in sources:
st.caption(src)
st.session_state.chat_history.append({
'role': 'assistant',
'content': answer,
'sources': sources
})
김개발 씨는 Streamlit을 처음 사용해보았습니다. "이게 정말 Python 코드만으로 웹페이지가 만들어진다고?" 반신반의하며 튜토리얼을 읽었습니다.
박선배 님은 확신에 차서 말했습니다. "Streamlit은 데이터 과학자들이 즐겨 쓰는 도구예요.
HTML, CSS, JavaScript 몰라도 됩니다. Python만 알면 충분해요." Streamlit이란 무엇일까요? Streamlit은 Python 스크립트를 웹 애플리케이션으로 변환해주는 프레임워크입니다.
마치 마법처럼, st.title()을 쓰면 제목이 나타나고, st.button()을 쓰면 버튼이 생깁니다. 복잡한 프론트엔드 개발 없이도 멋진 UI를 만들 수 있습니다.
김개발 씨는 먼저 페이지 레이아웃을 설계했습니다. 상단에는 제목과 간단한 설명, 중간에는 채팅 기록, 하단에는 질문 입력창을 배치하기로 했습니다.
st.set_page_config()로 페이지 제목과 아이콘을 설정합니다. page_icon="📚"는 브라우저 탭에 책 이모지가 표시되게 합니다.
작은 디테일이지만 사용자 경험을 향상시킵니다. 다음으로 세션 상태를 설정합니다.
웹 애플리케이션은 기본적으로 매 인터랙션마다 처음부터 다시 실행됩니다. 하지만 채팅 기록은 유지되어야 합니다.
st.session_state는 사용자 세션 동안 데이터를 저장하는 공간입니다. 마치 브라우저의 로컬 스토리지와 비슷합니다.
get_answer 함수는 앞서 테스트할 때 사용했던 코드와 동일합니다. Bedrock에 질문을 보내고 답변과 출처를 받아옵니다.
이제 UI를 구성합니다. st.title()로 큰 제목을 표시하고, st.markdown()으로 설명 문구를 추가합니다.
채팅 히스토리를 표시하는 부분이 흥미롭습니다. st.chat_message()는 Streamlit에서 제공하는 채팅 UI 컴포넌트입니다.
자동으로 아바타와 말풍선을 만들어줍니다. "user" 역할은 오른쪽에, "assistant" 역할은 왼쪽에 표시됩니다.
출처 문서는 st.expander()로 감쌌습니다. 기본적으로는 접혀있고, 사용자가 클릭하면 펼쳐집니다.
화면을 깔끔하게 유지하면서도 필요한 정보를 제공할 수 있습니다. 가장 중요한 부분은 질문 입력입니다.
st.chat_input()은 최신 Streamlit 버전에서 추가된 기능으로, 채팅 앱에 최적화된 입력창입니다. 사용자가 엔터를 누르면 질문이 전송됩니다.
:= 연산자를 주목하세요. 이것은 Python의 walrus operator입니다.
값을 할당하면서 동시에 조건을 검사합니다. if question := st.chat_input()은 "입력이 있으면 question 변수에 저장하고 if 블록을 실행해라"는 의미입니다.
질문이 입력되면 먼저 사용자 메시지를 화면에 표시하고 히스토리에 추가합니다. 그 다음 st.spinner()로 로딩 표시를 합니다.
"답변을 생성하고 있습니다..."라는 메시지와 함께 빙글빙글 도는 아이콘이 나타납니다. get_answer()를 호출하여 AI 답변을 받아옵니다.
답변이 도착하면 화면에 표시하고, 출처도 expander 안에 보여줍니다. 마지막으로 히스토리에 저장합니다.
김개발 씨는 터미널에서 streamlit run app.py를 실행했습니다. 브라우저가 자동으로 열리며 웹 애플리케이션이 나타났습니다.
"와, 진짜로 작동하네요!" 김개발 씨는 질문을 입력해보았습니다. 채팅창이 자연스럽게 스크롤되고, 답변이 실시간으로 표시되었습니다.
마치 ChatGPT처럼 보였습니다. 박선배 님도 만족스러워했습니다.
"이 정도면 충분히 실무에 사용할 수 있겠는데요? 복잡한 React 개발 없이도 이렇게 깔끔한 UI를 만들 수 있다니." 김개발 씨는 몇 가지 개선 사항을 추가했습니다.
사이드바에 필터 옵션을 넣고, 검색 히스토리를 CSV로 다운로드할 수 있는 버튼도 만들었습니다. Streamlit의 다양한 위젯을 활용하면 이런 기능을 쉽게 추가할 수 있습니다.
하지만 한 가지 주의할 점이 있습니다. Streamlit은 프로토타입이나 내부 도구에는 완벽하지만, 대규모 프로덕션 서비스에는 적합하지 않을 수 있습니다.
동시 사용자가 많으면 성능 이슈가 생길 수 있기 때문입니다. 김개발 씨는 "우리 회사는 100명 정도니까 충분할 것 같아요"라고 판단했습니다.
실전 팁
💡 - st.cache_data 데코레이터로 API 호출 결과를 캐싱하면 성능이 향상됩니다
- Streamlit Cloud를 사용하면 무료로 앱을 배포할 수 있습니다
- 사용자 피드백 버튼을 추가하여 답변 품질을 모니터링하세요
6. 시스템 테스트 및 개선
UI까지 완성한 김개발 씨는 이제 실제 직원들에게 테스트를 요청했습니다. "베타 테스터를 모집합니다!" 사내 게시판에 공지를 올렸고, 10명의 동료가 자원했습니다.
진짜 피드백을 받을 시간입니다.
시스템 테스트 및 개선은 실제 사용자의 피드백을 바탕으로 시스템을 다듬는 마지막 단계입니다. 마치 신차를 출시하기 전에 시험 주행을 하는 것처럼, 실제 사용 환경에서 문제점을 발견하고 개선합니다.
사용자 행동을 로깅하고, 답변 품질을 측정하고, 지속적으로 업데이트하는 프로세스를 구축합니다.
다음 코드를 살펴봅시다.
import streamlit as st
import boto3
from datetime import datetime
import json
import logging
# 로깅 설정
logging.basicConfig(
filename='qa_system.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def log_interaction(question, answer, feedback=None):
"""사용자 인터랙션 로깅"""
log_data = {
'timestamp': datetime.now().isoformat(),
'question': question,
'answer': answer[:100], # 앞부분만
'feedback': feedback,
'user': st.session_state.get('user_email', 'anonymous')
}
logging.info(json.dumps(log_data, ensure_ascii=False))
def display_feedback_buttons(question, answer):
"""피드백 버튼 표시"""
col1, col2 = st.columns(2)
with col1:
if st.button("👍 도움됐어요", key=f"good_{question}"):
log_interaction(question, answer, feedback="positive")
st.success("피드백 감사합니다!")
with col2:
if st.button("👎 별로예요", key=f"bad_{question}"):
log_interaction(question, answer, feedback="negative")
st.info("개선하겠습니다. 추가 의견을 남겨주세요.")
feedback_text = st.text_area("무엇이 문제였나요?", key=f"detail_{question}")
if feedback_text:
logging.info(f"Detailed feedback: {feedback_text}")
# 성능 모니터링
def measure_response_time(func):
"""응답 시간 측정 데코레이터"""
def wrapper(*args, **kwargs):
start_time = datetime.now()
result = func(*args, **kwargs)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
logging.info(f"Response time: {duration:.2f} seconds")
# 5초 이상 걸리면 경고
if duration > 5:
logging.warning(f"Slow response detected: {duration:.2f}s")
return result
return wrapper
@measure_response_time
def get_answer_with_monitoring(question):
"""모니터링이 포함된 답변 생성"""
# 기존 get_answer 함수 호출
return get_answer(question)
# 정기적인 Knowledge Base 동기화
def schedule_kb_sync():
"""매일 자정에 Knowledge Base 동기화"""
bedrock_agent = boto3.client('bedrock-agent')
response = bedrock_agent.start_ingestion_job(
knowledgeBaseId='kb-abc123',
dataSourceId='ds-xyz789'
)
logging.info(f"KB sync started: {response['ingestionJob']['ingestionJobId']}")
베타 테스트 첫날, 김개발 씨는 긴장한 마음으로 사용 현황을 지켜보았습니다. 10명의 테스터가 다양한 질문을 쏟아냈습니다.
"휴가 신청 방법", "VPN 설정", "경조사 휴가 일수", "코드 리뷰 절차" 등 실제로 궁금해하는 질문들이었습니다. 대부분은 정확한 답변을 받았지만, 몇 가지 문제도 발견되었습니다.
인사팀 김대리 님이 슬랙으로 메시지를 보냈습니다. "답변은 정확한데, 출처 문서를 클릭할 수 없어요.
PDF를 직접 볼 수 있으면 좋겠어요." 개발팀 이주임 님은 다른 피드백을 주었습니다. "어떤 질문은 5초 만에 답변이 나오는데, 어떤 질문은 15초나 걸려요.
왜 그런가요?" 김개발 씨는 이런 피드백을 체계적으로 수집해야겠다고 생각했습니다. 박선배 님의 조언을 받아 피드백 시스템을 구축하기로 했습니다.
피드백 시스템이란 무엇일까요? 사용자가 답변이 도움이 되었는지, 아니면 문제가 있었는지 평가할 수 있게 하는 기능입니다. 마치 유튜브의 좋아요/싫어요 버튼처럼, 간단하면서도 강력한 도구입니다.
위의 코드를 보면 display_feedback_buttons 함수가 있습니다. 엄지척 버튼과 엄지내림 버튼을 표시합니다.
사용자가 버튼을 누르면 log_interaction 함수가 호출되어 로그 파일에 기록됩니다. 로그 파일은 JSON 형식으로 저장됩니다.
나중에 이 데이터를 분석하면 어떤 질문이 자주 나오는지, 어떤 답변이 만족도가 낮은지 알 수 있습니다. 김개발 씨는 로그를 분석하다가 흥미로운 패턴을 발견했습니다.
"AWS 관련 질문"의 만족도가 낮았습니다. 이유를 찾아보니 AWS 문서가 너무 기술적이어서 일반 직원들이 이해하기 어려웠던 것입니다.
해결책은 간단했습니다. 프롬프트를 수정하여 "초보자도 이해할 수 있게 쉽게 설명해주세요"라는 지시를 추가했습니다.
답변 품질이 크게 개선되었습니다. 다음 문제는 응답 속도였습니다.
이주임 님의 지적처럼 질문에 따라 응답 시간이 들쑥날쑥했습니다. 김개발 씨는 measure_response_time 데코레이터를 만들어 모든 질문의 응답 시간을 측정하기 시작했습니다.
데코레이터는 함수를 감싸는 함수입니다. 마치 선물 포장지처럼, 원래 함수의 앞뒤에 추가 기능을 덧붙입니다.
이 경우에는 함수 실행 전후의 시간을 재서 로그에 기록합니다. 분석 결과, 긴 질문일수록 시간이 오래 걸렸습니다.
또한 처음 보는 질문보다 이미 검색했던 질문이 더 빨랐습니다. 이것은 Bedrock 내부에서 일종의 캐싱이 일어나기 때문입니다.
김대리 님의 요청대로 출처 문서를 직접 볼 수 있게 하려면 S3 Presigned URL을 생성해야 합니다. 이것은 임시로 S3 파일에 접근할 수 있는 링크입니다.
보안을 유지하면서도 사용자에게 문서를 제공할 수 있습니다. 또 다른 중요한 작업은 정기 동기화입니다.
회사 문서는 계속 업데이트됩니다. 새 문서가 추가되거나 기존 문서가 수정되면 Knowledge Base도 업데이트되어야 합니다.
김개발 씨는 매일 자정에 자동으로 동기화가 실행되도록 설정했습니다. AWS Lambda와 EventBridge를 사용하면 이런 스케줄링을 쉽게 구현할 수 있습니다.
2주간의 베타 테스트가 끝났습니다. 수집된 데이터를 보니 총 237개의 질문이 들어왔고, 긍정 피드백이 89%, 부정 피드백이 11%였습니다.
평균 응답 시간은 3.2초였습니다. 김개발 씨는 부정 피드백을 하나씩 살펴보았습니다.
대부분은 문서 자체에 정보가 부족하거나 오래된 경우였습니다. 해당 부서에 문서 업데이트를 요청했습니다.
팀장님은 만족스러워했습니다. "이제 정식으로 전사에 오픈해도 되겠어요.
수고하셨습니다!" 김개발 씨는 뿌듯했습니다. 처음 제안받았을 때는 막연했지만, 한 단계씩 진행하니 완성된 시스템이 되었습니다.
시스템을 만드는 것보다 중요한 것은 유지보수입니다. 김개발 씨는 주간 리포트를 작성하여 사용 통계, 주요 질문, 개선 사항을 정리하기로 했습니다.
이렇게 하면 시스템이 점점 더 똑똑해질 것입니다. 프로젝트가 성공적으로 마무리되었지만, 진짜 여정은 이제 시작입니다.
실제 사용자들의 피드백을 바탕으로 지속적으로 개선하는 것, 그것이 진정한 AI 시스템 구축의 핵심입니다.
실전 팁
💡 - 사용자 피드백은 시스템 개선의 보물창고입니다. 꼭 수집하세요
- CloudWatch나 Datadog 같은 모니터링 도구를 연동하면 더 강력합니다
- 정기적으로 로그를 분석하여 자주 묻는 질문을 FAQ로 정리하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.