ASAC-SK플래닛 T아카데미 데이터 엔지니어

25.12.22 53일차 [llm_langchain_bedrock 기반 서비스 만들기 실습, 프론트, 백엔드 연동, RAG 관련 기술 업그레이드 ]

Datadesigner 2025. 12. 22. 17:48

오늘은 bedrock 기반 서비스 만들기 첫번째 실습인 프론트엔드, 백엔드 연동 파트를 마무리한 후

 

bedrock llm 업그레이드를 위한 rag와 mcp vectorDB 등의 기술을 하나씩 배우며 LLM 모델을 업그레이드 하는 파트를 진행했다.

 


프론트엔드, 백엔드 연동

import streamlit as st
import requests as req

# a = 1
# print('사용자 입력후 엔터치면 계속 전체가 구동되는지 점검')#, a)
# 전역설정
API_URL = 'http://localhost:8000/chat' # fastapi 주소
st.set_page_config(page_title='식사 메뉴 해결사', page_icon='🍔')
st.title('AI 식사 메뉴 해결사 - 킹')
st.caption('예상, 점심/저녁 등 시점, 날씨, 기분, 단체여부 등 알려주시면 메뉴를 추천해드립니다.')

# session state 초기화 -> 현재 코드가 몇 번이고 재실행되더라도 데이터를 유지, 전역
if 'messages' not in st.session_state: # 최초에는 아무것도 없음
    st.session_state.messages = [
        # 페르소나는 백엔드에서 구성
        {
            'role' : 'assistant',
            'content' : '안녕하세요! 오늘 식사는 어떤 것이 땡기나요?(예산, 점심/저녁 등 시점, 날씨, 기분, 단체여부 등 알려주시면 메뉴를 추천해드립니다.)' 
        }
    ]

# 이전 대화내용 화면 출력
for msg in st.session_state.messages:
    with st.chat_message(msg['role']): # assistant or user 
        st.markdown(msg['content'])
# ui
# prompt = st.chat_input('현재 상황을 자세히 입력하세요')
# print(prompt)
# a += 1
# if prompt:
    # prompt 입력값을 받아서 -> 존재하면 -> 작업 진행

# 대입 표현식(혹은 왈러스 연산자) -> 나오지 않은 문법임 
if prompt:= st.chat_input('현재 상황을 자세히 입력하세요'):
    # 사용자 질의 처리 진행
    # 1. 사용자의 입력 내용을 전역 상태 관리 변수에 추가
    st.session_state.messages.append({
            'role':'user',
            'content' : prompt
    })
    # 2. 사용자 입력 후 -> 마크다운 표기
    # 화면에 방금 추가된 내용을 바로 반영하여 출력해라
    with st.chat_message('user'): # user로 고정했음
        st.markdown(prompt) # 화면에 텍스트 내용 출력
        pass

    # 3. LLM에게 문의 -> 서버 요청 -> bedrock 요청 -> bedrock 응답 
    # 서버 응답 -> assistant의 응답
    with st.chat_message('assistant'):
        msg_holder = st.empty()
        msg_holder.markdown('고민중....ㅡ.ㅡ')

        # 3-1. 서버측으로 사용자의 질의 전송
        result = None
        try:
            res = req.post(API_URL,json={'question':prompt})
            if res.status_code == 200: # 응답 성공 뜻
                result = res.json().get('response','응답 없음')
                pass
            else:
                result = f'서버측 오류 {res.status_code}'
        # 추후, 백엔드 구성 후 교체
        # import time
        # time.sleep(3)
        # res = '더미 응답 : 치킨으로 가보세요!'
        except Exception as e:
            # 더미 예외처리 구성
            print('에러',e)
            result='LLM 사용자가 너무 많습니다, 10초후에 다시 시도해주세요'
        # 3-2. 화면 처리
        msg_holder.markdown(result)
        # 3-3. 전역 상태 관리 변수에 추가
        st.session_state.messages.append({
            'role':'assistant',
            'content':'res'
        })
        pass
    pass

 

