안녕하세요 미남홀란드 입니다.
이번에 소개해드릴 컨텐츠는 Langchain 을 활용해서 보다 쉽게 RAG 서비스를 구축하는 방법입니다.
다들 맛집 좋아하실텐데 평점을 얼마나 믿으시는지는 모르겠습니다. 사실 구축단계에서 한국 사람들이 아무래도 네이버라는 플랫폼을 많이쓰고 신뢰하고 정보도 많기 때문에 NAVER API 를 활용하고 싶었으나, 진짜 감촉같이 이거 조금만 api 데이터쓰면 item 이다 할만한것만 지원을 하지 않는 모습이더라구요. 그래서 더욱 범용적인 Google maps 를 활용해서 만들어보았습니다. 이전에 사실 카카오톡맵기반으로 크롤링을 해서 데이터를 구축해서 만들려고 시도를 해보았으나, 자꾸 동적페이지 다음페이지 넘어가면서 Chrome Driver 가 오류가 나는 바람에 데이터를 구축하지를 못했습니다. 실시간 평점 데이터라 더 재밌었을텐데 아쉽습니다. 그건 나중에라도 크롤링에 성공한다면 꼭 시도를 해보겠습니다.
1. google maps api 신청 및 발급 받기
Google Maps Platform | Google for Developersgoogle cloud 에 들어가서 API key 를 생성을 해줍니다.
2. google maps api 코드
중요한 파라미터는 Location, Keyword, Radius 입니다. 각각 보다시피 지역, 검색할 키워드, 근방의 거리까지 입니다.
위를 참고해서 파라미터를 잘 입력해주세요.
from googleplaces import GooglePlaces, types
YOUR_API_KEY = 'your API key'
google_places = GooglePlaces(YOUR_API_KEY)
query_result = google_places.nearby_search(
location='Sunaedong, Bundang-gu, Korea', keyword='Korean Food',
radius=500, types=[types.TYPE_FOOD])
places_data = [] # 장소 데이터를 저장할 리스트 생성
if query_result.has_attributions:
print(query_result.html_attributions)
for place in query_result.places:
# 각 장소의 상세 정보를 가져옵니다.
place.get_details()
# 각 장소의 데이터를 딕셔너리 형태로 저장합니다.
place_data = {
"Name": place.name,
"Rating": place.rating,
"Types": place.types,
"Address": place.vicinity
}
places_data.append(place_data) # 리스트에 추가
# 결과 페이지가 더 있는지 확인하고, 있으면 추가로 데이터를 가져옵니다.
if query_result.has_next_page_token:
query_result_next_page = google_places.nearby_search(
pagetoken=query_result.next_page_token)
for place in query_result_next_page.places:
place.get_details()
place_data = {
"Name": place.name,
"Rating": place.rating,
"Types": place.types,
"Address": place.vicinity
}
# places_data 리스트에 저장된 데이터를 출력합니다.
for place in places_data:
print(place)
저는 수내동, 한식, 500 미터 근방으로 설정을하고 가져오는 값들은 이름, 평점, 장소의 형태, 주소를 받아오는 파라미터를 data로 구축하기 로 했습니다. 가져올 수 있는 API 관련해서는 Google maps API 에 자세하게 나와있으니 참고 바랍니다.
위처럼 List로 받아왔는데요. 여기서 제가 이 것 그대로 리트리버를 쓰고싶어서 여러 시도를 해보았지만 아무래도 Prompt 를 따로 주고 참고하게 하는게 아니라면 좀 어려울듯 합니다. 예로들어 retriver 변수에 그냥 저장하고 Prompt = {retriver}의 데이터를 참고해서 알려주세요 이런식으로도 활용이 가능합니다.
3. Langchain Retriver 형식의 Document 형식으로 변환하기
import json
from decimal import Decimal
# 사용자 정의 JSON 인코더
def decimal_default(obj):
if isinstance(obj, Decimal):
return float(obj) # 또는 str(obj)로 변환할 수도 있습니다.
raise TypeError
# 리스트를 JSON 형식의 문자열로 변환
json_data = json.dumps(places_data, indent=4, default=decimal_default)
# 파일에 쓰기
with open("data.txt", "w") as file:
file.write(json_data)
랭체인에서 일반적으로 도큐멘트의 형식의 Txt file 을 로드한후 TextSpliter 로 청크해서 VectorDB 벡터스토어에 저장을 일반적으로 하더라구요. 그래서 이걸 다른방법으로 해보려고 여러시도를 해봤지만 잘 되지는 않았습니다.
4. Vector Store 에 저장하기
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS
from langchain.embeddings import OpenAIEmbeddings
loader = TextLoader('/Users/kdb/RAG_langchain/data.txt')
api_key = "Your open AI key"
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
texts = text_splitter.split_documents(documents)
embeddings = OpenAIEmbeddings(api_key=api_key)
db = FAISS.from_documents(texts, embeddings)
#코사인유사도
retriever = db.as_retriever(search_type="similarity_score_threshold", search_kwargs={"score_threshold": .5})
도큐멘트 단위로 아까말한대로 저장을해서 TextSplitter 를 활용해서 청크해주고 OpenAI Embeddings 을 활용해서 벡터단위로 임베딩을 해주고 벡터디비를 만들어 줍니다.
5. Retriver 선언후 langchain 을 통해 LLM 과 붙혀주기
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from langchain.schema import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI(temperature=0,api_key=api_key, model_name="gpt-4-1106-preview")
def format_docs(docs):
return "\n\n".join([d.page_content for d in docs])
chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
Chain 을 보시면 리트리버는 컨텍스트가 되어 LLM 이 리트리버의 정보를 통해서 RunnablePassthrough() 가 이제 유저의 질문이 되는 형태라고 보시면 됩니다. 사용자가 질문을 하면 이제 StrOutputParser() 로 가독성있는 문자열 형식으로 변환하여 출력을 해줍니다.
결국 리트리버가 들어가서 formats_docs를 거치면서 아까 청크한 문서의 단위대로 반복문이 돌고 리트리버가 저장이되어 컨텍스트가 형성이 됩니다.
6. 실행하기
지금 결과값자체가 Encode 문제로 google api 받아올때 인코딩이 덜 되어서 아스키코드나 , 이상하게 값이 들어가 있지만 별점은 맞았습니다. 데이터가 올바르게 들어갔는지 처음에 꼭 검증을 해보시길 권장드립니다.
chain.invoke("수내동에서 평점이 제일높은 맛집은어디야?")
확인을 해보니 소진이네 떡볶이, 떡갈비 1982 둘다 5점이 맞더라구요. 아마 인코딩 과정에서 깨진듯 합니다.
이렇게 RAG 를 통해서 어떻게보면 비교적 API 를 써서 최신의 데이터를 수집하고 RAG로 그 정보기반의 챗봇을 만들 수 있었습니다.
별로 어렵지 않습니다. 왜냐 랭체인이 다해놨거든요.. 천천히 따라해보시기 바랍니다. 감사합니다.
깃허브 에 따로 업로드 해두겠습니다 소스코드는 업그레이드 많이 해주시면 재밌을거 같습니다 ㅎㅎ😀
감사합니다