AI 부트캠프/챕터3(11.08~12.04)

TIL 43 도전 과제

musukie 2024. 11. 19. 20:49

도전 구현 과제

LangSmith의 Prompt Library를 참고하여 prompt engineering을 수행해보자.  RAG의 성능은 prompt 의 품질에도 많은 영향을 받는다. 이번 도전 구현과제는 prompt engineering을 해보고, prompt 실험 결과를 외부에서 잘 비교 정리할 수 있도록 코드를 고쳐보는 것이다.

예시)
.
├── main.jupynb
└── Prompts/
    ├── prompt1.txt
    ├── prompt2.txt
    └── prompt3.txt

 

  • 각 프롬프트를 외부에서 불러와서 실행할 수 있도록 코드를 고쳐보자.
  • 실행 결과는 자동으로 Result 디렉토리에 저장돼야 한다. 이때, 실험 결과 파일 이름은 아래의 예시와 같이 실험에 쓰인 프롬프트의 이름과 timestamp을 포함해야 한다.
예시) 
.
├── main.jupynb
└── Prompts/
    ├── prompt1.txt
    ├── prompt2.txt
    └── prompt3.txt
└── Results/
    ├── prompt1_result_1731314042.txt
    ├── prompt2_result_1731314050.txt
    └── prompt3_result_1731314050.txt

 

LLM 과 RAG를 활용한 AI 챗봇을 구현한 코드는 아래와 같다.

더보기
더보기
# 사용환경 준비
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
import faiss
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough

load_dotenv()

API_KEY = os.getenv('sparta_api_key')

if API_KEY is None:
    raise ValueError("API key is missing from .env file")

os.environ['OPENAI_API_KEY'] = API_KEY

# 모델 로드하기
model = ChatOpenAI(model="gpt-4o-mini")

# 문서 로드하기
loader = PyPDFLoader("./[2024 한권으로 ok 주식과 세금].pdf")

docs = loader.load()

# 문서 청크로 나누기 - RecursiveCharacterTextSplitter
text_splitter = CharacterTextSplitter(
    separator="\n\n",   # 두 개의 개행 문자를 구분자로 사용
    chunk_size=100, # 최대 100자씩 나눈다.
    chunk_overlap=10,   # 각 조각에 앞뒤로 5자의 중복을 포함한다.
    length_function=len,    # 길이를 계산할 때 문자 수(len)를 기준으로 한다.
    is_separator_regex=False,   # 구분자를 단순 문자열로 처리
)

splits = text_splitter.split_documents(docs)

print(splits[:10]) # 청킹된 내용 상위 10개 출력

# 벡터 임베딩 생성. OpenAI 임베딩 모델 초기화
embeddings = OpenAIEmbeddings(model="text-embedding-ada-002")

# 벡터 스토어 생성
vectorstore = FAISS.from_documents(documents=splits, embedding=embeddings)

# FAISS를 Retriever로 변환
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 1})

# 프롬프트 템플릿 정의
contextual_prompt = ChatPromptTemplate.from_messages([
    ("system", "Answer the question using only the following context."),
    ("user", "Context: {context}\\n\\nQuestion: {question}")
])

# RAG 체인 구성
class DebugPassThrough(RunnablePassthrough):
    def invoke(self, *args, **kwargs):
        # 부모 클래스의 invoke 메서드를 호출하여 처리된 결과를 받아온다.
        output = super().invoke(*args, **kwargs)
        # 처리된 결과를 출력하여 디버깅 용도로 확인한다.
        print("Debug Output:", output)
        # 처리된 결과를 그대로 반환한다.
        return output

class ContextToText(RunnablePassthrough):
    def invoke(self, inputs, config=None, **kwargs):  # config 인수도 받을 수 있도록 설정
        # context의 각 문서를 텍스트로 결합한다.
        context_text = "\n".join([doc.page_content for doc in inputs["context"]])
        # 결합된 텍스트와 사용자 질문을 함께 반환한다.
        return {"context": context_text, "question": inputs["question"]}

rag_chain_debug = {
    "context": retriever,                  # retriever는 context를 가져오는 단계다.
    "question": DebugPassThrough()         # DebugPassThrough는 question을 그대로 전달하며 디버깅을 출력한다.
} | DebugPassThrough() | ContextToText() | contextual_prompt | model  # 각 단계에 디버깅과 텍스트 변환을 추가한 파이프라인

이제 각 프롬프트를 외부에서 불러와서 실행할 수 있는 코드를 작성해보자.

 

먼저 필요한 라이브러리를 불러온다.

import os  # 운영 체제 기능을 제공하는 os 모듈을 임포트
import time  # 시간 관련 기능을 제공하는 time 모듈을 임포트

 타임스탬프를 생성할 수 있게 time 모듈을 불러왔다. os 모듈은 Results 폴더가 없으면 만들기 위해 불러왔다.

# 결과를 저장할 디렉터리 생성 (존재하지 않으면 생성)
if not os.path.exists("Results"):  # "Results" 폴더가 존재하지 않으면
    os.makedirs("Results")  # 폴더를 생성한다.