어제 코드에서 추가된 부분은 3-1 서버측으로 사용자의 질의 전송 파트이다.

 

추가된 코드는 서버측으로 사용자의 질의 전송하는 파트이다.

 

이제 백엔드에서 llm을 연동시켜서 fastapi를 통해서 웹으로 받은 데이터를 뿌릴 예정인데.

 

서버 응답 코드 200과 화면 처리 등을 추가하였다, 다른 파트는 백엔드에서 코드로 작성한다.

 

# 1. 필요한 패키지 가져오기
#       api 구성 용도
from fastapi import FastAPI
from pydantic import BaseModel
#       AWS SKD로 bedrock 사용 위해서
import boto3
#       환경변수 로드 -> os단 환경변수 세팅
from dotenv import load_dotenv
#       langchain
#       llm 모델을 호출하는 객체
from langchain_aws import ChatBedrockConverse
#       프롬프트 구성
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate

import os
# 2. 환경변수 로드 
load_dotenv()
print('사용 리전 확인', os.getenv('AWS_REGION'))

# 3. fastapi 앱 생성
app = FastAPI(title='식사 메뉴 추천 AI')

# 4. LLM 관련 모든 작업
# 4-1. AWS Bedrock 접근 객체 획득(bedrock 등록된 모든 LLM 사와 중계 담당)
bedrock_client = boto3.client(service_name='bedrock-runtime',
                              region_name = os.getenv('AWS_REGION')
                            )
# 4-2. bedrock을 통해서 llm 특정(구글 gemma) 해서 객체 (접근권한을 가진) 획득
llm = ChatBedrockConverse(
    client = bedrock_client,
    model  = os.getenv('BEDROCK_MODEL_ID'),
    temperature = 0.7,
    max_tokens  = 500 # 비용 부분 해당 
    )

# 4-3. 프롬프트 엔지니어링 -> 개발사쪽에서 커스텀(고객의 질의 + 예시(퓨샷) + 페르소나 지정 + ...)
# 4-3-1. few_shot 샘플 준비 => 개발사 준비사항
#        LLM(AI)에게 몇 개의 데이터를 학습시켜서 이런 상황에는 이렇게 응답하라고 훈련
#        개발사의 의도대로 AI 방향성 설정
fewshot_samples = [
    #input => human, output=> ai로 매칭 가능함
    {
        "input": "오늘 점심 메뉴 추천해줘. 비가 오고 있어서 나가기 귀찮아. 혼자 먹을 거야.",
        "output": "비 오는 날, 집에서 편하게 즐길 수 있는 **'따뜻한 김치수제비와 해물파전'**을 추천합니다! "
        "빗소리와 부침개 부치는 소리가 비슷해 감성을 자극하고, 칼칼한 국물은 가라앉은 기분을 전환해줄 거예요. "
        "배달 앱을 이용하신다면 근처 칼국수 맛집의 세트 메뉴를 확인해보세요."
    },
    {
        "input": "직장 동료들 5명이랑 저녁 회식 메뉴 골라줘. 날씨는 맑고 다들 기분 최고야! 고기 종류면 좋겠어.",
        "output": "기분 좋은 저녁 회식에는 역시 **'제주 흑돼지 오겹살'**이 제격입니다! "
        "5명이라면 고기를 직접 구워주는 전문점에 가셔서 편하게 대화를 나눠보세요. "
        "맑은 날씨에 어울리는 시원한 '김치말이 국수'를 후식으로 곁들이면 완벽한 마무리가 될 거예요."
    },
    {
        "input": "내일 점심에 데이트가 있어. 날씨는 화창하대. 여자친구가 가벼운 걸 먹고 싶어 하는데 분위기 좋은 메뉴 없을까?",
        "output": "화창한 날 데이트라면 신선한 **'부라타 치즈 샐러드를 곁들인 바질 페스토 파스타'**는 어떨까요? "
        "건강하고 가벼우면서도 색감이 예뻐 사진 찍기에도 좋습니다. "
        "통창이 있는 이탈리안 레스토랑에서 화이트 와인 한 잔을 곁들이면 로맨틱한 분위기가 배가될 거예요."
    },
    {
        "input": "금요일 퇴근길인데 너무 스트레스 받아. 매운 거 먹고 싶은데 혼자 먹기 좋은 저녁 메뉴 추천해줘.",
        "output": "일주일 동안 정말 고생 많으셨어요! 스트레스를 한 방에 날려줄 **'마라탕과 꿔바로우 소(小)'**를 추천합니다."
        "원하는 재료를 듬뿍 넣어 아주 매운 단계로 즐겨보세요. 얼얼한 매운맛이 엔도르핀을 돌게 해줄 거예요. "
        "시원한 캔맥주 하나 사서 집에서 넷플릭스를 보며 드시는 건 어떨까요?"
    }
]

