AWS 기술 블로그

Multi-RAG와 Multi-Region LLM로 한국어 Chatbot 만들기

사전학습(pretrained)되지 않은 데이터나 민감한 정보를 가지고 있어서 사전학습 할 수 없는 기업의 중요한 데이터는 RAG(Retrieval-Augmented Generation)을 이용하여 LLM(Large Language Model)에서 이용될 수 있습니다. RAG는 지식저장소(Knowledge Store)의 연관성 검색(sementic search)을 이용해, 질문과 가장 가까운 문서를 LLM의 Context로 활용합니다. 이러한 지식저장소에는 대표적인 In-memory vector store인 Faiss, persistent store이면서 대용량 병렬처리가 가능한 Amazon OpenSearch와 완전관리형 검색서비스인 Amazon Kendra가 있습니다. 또한, 2023년 re:Invent에서는 Amazon Aurora, OpenSearch, Kendra뿐 아니라 Amazon DocumentDBAmazon Neptune등 거의 모든 데이터베이스의 RAG 지원이 발표되었으므로, 향후 다양한 Database가 RAG의 지식저장소로 활용될 수 있을 것으로 기대됩니다.

Amazon Bedrock은 On-Demand와 Provisioned 방식으로 나누어 Request와 Token 수를 제한하고 있습니다. On-Demand 방식은 사용한 만큼만 과금하는 방식으로 LLM 어플리케이션 개발 및 초기 사업화 시 Provisoned 방식 대비 유용합니다. Bedrock은 리전을 선택할 수 있으므로, Multi-Region 환경에서 여러 리전을 이용하여 On-Demand 방식의 허용 용량을 증대 시킬 수 있습니다.

본 게시글은 Multi-Region의 Claude LLM(Large Language Models)을 이용하여, 여러 종류의 지식저장소(Knowledge Store)를 Multi-RAG로 활용하는 방법을 설명합니다. Multi-RAG와 Multi-Region를 구현하기 위해서는 아래와 같은 기능을 구현하여야 합니다.

  • 2023년 11월 출시된 Claude 2.1은 Context Window로 200k tokens을 제공하므로 기존 대비 더 많은 RAG 문서를 활용할 수 있게 되었습니다. 하지만, Multi-RAG에서는 RAG의 숫자만큼 관련 문서(Relevant Documents)의 수가 증가하므로 Claude2.1을 활용하더라도 RAG 문서의 숫자를 제한할 필요가 있습니다. 또한 RAG의 용도 또는 RAG에 저장된 데이터의 차이에 따라서 어떤 RAG는 원했던 관련된 문서를 주지 못하거나 관련성이 적은 문서를 줄 수 있고, 관련 문서의 순서나 위치는 LLM의 결과에 큰 영향을 주므로, 관련도가 높은 문서가 Context의 상단에 있을 수 있도록 배치할 수 있어야 합니다. 따라서, 각 RAG가 조회한 관련 문서들을 Context Window 크기에 맞게 선택하고, 중요도에 따라 순서대로 선택하여 하나의 Context로 만들 수 있어야 합니다.
  • Chatbot에서 질문 후 답변까지의 시간은 사용성에 매우 중요한 요소입니다. 여러개의 RAG를 순차적으로 Query를 하면, RAG의 숫자만큼 지연시간이 늘어나게 됩니다. 따라서, RAG에 대한 Query를 병렬로 수행하여 지연시간을 단축할 수 있어야 합니다.
  • Amazon Bedrock은 API 기반이므로 다른 리전의 Bedrock을 이용할 수 있지만, 동적으로 사용하기 위한 로드밸런싱(load balancing)이 필요합니다.

Architecture 개요

전체적인 아키텍처는 아래와 같습니다. 대화창에서 메시지를 입력하면 WebSocket 방식을 지원하는 Amazon API Gateway를 통해 lambda(chat)에서 메시지를 처리합니다. 여기에서는 Multi-RAG로 Faiss, OpenSearch, Kendra를 사용합니다. Faiss는 lambda(chat)의 프로세스와 메모리를 활용하고, Amazon Kendra와 OpenSearch는 AWS CDK를 이용해 설치합니다. 대화이력은 Amazon DynamoDB을 이용해 json 포맷으로 저장하고 관리합니다. Multi-Region로는 N.Virginia(us-east-1)과 Oregon(us-west-2) 리전을 활용하는데, AWS의 다른 Bedrock 리전을 추가하여 사용할 수 있습니다.

