실제 서비스에서 Knowledge Bases for Amazon Bedrock 활용 (with API, LangChain)

Seongwoo Choi
15 min readFeb 18, 2024

--

Knowledge Bases for Amazon Bedrock 출시 이후, 직접 자료를 임베딩하여 벡터 데이터베이스를 구축하는 번거로움이 크게 줄어들었다. 많은 과정이 추상화되어 단순히 Knowledge Bases의 API 호출만으로도 RAG로 LLM의 답변을 강화할 수 있게 되었다.
이번 포스팅에서는 다양한 Knowledge Bases API의 소개와 LangChain 생태계와의 통합 등 실제 서비스에서의 Knowledge Bases 사용법에 대해 고민한 내용을 담아보려 한다.

벡터 데이터베이스 빠르게 생성하기

OpenSearch Serverless를 벡터 저장소로 하여 가장 간단하게 Knowledge Bases를 구축할 수 있다. Knowledge Bases 콘솔로 진입 후 지식 기반 생성을 시작한다. Knowledge Bases의 소스로서 S3가 사용되기 때문에, 사전에 S3 버킷을 생성하고 임베딩을 원하는 파일을 넣어두어야 한다.

Knowledge Bases — Data Source Setting

데이터 소스 설정 단계에서 S3 소스를 명시하며, [고급 설정]에서 chunk를 어떻게 자를 것인지에 대해 설정하는 청킹 전략(Chunking strategy)를 설정할 수 있다. 아래와 같은 세 가지 옵션이 존재하며, 이번 포스팅에서는 기본 청킹으로 설정하겠다.

  • 기본 청킹 : 소스 데이터를 300개의 토큰을 포함하는 chunk로 분할
  • 고정 크기 청킹 : 20개부터 Titan Text Embedding의 최댓값인 8192개까지의 토큰을 포함한 chunk로 분할하며, 퍼센트를 명시하여 chunk 간 겹치는 구간(sliding window) 정의
  • 청킹 없음 : 사전에 문서에 대한 chunking을 완료한 경우 사용하며, 소스를 추가 chunk로 자르지 않음
Knowledge Bases — Embedding and Vector Database

[새로운 벡터 저장소 빠른 생성]을 통하면 별도의 수동 과정 필요 없이 Bedrock이 대신 OpenSearch Serverless 벡터 저장소를 생성해 주고, 데이터 소스를 임베딩 후 쿼리할 수 있는 형태로 만들어 준다. 모든 과정이 원클릭으로 구성되기 때문에 편리하다. 다만 아래와 같은 몇 가지 주의점이 있다.

  • 이중화 활성화 : 프로덕션용으로 사용 시에 가용성 보장을 위해 Multi-AZ 구성이 필요하다. 활성화한 경우 OCU 4개로 한 달에 $702.72 가 부과되고, 비활성화하면 그 절반인 $301.36 이 부과된다.
  • Knowledge Bases 삭제 시 : Bedrock이 생성해 준 OpenSearch Serverless까지 같이 삭제되지는 않는다. 불필요한 과금을 방지하기 위해 수동 삭제 혹은 IaC 구성이 필요하다.
Knowledge Bases — Data Source Sync

생성 완료 후에는 데이터 소스를 동기화해주는 과정이 한 번 필요하다. 이후 데이터 소스가 변경될 때마다 Sync 작업을 해줄 수 있다. 서비스 로직 중에 추가적인 동기화 작업이 필요하다면 StartIngestionJob API를 호출해야 한다.

만약 OpenSearch Serverless가 아니라 pgvector를 통하여 테스트 용도로 보다 비용 효율적인 구축을 원한다면, 필자의 쉽고 경제적인 RAG 구축을 위한 Knowledge Bases for Amazon Bedrock 안내서 포스팅을 읽어보기를 추천한다.

Knowledge Bases API 사용 가이드

구성된 Knowledge Bases를 통해 retrieve retrieve_and_generate 2가지 작업을 수행할 수 있다. 사용하기 위한 준비물로는 Knowledge Bases의 ID만이 필요하다. ID는 10자리의 난수(e.g. 0A1BC4D5E6)로 설정되어 있다.

아래 각각 예시 코드는 AWS Lambda에서 작동하는 코드로, Python 3.12(boto3)를 통해 구현하였다. 현재 Lambda에 내장된 boto3에는 bedrock-agent-runtime이 포함되어 있지 않다. 필요한 경우 zip 파일을 layer로 올려주어야 한다.

  • retrieve

