[8기 랭체인] 평점기반의 맛집 RAG 맛집챗봇 구축하기

안녕하세요 미남홀란드 입니다.

이번에 소개해드릴 컨텐츠는 Langchain 을 활용해서 보다 쉽게 RAG 서비스를 구축하는 방법입니다.

다들 맛집 좋아하실텐데 평점을 얼마나 믿으시는지는 모르겠습니다. 사실 구축단계에서 한국 사람들이 아무래도 네이버라는 플랫폼을 많이쓰고 신뢰하고 정보도 많기 때문에 NAVER API 를 활용하고 싶었으나, 진짜 감촉같이 이거 조금만 api 데이터쓰면 item 이다 할만한것만 지원을 하지 않는 모습이더라구요. 그래서 더욱 범용적인 Google maps 를 활용해서 만들어보았습니다. 이전에 사실 카카오톡맵기반으로 크롤링을 해서 데이터를 구축해서 만들려고 시도를 해보았으나, 자꾸 동적페이지 다음페이지 넘어가면서 Chrome Driver 가 오류가 나는 바람에 데이터를 구축하지를 못했습니다. 실시간 평점 데이터라 더 재밌었을텐데 아쉽습니다. 그건 나중에라도 크롤링에 성공한다면 꼭 시도를 해보겠습니다.


1. google maps api 신청 및 발급 받기

Google Maps Platform | Google for Developers



google 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로 그 정보기반의 챗봇을 만들 수 있었습니다.

별로 어렵지 않습니다. 왜냐 랭체인이 다해놨거든요.. 천천히 따라해보시기 바랍니다. 감사합니다.

깃허브 에 따로 업로드 해두겠습니다 소스코드는 업그레이드 많이 해주시면 재밌을거 같습니다 ㅎㅎ😀

감사합니다


8
5개의 답글

👉 이 게시글도 읽어보세요

모집 중인 AI 스터디