상세하게 단계별로 설명하면 아래와 같습니다.

단계1: 채팅창에 사용자가 메시지를 입력하면, Amazon API Gateway를 통해 lambda(chat)에 event로 메시지가 전달됩니다.

단계2: lambda(chat)은 기존 대화이력을 DynamoDB에서 읽어오고, Assistant와 상호작용(interaction)을 고려한 새로운 질문(Revised Question)을 생성하기 위하여, 대화이력을 Context로 만들어서 Bedrock을 이용해 생성합니다. 이때 DynamoDB에서 대화이력을 가져오는 동작은 최초 1회만 수행합니다.

단계3: RAG를 위한 지식저장소(Knowledge Store)인 Faiss, Amazon Kendra, Amazon OpenSearch로 부터 관련된 문서(Relevant Documents)를 조회합니다. 각 RAG로 부터 수집된 관련된 문서들(Relevant Documents)은 새로운 질문(Revised Question)과의 관련성에 따라 재배치하여, 선택된 관련된 문서(Selected Relevant Documents)를 생성합니다.

단계4: Bedrock의 Claude LLM에 새로운 질문과 선택된 관련된 문서(Select Relevant Documents)를 전달합니다. 이때, 각 event는 us-east-1과 us-west-2의 Bedrock로 번갈아서 요청을 보냅니다.

단계5: lambda(chat)은 LLM의 응답을 Dialog로 저장하고, 사용자에게 답변을 전달합니다.

단계6: 사용자는 답변을 확인하고, 필요시 Amazon S3에 저장된 문서를 Amazon CloudFront를 이용해 안전하게 보여줍니다.

이때의 Sequence diagram은 아래와 같습니다. lambda(chat)은 하나의 질문(question)을 위해 2번의 LLM query와 3개의 지식 저장소에 대한 RAG query 동작을 수행합니다. DynamoDB로 부터 대화이력을 읽어오는 과정은 첫번째 event에 대해서만 수행하고 이후로는 Lambda의 내부 메모리에 대화이력을 저장 후 활용합니다.

주요 구성

Multi-Region 환경구성

Multi-Region 환경에서 lambda(chat)로 들어온 event를 각각의 LLM로 분산 할 수 있어야 합니다. Elastic Load Balancing은 Regional service이므로 이런 용도에 맞지 않으며, Fan-out을 제공하는 SNS는 질문 후 답변까지의 시간이 추가될 수 있습니다. 따라서 Multi-Region에 맞게 LangChain의 LLM client를 재설정하는 방식으로 Round-robin scheduling을 구현합니다. 여기서는 us-east-1과, us-west-2를 이용해 Multi-Region을 구성하고 동적으로 할당하여 사용하는 방법을 설명합니다.

cdk-multi-rag-chatbot-stack.ts에서는 아래와 같이 LLM의 profile을 저장한 후에 LLM을 처리하는 lambda(chat)에 관련 정보를 Environment variables로 전달합니다. 아래에서는 us-west-2와, us-east-1의 Claude v2.1을 사용할 수 있도록 profile을 설정하고 있습니다. Claude Instance v1.0은 100k token을 제공하지만 Claude v2.1보다 빠른 응답속도를 가지고 있습니다. Claude Instace의 모델ID는 “anthropic.claude-instant-v1“입니다.

const profile_of_LLMs = JSON.stringify([
  {
    "bedrock_region": "us-west-2",
    "model_type": "claude",
    "model_id": "anthropic.claude-v2:1", // anthropic.claude-instant-v1    "maxOutputTokens": "8196"
  },
  {
    "bedrock_region": "us-east-1",
    "model_type": "claude",
    "model_id": "anthropic.claude-v2:1",
    "maxOutputTokens": "8196"
  },
]);