질문과 가장 의미론적으로 유사한 문서 내용과 참조 위치를 여러 개 반환한다. 유사도 점수 또한 0~1 사이의 값으로 반환한다. LLM으로 답변을 생성하지 않고 관련한 레퍼런스만을 조회할 때 유용하다.

import boto3

bedrock_agent_runtime = boto3.client(
service_name = "bedrock-agent-runtime"
)

def retrieve(query, kbId, numberOfResults=5):
return bedrock_agent_runtime.retrieve(
retrievalQuery= {
'text': query
},
knowledgeBaseId=kbId,
retrievalConfiguration= {
'vectorSearchConfiguration': {
'numberOfResults': numberOfResults
}
}
)

def lambda_handler(event, context):
response = retrieve("생애최초 특별공급은 어떻게 신청하나요?", "{KnowledgeBaseID}")
results = response["retrievalResults"]
return results
  • retrieve_and_generate

RAG로 LLM을 통한 답변을 생성한다. 아래 코드에서 KnowledgeBaseID 값만 수정해 주면 바로 작동 가능하다. 추가적으로 Lambda 실행 권한에 Bedrock에 대한 IAM Policy가 필요하다.

import boto3

bedrock_agent_runtime = boto3.client(
service_name = "bedrock-agent-runtime"
)

def retrieve(query, kbId):
modelArn = 'arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-v2:1'

return bedrock_agent_runtime.retrieve_and_generate(
input={
'text': query,
},
retrieveAndGenerateConfiguration={
'type': 'KNOWLEDGE_BASE',
'knowledgeBaseConfiguration': {
'knowledgeBaseId': kbId,
'modelArn': modelArn,
}
}
)

def lambda_handler(event, context):
response = retrieve("생애최초 특별공급은 어떻게 신청하나요?", "{KnowledgeBaseID}")
output = response["output"]
citations = response["citations"]

return output

특기할 만한 사항은 일반적인 RAG와는 다르게 전체 답변을 레퍼런스에 해당하는 몇 개의 chunk로 잘라서 참조 문서와 함께 전달해 준다는 점이다. LangChain을 활용한 RAG에서는 하나의 답변 + 참조 문서 chunk(pdf의 경우 페이지 정보 정도)만 반환한다는 점에서 차이가 있다. retrieve_and_generate API 호출 시 아래와 같은 구조로 응답이 반환된다.

[
{
"generatedResponsePart": {
"textResponsePart": {
"text": "생애최초 특별공급 신청자격은 입주자모집공고일 현재 만40세 미만의 무주택세대구성원으로서 입주자저축에 가입하여 청약통장 가입요건을 갖춘 자를 대상으로 합니다.",
"span": {
"start": 0,
"end": 86
}
}
},
"retrievedReferences": [
{
"content": {
"text": "가구당 월평균소득을 말함)”의 100% 이하인 자를 대상으로 주택형별 공급량의 70%(소수점이하는 올림)..."
},
"location": {
"type": "S3",
"s3Location": {
"uri": "s3://test-12345/뉴홈 입주자모집공고문.pdf"
}
}
}
]
},
{
"generatedResponsePart": {
"textResponsePart": {
"text": "생애최초 특별공급은 주택형별 공급세대수의 70%를 우선공급하며, 경쟁이 있을 경우 추첨으로 당첨자를 선정합니다.",
"span": {
"start": 88,
"end": 149
}
}
},
"retrievedReferences": [
{
"content": {
"text": "공급유형 생애최초 특별공급 소득기준 3인 이하 4인 5인 6인 7인 8인...."
},
"location": {
"type": "S3",
"s3Location": {
"uri": "s3://test-12345/뉴홈 입주자모집공고문.pdf"
}
}
}
]
}
]

위와 같은 구조로 반환되기 때문에 프론트 화면에서 활용도가 뛰어나며, Bedrock 콘솔에서 Knowledge Bases를 테스트하는 것과 유사하게 hover(마우스 오버) UI를 구현할 수 있다.

Knowledge Bases UI Example

더 쉽게 구현하는 방법도 있겠지만 아래 HTML을 통해 위와 같은 UI를 구현할 수 있다.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hyperlink with Tooltip</title>
<style>
#content {
width: 500px; /* 박스의 너비를 500px로 설정 */
border: 1px solid #ccc; /* 테두리 추가 */
padding: 10px; /* 내부 여백 추가 */
overflow-wrap: break-word; /* 단어가 너무 길어 박스를 벗어날 경우 자동 줄바꿈 */
}

