도전 구현 과제
LangSmith의 Prompt Library를 참고하여 prompt engineering을 수행해보자. RAG의 성능은 prompt 의 품질에도 많은 영향을 받는다. 이번 도전 구현과제는 prompt engineering을 해보고, prompt 실험 결과를 외부에서 잘 비교 정리할 수 있도록 코드를 고쳐보는 것이다.
- LangSmith의 Prompt Library 를 참고하여 프롬프트를 3개 이상 아래와 같은 파일 구조로 저장하자.
예시)
.
├── 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")
# 실행된 결과가 저장된 파일 경로를 출력하여 확인할 수 있다.
코드를 실행시켜보니 프롬프트는 잘 불러와지고, 챗봇 구동도 잘 되었다. 그러나 답변을 보니 프롬프트가 잘 먹히는 것 같지는 않았다. 프롬프트를 바꿔보고, 내용을 강조해보고, 말투나 어조도 지시해보았지만 답변이 프롬프트를 크게 반영하는 것 같지는 않았다. 원래 프롬프트에 실행을 해도 마찬가지였다. 프롬프트 엔지니어링을 잘 하지 못해서 이런 결과가 나온 것 같다. 프롬프트 엔지니어링을 좀 더 공부해야겠다는 생각이 들었다.