사용자가 보낸 메시지가 lambda(chat)에 event로 전달되면 아래와 같이 boto3로 bedrock client를 정의한 후에, LangChain로 Bedrock과 BedrockEmbeddings를 llm과 embeddings을 정의합니다. LLM의 리전(region)과 모델(medelId) 정보는 LLM profile로 관리되므로, lambda(chat)에 전달되는 event는 매번 다른 profile을 이용하게 됩니다.

profile_of_LLMs = json.loads(os.environ.get('profile_of_LLMs'))
selected_LLM = 0

profile = profile_of_LLMs[selected_LLM]
bedrock_region = profile['bedrock_region']
modelId = profile['model_id']

boto3_bedrock = boto3.client(
    service_name = 'bedrock-runtime',
    region_name = bedrock_region,
    config = Config(
        retries = {
            'max_attempts': 30
        }
    )
)
parameters = get_parameter(profile['model_type'], int(profile['maxOutputTokens']))

llm = Bedrock(
    model_id = modelId,
    client = boto3_bedrock,
    streaming = True,
    callbacks = [StreamingStdOutCallbackHandler()],
    model_kwargs = parameters)

bedrock_embeddings = BedrockEmbeddings(
    client = boto3_bedrock,
    region_name = bedrock_region,
    model_id = 'amazon.titan-embed-text-v1'
)   

아래와 같이 Lambda(chat)은 event를 받을 때마다 새로운 LLM로 교차하게 되므로, N개의 리전을 활용하면, N배의 용량이 증가하게 됩니다.

if selected_LLM >= number_of_LLMs - 1:
    selected_LLM = 0
else:
    selected_LLM = selected_LLM + 1

지식저장소(Knowledge Store)에서 관련된 문서 가져오기

Mult-RAG에서는 다양한 지식저장소(Knowledge Store)를 RAG로 활용함으로써 관련된 문서를 검색할 수 있는 확률을 높이고, 여러 곳에 분산되어 저장된 문서를 RAG의 데이터소스로 활용할 수 있는 기회를 제공합니다. 여기서는 지식저장소로 OpenSearch, Faiss, Kendra를 활용합니다. Knowledge Store는 application에 맞게 추가하거나 제외할 수 있습니다.

OpenSearchVectorSearch을 이용해 vector store를 정의합니다.

from langchain.vectorstores import OpenSearchVectorSearch

vectorstore_opensearch = OpenSearchVectorSearch(
    index_name = "rag-index-*", 
    is_aoss = False,
    ef_search = 1024, 
    m = 48,
    embedding_function = bedrock_embeddings,
    opensearch_url = opensearch_url,
    http_auth = (opensearch_account, opensearch_passwd)
)

데이터는 아래와 같이 add_documents()로 넣을 수 있습니다. index에 userId를 넣으면, 특정 사용자가 올린 문서만을 참조할 수 있습니다.

def store_document_for_opensearch(bedrock_embeddings, docs, userId, documentId):
    new_vectorstore = OpenSearchVectorSearch(
        index_name="rag-index-"+userId,
        is_aoss = False,
        embedding_function = bedrock_embeddings,
        opensearch_url = opensearch_url,
        http_auth=(opensearch_account, opensearch_passwd),
    )
    new_vectorstore.add_documents(docs)

관련된 문서(relevant docs)는 아래처럼 검색할 수 있습니다. 문서가 검색이 되면 아래와 같이 metadata에서 문서의 이름(title), 파일의 경로(source) 및 발췌문(excerpt)를 추출해서 관련된 문서(Relevant Document)에 메타 정보로 추가할 수 있습니다. OpenSearch에 Query할 때에 similarity_search_with_score를 사용하면, 결과값의 신뢰도를 score로 구할 수 있는데, “0.008877229”와 같이 1보다 작은 실수로 표현됩니다.

relevant_documents = vectorstore_opensearch.similarity_search_with_score(
    query = query,
    k = top_k,
)

for i, document in enumerate(relevant_documents):
    name = document[0].metadata['name']
    uri = document[0].metadata['uri']

    excerpt = document[0].page_content
    confidence = str(document[1])
    assessed_score = str(document[1])

    doc_info = {
        "rag_type": rag_type,
        "confidence": confidence,
        "metadata": {
            "source": uri,
            "title": name,
            "excerpt": excerpt,
        },
        "assessed_score": assessed_score,
    }
    relevant_docs.append(doc_info)