.tooltip {
display: inline-block;
position: relative;
cursor: pointer;
text-decoration: none; /* 링크의 기본 밑줄 제거 */
color: blue; /* 링크 색상 설정 */
}

.tooltip .tooltiptext {
visibility: hidden;
width: 500px;
background-color: black;
color: #fff;
text-align: left;
padding: 5px 0;
border-radius: 6px;

/* Position the tooltip text */
position: absolute;
z-index: 1;
top: 100%;
left: 50%;
margin-left: -150px;
}

.tooltip:hover .tooltiptext {
visibility: visible;
}
</style>
</head>
<body>

<br/>
<br/>
<div id="content"></div>

<script>
// 예시 JSON 데이터
const data = [
];

// HTML 요소에 텍스트와 툴팁 적용
function applyHyperlinksWithTooltips(data) {
const contentDiv = document.getElementById('content');
data.forEach((item, index) => {
const textPart = item.generatedResponsePart.textResponsePart;
const text = textPart.text;
const referenceURI = item.retrievedReferences[0].location.s3Location.uri;
let referenceText = item.retrievedReferences[0].content.text;

referenceText = "URI: " + referenceURI + "<br><br>" + referenceText;
if (referenceText.length > 300) {
referenceText = referenceText.substring(0, 300) + "...";
}


// 텍스트를 하이퍼링크로 분할
const firstPart = text.substring(0, textPart.span.start);
const linkedPart = text.substring(textPart.span.start, textPart.span.end);
const lastPart = text.substring(textPart.span.end);

contentDiv.innerHTML += firstPart + linkedPart;

// 하이퍼링크를 숫자에 적용
const numberLink = document.createElement('a');
numberLink.href = 'javascript:void(0);'; // 클릭 시 이동 방지
numberLink.className = 'tooltip';
numberLink.innerHTML = `<sup>[${index + 1}]</sup>`;

const tooltipSpan = document.createElement('span');
tooltipSpan.className = 'tooltiptext';
tooltipSpan.innerHTML = referenceText;
numberLink.appendChild(tooltipSpan);

contentDiv.appendChild(numberLink);
contentDiv.innerHTML += lastPart;
});
}

applyHyperlinksWithTooltips(data);
</script>

</body>
</html>

LangChain과 함께 사용하기

from langchain.chains import RetrievalQA
from langchain.retrievers import AmazonKnowledgeBasesRetriever

qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=AmazonKnowledgeBasesRetriever(
region_name="us-west-2",
knowledge_base_id="{KnowledgeBaseID}",
retrieval_config={"vectorSearchConfiguration": {"numberOfResults": 4}}
),
return_source_documents=True,
chain_type_kwargs={
"prompt": get_prompt(),
}
)

위 코드와 같이 LangChain Retrievers 라이브러리 내 Knowledge Bases for Amazon Bedrock이 통합되어 있다. 여기서도 Knowledge Base ID 입력만으로 RAG를 쉽게 구축할 수 있으며, Claude가 아닌 다른 LLM으로 변경할 수도 있기에 확장성 측면에서는 훨씬 유리한 방식이다.

한 가지 아쉬운 점은 Knowledge Bases API를 사용할 때와는 달리 전체 답변을 레퍼런스에 해당하는 chunk로 잘라서 전달 주지는 않는다는 것이다. LangChain 입장에서는 모든 리트리버에 대한 출력 통일성을 위한 처사였겠지만, 기능 및 활용성 축소라는 점에서 아쉬운 부분이라 추후 개선을 기대한다.

Conclusion

2023년 한 해 동안 많은 CSP 사에서 LangChain 내 몇 가지 기능들을 어떻게 쉽게 추상화하고 딜리버리할지 고민하였고 관련한 서비스를 하나둘씩 출시하고 있다. 해당 서비스들이 파편화되어 있고 조합하여 사용하기 불편하다면 사용자의 선택을 받기가 어려울 것이다.
그런 점에서 Knowledge Bases for Amazon Bedrock는 굉장히 유리한 위치에 있다. 원클릭만으로 쉽게 RAG를 구축할 수 있고, API의 활용성이 뛰어나며 LLM 오픈소스 생태계와도 통합되어 있기 때문에 당신의 LLM 기반 AI 사업을 가속화해줄 수 있을 것이다.

--

--