공모전에서 챗봇을 개발하는 부분을 맡아 처음에는 LLM(Large Language Models)을 파인 튜닝(fine-tuning) 통해 우리 프로그램 만의 AI를 만들려고 했다. 파인 튜닝은 사전 학습 모델에 새로운 추가 데이터를 학습 시켜 모델을 최적화하는 방법이다.
하지만, 학습 시킬 수 있는 데이터의 개수가 부족하기 때문인지 정확도가 좋지 않았고 학습하는 시간도 너무 오래 걸리기 때문에 시간 낭비인듯한 느낌이 들었다.
또한, 프롬프트 엔지니어링(prompt-engineering) 만으로 우리가 원하는 AI 기능을 개발할 수 없었기 때문에 RAG를 도입하기로 결정했다.
만약 나처럼 챗봇을 처음 개발해야 한다면 아래의 단계대로 챗봇 구현 가능성에 대해 파악하면 좋겠다.
프롬프트 엔지니어링(prompt-engineering)을 시도해 본다.
프롬프트 엔지니어링은 언어 모델에게 원하는 결과를 얻기 위해서 언어 모델에게 전달될 텍스트들을 효율적으로 설계하는 것이라고 볼 수 있다.
OpenAI 공식 문서에서 프롬프트 엔지니어링을 위한 전략들과 예시들을 확인해 볼 수 있다.
프롬프트 엔지니어링으로 해결되지 않는다면 RAG(Retrieval-Augmented Generation)를 도입한다.
RAG가 무엇인지 아래에서 자세하게 살펴보겠다.
RAG를 사용해도 해결되지 않을 때 파인 튜닝을 이용해본다.
파인 튜닝으로 데이터 학습의 효과를 나타내고 싶다면 학습 데이터의 개수가 최소 10만건이 존재해야 한다.
RAG(Retrieval-Augmented Generation)는 LLM의 단점을 개선시키기 위해 학습 데이터 소스 이외에 신뢰할 수 있는 외부 지식 베이스를 참조하도록 하는 기술이다.
간단하게 예를 든다면, 대규모 언어 모델이 시험을 치룰 때 자신이 알고 있는 지식으로만 시험을 보는 것이 아닌 참고할 수 있는 오픈북 하나를 가지고 시험을 볼 수 있는 것이다.
참고할 수 없는 오픈북이 없을 때 LLM이 발생시킬 수 있는 문제점들이 존재한다.
RAG를 이용한다면 이러한 문제점들을 개선 시킬 수 있다.
실제 프로젝트에 RAG를 도입하면서 모델의 답변 정확도와 신뢰성이 향상되었기 때문에 매우 효율적인 방법으로 자연어 처리 어플리케이션을 구현할 수 있었고, 모델을 학습시키는데에 큰 비용과 시간이 들지 않았다.
RAG는 아래와 같은 방식으로 동작하기 때문에 RAG를 구현하기 위해선 벡터 데이터베이스, 텍스트 임베딩 모델, LLM 이 세가지 구성 요소가 꼭 필요하다.
내가 선택한 세가지 구성요소는 아래와 같다. DB 생성 과정은 이전 게시물을 참고 하길 바란다.
이제 작동 방식을 알아보았으니 이와 같은 방식으로 RAG를 간단하게 구현해보고자 한다. 구현 테스트는 colab에서 진행 했으며 LLM 챗봇에게 어떤 특정한 장소를 추천 받는 상황으로 가정한다.
필요한 라이브러리를 임포트 하고 LLM이 참고할 데이터가 저장된 벡터 DB 테이블을 로드시켜 준비한다.
import pandas as pd
import numpy as np
import time
import os
from pymilvus import MilvusClient
from pymilvus import FieldSchema, DataType
from pymilvus import FieldSchema, CollectionSchema
from openai import OpenAI
from google.colab import userdata
#openai api 쓰기 위한 환경변수 설정
EMBEDDINGS_KEY = userdata.get('EMBEDDINGS_KEY')
os.environ["OPENAI_API_KEY"] = EMBEDDINGS_KEY
openAI_api = OpenAI(
# This is the default and can be omitted
api_key=os.environ.get("OPENAI_API_KEY"),
)
# 벡터 데이터베이스 연결
url = userdata.get("URL")
client = MilvusClient(url)
# 컬렉션(=테이블)이름 리스트
collection_list = ['myStartup_travel_sites', 'nowlocal_travel_sites', 'nature_travel_sites']
# 컬렉션 로드하기
for collection_name in collection_list:
client.load_collection(
collection_name=collection_name,
)
# 로드된 상태인지 확인
res = client.get_load_state(
collection_name=collection_name
)
print(res)
출력 결과는 아래와 같다.
DEBUG:pymilvus.milvus_client.milvus_client:Created new connection using: 117e983a7c4
{'state': <LoadState: Loaded>}
{'state': <LoadState: Loaded>}
{'state': <LoadState: Loaded>}
# 질문 벡터화 함수
def embed_question(question):
response = openAI_api.embeddings.create(
input=question,
model="text-embedding-3-small",
dimensions=768
)
embedding = response.data[0].embedding
return embedding
# 사용자 질문 저장
example_question = "서울 카페 추천해줘"
# 질문 임베딩
embedding = embed_question(example_question)
사용자는 챗봇에게 "서울 카페를 추천해줘" 라고 질문한다.
embedding 변수에 사용자 질문이 768 차원의 벡터로 변환되어 저장된다.
# 단일 테이블 검색 함수
def search_table(table_name, embedding, top_k):
search_params = {"metric_type": "IP", "params": {}}
results = client.search(
collection_name=table_name,
data=[embedding],
anns_field="embedding",
search_params=search_params,
output_fields=["id", "text"],
limit=top_k
)
return results
# 여러 테이블 검색 함수
def search_all_tables(embedding):
results_myCreator = search_table('myStartup_travel_sites', embedding, top_k=6)
results_nowLocal = search_table('nowlocal_travel_sites', embedding, top_k=5)
results_nature = search_table('nature_travel_sites', embedding, top_k=4)
return results_myCreator, results_nowLocal, results_nature
# 테이블 검색
results_myCreator, results_nowLocal, results_nature = search_all_tables(embedding)
테이블 세개에 각각 쿼리를 날려 사용자 질문에 기반한 관련 정보를 가져온다. 각각의 쿼리 결과로 데이터의 기본 키 id와 text 문서를 리턴한다.
벡터 DB 검색 시 유사도 metric 유형은 IP이며 데이터 베이스에 존재한 벡터와 사용자 질문 벡터의 거리를 측정하여 가까운 거리의 데이터들을 반환한다.
Milvus 벡터 검색 함수들을 구체적으로 알고 싶다면 single vector search 공식 문서를 참고해주길 바란다.
# 쿼리 검색 결과 하나의 문자열로 수정
def format_results(results_myCreator, results_nowLocal, results_nature):
list_of_results = [results_myCreator[0], results_nowLocal[0], results_nature[0]]
formatted_results = ""
for result in list_of_results:
length = len(result)
for num in range(length):
formatted_results += result[num]['entity']['text'] + "\n"
return formatted_results
# 쿼리 결과 하나의 문자열로 묶기
formatted_results = format_results(results_myCreator, results_nowLocal, results_nature)
formatted_results
쿼리 결과로 얻은 데이터를 하나의 텍스트로 구성하여 묶는다. 아래와 같은 형식으로 쿼리 결과가 하나의 텍스트로 만들어 진다.
장소명: 구욱희씨 서울숲 본점
카테고리: 카페/디저트
장소 키워드: 디저트 카페, 카페
위치: 서울 성동구 성수동1가 685-271 1, 2층
장소명: 새서울
카테고리: 주류
장소 키워드: 추억여행, 바
위치: 서울 종로구 돈화문로11길 28-5
장소명: 서울앵무새
카테고리: 카페/디저트
장소 키워드: 디저트, 카페, 앵무새, 볼거리
위치: 서울 성동구 성수동1가 685-213 B1~2F
장소명: 성수동대림창고갤러리
카테고리: 카페/디저트
장소 키워드: 카페, 디저트
위치: 서울 성동구 성수동2가 322-32
....
# LLM 답변 생성 함수
def sendLLM(question, results):
response = openAI_api.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "system", "content": "- 당신은 여행을 계획하는데 도움을 주는 챗봇 TBTI입니다. \n-당신의 역할은 사용자의 질문에 reference를 바탕으로 답변하는 것 입니다. \n- 만약 사용자의 질문이 reference와 관련이 없다면, {제가 가지고 있는 정보로는 답변할 수 없습니다.}라고만 반드시 말하세요."},
{"role": "user", "content":f"사용자 질문: {question} \n reference: {results}" }
],
)
return response.choices[0].message.content
#LLM에 전달
LLM_response = sendLLM(example_question, formatted_results)
LLM_response
LLM에 참고 자료를 전달해 답변을 생성했다. 출력 결과를 확인해보면 참고 자료의 정보만을 이용해 답변을 생성한 것을 볼 수 있다.
[ 출력 결과 ]
서울에서 추천할 만한 카페는 아래와 같습니다:
1. **구욱희씨 서울숲 본점**
- 위치: 서울 성동구 성수동1가 685-271 1, 2층
- 키워드: 디저트 카페, 카페
2. **서울앵무새**
- 위치: 서울 성동구 성수동1가 685-213 B1~2F
- 키워드: 디저트, 카페, 앵무새, 볼거리
3. **성수동대림창고갤러리**
- 위치: 서울 성동구 성수동2가 322-32
- 키워드: 카페, 디저트
4. **LCDC SEOUL**
- 위치: 서울 성동구 성수동2가 275-28
- 키워드: LCDC,카페, bar, 베이커리, 문화공간
이 카페들은 성수동 일대에 위치하고 있어 접근성도 좋고 다양한 경험을 할 수 있습니다.
System :
User:
이렇게 간단하게 RAG를 구현하여 LLM의 환각 증세를 줄이고 원하는 답변 결과를 얻을 수 있었다. 만약 챗봇의 말투를 다른 방식으로 변화 시키고 싶다면 사용 언어 모델의 파라미터나 파인 튜닝 기법으로 변화 시킬 수 있을 것이다.
더 유연한 챗봇 개발을 위해 langchain 프레임 워크를 활용하여 간편하고 빠른 어플리케이션을 구현해볼 수 있다.
[ 참고 자료 ]
https://aws.amazon.com/ko/what-is/retrieval-augmented-generation/
https://www.ncloud-forums.com/topic/277/
https://modulabs.co.kr/blog/retrieval-augmented-generation/
https://milvus.io/docs/build-rag-with-milvus.md
https://velog.io/@pearl1058/Fine-tuning-%ED%8C%81%EB%93%A4AWSKRUG-%ED%9B%84%EA%B8%B0
RAG의 구조가 많이 헷갈렸는데 작성해주신 글이 큰 도움이 되었습니다. 감사합니다!