return relevant_docs

Faiss는 문서를 처음 등록할 때에 아래와 같이 vector store로 정의합니다. 이후로 추가되는 문서는 아래처럼 add_documents()를 이용해 추가합니다. Faiss는 in-memory vector store이므로, Faiss에 저장한 문서는 Lambda 인스턴스가 유지될 동안만 사용할 수 있습니다.

if isReady == False:
    embeddings = bedrock_embeddings
    vectorstore_faiss = FAISS.from_documents( 
        docs,  # documents
        embeddings  # embeddings
    )
    isReady = True
else:
    vectorstore_faiss.add_documents(docs) 

Faiss의 similarity_search_with_score()를 이용하면 similarity에 대한 score를 얻을 수 있습니다. Faiss는 관련도가 높은 순서로 문서를 전달하는데, 관련도가 높을 수록 score의 값은 작은 값을 가집니다. 문서에서 이름(title), 신뢰도(assessed_score), 발췌문(excerpt)을 추출합니다. Faiss의 score는 56와 같은 1보다 큰 값을 가집니다.

relevant_documents = vectorstore_faiss.similarity_search_with_score(
    query = query,
    k = top_k,
)

for i, document in enumerate(relevant_documents):    
    name = document[0].metadata['name']
    uri = document[0].metadata['uri']
    confidence = int(document[1])
    assessed_score = int(document[1])

    doc_info = {
        "rag_type": rag_type,
        "confidence": confidence,
        "metadata": {
            "source": uri,
            "title": name,
            "excerpt": document[0].page_content,
        },
        "assessed_score": assessed_score,
    }

Kendra에 문서를 넣을 때는 아래와 같이 S3 bucket을 이용합니다. 문서의 경로(source_uri)는 CloudFront와 연결된 S3의 경로를 이용해 구할 수 있는데, 파일명은 URL encoding을 하여야 합니다. Kendra에 저장되는 문서는 아래와 같은 파일 포맷으로 표현되어야 하며, boto3의 batch_put_document()을 이용해 등록합니다.

def store_document_for_kendra(path, s3_file_name, documentId):
    encoded_name = parse.quote(s3_file_name)
    source_uri = path + encoded_name    
    ext = (s3_file_name[s3_file_name.rfind('.')+1:len(s3_file_name)]).upper()

    # PLAIN_TEXT, XSLT, MS_WORD, RTF, CSV, JSON, HTML, PDF, PPT, MD, XML, MS_EXCEL
    if(ext == 'PPTX'):
        file_type = 'PPT'
    elif(ext == 'TXT'):
        file_type = 'PLAIN_TEXT'         
    elif(ext == 'XLS' or ext == 'XLSX'):
        file_type = 'MS_EXCEL'      
    elif(ext == 'DOC' or ext == 'DOCX'):
        file_type = 'MS_WORD'
    else:
        file_type = ext

    kendra_client = boto3.client(
        service_name='kendra', 
        region_name=kendra_region,
        config = Config(
            retries=dict(
                max_attempts=10
            )
        )
    )

    documents = [
        {
            "Id": documentId,
            "Title": s3_file_name,
            "S3Path": {
                "Bucket": s3_bucket,
                "Key": s3_prefix+'/'+s3_file_name
            },
            "Attributes": [
                {
                    "Key": '_source_uri',
                    'Value': {
                        'StringValue': source_uri
                    }
                },
                {
                    "Key": '_language_code',
                    'Value': {
                        'StringValue': "ko"
                    }
                },
            ],
            "ContentType": file_type
        }
    ]

    result = kendra_client.batch_put_document(
        IndexId = kendraIndex,
        RoleArn = roleArn,
        Documents = documents       
    )

LangChain의 Kendra Retriever를 이용하여, 아래와 같이 Kendra Retriever를 생성합니다. 파일을 등록할 때와 동일하게 “_language_code”을 “ko”로 설정합니다.