# 4-3-2. few_shot을 프롬프트 형태로 변환하여 템플릿 구성
#        최초부터 프롬프트 형태로 구성해도 ok, 준비한 데이터를 변환해도 ok
fewshot_example_prompt = ChatPromptTemplate.from_messages([
    ('human','{input}'),
    ('ai','{output}')
])
# print(fewshot_example_prompt)

# 4-3-3. 퓨샷 프롬프트 + 퓨샷 샘플 => 샘플 제공되는 프롬프트 구성 완료
fewshot_prompt = FewShotChatMessagePromptTemplate(
    examples       = fewshot_samples,
    example_prompt = fewshot_example_prompt
)
# 확장 : 퓨샷 샘플을 DB에 대량으로 준비하고 (주제별 등등) -> 디비쿼리후 획득 -> 퓨샷 구성

# 4-3-4. 최종 프롬프트 구성
#        페르소나 + 퓨샷 + 고객 질문
final_prompt = ChatPromptTemplate.from_messages([
    ('system','당신은 직장인들의 식사 메뉴 고민을 해결해주는 계획적인 "식사 해결사"입니다. 상황에 맞게 계획적으로\
    메뉴를 추천해주세요'), # 페르소나
    fewshot_prompt, # 샘플
    ('human','{user_cur_input}') # 실제 사용자의 질의
    # {user_cur_input:질의} 형태로 llm 호출시 전달

])
# 4-4. 체인 구성 (파이프라인 구성 => 연속된 flow를 구성)
chain = final_prompt | llm
# 해당 파이프라인에서 사용자의 질의 주입하면 연속적으로 작동됨

# 5. pydantic => 요청 혹은 응답의 구조를 정의한 클래스 설계
#    요청 데이터를 => 특정 클래스로 바로 받아서 객체 생성
#    응답 데이터를 => 특정 클래스로 바로 세팅해서 객체 생성 => 응답
class UserRequest(BaseModel): # 4-1. BaseModel 클래스를 반드시 상속받아야함
    # 5-2. 구조 커스텀 설계
    question:str # 멤버명:타입힌트 부여 => 클라이언트는 {"question":prompt} 보냄

# 6. API 엔드포인트 구성
@app.post('/chat')
async def llm_endpoint(req:UserRequest): # req => 매개변수 :UserRequest => 타입 힌트
    # return {"response":f"에코 응답:{req.question}"}
    # 6-1. `llm`을 호출하여 유저의 질의(프롬프트)를 전달 -> 결과를 받아서 -> 응답
    # langchain을 호출하는 행위
    response = chain.invoke({'user_cur_input':req.question})
    return {"response":response.content}

백엔드 전체 코드이다, 하나씩 찢어보자

 

 

1. 모듈 가져오기, 환경 변수 생성

# 1. 필요한 패키지 가져오기
#       api 구성 용도
from fastapi import FastAPI
from pydantic import BaseModel
#       AWS SKD로 bedrock 사용 위해서
import boto3
#       환경변수 로드 -> os단 환경변수 세팅
from dotenv import load_dotenv
#       langchain
#       llm 모델을 호출하는 객체
from langchain_aws import ChatBedrockConverse
#       프롬프트 구성
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate

import os

# 2. 환경변수 로드 
load_dotenv()
print('사용 리전 확인', os.getenv('AWS_REGION'))

필요한 패키지를 가져오고 환경변수를 로드해온다, 여기까지는 기존에 작업한 작업물과 크게 다르지 않다.

 

 

2. fastapi 생성, LLM 관련  기초 세팅 (BEDROCK 접근 객체 획득, 사용할 LLM 특정하여 객체 획득

# 3. fastapi 앱 생성
app = FastAPI(title='식사 메뉴 추천 AI')

# 4. LLM 관련 모든 작업
# 4-1. AWS Bedrock 접근 객체 획득(bedrock 등록된 모든 LLM 사와 중계 담당)
bedrock_client = boto3.client(service_name='bedrock-runtime',
                              region_name = os.getenv('AWS_REGION')
                            )
# 4-2. bedrock을 통해서 llm 특정(구글 gemma) 해서 객체 (접근권한을 가진) 획득
llm = ChatBedrockConverse(
    client = bedrock_client,
    model  = os.getenv('BEDROCK_MODEL_ID'),
    temperature = 0.7,
    max_tokens  = 500 # 비용 부분 해당 
    )

bedrock 객체, LLM 객체를 가져오고 세팅한다.

 

3. 프롬프트 엔지니어링 (fewshot 기법)

# 4-3. 프롬프트 엔지니어링 -> 개발사쪽에서 커스텀(고객의 질의 + 예시(퓨샷) + 페르소나 지정 + ...)
# 4-3-1. few_shot 샘플 준비 => 개발사 준비사항
#        LLM(AI)에게 몇 개의 데이터를 학습시켜서 이런 상황에는 이렇게 응답하라고 훈련
#        개발사의 의도대로 AI 방향성 설정
fewshot_samples = [
    #input => human, output=> ai로 매칭 가능함
    {
        "input": "오늘 점심 메뉴 추천해줘. 비가 오고 있어서 나가기 귀찮아. 혼자 먹을 거야.",
        "output": "비 오는 날, 집에서 편하게 즐길 수 있는 **'따뜻한 김치수제비와 해물파전'**을 추천합니다! "
        "빗소리와 부침개 부치는 소리가 비슷해 감성을 자극하고, 칼칼한 국물은 가라앉은 기분을 전환해줄 거예요. "
        "배달 앱을 이용하신다면 근처 칼국수 맛집의 세트 메뉴를 확인해보세요."
    },
    {
        "input": "직장 동료들 5명이랑 저녁 회식 메뉴 골라줘. 날씨는 맑고 다들 기분 최고야! 고기 종류면 좋겠어.",
        "output": "기분 좋은 저녁 회식에는 역시 **'제주 흑돼지 오겹살'**이 제격입니다! "
        "5명이라면 고기를 직접 구워주는 전문점에 가셔서 편하게 대화를 나눠보세요. "
        "맑은 날씨에 어울리는 시원한 '김치말이 국수'를 후식으로 곁들이면 완벽한 마무리가 될 거예요."
    },
    {
        "input": "내일 점심에 데이트가 있어. 날씨는 화창하대. 여자친구가 가벼운 걸 먹고 싶어 하는데 분위기 좋은 메뉴 없을까?",
        "output": "화창한 날 데이트라면 신선한 **'부라타 치즈 샐러드를 곁들인 바질 페스토 파스타'**는 어떨까요? "
        "건강하고 가벼우면서도 색감이 예뻐 사진 찍기에도 좋습니다. "
        "통창이 있는 이탈리안 레스토랑에서 화이트 와인 한 잔을 곁들이면 로맨틱한 분위기가 배가될 거예요."
    },
    {
        "input": "금요일 퇴근길인데 너무 스트레스 받아. 매운 거 먹고 싶은데 혼자 먹기 좋은 저녁 메뉴 추천해줘.",
        "output": "일주일 동안 정말 고생 많으셨어요! 스트레스를 한 방에 날려줄 **'마라탕과 꿔바로우 소(小)'**를 추천합니다."
        "원하는 재료를 듬뿍 넣어 아주 매운 단계로 즐겨보세요. 얼얼한 매운맛이 엔도르핀을 돌게 해줄 거예요. "
        "시원한 캔맥주 하나 사서 집에서 넷플릭스를 보며 드시는 건 어떨까요?"
    }
]

 

몇 가지 예시를 제공하고 그에 따른 답을 도출해 내는 fewshot 프롬프트 기법을 사용한다.

 

 

4. 프롬프트 엔지니어링 -> fewshot 프롬프트 변환

# 4-3-2. few_shot을 프롬프트 형태로 변환하여 템플릿 구성
#        최초부터 프롬프트 형태로 구성해도 ok, 준비한 데이터를 변환해도 ok
fewshot_example_prompt = ChatPromptTemplate.from_messages([
    ('human','{input}'),
    ('ai','{output}')
])

위의 예시의 템플릿을 따라서 fewshot 프롬프트 예시 변수를 만들어준다, 사람이 input ai가 output

 

5. fewshot 프롬프트 + fewshot sample 

# 4-3-3. 퓨샷 프롬프트 + 퓨샷 샘플 => 샘플 제공되는 프롬프트 구성 완료
fewshot_prompt = FewShotChatMessagePromptTemplate(
    examples       = fewshot_samples,
    example_prompt = fewshot_example_prompt
)

6. 최종 프롬프트 구성

# 4-3-4. 최종 프롬프트 구성
#        페르소나 + 퓨샷 + 고객 질문
final_prompt = ChatPromptTemplate.from_messages([
    ('system','당신은 직장인들의 식사 메뉴 고민을 해결해주는 계획적인 "식사 해결사"입니다. 상황에 맞게 계획적으로\
    메뉴를 추천해주세요'), # 페르소나
    fewshot_prompt, # 샘플
    ('human','{user_cur_input}') # 실제 사용자의 질의
    # {user_cur_input:질의} 형태로 llm 호출시 전달
])

퓨샷 샘플 + 퓨샷 example promot = fewshot prompt

fewshot prompt 에 chatprompttemplate 을 써서 페르소나를 부여하고 샘플 프롬프트를 넣어준다.

 

 

7. chain 구성

# 4-4. 체인 구성 (파이프라인 구성 => 연속된 flow를 구성)
chain = final_prompt | llm
# 해당 파이프라인에서 사용자의 질의 주입하면 연속적으로 작동됨

체인 구성

 

8. fastAPI 엔드포인트 구성

# 5. pydantic => 요청 혹은 응답의 구조를 정의한 클래스 설계
#    요청 데이터를 => 특정 클래스로 바로 받아서 객체 생성
#    응답 데이터를 => 특정 클래스로 바로 세팅해서 객체 생성 => 응답
class UserRequest(BaseModel): # 4-1. BaseModel 클래스를 반드시 상속받아야함
    # 5-2. 구조 커스텀 설계
    question:str # 멤버명:타입힌트 부여 => 클라이언트는 {"question":prompt} 보냄

# 6. API 엔드포인트 구성
@app.post('/chat')
async def llm_endpoint(req:UserRequest): # req => 매개변수 :UserRequest => 타입 힌트
    # return {"response":f"에코 응답:{req.question}"}
    # 6-1. `llm`을 호출하여 유저의 질의(프롬프트)를 전달 -> 결과를 받아서 -> 응답
    # langchain을 호출하는 행위
    response = chain.invoke({'user_cur_input':req.question})
    return {"response":response.content}

FASTAPI 사용법은 기존의 방식과 같다, 하지만 클래스가 달라지고 체인을 구성함에 따라 변하는 코드 디테일들이 있어서 이 부분들은 추후 더 살펴봐야 할 것 같다.

 

이렇게 모두 완성한 이후 터미널을 두개 키고 streamlit 과 uvicorn을 가동하면

 

요렇게 나온다, 싱기방기

 

다음으로는 RAG 연습이다.

 


README

 

# advance (발전적 확장) -> 프로젝트 구성 사항
    - rag(검색 증강) : LLM이 한번도 접하지 못한 사내 데이터 (내부 데이터) 활용
        - 자체 구축 -> 오픈 소스(메타의 라마 등) 활용 -> 사내 데이터 학습(파인 튜닝) -> LLM 활용 -> 시간/비용 발생
        - 사내 데이터 노출 X, LLM 강력한 추론/ 생성 등 기능 활용 -> rag
        -정의 및 장점
            - RAG는 검색 증강 생성(Retrieval-Augmented Generation)의 약자로, 거대 언어 모델(LLM)이 답변을 만들 때 외부의 최신 데이터베이스나 문서를 검색하여 정보를 가져와 답변을 보강하는 기술입니다. 
            - 이는 LLM이 학습하지 않은 최신 정보나 특정 회사 내부 지식 기반의 질문에 대해 더 정확하고 신뢰할 수 있는 답변을 생성하도록 돕고, 환각(Hallucination) 현상을 줄여줍니다. 
            - 모델 자체를 재학습시키는 대신 외부 데이터베이스만 업데이트하면 되므로 효율적 (비용,시간)
        - 목표
            - 사내(특정) 데이터 노출 X
            - 사내(특정 서비스)에 LLM을 도입하고 싶음 (사내 전용 혹은 도구)
            - 사내(특정) 데이터에 적합한 답변 원함
            - LLM 학습 X 

    - vectordb      : 대화 내용을 기록 (장기기억 담당), rag등 데이터를 저장하는 공간으로 활용, 유사도 체크 기능 활용
        - 대량의 외부 데이터는 기존 방식대로 RDB나 No-SQL 계열 저장해두는가?
            - 사용자 질의에 대한 유사도 검사를 지원 X (일부 있을 수 있음, 제품별로)
            - 디비 => 자연어를 벡터화하여 저장하는 벡터 디비가 필요함 => 유사도 계산 정확해짐 => 검색 정확해짐 
            - => 이를 참고하는 LLM 답변 정확해짐
        - 제품
            - 메모리 베이스 디비(로컬) = faiss
                - 유사성 검색 및 벡터 클러스터링을 위한 오픈 소스 라이브러리
                - pip install -c faiss-cpu
                  or
                - pip install -c faiss-gpu
            - 외부 디비 = 파인콘 -> 무료 1개 스토리지 제공 (1개 도메인 구성만 가능), 2개부터는 유료
            - (https://www.pinecone.io/)
        - 말뭉치(자연어) -> 토크나이저를 이용한 백터화 -> 백터디비

    - mcp           : 외부 자원 (db, rag, 검색, ... <- tool들 중계 호출 관리)
    - 위의 flow(질의 -> llm 대응하도록 구성하는)을 graph로 설계
    - 이것을 agent 단위로 개발 
    - 질의당 1개의 agent 대응 or 질의당 여러개의 agent 대응 (A to A) -> Agent Engineering

 

BEDROCK LLM 업그레이드를 위한 추가 내용들이다, 먼저 RAG에 대해서 수업을 진행한다.


RAG_TEST1

# 검색 증강 생성 => RAG

# 1, 모듈 가져오기
# 백터 디비
from langchain_community.vectorstores import FAISS
# 말뭉치 => 토큰화를 통한 백터 처리 -> 토크나이저
from langchain_aws import BedrockEmbeddings
# AWS SDK
import boto3
# 환경변수
from dotenv import load_dotenv
import os

# 1-1. 환경변수 로드
load_dotenv()

# 2. 데이터 준비
#    더미 임시 데이터, 짧게 구성
data = ["맥도날드 대표 제품은 빅맥이다",
        "버거킹의 대표 제품은 와퍼이다",
        "맘스터치 대표 제품은 휠레이다",
        "롯데리아의 대표 제품은 새우버거이다"]

# 3. 임베딩 (말뭉치 -> 분절화 ->(사전화는 이미 되어있음) 백터화 -> 패딩 -> 임베딩)
tokenizer = BedrockEmbeddings(model_id="amazon.titan-embed-text-v1",
                              region_name=os.getenv('AWS_REGION') )

# 4. 백터 디비에 토큰화된 내용을 입력
db = FAISS.from_texts(data, tokenizer) # 디비 생성 완료, 데이터 삽입 완료

# 5. 백터 디비 => 검색 => 유사도 검사 후 응답
docs = db.similarity_search('버거킹의 대표 버거는?')

# 6. 검색 결과 확인
print(docs[0].page_content) # 유사도 순으로 데이터가 검색되어 출력됨 (docs)
# 의미론적으로 가장 가까운 의미를 가진 데이터 출력

 

먼저 첫번째 파트는 기본적인 rag의 개념을 파악하는 시간이다,

말뭉치를 이용해서 디비에 토큰화된 내용과 가장 유사한 내용을 가져오는 예전에 몇번 해본 그 느낌을 쓰는거다.

 

결과는 이렇게 나온다. 버거킹의 대표 제품은 와퍼이다.
쉑쉑버거로 바꿔봤는데 데이터가 너무 없어서 안나온다.

 


RAG_TEST2

 

# vectorDB에 거대한 말뭉치 삽입
# 말뭉치 -> 특정 크기의 청크 단위로 분할해서 처리 -> 토큰 제한
# 최근 LLM에서는 거대한 양으로 입력 가능함 (백터디비의 크기 단위 체크 필요)

# 1. 모듈 가져오기
from langchain_community.vectorstores import FAISS
from langchain_aws import BedrockEmbeddings
import boto3
from dotenv import load_dotenv
import os
# 대량의 말뭉치 (문서들) 로드, 특정 단위로 분할
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

load_dotenv()
AWS_REGION = os.getenv('AWS_REGION')
# print(AWS_REGION)

# 2. 파일 목록 가져오기
#   ["xxx.txt","xxx.txt","xxx.txt","xxx.txt"]
import glob
# 경로법은 상대경로인 이상 프로젝트 루트부터 경로를 따짐
file_paths = glob.glob("./data/*.txt")
# print(file_paths)

# 3. TextLoader를 이용하여 문서 로드
# raw_docs = [TextLoader(file,encoding='utf-8').load() for file in file_paths]
raw_docs = [TextLoader(file,encoding='utf-8').load()[0] for file in file_paths]
# print('파일 로드 완료', len(raw_docs), raw_docs[0])

# 4. 텍스트 분할 (특정 크기의 청크 단위로 데이터를 분할하여 준비)
# #    성능을 위해 적절한 크기 설정 필요
splitter= RecursiveCharacterTextSplitter(
    chunk_size    = 512, # 자를 문자 수
    chunk_overlap = 100  # 문맥 유지를 위해 겹치는 구간
)
splites = splitter.split_documents(raw_docs)
# print('총 청크 수', len(splites), splites[0])

# 5. 임베딩 모델
tokenizer = BedrockEmbeddings(model_id="amazon.titan-embed-text-v1",
                              region_name=os.getenv('AWS_REGION') )

# 6. 백터 디비 생성
db = FAISS.from_documents(splites, tokenizer) # 디비 생성 완료, 데이터 삽입 완료

# 7. 백터 디비화 된 데이터를 저장 -> 매번 로드되는 현상 해결 (프로젝트떄는 사전 입력)
db.save_local('hp_story') 

# 8. 검색 테스트
query = '해리포터의 친구는?' # 추론의 질의임 (결과값이 안좋을 수 있음)
docs = db.similarity_search(query)

# 9. 답 출력 (유사도중 가장 높은 점수를 받은 문장)
print(query, docs[0].page_content)

 

RAG_TEST2도 코드별로 뜯어보자]

 

1. 모듈 가져오기

# vectorDB에 거대한 말뭉치 삽입
# 말뭉치 -> 특정 크기의 청크 단위로 분할해서 처리 -> 토큰 제한
# 최근 LLM에서는 거대한 양으로 입력 가능함 (백터디비의 크기 단위 체크 필요)

# 1. 모듈 가져오기
from langchain_community.vectorstores import FAISS
from langchain_aws import BedrockEmbeddings
import boto3
from dotenv import load_dotenv
import os
# 대량의 말뭉치 (문서들) 로드, 특정 단위로 분할
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

load_dotenv()
AWS_REGION = os.getenv('AWS_REGION')
# print(AWS_REGION)

# 2. 파일 목록 가져오기
#   ["xxx.txt","xxx.txt","xxx.txt","xxx.txt"]
import glob
# 경로법은 상대경로인 이상 프로젝트 루트부터 경로를 따짐
file_paths = glob.glob("./data/*.txt")
# print(file_paths)

모듈가져오기는 뭐 늘 똑같다. 그 대신 말뭉치를 로드하고 분할해주는 모듈을 새로 추가했다.

 

그 이후 파일 목록 가져오는 GLOB를 사용하여 폴더내에 있는 모든 TXT파일 이름을 가져왔다.

 

 

3. TextLoader를 이용하여 문서 로드

# 3. TextLoader를 이용하여 문서 로드
# raw_docs = [TextLoader(file,encoding='utf-8').load() for file in file_paths]
raw_docs = [TextLoader(file,encoding='utf-8').load()[0] for file in file_paths]
print('파일 로드 완료', len(raw_docs), raw_docs[0])

글을 가져온다. 이거 강사님이 주신 말뭉친데 해리포터 영화 스포 내용이라서 일단 다 가렸다, ㅋㅋ

 

 

4. 텍스트 분할 (텍스트마다 길이가 다 다르니까)

# 4. 텍스트 분할 (특정 크기의 청크 단위로 데이터를 분할하여 준비)
# # #    성능을 위해 적절한 크기 설정 필요
splitter= RecursiveCharacterTextSplitter(
    chunk_size    = 512, # 자를 문자 수
    chunk_overlap = 100  # 문맥 유지를 위해 겹치는 구간
)
splites = splitter.split_documents(raw_docs)
print('총 청크 수', len(splites), splites[0])

데이터를 분할할때에도 옵션값을 주어서 정상적으로 분할되도록 한다. 총 70개의 청크가 있다.

 

5. 임베딩 모델, 벡터 디비 생성, 벡터 디비 데이터 로컬에 저장, 검색 테스트

# # 5. 임베딩 모델
tokenizer = BedrockEmbeddings(model_id="amazon.titan-embed-text-v1",
                              region_name=os.getenv('AWS_REGION') )

# 6. 백터 디비 생성
db = FAISS.from_documents(splites, tokenizer) # 디비 생성 완료, 데이터 삽입 완료

# 7. 백터 디비화 된 데이터를 저장 -> 매번 로드되는 현상 해결 (프로젝트떄는 사전 입력)
db.save_local('hp_story') 

# 8. 검색 테스트
query = '해리포터의 친구는?' # 추론의 질의임 (결과값이 안좋을 수 있음)
docs = db.similarity_search(query)

# 9. 답 출력 (유사도중 가장 높은 점수를 받은 문장)
print(query, docs[0].page_content)

그 다음으로는 임베딩, 벡터 디비 생성, 디비화한 데이터 저장, 검색 테스트 등이다.

 

유사도를 체크하여 높은 점수인 문장만 가져오기 때문에 llm처럼 답을 주진 않고 최대한 비슷한 부분을 가져온다.

 

마지막 줄에 해리와 론과 헤르미온느는 세상에 둘도 없는 친구가 된다. 는 알았으니까 뭐 유사도가 높은 답이 오긴 했다.

 

이렇게 llm모델에 해리포터 말뭉치를 주고서 그 말뭉치에 있는 데이터에서 결과를 가져오는게 RAG방식이다.

 

 


RAG_TEST3

은 내일 진행하겠다