본 실습에서는 Lambda를 활용한 실시간 데이터 수집 대신, S3에 정적 예제 데이터를 업로드하여 데이터 파이프라인 전반과 Bedrock을 사용한 SQL 쿼리 생성을 검증하였다.
참고사항
AWS Glue는 서버리스 기반의 완전관리형 데이터 통합 서비스로, 데이터를 수집(Extract), 변환(Transform), 적재(Load)하는 ETL 작업을 자동화할 수 있도록 도와준다.
Glue는 단순한 ETL 도구를 넘어서, AWS 데이터 분석 생태계의 허브 역할을 한다.
AWS Glue Crawler는 S3나 JDBC 연결 데이터베이스 등 다양한 소스를 주기적으로 스캔하고, 데이터의 구조(스키마)를 자동으로 인식하여 Glue Data Catalog에 테이블로 등록해주는 기능을 제공한다.
반복적인 스키마 정의 작업 없이, 신규 데이터가 유입될 때마다 자동으로 카탈로그화할 수 있어 운영 자동화에 유리하다.
AWS Glue ETL Job은 Glue에서 제공하는 핵심 기능으로, 데이터를 추출, 변환, 적재하는 작업을 코드 기반으로 자동 실행할 수 있게 한다.
AWS Glue Data Catalog는 AWS 전역에서 공유되는 중앙 메타데이터 저장소로, 테이블, 스키마, 파티션 등의 정보를 저장하고 관리한다.
Glue Data Catalog는 "데이터에 대한 설명서"이자, 분석 도구의 인덱스 역할을 한다.
Amazon Athena는 S3에 저장된 데이터를 SQL로 직접 분석할 수 있는 서버리스 쿼리 서비스다. 즉, 데이터를 이동시키지 않고도 분석할 수 있어 데이터 레이크 분석에 적합하다.
SQL만 알면 대용량 데이터를 빠르게 분석할 수 있는 매우 강력한 도구이다.
Amazon Bedrock은 생성형 AI 서비스를 손쉽게 애플리케이션에 통합할 수 있도록 지원하는 완전관리형 서비스이다. 다양한 기성 모델(Foundation Models, FMs)을 API 기반으로 제공하며, 사용자는 별도의 인프라 설정 없이 생성형 AI 기능을 애플리케이션에 바로 연동할 수 있다.
예시로, 자연어 질의(NLQ) → SQL 변환(Text-to-SQL) 과 같은 LLM 기반 인터페이스를 구축할 때, 언어 이해 및 생성 능력을 제공하는 핵심 서비스이다.
Amazon Bedrock Agent는 LLM 기반 애플리케이션에 “작업 실행 능력”과 “지식 기반 접근”을 부여하는 완전관리형 구성 요소로, 사용자의 자연어 요청을 분석하고 외부 API 호출, 데이터베이스 질의, 지식 참조 등 복잡한 작업을 자동으로 처리할 수 있게 한다.
Text-to-SQL 시스템에서 Bedrock Agent는 다음과 같이 활용될 수 있다:
질문 : "지난 분기의 제품별 매출을 알려줘"
→ Bedrock Agent는 지식 기반에서 과거 쿼리를 검색하고, 필요 시 Lambda를 호출해 테이블 스키마를 확인한 뒤, 정보를 바탕으로 SQL을 생성해 Athena에 질의한다.
Bedrock Agent는 LLM이 단순한 질문 응답을 넘어, “지능형 업무 수행자”로 진화하도록 만드는 핵심 서비스이다.
장점:
단점:
Bedrock Knowledge Base는 생성형 AI가 신뢰할 수 있는 사전 정의된 데이터에 기반하여 응답할 수 있도록 지원하는 기능으로, LLM + 벡터 기반 검색(RAG)을 결합하여 정확하고 맥락에 맞는 응답을 생성하도록 설계된 구성 요소이다.
Text-to-SQL 시스템에서는 사용자가 "작년 5월 매출 알려줘"
이라는 질의를 입력하면, Knowledge Base가 과거 SQL 예제 또는 스키마 문서 등을 참조해 LLM이 더 정확한 SQL 쿼리를 생성할 수 있도록 도와준다.
Knowledge Base는 단순 문서 검색을 넘어서, SQL 스키마 참조, 샘플 쿼리 제공, 시스템 호출 정보 등 실시간 의사결정을 위한 핵심 메타데이터 제공 역할을 수행하며, 생성형 AI의 정확도와 신뢰성을 높이는 핵심 도구이다.
Bedrock Embedding 기능은 입력된 텍스트를 벡터(Vector) 형태로 변환하는 임베딩 모델을 제공하며, 문맥 기반 검색 및 의미 기반 질의 대응을 가능하게 한다.
"매출이 높은 상품은?"
→ 의미 유사한 과거 질문을 벡터 기반으로 매칭Text-to-SQL 시스템에서, 사용자의 질의를 벡터로 변환하여 기존 쿼리/문서와 유사도 기반 검색(RAG 방식)을 수행하여 정확도 향상에 활용된다.
그리고 Bedrock의 RAG의 전반적인 이해를 위한 유튜브 링크도 참고하면 이해에 도움이 될 것이다.
Amazon OpenSearch는 검색 및 분석을 위한 오픈소스 기반 분산 검색 엔진 서비스로, 대규모 텍스트 데이터 및 벡터 데이터에 대한 빠른 검색이 가능하다.
특히, RAG(Retrieval-Augmented Generation) 구조에서, OpenSearch는 유사한 자연어 질의 또는 예제 SQL을 빠르게 검색하여, Bedrock의 생성 모델이 더 정확한 SQL을 생성하도록 지원한다.
실습에 필요한 샘플 데이터 및 코드 예시는 Github 링크를 참고하길 바라며, 본 문서에서는 해당 샘플을 기반으로 단계 별 실습을 진행한다.
데이터 경로 : glue-cralwers-src/user_info/user_info.jsonl
데이터 경로 : glue-cralwers-src/add_info/addd_info.jsonl
각각의 경로에는 sample_data폴더 아래에 있는 실습 파일이 있으며, 예시 데이터는 아래와 같다.
# user_info.jsonl
{"id":"Denise1","name":"Denise Hall","Age":35,"Gender":"F","Email":"jonathansims@hotmail.com","Country":"Japan"}
{"id":"Scott2","name":"Scott Walker","Age":53,"Gender":"M","Email":"elainesmith@yahoo.com","Country":"UK"}
...
]
# add_info.jsonl
{"id":"Denise1","JoinDate":"2022-02-05","IsActive":"TRUE","Role":"Viewer","Score":73.4}
{"id":"Scott2","JoinDate":"2023-04-27","IsActive":"FALSE","Role":"Admin","Score":81.2}
...
]
왜 json 형식은 Athena에서 불리한가?
→ JSON 배열은 구조를 미리 알 수 없어 schema 추론이 어렵고, 분산처리 성능도 떨어지기 때문이다.
항목 | JSON | JSONL |
---|---|---|
구조 | 배열로 한 번에 감쌈 | 각 줄마다 독립 객체 |
Athena 호환성 | 낮음 | 높음 |
처리 성능 | 낮음 | 높음 |
크롤링 가능성 | 낮음 | 높음 |
먼저 데이터를 수집하기 위한 Crawlers 설정을 진행한다.
크롤링을 위해 Data source는 이전 S3에 업로드했던 경로를 선택하고, user_info, add_info 경로를 각각 선택하여 크롤러를 2개를 생성한다.
실습은 각 경로에 있는 jsonl 파일로 Gule ETL Job 으로 id
값을 기준으로 통합하겠다.
생성할 크롤러
1. crawler-add_info
2. crawler-user_info
# Glue Crawler IAM Role
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::glue-crawlers-src/*",
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::glue-crawlers-dst/*"
]
}
]
}
크롤러를 실행하기 위해서는 위와 같은 S3에 대한 권한이 있어야 하며, AWSGlueServiceRole 정책도 함께 필요하다. 왜냐하면 크롤러 실행 이후 Glue ETL 작업도 추가적으로 수행해야 하기 때문이다.
크롤링한 데이터를 저장할 대상 데이터베이스를 선택을 한다면 Crawlers 설정은 끝이다.
화면과 같이 각각의 크롤러를 선택 후 Run crawler를 클릭하여 실행한다.
glue-customer-db에 있는 Table 정보이며, 각각의 크롤러를 생성 후 실행하여 2개의 경로에 테이블이 생성이 된 것을 확인할 수 있다.
Column : id, joindate, isactive, role, score
Column : id, name, age, gender, email, country
여기서, add_info, user_info 테이블을 기준으로 Column name에는 id, name, joindate, 등 값이 있고, Data type에는 각 데이터의 타입을 자세히 확인할 수 있다.
JSON 포맷을 아래 jsonl 형태로 만들 시 Column name, Data type이 지정한 테이블 선택 시 형태가 보이는 것을 확인할 수 있었다.
# user_info.jsonl 포맷 예시
{"id":"Denise1","JoinDate":"2022-02-05","IsActive":"TRUE","Role":"Viewer","Score":73.4}
{"id":"Scott2","JoinDate":"2023-04-27","IsActive":"FALSE","Role":"Admin","Score":81.2}
Athena에서는 배열 형태의 JSON은 파싱이 불가능하므로, 각 객체가 한 줄씩 기록된 JSONL 형식이 요구된다.
만약 json 으로 하더라도, 테이블에서 datatype 을 미리 볼 수 없다는 것을 참고해야한다.
# user_info.json 포맷예시
[
{
"id": "Denise1",
"JoinDate": "2022-02-05",
"IsActive": "TRUE",
"Role": "Viewer",
"Score": 73.4
},
{
"id": "Scott2",
"JoinDate": "2023-04-27",
"IsActive": "FALSE",
"Role": "Admin",
"Score": 81.2
}
]
add_info 테이블에서 json 을 예시 json 처럼 S3에 업로드 후 크롤링을 한다면 위와 같이 컬럼에 대한 이름과 데이터 타입이 다른 것을 확인할 수 있다.
실제 데이터를 수집한 데이터는 array 인 배열 형태로 저장이 되는 것을 확인을 할 수 있다.
위와 같이 쿼리 시 실패가 되었고, 일반적인 json 포맷이 athena 에서 지원하지 않기 때문이다.
우리는 위와 같이 데이터를 유효한 형식으로 변환해야 athena를 사용하여 쿼리를 할 수 있는 것을 확인하였다.
user_info
와 add_info
테이블은 공통 ID 값을 기준으로 병합할 수 있으며, 이 작업은 Glue ETL Job을 통해 수행된다. 병합된 결과는 분석에 적합한 Parquet 포맷으로 저장된다.
먼저, Script editor 를 선택 후 Engine은 Spark 로 선택을 한다.
etl_script/customer-etl-spark.py 파일을 사용해 테이블을 병합하는 스크립트이며, Job details 에서 사용할 IAM Role 및 실행한 스크립트 파일, 로그 등 저장할 S3 를 선택하여 저장 후 실행을 한다.
추가적으로 etl_script/customer-etl-python.py 파일을 통해서도 가능하지만, Glue Python Shell은 기본적으로 pandas
, pyarrow
가 없기에, .zip
파일로 압축 후 S3에 업로드하여 Glue Job의 Python library path를 지정해야한다.
ETL 작업이 끝난 후, glue-crawlers-dst/customer/ 아래의 데이터를 크롤러를 통해 데이터 카탈로그화하고, 지정한 DB에서 확인한 화면이며, 해당 테이블의 스키마 정보를 확인 시 user_info
, add_info
테이블을 "id
" 기준으로 join 하여 총 10개의 컬럼이 있는 것을 확인할 수 있다.
참고로, Parquet(파켓) 형태로 저장한 이유는 압축률이 높고, 쿼리 성능을 향상시키며, 효율적인 데이터 액세스를 제공하여 많은 이점을 가지고 있기 때문이다.
만약 해당 포맷을 하고 싶지 않다면, ETL 작업 시 다른 포맷을 꿔서 작업을 진행하면 된다.
glueContext.write_dynamic_frame.from_options(
frame=joined_dyf,
connection_type="s3",
connection_options={"path": "s3://<DestinationBucket>/<DestinationPath>/"},
format="parquet" # 필요 시 format 변경
)
먼저, Athena를 사용하기 위해서 쿼리 결과를 저장할 버킷을 설정해야 쿼리가 가능하다.
SELECT *
FROM "glue-customer-db"."customer"
limit 10000;
쿼리문은 간단하게 Select 문을 통해 모든 데이터를 확인 시 모든 데이터에 대한 정보가 확인되는 것을 확인하였다.
다음은 AWS 공식 문서에서 설명하는 지식 기반의 작동 방식으로, RAG(Retrieval-Augmented Generation)를 통해 사용자 쿼리에 어떻게 응답을 보완하고 증강하는지를 시각적으로 보여주는 그림이다.
이후 생성할 Bedrock Agent 에서 SQL문에 대한 정확성과 신뢰도를 높이기 위해 지식 기반을 만들어야 한다. 자연어로 질문하면 Bedrock Agent는 쿼리할 데이터베이스에 어떤 스키마값이 정의 되어 있는지 알 수 없다. 그렇기 때문에 사전에 학습할 데이터를 추가 후 RAG(검색 증강) 기반 응답을 생성할 수 있도록 한다.
RAG(Retrieval-Augmented Generation)란?
RAG는 기존 LLM이 가진 지식의 한계를 보완하기 위해 고안된 구조이다.
단순히 모델에 질문을 던지는 것이 아닌, 질문에 관련된 외부 지식을 검색하여 함께 답변을 생성한다.
RAG 아키텍처
RAG 아키텍처 작동 흐름
추가적인 설명이 필요하다면, Amazon Bedrock 지식 기반을 사용한 간소화된 RAG 구현해당 문서도 함께 참고하면 좋을 것 같다.
# 지식 기반의 신뢰 관계
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AmazonBedrockKnowledgeBaseTrustPolicy",
"Effect": "Allow",
"Principal": {
"Service": "bedrock.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<Account>"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:bedrock:ap-northeast-2:<Account>:knowledge-base/*"
}
}
}
]
}
# 지식 기반에서 사용할 IAM Role의 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BedrockInvokeModelStatement",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": [
"arn:aws:bedrock:ap-northeast-2::foundation-model/amazon.titan-embed-text-v2:0"
]
},
{
"Sid": "OpenSearchServerlessAPIAccessAllStatement",
"Effect": "Allow",
"Action": [
"aoss:APIAccessAll"
],
"Resource": [
"arn:aws:aoss:ap-northeast-2:<Account>:collection/<Opensearch Collection ID>"
]
},
{
"Sid": "S3ListBucketStatement",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::<Knowledge-base-bucket>"
]
},
{
"Sid": "S3GetObjectStatement",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::<Knowledge-base-bucket/prefix/*>"
]
}
]
}
지식 기반에서는 위와 같은 Role, Policy를 미리 생성하거나, 지식 기반 생성 시 신규로 생성이 되도록 할 수 있다.
Data source는 S3를 선택하며, 지정한 버킷에 txt
형태로 샘플쿼리, 샘플스키마를 업로드 할 예정이다.
관련하여 설정을 하고, 실제 소스 버킷을 선택을 한다.
S3로 데이터 소스를 사용 시 Bedrock Knowledge Base에서는 JSON 파일 형식은 현재 지원되지 않아, 텍스트(.txt) 형식으로 변환하여 업로드해야한다. 기존 json
을 txt
로 바꿔서 지식 기반을 마련하였다.
참고로, 지식 기반 데이터의 사전 조건 해당 문서에서 지원하는 확장자를 확인할 수 있다.
현재는 서울 리전 기준으로는 Titan Text Embeddings V2
임베딩 모델만 지원을 한다.
Vector Embedding 이란?
텍스트를 고차원 공간에서 수치화된 벡터로 변환하는 과정
텍스트의 의미와 유사성 파악을 위해 필수적인 단계이며, Embedding 결과는 벡터 공간에서 점으로 표현된다.
Vector Embedding 과정
위 예시처럼 Opensearch 개발자 도구에서 샘플 쿼리에 대한 자연어 질문과 이에 매핑되는 벡터 임베딩 필드와 샘플 스키마의 경우 테이블, 컬럼에 대한 정보도 텍스트와 벡터 임베딩 필드로 저장이 된다.
해당 문서들은 LLM이 사용자의 요청에 알맞은 쿼리를 작성하기 위해 매우 중요한 사전 정보이다.
# OpenSearch 색인 데이터 조회 쿼리
GET bedrock-knowledge-base-default-index/_search
{
"_source": [
"x-amz-bedrock-kb-source-uri",
"AMAZON_BEDROCK_TEXT",
"AMAZON_BEDROCK_METADATA",
"x-amz-bedrock-kb-data-source-id",
"bedrock-knowledge-base-default-vector"
],
"query": {
"match_all": {}
}
}
# 예시 데이터
{
...중략...
{
"_index": "bedrock-knowledge-base-default-index",
"_id": "1%3A0%3Avucz65YBOyFG24q_6Wu-",
"_score": 1,
"_source": {
"x-amz-bedrock-kb-source-uri": "s3://sample-genai-base/sample_query.txt",
"AMAZON_BEDROCK_TEXT": "Input: 테이블의 모든 정보 Query: SELECT * FROM customer; Input: 테이블에서 2023년 이후에 합류한 사용자의 모든 데이터 Query: SELECT * FROM customer WHERE joindate >= '2023-01-01'; Input: 나이가 30살 이상 35 이하이고, 성별이 여자인 사용자 Query: SELECT * FROM customer WHERE age BETWEEN 30 AND 35 AND gender = 'F'; Input: 최근 가입한 활성 사용자 중 상위 점수 보유자 5명 Query: SELECT id, name, score, joindate FROM customer WHERE isactive = 'TRUE' ORDER BY joindate DESC, score DESC LIMIT 5; Input: 국가별 평균 점수가 전체 평균보다 높은 국가 Query: SELECT country, AVG(CAST(score AS DOUBLE)) AS avg_score FROM customer GROUP BY country HAVING AVG(CAST(score AS DOUBLE)) > (SELECT AVG(CAST(score AS DOUBLE)) FROM customer); Input: 역할별 최고 점수를 받은 사용자 Query: SELECT c.* FROM customer c JOIN (SELECT role, MAX(CAST(score AS DOUBLE)) AS max_score FROM customer GROUP BY role) r ON",
"AMAZON_BEDROCK_METADATA": """{"source":"s3://sample-genai-base/sample_query.txt"}""",
"x-amz-bedrock-kb-data-source-id": "QV0TLKQ21Y",
"bedrock-knowledge-base-default-vector": [
-0.049746715,
0.018723352,
-0.060133394,
0.021456689,
-0.043460045,
0.09621343,
-0.0063891727,
0.0012812513,
0.02692336,
-0.0329367,
...중략...
}
...중략...
}
OpenSearch Dev Tools에서 실행한 _search
쿼리와 그 결과의 내용을 기반으로, 각 구성 요소와 의미를 정리하였다.
필드 설명
x-amz-bedrock-kb-source-uri
AMAZON_BEDROCK_TEXT
AMAZON_BEDROCK_METADATA
x-amz-bedrock-kb-data-source-id
bedrock-knowledge-base-default-vector
AMAZON_BEDROCK_TEXT
의 임베딩 벡터 (고차원 수치 표현)Knowledge Bases 에서 기본 청킹을 진행하게 된다면 텍스트를 약 300개의 토큰 크기(청크)로 분할을 한다. Bedrock 에서 테스트나 실제 사용에는 문제가 없었지만, OpenSearch에서 인덱스의 내용을 읽기에 불편함이 발생한다.
위 예시처럼 기본 청킹 결과는 한눈에 쿼리문을 확인하기 어려워, 가독성이 떨어질 수 밖에 없다.
내가 원하는 조건으로 청킹을 진행한다면, 인덱스의 정보를 더 쉽게 확인할 수 있을 것이다.
문맥 단절
기본 청킹은 보통 300~500개 토큰 단위로 텍스트를 일정하게 나누기 때문에, SQL 쿼리문처럼 하나의 의미 덩어리로 묶여야 할 내용이 중간에 끊겨버릴 수 있다. 이로 인해 청크 하나만 봐서는 전체 의미를 파악하기 어려워진다.
OpenSearch에서의 가독성 저하
OpenSearch Dashboards에서는 각 도큐먼트(document)의 _source
필드를 한눈에 확인하기 때문에, 청크가 어중간하게 끊겨 있으면 input
과 query
전체를 보기가 어렵고, 특정 쿼리나 질문을 바로 찾기 불편하다.
즉, 사람이 보기엔 비직관적이다.
지식 기반 응답 품질 저하 가능성
Bedrock은 청크 단위로 의미를 파악하기 때문에, 청킹된 데이터가 애매하면 응답 품질이 떨어질 수 있다.특히 질의와 응답(SQL)이 하나의 단위로 있어야 학습/검색 효율이 높아지는데, 그게 깨질 수 있다.
먼저, 커스텀 청킹을 위해서 작성자는 Opensearch Serverless를 미리 생성하였으며, 벡터 인덱스 이름, 필드 등 위와 같이 설정을 하였다. 지식 기반을 생성할 때 위와 같이 설정을 진행하였다.
벡터 인덱스 이름 : text2sql-index
벡터 필드 이름 : text2sql-vector
텍스트 필드 이름 : text2sql-text
메타데이터 필드 이름 : text2sql-metadata
Opensearch의 컬렉션에서 인덱스 생성 시 지식 기반에서 설정한 인덱스, 필드 등 동일하게 맞춰야 한다.
설정에서 유의할 점이청킹 전략에서 ‘청킹 없음’을 선택해야, Bedrock이 자동으로 처리하지 않고 Lambda에서 커스텀 청킹이 가능하다.
여기서 S3 source는 sample_knowedge-bases폴더 아래의 json 데이터만 있는 버킷을 지정하였다.
Lambda 함수용 S3 버킷이 필요한 이유는 기존 Source 버킷의 파일 내용을 JSON 형태로 가져와 Lambda 로 처리하기 위해 존재한다.
# Bedrock 동기화 시 Lamba 로 전달 받는 이벤트 예시
{
"version": "1.0",
"knowledgeBaseId": "KQXIRZR24J",
"bucketName": "sample-genai-base-result",
"dataSourceId": "XOJT4DO3QJ",
"ingestionJobId": "CYOVVSL4VA",
"priorTask": "CHUNKING",
"inputFiles": [
{
"contentBatches": [
{
"key": "aws/bedrock/knowledge_bases/KQXIRZR24J/XOJT4DO3QJ/CYOVVSL4VA/sample_query_1.JSON"
}
],
"originalFileLocation": {
"type": "S3",
"s3_location": {
"uri": "s3://text-to-sql-base/sample_query.json"
}
}
}
]
}
내용에 대한 자세한 사항은 Lambda를 통한 데이터 청킹해당 문서를 참고하면 좋을 것 같다.
커스텀 청킹에 관련하여 내용을 이해와 return을 위한 json 포맷 많은 어려움이 있었다. Github의 Lambda 청킹 함수를 참고하길 바라며...
또한, 청킹 관련하여 좀 더 자세한 사항은 문서인 컨텐츠 청킹에서 좀 더 다양한 내용을 확인할 수 있다.
지식 기반에서 데이터 소스 생성이 완료 되었다면, 동기화를 클릭 후 Lambda가 실행이 되었다면, 아래 S3를 확인해보자.
Lambda용 S3 버킷에 aws/, chunk/ 라는 폴더가 생성이 되었다.
aws 폴더에는 위 설명과 같이 Source 버킷의 파일 내용을 JSON 형태로 가져온 파일들이 있을 것이다.
aws/bedrock/knowledge_bases/<지식 기반 ID>/<데이터 소스 ID>/<작업 ID>/ 경로 확인 시 위와 같이 파일이 존재하며, 파일의 내용 예시는 아래와 같다.
{
"fileContents": [
{
"contentType": "PLAIN_TEXT",
"contentBody": "<본문 내용>",
"contentMetadata": {}
}
]
}
# sample_query_1.JSON
{
"fileContents": [
{
"contentType": "PLAIN_TEXT",
"contentBody": "[\n {\n \"input\": \"테이블의 모든 정보\",\n \"query\": \"SELECT * FROM customer;\"\n },\n {\n \"input\": \"2023년 이후에 합류한 사용자의 모든 데이터\",\n \"query\": \"SELECT * FROM customer WHERE joindate >= '2023-01-01';\"\n }\"input\": \"가장 많은 사용자를 보유한 역할과 그 사용자들의 평균 점수\",\n \"query\": \"SELECT role, ROUND(AVG(CAST(score AS DOUBLE)), 1) AS avg_score FROM customer WHERE role = (SELECT role FROM customer GROUP BY role ORDER BY COUNT(*) DESC LIMIT 1) GROUP BY role;\"\n }\n]",
"contentMetadata": {}
}
]
}
chunk/ 폴더 아래에는 커스텀하게 청킹한 22개의 객체가 있는 것을 확인할 수 있다.
이전 생성했던 인덱스를 확인 시 문서는 총 22개로 Lambda를 사용한 커스텀 청킹 문서의 갯수 만큼 존재하는 것을 확인할 수 있다.
또한, 지식 기반 테스트에서는 샘플 쿼리에 대한 쿼리문 요청 시 Opensearch에 인덱싱된 필드를 참조하여 답변을 준 것을 확인할 수 있다.
커스텀 청킹을 하지 않았다면, Details 를 눌러 어떤 소스를 참고했는지 확인 시 가독성이 현저하게 떨어지는 것을 알 수 있다.
위는 Opensearch에서 index에 존재하는 각 Document와 Field에서는 샘플 쿼리에 대한 내용이 하나씩 있는 것을 확인할 수 있다.
커스텀 청킹을 통해서 Bedrock을 더 신뢰성과 정확성을 높이고자 한 내용이며, 이 부분은 사용자에 맞게 선택 사항으로 작업을 진행하면 될 것 같다.
Bedrock Agent는 프롬프트, 지식 기반, 사용자 요청을 조합하여 자동 질의응답 시스템을 구성할 수 있는 서비스이며, 사용자가 직접 Agent를 손쉽게 커스텀하여, 단순한 LLM 호출이 아니라 고급 제어와 설정을 통해서 자연어에 대한 SQL문 작성을 위해 설정을 진행한다.
# Bedrock Agent 의 신뢰 관계
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AmazonBedrockAgentBedrockFoundationModelPolicyProd",
"Effect": "Allow",
"Principal": {
"Service": "bedrock.amazonaws.com"
},
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "<Account>"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:bedrock:ap-northeast-2:<Account>:agent/*"
}
}
}
]
}
# Bedrock Agent 의 정책
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AmazonBedrockAgentBedrockFoundationModelPolicyProd",
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": [ # 사용할 모델 기입
"arn:aws:bedrock:ap-northeast-2::foundation-model/anthropic.claude-v2:1",
"arn:aws:bedrock:ap-northeast-2::foundation-model/anthropic.claude-3-haiku-20240307-v1:0",
"arn:aws:bedrock:ap-northeast-2::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"
]
},
{
"Sid": "AmazonBedrockAgentRetrieveKnowledgeBasePolicyProd",
"Effect": "Allow",
"Action": [
"bedrock:Retrieve"
],
"Resource": [
"arn:aws:bedrock:ap-northeast-2:<Account>:knowledge-base/<knowledge-base-id>"
]
}
]
}
에이전트 빌더
설정에서 사용할 Model 은 Claude 3.5 Sonnet
이며, 에이전트 지침은 agent_instructions.txt 파일의 내용을 참고하면 좋은 응답을 받을 수 있을 것이다.
사용자 입력 기능이 비활성화된 경우, 에이전트는 명확한 추가 정보를 얻지 못해 지침을 제대로 따르지 못할 수 있다. 지침에 맞춰서 답변을 받아야 하기에 현재 설정에서 활성화가 필수이다.
조건
사용자 입력 기능
왜 지침을 따르지 않을까?
이전 대화의 내용에서 사용자 요청에 대하여 정확하고 신뢰성 높은 답변을 위해서는 메모리 기능을 사용한다.
메모리 기능
Bedrock 에이전트의 메모리는 이전 대화 이력을 요약하여 저장하고, 이후 세션에서 이를 참조할 수 있는 기능입니다. 이를 통해 에이전트는 사용자와의 연속된 맥락을 인식하고 일관된 응답을 제공한다.
메모리 주요 구성 요소
테스트
"2023년 이후에 합류한 사용자의 모든 데이터" 라는 요청을 하였고, 샘플 쿼리에 있는 내용 중 하나로 질문 시 에이전트가 지식 기반을 활용하여 질문과 가장 근접한 내용을 찾고, 이에 대한 내용을 반환하는 것을 확인할 수 있었다.
--생성한 SQL
SELECT * FROM customer WHERE joindate >= '2023-01-01';
--지식 기반에 있는 샘플 쿼리
SELECT * FROM customer WHERE joindate >= '2023-01-01';
"2024년 이후에 합류한 사용자에서 나이가 30살이상 35살 이하" 인 데이터 요청 시 지식 기반에서는 샘플 쿼리가 없었는데, LLM 자체가 자연어 이해 + SQL 작성 능력을 통해 질문을 이해하고 SQL 문법을 적용해 스스로 쿼리를 생성을 한 것을 확인할 수 있었다.
--생성한 SQL
SELECT * FROM customer WHERE CAST(joindate AS DATE)>=DATE'2024-01-01' AND CAST(age AS INTEGER)BETWEEN 30 AND 35;
동작 단계
질문을 이해하고 SQL 쿼리를 생성하는 프로세스는 다음과 같다.
단계 | 설명 |
---|---|
1. 자연어 파싱 | 사용자의 질문을 문법적으로 분석하고, 핵심 조건을 추출 예: “2024년 이후”, “나이 30~35세” |
2. 조건 매핑 | SQL 조건으로 바꿔야 할 부분을 식별 예: 날짜 비교는 CAST(joindate AS DATE) >= DATE '2024-01-01' , 나이는 CAST(age AS INTEGER) BETWEEN 30 AND 35 |
3. 테이블 구조 추정 | "customer" 테이블에 있는 컬럼 이름들(id, name, joindate, age 등)을 기존 답변이나 문맥에서 유추하거나 전형적인 이름을 사용 |
4. SQL 생성 | 위의 조건과 구조를 바탕으로 전체 SQL 문을 구성 |
5. 설명 생성 | 쿼리에 대한 설명을 자연어로 만들어 사용자에게 함께 제공 |
지식 기반이 없어도 답변할 수 있는 이유
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults"
],
"Resource": "*"
},
{
"Sid": "VisualEditor1",
"Effect": "Allow",
"Action": [
"glue:GetDatabase",
"glue:GetTable",
"glue:GetDatabase"
],
"Resource": [
"arn:aws:glue:ap-northeast-2:<Account>:catalog",
"arn:aws:glue:ap-northeast-2:<Account>:table/<Glue DB Name>/<Glue Table Name>",
"arn:aws:glue:ap-northeast-2:<Account>:database/<Glue DB Name>"
]
},
{
"Sid": "VisualEditor2",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::<Athena Result Bucket>",
"arn:aws:s3:::<Athena Result Bucket>/*"
]
},
{
"Sid": "VisualEditor3",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::<Query Bucket>",
"arn:aws:s3:::<Query Bucket>/*"
]
},
{
"Sid": "VisualEditor4",
"Effect": "Allow",
"Action": [
"bedrock:InvokeAgent"
],
"Resource": [
"arn:aws:bedrock:ap-northeast-2:<Account>:agent-alias/<Agent ID>/*"
]
}
]
}
Text-to-SQL 환경 구성 시 위와 같은 정책이 EC2에 부여가 되어야 한다.
Bedrock Agent
를 호출하여 SQL문을 요청해야 하며, Glue
, Athena
, S3
는 Bedrock에서 응답받은 SQL문을 통해 쿼리를 수행해야한다.
# 패키지 및 개발 도구 설치
yum update -y
yum groupinstall -y "Development Tools"
yum install -y gcc openssl-devel bzip2-devel libffi-devel wget
# zlib-devel xz-devel readline-devel sqlite-devel
# Python 3.9.18 다운로드 및 압축 해제
cd /usr/src
wget https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz
tar xzf Python-3.9.18.tgz
cd Python-3.9.18
# Python 컴파일 및 설치
./configure --enable-optimizations
make
make altinstall # 기존 python 보존
cd ..
# pip 설치
curl -O https://bootstrap.pypa.io/get-pip.py
python3.9 get-pip.py
# 심볼릭 링크 (선택적)
ln -s /usr/local/bin/python3.9 /usr/bin/python3
ln -s /usr/local/bin/pip3.9 /usr/bin/pip3
# 설치 확인 (작성자 기준)
python3.9 --version
-> Python 3.9.18
python3 --version
-> Python 3.9.22
readlink -f $(which python3.9)
-> /usr/local/bin/python3.9
eadlink -f $(which python3)
-> /usr/bin/python3.9
pip3.9 --version
-> pip 23.0.1 from /usr/local/lib/python3.9/site-packages/pip (python 3.9)
환경은 AmazonLinux2023
으로 진행을 하였고, Python버전은 3.9.18
버전으로 진행하였다.
작성자를 기준으로 설정한 내용이며, Python 버전에 따른 패키지 버전도 차이가 발생할 수 있다는 점을 유의해야 한다.
# 홈 디렉토리로 이동
cd ~
# 프로젝트 디렉토리 생성 및 이동
mkdir text2sql
cd text2sql
# Python 3.9 기반 가상환경 생성
python3.9 -m venv venv
# 가상환경 활성화
source venv/bin/activate
# requirements.txt에 정의된 패키지 설치
pip3.9 install -r requirements.txt
# 설치된 패키지 목록 확인
pip3.9 list
# 아래 패키지는 필수 설치이며, 의존성에 의해 다른 패키지도 함께 설치가 된다. (작성자 기준)
streamlit==1.45.1
boto3==1.38.19
pandas==2.2.3
# Streamlit 앱 백그라운드 실행, nohup은 터미널 종료 후에도 계속 실행
nohup streamlit run app.py --server.port=8080
프로젝트를 위한 디렉토리 설정 및 애플리케이션 기동 시 연결된 ALB, 또는 EC2 로 지정한 포트로 접속이 가능하다.
ALB 사용한다면, 80 or 443 Port로 사용하며, EC2 직접 접근 시에는 지정한 Port로 브라우저에서 접속을 진행한다.
Text-to-SQL 부분을 더 자세하게 풀어낸 아키텍처이다.
처음 질문은 지식 기반에 있는 내용이며, "2023년 이후에 합류한 사용자의 모든 데이터"를 보여달라고 하였고, AWS 콘솔에서 테스트한 쿼리문과 동일한 내용을 반환한 것을 확인할 수 있었다.
"2024년 이후에 합류한 사용자에서 나이가 30살이상 35살 이하인 사용자의 모든 데이터도 보여줘" 라는 질문을 하였고, AWS 콘솔에서 테스트한 쿼리문과는 다르지만, 결과는 동일한 쿼리문을 작성하여 데이터를 반환해준 것을 확인을 하였다.
--AWS 콘솔 기반 테스트
SELECT * FROM customer WHERE CAST(joindate AS DATE)>=DATE'2024-01-01' AND CAST(age AS INTEGER)BETWEEN 30 AND 35;
--Bedrock API를 사용한 테스트
SELECT * FROM customer WHERE joindate >= '2024-01-01' AND age BETWEEN 30 AND 35;
Chat GPT를 사용하여 "2023년 이후 가입한 사용자 중, 각 역할별 점수가 해당 역할 평균 이상이며, 이름이 K로 시작하는 사용자를 조회하는 쿼리랑 결과를 알려주는 SQL문을 작성해줘" 라는 입력을 하였다.
먼저 위 쿼리에서는 joindate
컬럼의 데이터 타입이 varchar
인 반면, 비교 대상은 DATE '2023-01-01'
로 DATE
타입이기 때문에 쿼리가 실패할 것이다. 즉, varchar
타입과 date
타입 간 비교는 허용되지 않는다.
그렇기에 아래와 같은 쿼리로 수정하여 Athena 에서 직접 쿼리를 수행한 결과를 보겠다.
SELECT *
FROM customer c
WHERE CAST(joindate AS DATE) >= DATE '2023-01-01'
AND name LIKE 'K%'
AND CAST(score AS DOUBLE) >= (
SELECT AVG(CAST(score AS DOUBLE))
FROM customer
WHERE role = c.role
);
수정된 쿼리 사용 시 총 14개의 데이터가 나온 것을 확인할 수 있었다.
"2023년 이후 가입한 사용자 중, 각 역할별 점수가 해당 역할 평균 이상이며, 이름이 K로 시작하는 사용자를 조회하는 쿼리랑 결과를 알려줘" 라는 질문을 하였고, GPT가 구현한 SQL과 Bedrock을 사용하여 나온 쿼리의 결과는 동일한 결과값을 보여준 것을 확인할 수 있었다. (쿼리 결과는 0번 row부터 시작하여 13번이 마지막)
결론적으로 GPT는 데이터에 대한 정보가 부족하여 joindate
컬럼의 데이터 타입을 알 수 없어, 쿼리문 수정을 했지만, Bedrock의 지식 기반 덕분에 테이블 스키마 정보를 이해하고, 정확한 쿼리를 한 번에 생성할 수 있었다.
요즘 GenAI가 뜨거운 주제인데, 이번 Text-to-SQL 시스템을 직접 만들어보면서 왜 많은 기업들이 이런 서비스를 도입하는지 실감할 수 있었다. 실제로 AI 서비스를 활용하니 반복 작업이 줄고, 분석 흐름도 훨씬 효율적으로 구성할 수 있어서 생산성이 눈에 띄게 향상된다는 걸 느꼈다.
처음 다뤄보는 서비스들이 많아 어려움도 있었지만, 하나씩 정리하고 개념을 익히며 직접 써보는 과정을 통해 점점 익숙해졌고, 결국 나만의 AI 기반 분석 시스템을 완성할 수 있었다는 점에서 큰 보람을 느꼈다.
특히, S3 기반의 데이터 수집부터 Glue ETL, Athena 분석, 그리고 Bedrock + OpenSearch를 활용한 자연어 질의 응답까지 전체 데이터 파이프라인을 직접 구성해보면서 AWS 생태계 내에서의 통합적 데이터 활용 방안을 몸소 체험할 수 있었다.
그 중에서도 Bedrock의 Agent와 Knowledge Base를 활용한 RAG 기반 아키텍처는 응답 정확도나 실무 적용 가능성 면에서 꽤 인상적이었고, 앞으로 AI 기반 분석 플랫폼을 구축할 때 강력한 무기가 될 수 있다고 생각했다.
무엇보다도, 처음에는 어렵게만 느껴졌던 생성형 AI 서비스들을 익히고 활용해보며 '나도 AI 서비스를 직접 만들 수 있구나' 하는 자신감을 얻은 것이 이번 경험에서 가장 큰 수확이었다.