from langchain.retrievers import AmazonKendraRetriever
kendraRetriever = AmazonKendraRetriever(
    index_id=kendraIndex, 
    region_name=kendra_region,
    attribute_filter = {
        "EqualsTo": {      
            "Key": "_language_code",
            "Value": {
                "StringValue": "ko"
            }
        },
    },
)

Kendra의 get_relevant_documents()을 이용하여 top_k개의 질문(query)과 관련된 문서들(Relevant Documents)를 가져옵니다.

rag_type = "kendra"
api_type = "kendraRetriever"
relevant_docs = []
relevant_documents = kendraRetriever.get_relevant_documents(
    query = query,
    top_k = top_k,
)

for i, document in enumerate(relevant_documents):
    result_id = document.metadata['result_id']
    document_id = document.metadata['document_id']
    title = document.metadata['title']
    excerpt = document.metadata['excerpt']
    uri = document.metadata['document_attributes']['_source_uri']
    page = document.metadata['document_attributes']['_excerpt_page_number']
    assessed_score = ""

    doc_info = {
        "rag_type": rag_type,
        "api_type": api_type,
        "metadata": {
            "document_id": document_id,
            "source": uri,
            "title": title,
            "excerpt": excerpt,
            "document_attributes": {
                "_excerpt_page_number": page
            }
        },
        "assessed_score": assessed_score,
        "result_id": result_id
    }
    relevant_docs.append(doc_info)

return relevant_docs

대화이력을 고려하여 새로운 질문 생성하기

대화이력을 이용해 사용자의 질문을 새로운 질문(revised_question)로 업데이트합니다. 이때, 질문이 한국어인지 아닌지를 확인하여 적절한 prompt를 준비하고, 대화이력을 history context로 이용하는 LLMChain을 이용하여, 아래와 같이 새로운 질문(revised_question)을 생성합니다.

revised_question = get_revised_question(llm, connectionId, requestId, text) # generate new prompt 

def get_revised_question(llm, connectionId, requestId, query):    
    pattern_hangul = re.compile('[\u3131-\u3163\uac00-\ud7a3]+')
    word_kor = pattern_hangul.search(str(query))

    if word_kor and word_kor != 'None':
        condense_template = """
        <history>
        {chat_history}
        </history>

        Human: <history>를 참조하여, 다음의 <question>의 뜻을 명확히 하는 새로운 질문을 한국어로 생성하세요. 새로운 질문은 원래 질문의 중요한 단어를 반드시 포함합니다.

        <question>            
        {question}
        </question>
            
        Assistant: 새로운 질문:"""
    else: 
        condense_template = """
        <history>
        {chat_history}
        </history>
        Answer only with the new question.

        Human: using <history>, rephrase the follow up <question> to be a standalone question. The standalone question must have main words of the original question.
         
        <quesion>
        {question}
        </question>

        Assistant: Standalone question:"""

    condense_prompt_claude = PromptTemplate.from_template(condense_template)        
    condense_prompt_chain = LLMChain(llm=llm, prompt=condense_prompt_claude)

    chat_history = extract_chat_history_from_memory()
    revised_question = condense_prompt_chain.run({"chat_history": chat_history, "question": query})
    
    return revised_question

Multi-RAG에서 병렬로 조회하기

Kendra와 Vector Store인 Faiss, OpenSearch에서 top_k개 만큼 질문(query)와 관련된 문서를 가져옵니다. 병렬로 조회하기 위하여, Lambda의 Multi thread를 이용합니다. 이때, 병렬 처리된 데이터를 연동 할 때에는 Pipe()을 이용합니다. 이와 같이 multiprocessing을 이용해 여러 개의 thread를 동시에 실행함으로써, RAG로 인한 지연시간을 최소화할 수 있습니다.

from multiprocessing import Process, Pipe

processes = []
parent_connections = []
for rag in capabilities:
    parent_conn, child_conn = Pipe()
    parent_connections.append(parent_conn)

    process = Process(target = retrieve_process_from_RAG, args = (child_conn, revised_question, top_k, rag))
    processes.append(process)

for process in processes:
    process.start()

for parent_conn in parent_connections:
    rel_docs = parent_conn.recv()

    if (len(rel_docs) >= 1):
        for doc in rel_docs:
            relevant_docs.append(doc)