# 프롬프트 파일 목록을 지정
prompt_files = ["./Prompts/prompt1.txt", "./Prompts/prompt2.txt", "./Prompts/prompt3.txt", "./Prompts/prompt4.txt"]
# 여기서, 프롬프트 파일의 경로를 리스트로 정의한다.

# 파일 목록 출력 및 사용자 선택
print("사용 가능한 프롬프트 파일 목록:")
for idx, file in enumerate(prompt_files, start=1):
    print(f"{idx}: {file}")

selected_index = int(input("사용할 프롬프트 파일 번호를 입력하세요: ")) - 1

if 0 <= selected_index < len(prompt_files): # 입력값 검증
    prompt_file = prompt_files[selected_index]  # 선택된 파일
else:
    print("잘못된 입력입니다.")
    exit

결과를 저장할 디렉토리는 'Results'인데, 이 폴더가 없다면 만들게 하는 코드를 작성했다. if문으로 폴더가 없다면 os.makedirs로 폴더를 만들게 한다. 프롬프트 파일을 원하는 파일로 불러오게 하고 싶어서, 프롬프트 파일 목록을 지정해주고 사용자가 입력을 통해 프롬프트 파일을 선택할 수 있게 만들었다.

# 선택된 프롬프트 파일 읽기
with open(prompt_file, "r", encoding="utf-8") as file:  # 지정된 경로로 파일을 연다.
    content = file.read().strip()  # 파일의 내용을 읽고, 앞뒤 공백을 제거한다.

# 프롬프트 내용 출력 (디버깅용)
print(f"Reading from {prompt_file}:\n{content}\n")

 선택된 프롬프트를 열고 파일 내용을 읽는데, print(f"Reading from {prompt_file}:\n{content}\n")를 통해 파일을 읽은 내용을 출력하여, 어떤 프롬프트가 읽혔는지 확인할 수 있다.

result = f"Executed result for {prompt_file}:\n\n{content}"

{content}에는 각 prompt.txt 파일에서 읽어온 내용이 저장돼있다. 이 코드는 prompt.txt 파일에서 읽은 내용을 그대로 결과로 저장하는 기능을 한다.

챗봇을 구동시켜보자.

while True:  
    print("========================")
    
    query = input("질문을 입력하세요 (종료하려면 'exit'를 입력하세요): ")

    if query.lower() == 'exit':
        print("프로그램을 종료합니다.")
        sys.exit(0)

    response = rag_chain_debug.invoke(query)

 

타임스탬프를 생성하고, 결과를 저장할 때 파일 이름에 타임스탬프를 포함한다. 결과 파일에 어떻게 결과를 저장할 지도 구성해주었다.

# 챗봇 구동 확인
while True:  
    print("========================")
    
    query = input("질문을 입력하세요 (종료하려면 'exit'를 입력하세요): ")

    if query.lower() == 'exit':
        print("프로그램을 종료합니다.")
        sys.exit(0)

    response = rag_chain_debug.invoke(query)  
    # 타임스탬프 생성
    timestamp = str(int(time.time()))  # 현재 시간을 초 단위로 타임스탬프 형식으로 변환한다.
    # time.time()은 현재 시간을 초 단위로 반환하며, int()를 사용하여 정수로 바꾼다.

    # 결과를 저장할 파일 경로 설정
    result_filename = f"Results/{os.path.basename(prompt_file).replace('.txt', f'_result_{timestamp}.txt')}"

    # 결과 파일에 저장할 내용 구성
    result_content = (
        f"=== Prompt File ===\n"
        f"{content}\n\n"
        f"=== User Query ===\n"
        f"{query}\n\n"
        f"=== Chatbot Response ===\n"
        f"{response}\n"
    )

    # 결과 파일 저장
    with open(result_filename, "w", encoding="utf-8") as result_file:  # 결과를 저장할 파일을 연다.
        result_file.write(result_content)  # 결과를 파일에 쓴다.

    # 저장된 파일 경로 출력
    print(f"Result saved to: {result_filename}\n")
    # 실행된 결과가 저장된 파일 경로를 출력하여 확인할 수 있다.

 

코드를 실행시켜보니 프롬프트는 잘 불러와지고, 챗봇 구동도 잘 되었다. 그러나 답변을 보니 프롬프트가 잘 먹히는 것 같지는 않았다. 프롬프트를 바꿔보고, 내용을 강조해보고, 말투나 어조도 지시해보았지만 답변이 프롬프트를 크게 반영하는 것 같지는 않았다. 원래 프롬프트에 실행을 해도 마찬가지였다. 프롬프트 엔지니어링을 잘 하지 못해서 이런 결과가 나온 것 같다. 프롬프트 엔지니어링을 좀 더 공부해야겠다는 생각이 들었다.

'AI 부트캠프 > 챕터3(11.08~12.04)' 카테고리의 다른 글

TIL 45  (0) 2024.11.21
TIL 44  (1) 2024.11.20
TIL 42  (1) 2024.11.18
TIL 41  (0) 2024.11.17
TIL 40  (2) 2024.11.16