Motivation
- 지금까지 배웠던 내용들을 활용할 프로젝트로 영화 추천 Chat Agent를 만들어보기로 했습니다.
- ChatGPT 영화 추천 대화 예시
- 위 대화 내용을 보면 최신 한국 영화를 소개해 달라고 했을 때, 없는 영화제목을 가져오는 hallucination이 발생하는 것을 볼 수 있었습니다.
- 사용자가 원하는 영화를 추천하기 위해 LangChain을 이용하여 Prompt Strategy, Fuction calling, RAG pipeline을 이용하여 개개인에 맞는 추천을 해주는 챗봇을 만들어보고자 했습니다.
- Project github
Architecture
- 볼 만한 영화 없나? 요새 날씨에 맞는 영화 추천해줘. 연말에 보기 좋은 영화 추천해줘. Mbti OOOO이 좋아할 만한 영화 추천해줘. 이러한 애매한 질문에도 답할 수 있도록 만들고 싶었습니다.
- 이를 위해서 다음과 같은 구조를 생각했습니다.

- LLM을 두개 띄워서 첫번째 Retriever에서 정보 수집을 담당, 두번째 Recommender에서는 받은 정보를 처리하는 구조를 생각했습니다.
Function Calling
- 이를 구현하기 위해서 날씨, 검색 결과 등의 function calling을 구현하고자 했다.
날씨
- OpenAPI를 이용하여 좌표를 입력 받으면 그 지역의 기온을 리턴해주는 함수
import requests
import datetime
from pydantic.v1 import BaseModel, Field
from langchain.agents import tool
class OpenMeteoInput(BaseModel):
latitude: float = Field(..., description="Latitude of the location to fetch weather data for")
longitude: float = Field(..., description="Longitude of the location to fetch weather data for")
@tool(args_schema=OpenMeteoInput)
def get_current_temperature(latitude: float, longitude: float) -> dict:
"""Fetch current temperature for given coordinates."""
BASE_URL = "https://api.open-meteo.com/v1/forecast"
params = {
'latitude': latitude,
'longitude': longitude,
'hourly': 'temperature_2m',
'forecast_days': 1,
}
response = requests.get(BASE_URL, params=params)
if response.status_code == 200:
results = response.json()
else:
raise Exception(f"API Request failed with status code: {response.status_code}")
current_utc_time = datetime.datetime.utcnow()
time_list = [datetime.datetime.fromisoformat(time_str.replace('Z', '+00:00')) for time_str in results['hourly']['time']]
temperature_list = results['hourly']['temperature_2m']
closest_time_index = min(range(len(time_list)), key=lambda i: abs(time_list[i] - current_utc_time))
current_temperature = temperature_list[closest_time_index]
return f'The current temperature is {current_temperature}°C'
from langchain.chat_models import ChatOpenAI
from langchain.tools.render import format_tool_to_openai_function
from langchain.prompts import ChatPromptTemplate
from utils import get_openai_api_key
from Tools.temperature import get_current_temperature
from langchain.agents.output_parsers import OpenAIFunctionsAgentOutputParser
from langchain.schema.agent import AgentFinish
import openai
openai.api_key = get_openai_api_key()
functions = [
format_tool_to_openai_function(f) for f in [get_current_temperature]
]
prompt = ChatPromptTemplate.from_messages([
("system", "You are helpful but sassy assistant. If user ask you about movie, you should answer sincerely. \
if you don't have information of user, suppose that the user is in Seoul and don't ask user"),
("user", "{input}"),
])
def route(result):
if isinstance(result, AgentFinish):
return result.return_values['output']
else:
tools = {
"get_current_temperature": get_current_temperature,
}
return tools[result.tool].run(result.tool_input)
model = ChatOpenAI(temperature=0).bind(functions=functions)
chain = prompt | model | OpenAIFunctionsAgentOutputParser() | route
chain.invoke({"input":"오늘 부산 날씨는 어떤것 같아"})
'The current temperature is 3.8°C'
chain = prompt | model
chain.invoke({"input":"오늘 부산 날씨는 어떤것 같아"})
# 결과
AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n "latitude": 35.1796,\n "longitude": 129.0756\n}', 'name': 'get_current_temperature'}})
chain.invoke({"input":"오늘 서울 날씨는 어떤것 같아"})
# 결과
AIMessage(content='', additional_kwargs={'function_call': {'arguments': '{\n "latitude": 37.5665,\n "longitude": 126.9780\n}', 'name': 'get_current_temperature'}})
검색
import re
from serpapi import GoogleSearch
from pydantic.v1 import BaseModel, Field
from langchain.agents import tool
from utils import get_serp_api_key
class SearchInput(BaseModel):
query: str = Field(..., description="Search using Google when user ask for more information about the movie")
serpapi_key = get_serp_api_key()
def filter_dict(dictionary, exclude_patterns):
if not isinstance(dictionary, dict):
return dictionary
filtered = {}
for key, value in dictionary.items():
if isinstance(value, dict):
filtered[key] = filter_dict(value, exclude_patterns)
elif isinstance(value, list):
filtered[key] = [filter_dict(item, exclude_patterns) for item in value]
else:
if not any(re.search(pattern, key) for pattern in exclude_patterns):
filtered[key] = value
return filtered
exclude_patterns = [r'links', r'image', r'link', r'source', r'extensions']
@tool(args_schema=SearchInput)
def get_serpapi_search(query: str) -> dict:
"""serpapi search API를 활용하여 관련 정보와 관련 뉴스를 반환하는 함수
Args:
query (str): 검색할 키워드
Returns:
dict: 검색 결과 dict로 반환
"""
params = {
"api_key": serpapi_key,
"engine": "google",
"q": query,
"location": "Seoul, Seoul, South Korea",
"google_domain": "google.co.kr",
"gl": "kr",
"hl": "ko"
}
search = GoogleSearch(params)
results = search.get_dict()
final_result = dict({"search_results": results['knowledge_graph']})
if results.get('top_stories'):
final_result['latest_news'] = results['top_stories']
return filter_dict(final_result, exclude_patterns)
openai.api_key = get_openai_api_key()
functions = [
format_tool_to_openai_function(f) for f in [get_current_temperature, get_serpapi_search]
]
prompt = ChatPromptTemplate.from_messages([
("system", "You are helpful but sassy assistant. If user ask you about movie, you should answer sincerely. \
If you don't have information of user, suppose that the user is in Seoul and don't ask user anything like genre.\
If needed, you can use latest movie informations {latest_movies} \
If user wants MORE informations, use 'get_serpapi_search' with extracted movie, director or cast name.\
Refuse all irrelevant questions."),
("user", "{input}"),
])
def route(result):
if isinstance(result, AgentFinish):
return result.return_values['output']
else:
tools = {
"get_current_temperature": get_current_temperature,
"get_serpapi_search": get_serpapi_search,
}
return tools[result.tool].run(result.tool_input)
model = ChatOpenAI(temperature=0).bind(functions=functions)
information_retriever = prompt | model | OpenAIFunctionsAgentOutputParser() | route
prompt2 = ChatPromptTemplate.from_messages([
("system", "You are a movie recommender. Use your knowledge to recommend movies that fit\
and rewrite with given informations {informations}. \
***ALWAYS USE EXACT FIGURES AND ONLY BASED ON GIVEN INFORMATIONS**\
You must recommned more than one movie.\
You shoud answer using only Korean."),
])
model2 = ChatOpenAI(temperature=0.4, max_tokens=1024)
rewrite = prompt2 | model2
informations = information_retriever.invoke({"input":"크리스토퍼 놀란")
result = rewrite.invoke({"informations": informations})
print(result.content)
크리스토퍼 놀란 감독의 작품 중 추천할 만한 영화는 다음과 같습니다:
1. 오펜하이머 (2023년) - 2023년에 개봉 예정인 크리스토퍼 놀란 감독의 신작입니다.
2. 다크 나이트 (2008년) - 크리스토퍼 놀란 감독의 대표작 중 하나로, 배트맨 시리즈의 세 번째 작품입니다.
3. 인터스텔라 (2014년) - 과학적인 요소를 담은 SF 영화로, 인류의 생존을 위한 우주 여행을 그린 작품입니다.
4. 인셉션: 코볼사의 일 (2010년) - 꿈 속에서 꿈을 꾸는 특이한 개념을 다룬 작품으로, 꿈 속에서의 현실과 환상을 구분하는 이야기입니다.
이 외에도 크리스토퍼 놀란 감독의 다른 작품들도 많이 있으니, 관심이 있다면 더 찾아보시기 바랍니다.
Crawling
- 최신영화 데이터를 받아오기 위해서 Daum영화를 selenium으로 크롤링하여 사용했다.


- 마우스를 올리면 줄거리까지 나오기 때문에 동적 크롤링으로 위의 경로를 넣어줬다.
- 크롤링 결과

- 일단은 함수형태 이전에 위 최신영화 정보 데이터프레임을 프롬프트에 직접 Context로 추가해줬다.
import pandas as pd
prompt3 = ChatPromptTemplate.from_messages([
("system", "You are a movie recommender. Use your knowledge to recommend movies that fit\
and rewrite with given informations {informations}. \
If needed, you can use latest movie informations {latest_movies} \
You shoud answer using only Korean and markdown structure"),
])
model3 = ChatOpenAI(temperature=0, max_tokens=1024)
chain3 = prompt3 | model3
result1 = chain.invoke({"input":"오늘 날씨에 맞는 영화 추천좀. 최신 영화중에 어떤게 좋을까?"})
result3 = chain3.invoke({"informations": result1, "latest_movies":pd.read_csv("./data/daum_movie/movie_list.csv")})
현재 온도가 1.4°C로 매우 추운 날씨입니다. 추위를 느끼기 쉬운 날씨이므로, 따뜻하고 편안한 영화를 추천해 드릴게요.
추천 영화: 서울의 봄
- 관람가: 12세 이상 관람가
- 평점: 9.5
- 예매율: 14.9%
- 개봉일: 23년 11월 22일
- 줄거리: 1979년 12월 12일, 수도 서울 군사반란 발생 그날, 대한민국의 운명이 바뀌었다. 이날, 군사반란에 맞서 싸웠던 사람들의 이야기를 그린 영화입니다. 추운 날씨에는 따뜻한 향수를 불러일으키는 이 영화를 추천합니다.
추천 영화: 말하고 싶은 비밀
- 관람가: 전체 관람가
- 평점: 9.4
- 예매율: 0.5%
- 개봉일: 23년 12월 13일
- 줄거리: 어느 날 갑자기 고백 사고!? 마음이 잘못 배달되었다! 주인공인 노조미는 자신의 책상 서랍에 갑작스럽게 나타난 비밀 고백장치를 통해 다른 사람들의 비밀 고백을 듣게 되는데요. 따뜻한 이야기와 함께 마음을 따뜻하게 녹일 수 있는 영화입니다.
이 두 영화는 추운 날씨에 따뜻한 감정을 불러일으킬 수 있는 영화입니다. 온 가족이 함께 즐길 수 있는 영화이니 추천해 드립니다.
- 외에도 mbti는 Retriever가 user query에서 얻은 mbti에 대한 설명을 return하여 recommender로 넘겨주는 형태의 함수로 만들었다.
문제점
- 할루시네이션 발생

- 연관된 영화를 알려줄 때 예전에 개봉한 영화기 때문에 데이터가 없음에도 최신영화 context에 기반해서 숫자를 가져와 예매율과 평점을 말하는 현상 발생
- 모델마다 최대 context length가 정해져 있는데 prompt가 길어지면서 이를 초과하게되어 오류 발생. 이를 해결하기 위해 prompt에 넣어주기 보다는 함수로 만들어야 했다.
- 두개의 LLM을 띄워서 하는 구조가 비효율적이라는 생각이 들어 하나의 Agent로 구현해보기로 했다.