for process in processes:
    process.join()

def retrieve_process_from_RAG(conn, query, top_k, rag_type):
    relevant_docs = []
    if rag_type == 'kendra':
        rel_docs = retrieve_from_kendra(query=query, top_k=top_k)      
    else:
        rel_docs = retrieve_from_vectorstore(query=query, top_k=top_k, rag_type=rag_type)
    
    if(len(rel_docs)>=1):
        for doc in rel_docs:
            relevant_docs.append(doc)    
    
    conn.send(relevant_docs)
    conn.close()

RAG 결과를 신뢰도에 따라 정렬하기

Multi-RAG로 얻은 관련 문서들(Relevant Documents)을 신뢰도에 따라 순서대로 정리한 후에 Context로 사용하고자 합니다. RAG 의 Knowledge Store에서 결과의 신뢰도를 나타내는 score를 제공하더라도, RAG방식마다 결과를 측정하는 방법이 달라서, RAG의 문서를 신뢰도에 맞추기 위해 다시 관련된 문서를 평가하고 재배치하는 과정이 필요합니다. 여기서는 In-memory 방식의 Faiss를 이용하여 각 RAG가 전달한 문서들 중에 가장 질문과 가까운 문서들을 고릅니다. Faiss를 이용하면, Lambda의 프로세스와 메모리를 활용하므로 추가적인 비용이 발생하지 않으며 속도도 빠릅니다.

아래의 check_confidence()와 같이 Faiss, OpenSearch, Kendra가 검색한 관련된 문서를 Faiss에 문서로 등록합니다. 이후 Faiss의 similarity search를 이용하여, top_k의 문서를 다시 고르는데, assessed_score가 200이하인 문서를 선택하여 선택된 관련 문서(selected_relevant_docs)로 활용합니다.

if len(relevant_docs) >= 1:
    selected_relevant_docs = priority_search(revised_question, relevant_docs, bedrock_embeddings)

def priority_search(query, relevant_docs, bedrock_embeddings):
    excerpts = []
    for i, doc in enumerate(relevant_docs):
        excerpts.append(
            Document(
                page_content=doc['metadata']['excerpt'],
                metadata={
                    'name': doc['metadata']['title'],
                    'order':i,
                }
            )
        )  

    embeddings = bedrock_embeddings
    vectorstore_confidence = FAISS.from_documents(
        excerpts,  # documents
        embeddings  # embeddings
    )            
    rel_documents = vectorstore_confidence.similarity_search_with_score(
        query=query,
        k=top_k
    )

    docs = []
    for i, document in enumerate(rel_documents):
        order = document[0].metadata['order']
        name = document[0].metadata['name']
        assessed_score = document[1]

        relevant_docs[order]['assessed_score'] = int(assessed_score)

        if assessed_score < 200:
            docs.append(relevant_docs[order])    

    return docs

선택된 관련된 문서들을 아래와 같이 하나의 context로 모읍니다. 이후 RAG용 PROMPT에 새로운 질문(revised_question)과 선택된 관련 문서(selected_relevant_docs)을 함께 넣어서 LLM에 답변을 요청합니다. 이후 답변은 stream로 화면에 보여줍니다.

relevant_context = ""
for document in relevant_docs:
    relevant_context = relevant_context + document['metadata']['excerpt'] + "\n\n"

stream = llm(PROMPT.format(context=relevant_context, question=revised_question))
msg = readStreamMsg(connectionId, requestId, stream)

Reference 표시하기

아래와 같이 Kendra는 doc의 metadata에서 reference에 대한 정보를 추출합니다. 여기서 file의 이름은 doc.metadata[‘title’]을 이용하고, 페이지는 doc.metadata[‘document_attributes’][‘_excerpt_page_number’]을 이용해 얻습니다.

def get_reference(docs):
    reference = "\n\nFrom\n"    
    for i, doc in enumerate(docs):
        page = ""
        if "document_attributes" in doc['metadata']:
            if "_excerpt_page_number" in doc['metadata']['document_attributes']:
                page = doc['metadata']['document_attributes']['_excerpt_page_number']
        uri = doc['metadata']['source']
        name = doc['metadata']['title']
        if page: 
            reference = reference + f"{i+1}. {page}page in <a href={uri} target=_blank>{name} </a>, {doc['rag_type']} ({doc['assessed_score']})\n"
        else:
            reference = reference + f"{i+1}. <a href={uri} target=_blank>{name} </a>, {doc['rag_type']} ({doc['assessed_score']})\n"
    return reference

직접 실습 해보기

AWS Cloud9에서 AWS CDK를 이용하여 인프라를 설치합니다. 또한 편의상 Kendra 설치가 가능한 Tokyo (ap-northeast-1) 리전을 사용합니다.

사전 준비 사항

이 솔루션을 사용하기 위해서는 사전에 AWS Account가 준비되어 있어야 합니다.

Bedrock 사용 권한 설정

본 실습을 위해서 Bedrock은 N.Virginia(us-east-1)과 Oregon(us-west-2) 리전을 사용합니다. Model access – N.Virginia와 Model access – Oregon에 접속해서 [Edit]를 선택하여 모든 모델을 사용할 수 있도록 설정합니다. 특히 Anthropic Claude와 “Titan Embeddings G1 – Text”은 LLM 및 Vector Embedding을 위해서 반드시 사용이 가능하여야 합니다. 본 실습에서는 빠른 동작을 위해 Claude Instance 모델을 사용합니다.

CDK를 이용한 인프라 설치

  1. Cloud9 Console에 접속하여 [Create environment]-[Name]에서 “chatbot”로 이름을 입력하고, EC2 instance는 “m5.large”를 선택합니다. 나머지는 기본값을 유지하고, 하단으로 스크롤하여 [Create]를 선택합니다.
  2. Environment에서 “chatbot”를 [Open]한 후에 아래와 같이 터미널을 실행합니다.
  3. 소스를 다운로드합니다.
    curl https://raw.githubusercontent.com/aws-samples/generative-ai-demo-using-amazon-sagemaker-jumpstart-kr/main/blogs/multi-rag-and-multi-region-llm-for-chatbot/multi-rag-and-multi-region-llm-for-chatbot.zip -o multi-rag-and-multi-region-llm-for-chatbot.zip && unzip multi-rag-and-multi-region-llm-for-chatbot.zip
  4. cdk 폴더로 이동하여 필요한 라이브러리를 설치합니다.
    cd multi-rag-and-multi-region-llm-for-chatbot/cdk-multi-rag-chatbot/ && npm install
  5. CDK 사용을 위해 Bootstrapping을 수행합니다.
    아래 명령어로 Account ID를 확인합니다.

    aws sts get-caller-identity --query Account --output text

    아래와 같이 bootstrap을 수행합니다. 여기서 “account-id”는 상기 명령어로 확인한 12자리의 Account ID입니다. bootstrap 1회만 수행하면 되므로, 기존에 cdk를 사용하고 있었다면 bootstrap은 건너뛰어도 됩니다.

    cdk bootstrap aws://[Account ID]/ap-northeast-1
  6. 인프라를 설치합니다.
    cdk deploy --all

    설치가 완료되면 아래와 같이 Output을 확인할 수 있습니다.

  7. HTML 파일을 S3에 복사합니다.
    아래와 같이 Output의 HtmlUpdateCommend을 터미널에 붙여넣기 해서 필요한 파일을 S3로 업로드합니다.
  8. Output의 WebUrlformultiragchatbot에 있는 URL을 복사하여 웹 브라우저로 접속합니다. User Id로 적당한 이름을 넣고, Conversation Type로는 “2. Question/Answering (RAG)”를 선택합니다.

실행 결과

error_code.pdf 파일을 다운로드 한 후에, 채팅 화면 하단의 파일 아이콘을 선택하여 업로드합니다. 본 실습에서는 Multi-RAG 동작을 테스트하기 위하여 파일 업로드시 모든 RAG에 문서로 등록하도록 구성하였습니다. 실사용시에는 RAG 별로 다른 데이터가 저장될 것으로 예상됩니다. 업로드가 완료되면 파일 내용을 요약하여 보여줍니다.

이후 채팅창에 “보일러가 갑자기 꺼졌어요. A396 에러가 나는데 어떻게 해야할까요?”로 입력한 후에 결과를 확인합니다. “From” 이하로 아래와 같이 Faiss, Kendra, OpenSearch로 검색한 정보가 표시됩니다. 여기서 오른쪽 괄호 안에는 Faiss로 얻은 similarity search 결과가 점수로 표시되는데 점수가 낮을수록 더 신뢰도가 있습니다.

대용량 언어 모델(LLM)의 특성상 실습의 답변은 상기 화면과 조금 다를 수 있습니다. 최초 실행 시, From에 Kendra 결과가 보여지지 않으면, Kendra의 인프라 준비가 아직 완료되지 않은 것이므로, 수분후에 파일을 재업로드하고 재시도 합니다.

리소스 정리하기

더이상 인프라를 사용하지 않는 경우에 아래처럼 모든 리소스를 삭제할 수 있습니다.

  1. API Gateway Console로 접속하여 “api-chatbot-for-multi-rag-chatbot”, “api-multi-rag-chatbot”을 삭제합니다.
  2. Cloud9 Console에 접속하여 아래의 명령어로 전체 삭제를 합니다.
    cd ~/environment/multi-rag-and-multi-region-llm-for-chatbot/cdk-multi-rag-chatbot/ && cdk destroy --all

결론

다양한 지식저장소(Knowledge Store)를 이용하여 RAG를 구성할 수 있도록 Multi-RAG를 구현하는 방법에 대해 설명하였습니다. Multi-RAG로 얻어진 여러 RAG의 관련 문서(Relevant Documents)들은 질문에 대한 관련도(similarity)에 따라 정렬하였고, 지연시간을 최소화하기 위하여 병렬 처리하는 방법을 소개 하였습니다. 또한 Multi-Region 환경을 구성하여 여러 리전의 LLM을 효율적으로 사용하여 On-Demend 방식의 용량 이슈를 완화할 수 있었습니다. 여기서 사용한 Architecture는 서버리스 서비스들을 이용하였으므로, RAG를 이용한 LLM 어플리케이션을 편리하게 개발하고 유지보수에 대한 부담 없이 운영할 수 있습니다. 근래에 기업이 보유한 데이터를 잘 활용하기 위하여 Amazon Q와 같은 서비스들이 활용되고 있습니다. 본 게시글이 제안하는 Multi-RAG와 Multi-Region 구조는 기업의 다양한 데이터베이스를 통합하여, 업무를 간소화하고 빠른 의사결정 및 문제를 해결할 수 있는 어플리케이션 개발에 활용될 수 있을 것으로 보여집니다.

실습 코드 및 도움이 되는 참조 블로그

아래의 링크에서 실습 소스 파일 및 기계 학습(ML)과 관련된 자료를 확인하실 수 있습니다.

Kyoung-Su Park

Kyoung-Su Park

박경수 솔루션즈 아키텍트는 다양한 워크로드에 대한 개발 경험을 바탕으로 고객이 최적의 솔루션을 선택하여 비즈니스 성과를 달성할 수 있도록 고객과 함께 효율적인 아키텍처를 구성하는 역할을 수행하고 있습니다. 현재 AWS의 Machine Learning, IoT, Analytics TFC에서 활동하고 있습니다.

SeongHee Kang

SeongHee Kang

강성희 Partner SA 는 데이터 베이스 제품 엔지니어와 대용량 정보계 시스템 / 빅데이터 플랫폼 솔루션 엔지니어로 다양한 인더스트리에서 구축과 프리세일즈를 모두 경험하였으며 현재는 AWS Partner Core 팀에서 파트너사 엔지니어들의 Analytic 서비스 역량 강화를 위해 일하고 있습니다.

Joonyong Park

Joonyong Park

박준용 파트너 솔루션스 아키텍트는 파트너 역량강화를 위하여 AWS Ambassador, BlackBelt, APN Immersion Day, JetPack 등의 프로그램을 수행하고 있으며 파트너사에 Data Analytics와 Machine Learning 분야 Enablement를 준비 및 제공하고 있습니다.