LangChain의 RunnableWithMessage와 Redis 활용하여 대화내용 저장하기

LangChain의 RunnableWithMessage와 Redis 활용하여 대화내용 저장하기

LangChain의 RunnableWithMessage는 LangChain에서 제공하는 유틸리티 클래스로, 주로 챗봇 애플리케이션 개발 시 이전의 대화 히스토리와 상호작용을 관리하기 위해 사용된다.
이 클래스는 주로 대화 기록을 저장하고 관리하여 대화 흐름을 유지하거나 개인화된 응답을 생성하는데 유용하다.

기본적으로 RunnableWithHistory 클래스는 대화 세션 내에서 사용자와 AI간의 대화 히스토리를 지속적으로 기록할 수 있도록 해주며, 필요한 경우 특정 조건에 따라 히스토리를 요약하거나 일부 삭제할 수 있는 기능도 제공한다.
이를통해 챗봇이 각 세션마다 대화 컨텍스트를 유지할 수 있어 더욱 자연스러운 대화 경험을 제공한다.

1
2
3
from dotenv import load_dotenv

load_dotenv(dotenv_path="../.env")
1
2
3
4
5
6
# LangSmith 추적을 설정합니다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력합니다.
logging.langsmith("langchain-Memory")
1
2
3
4
5
6
# LangSmith 추적을 설정합니다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력합니다.
logging.langsmith("langchain-Memory")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI

model = ChatOpenAI()
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"당신은 {ability} 에 능숙한 어시스턴트입니다. 20자 이내로 응답하세요",
),
# 대화 기록을 변수로 사용, history 가 MessageHistory 의 key 가 됨
MessagesPlaceholder(variable_name="history"),
("human", "{input}"), # 사용자 입력을 변수로 사용
]
)
runnable = prompt | model # 프롬프트와 모델을 연결하여 runnable 객체 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {} # 세션 기록을 저장할 딕셔너리


# 세션 ID를 기반으로 세션 기록을 가져오는 함수
def get_session_history(session_ids: str) -> BaseChatMessageHistory:
print(session_ids)
if session_ids not in store: # 세션 ID가 store에 없는 경우
# 새로운 ChatMessageHistory 객체를 생성하여 store에 저장
store[session_ids] = ChatMessageHistory()
return store[session_ids] # 해당 세션 ID에 대한 세션 기록 반환


with_message_history = (
RunnableWithMessageHistory( # RunnableWithMessageHistory 객체 생성
runnable, # 실행할 Runnable 객체
get_session_history, # 세션 기록을 가져오는 함수
input_messages_key="input", # 입력 메시지의 키
history_messages_key="history", # 기록 메시지의 키
)
)

영구 저장소 (Persistent Storage)

영구 저장소(Persistent Storage)는 프로그램이 종료되거나 시스템이 재부팅되더라도 데이터를 유지하는 저장 메커니즘을 말한다.
이는 데이터베이스, 파일시스템, 또는 기타 비휘발성 저장 장치를 통해 구현될 수 있다.

영구 저장소는 애플리케이션의 상태를 저장하고, 사용자 설정을 유지하며, 장기간 데이터를 보존하는 데 필수적이다.
이를 통해 프로그램은 이전 실행에서 중단된 지점부터 다시 시작할 수 있으며, 사용자는 데이터 손실 없이 작업을 계속 할 수 있다.

  • RunnableWithMessageHistory는 get_session_history 호출 가능 객체가 채팅 메시지 기록을 어떻게 검색하는지에 대해 독립적이다.

Redis 설치

Redis가 설치되어 있지 않다면 먼저 설치해야 한다.

폐쇄망 EC2이기 때문에, 인터넷이 연결된 로컬 컴퓨터에서 EC2 서버에 설치할 패키지를 다운로드 한다.

1
2
3
4
5
6
% pip download redis
Collecting redis
Using cached redis-5.2.0-py3-none-any.whl.metadata (9.1 kB)
Using cached redis-5.2.0-py3-none-any.whl (261 kB)
Saved ./redis-5.2.0-py3-none-any.whl
Successfully downloaded redis

S3에 redis whl 파일을 업로드한다.
그리고 EC2에 S3에서 whl 파일을 가져온다.

1
2
3
4
5
# aws configure
# export AWS_ACCESS_KEY_ID=...
...
# aws s3 cp s3://aipin-bucket/redis-5.2.0-py3-none-any.whl /root
download: s3://aipin-bucket/redis-5.2.0-py3-none-any.whl to ./redis-5.2.0-py3-none-any.whl

다운로드한 파일들을 EC2에서 설치한다.

1
2
3
4
5
# pip3 install redis-5.2.0-py3-none-any.whl --no-index --find-links .
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Looking in links: .
Processing ./redis-5.2.0-py3-none-any.whl
ERROR: Package 'redis' requires a different Python: 3.7.16 not in '>=3.8'
  • –no-index 옵션을 사용하여 외부 PyPI에 접속하지 않고, 지정된 디렉토리(–find-links .)에서만 패키지를 찾도록 한다.
  • redis 패키지가 Python3.8 이상의 버전을 요구하고 현재 시스템에 설치된 Python 버전이 3.7.16이기 때문에, Python 3.8이상으로 업그레이드 해야한다.

Python 버전 업그레이드 방법 (Amazon Linux 환경)

  1. Python 3.8 설치
    1
    2
    3
    sudo yum install -y amazon-linux-extras
    sudo amazon-linux-extras enable python3.8
    sudo yum install -y python3.8
  2. 기본 Python 버전 변경
  • python 3.8을 python3로 대체하려면 아래와 같이 심볼릭 링크를 업데이트 할 수 있음
    1
    sudo ln -sf /usr/bin/python3.8 /usr/bin/python3
  • pip3 역시 Python 3.8에 맞는 버전으로 변경
    1
    sudo ln -sf /usr/bin/pip3.8 /usr/bin/pip3
  1. Python 버전 확인
    1
    python3 --version

이제 Redis 패키지 설치 재시도를 한다.

1
2
3
4
5
6
7
# pip3 install redis-5.2.0-py3-none-any.whl --no-index --find-links .

WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Looking in links: .
Processing ./redis-5.2.0-py3-none-any.whl
ERROR: Could not find a version that satisfies the requirement async-timeout>=4.0.3; python_full_version < "3.11.3" (from redis)
ERROR: No matching distribution found for async-timeout>=4.0.3; python_full_version < "3.11.3"

이번엔 이런 에러가 나는데, redis 패키지가 async-timeout 패키지의 특정 버전 (>=4.0.3)을 필요로 하는데, 현재 환경에서는 해당 버전을 찾을 수 없어서 발생한 것이다.
pip download가 기본적으로 상위 패키지만 다운로드하고, 해당 패키지의 모든 의존성을 포함하지 않기 때문이다.
모든 의존성 패키지를 포함해 다운로드 하려면 pip에 추가 옵션을 지정해주어야 한다.

1
pip download --no-binary=:all: redis
  • 위와 같이 –no-binary 옵션을 사용해 의존성 패키지까지 함께 다운로드 할 수 있다.
  • redis와 모든 의존성 패키지들이 .whl 또는 .tar.gz 파일로 함께 다운로드 될 것이다.

tar.gz 파일을 S3를 통해 EC2로 전송하여 설치를 진행한다.

1
2
3
4
# aws s3 cp s3://aipin-bucket/redis-5.2.0.tar.gz /root
# tar -xzf async-timeout-4.0.3.tar.gz
# cd redis-5.2.0
# python3 setup.py install

그런데 또 에러가 난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# python3 setup.py install
/usr/lib64/python3.8/distutils/dist.py:274: UserWarning: Unknown distribution option: 'long_description_content_type'
warnings.warn(msg)
running install
error: can't create or remove files in install directory

The following error occurred while trying to add or remove files in the
installation directory:

[Errno 2] No such file or directory: '/usr/local/lib/python3.8/site-packages/test-easy-install-2904.write-test'

The installation directory you specified (via --install-dir, --prefix, or
the distutils default setting) was:

/usr/local/lib/python3.8/site-packages/

This directory does not currently exist. Please create it and try again, or
choose a different installation directory (using the -d or --install-dir
option).

이 오류는 /usr/local/lib/python3.8/site-packages/ 디렉토리가 존재하지 않아서 발생한 것이다.

-> 디렉토리 생성 후 설치 시도

1
2
sudo mkdir -p /usr/local/lib/python3.8/site-packages/
sudo python3 setup.py install

이번엔 setup.py를 사용하여 설치할 때 발생하는 marshal 모듈 관련 에러가 난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Traceback (most recent call last):
File "setup.py", line 4, in <module>
setup(
File "/usr/lib/python3.8/site-packages/setuptools/__init__.py", line 129, in setup
return distutils.core.setup(**attrs)
File "/usr/lib64/python3.8/distutils/core.py", line 148, in setup
dist.run_commands()
File "/usr/lib64/python3.8/distutils/dist.py", line 966, in run_commands
self.run_command(cmd)
File "/usr/lib64/python3.8/distutils/dist.py", line 985, in run_command
cmd_obj.run()
File "/usr/lib/python3.8/site-packages/setuptools/command/install.py", line 67, in run
self.do_egg_install()
File "/usr/lib/python3.8/site-packages/setuptools/command/install.py", line 109, in do_egg_install
self.run_command('bdist_egg')
File "/usr/lib64/python3.8/distutils/cmd.py", line 313, in run_command
self.distribution.run_command(command)
File "/usr/lib64/python3.8/distutils/dist.py", line 985, in run_command
cmd_obj.run()
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 218, in run
os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 269, in zip_safe
return analyze_egg(self.bdist_dir, self.stubs)
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 379, in analyze_egg
safe = scan_module(egg_dir, base, name, stubs) and safe
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 416, in scan_module
code = marshal.load(f)
ValueError: bad marshal data (unknown type code)

아예 방법을 바꿔서 Redis 서버를 설치하겠다.

Redis 서버 설치

Redis 서버를 설치하여 EC2 인스턴스에서 직접 Redis 서버를 운영하겠다.

  1. 인터넷이 연결된 환경에서 Redis 소스파일을 다운로드 한다.
    1
    wget http://download.redis.io/redis-stable.tar.gz
  2. 다운로드한 redis-stable.tar.gz 파일을 EC2 인스턴스로 전송한다.
  3. EC2에서 Redis 압축을 푼다.
    1
    2
    tar -xzf redis-stable.tar.gz
    cd redis-stable
  4. Redis를 컴파일하고 설치한다.
    1
    2
    make
    sudo make install
  5. Redis 서버를 실행하여 설치가 완료되었는지 확인한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    redis-server
    11366:C 07 Nov 2024 18:11:49.254 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then rebootor run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
    11366:C 07 Nov 2024 18:11:49.254 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    11366:C 07 Nov 2024 18:11:49.254 * Redis version=7.4.1, bits=64, commit=00000000, modified=0, pid=11366, just started
    11366:C 07 Nov 2024 18:11:49.254 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
    11366:M 07 Nov 2024 18:11:49.254 * monotonic clock: POSIX clock_gettime
    _._
    _.-``__ ''-._
    _.-`` `. `_. ''-._ Redis Community Edition
    .-`` .-```. ```\/ _.,_ ''-._ 7.4.1 (00000000/0) 64 bit
    ( ' , .-` | `, ) Running in standalone mode
    |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
    | `-._ `._ / _.-' | PID: 11366
    `-._ `-._ `-./ _.-' _.-'
    |`-._`-._ `-.__.-' _.-'_.-'|
    | `-._`-._ _.-'_.-' | https://redis.io
    `-._ `-._`-.__.-'_.-' _.-'
    |`-._`-._ `-.__.-' _.-'_.-'|
    | `-._`-._ _.-'_.-' |
    `-._ `-._`-.__.-'_.-' _.-'
    `-._ `-.__.-' _.-'
    `-._ _.-'
    `-.__.-'

    11366:M 07 Nov 2024 18:11:49.255 * Server initialized
    11366:M 07 Nov 2024 18:11:49.255 * Ready to accept connections tcp
1
2
# Redis 서버의 URL을 지정합니다.
REDIS_URL = "redis://localhost:6379/0"
1
2
3
4
5
6
7
8
9
from dotenv import load_dotenv
import os

load_dotenv()

# LANGCHAIN_TRACING_V2 환경 변수를 "true"로 설정합니다.
os.environ["LANGCHAIN_TRACING_V2"] = "true"
# LANGCHAIN_PROJECT 설정
os.environ["LANGCHAIN_PROJECT"] = "RunnableWithMessageHistory"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain_community.chat_message_histories import RedisChatMessageHistory


def get_message_history(session_id: str) -> RedisChatMessageHistory:
# 세션 ID를 기반으로 RedisChatMessageHistory 객체를 반환합니다.
return RedisChatMessageHistory(session_id, url=REDIS_URL)


with_message_history = RunnableWithMessageHistory(
runnable, # 실행 가능한 객체
get_message_history, # 메시지 기록을 가져오는 함수
input_messages_key="input", # 입력 메시지의 키
history_messages_key="history", # 기록 메시지의 키
)
1
2
3
4
5
6
with_message_history.invoke(
# 수학 관련 질문 "코사인의 의미는 무엇인가요?"를 입력으로 전달합니다.
{"ability": "math", "input": "What does cosine mean?"},
# 설정 옵션으로 세션 ID를 "redis123" 로 지정합니다.
config={"configurable": {"session_id": "redis123"}},
)
1
2
3
4
5
6
with_message_history.invoke(
# 이전 답변에 대한 한글 번역을 요청합니다.
{"ability": "math", "input": "이전의 답변을 한글로 번역해 주세요."},
# 설정 값으로 세션 ID를 "foobar"로 지정합니다.
config={"configurable": {"session_id": "redis123"}},
)
1
2
3
4
5
6
with_message_history.invoke(
# 이전 답변에 대한 한글 번역을 요청합니다.
{"ability": "math", "input": "이전의 답변을 한글로 번역해 주세요."},
# 설정 값으로 세션 ID를 "redis456"로 지정합니다.
config={"configurable": {"session_id": "redis456"}},
)

Redis 컨테이너에서…

1
2
3
docker exec -it redis-container sh
redis-cli
keys *
1
2
3
4
5
6
7
8
9
10
11
get message_store:redis456
get message_store:redis123

LRANGE message_store:redis456
LRANGE message_store:redis123

hgetall message_store:redis456
hgetall message_store:redis123

type message_store:redis456
type message_store:redis123

Redis 조회 시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> keys *
1) "message_store:redis456"
2) "message_store:redis123"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> lrange message_store:redis456 0 -1
1) "{\"type\": \"ai\", \"data\": {\"content\": \"\\uc218\\ud559\\uc5d0 \\ub2a5\\uc219\\ud55c \\ub3c4\\uc6b0\\ubbf8\\uc785\\ub2c8\\ub2e4.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 17, \"prompt_tokens\": 103, \"total_tokens\": 120, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-1507d5ba-7bce-458d-b9ff-b6c93252dab3-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 103, \"output_tokens\": 17, \"total_tokens\": 120, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
2) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc774\\uc804\\uc758 \\ub2f5\\ubcc0\\uc744 \\ud55c\\uae00\\ub85c \\ubc88\\uc5ed\\ud574 \\uc8fc\\uc138\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
3) "{\"type\": \"ai\", \"data\": {\"content\": \"\\uc218\\ud559\\uc5d0 \\ub2a5\\uc219\\ud55c \\ub3c4\\uc6b0\\ubbf8\\uc785\\ub2c8\\ub2e4.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 17, \"prompt_tokens\": 60, \"total_tokens\": 77, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-c7ce147f-2ac9-4068-8405-6bc91669a52e-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 60, \"output_tokens\": 17, \"total_tokens\": 77, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
4) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc774\\uc804\\uc758 \\ub2f5\\ubcc0\\uc744 \\ud55c\\uae00\\ub85c \\ubc88\\uc5ed\\ud574 \\uc8fc\\uc138\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> lrange message_store:redis123 0 -1
1) "{\"type\": \"ai\", \"data\": {\"content\": \"Shohei Ohtani is a Japanese professional baseball player who plays for the Los Angeles Angels in Major League Baseball (MLB). He is known for his exceptional skills as both a pitcher and a hitter.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 42, \"prompt_tokens\": 185, \"total_tokens\": 227, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-8ba62e18-27de-438b-8a88-a85b399951a6-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 185, \"output_tokens\": 42, \"total_tokens\": 227, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
2) "{\"type\": \"human\", \"data\": {\"content\": \"Who ohtani.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
3) "{\"type\": \"ai\", \"data\": {\"content\": \"Cosine is a trigonometric function that represents the ratio of the adjacent side to the hypotenuse in a right triangle.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 26, \"prompt_tokens\": 146, \"total_tokens\": 172, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-56f41ea1-25d2-4071-aa8e-24693cc3dead-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 146, \"output_tokens\": 26, \"total_tokens\": 172, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
4) "{\"type\": \"human\", \"data\": {\"content\": \"What does cosine mean?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
5) "{\"type\": \"ai\", \"data\": {\"content\": \"\\ucf54\\uc0ac\\uc778\\uc740 \\uc9c1\\uac01 \\uc0bc\\uac01\\ud615\\uc5d0\\uc11c \\uc778\\uc811\\ubcc0\\uacfc \\ube57\\ubcc0\\uc758 \\ube44\\uc728\\uc744 \\ub098\\ud0c0\\ub0c5\\ub2c8\\ub2e4.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 41, \"prompt_tokens\": 92, \"total_tokens\": 133, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-986633f9-22a6-4c16-89d9-3918780b585d-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 92, \"output_tokens\": 41, \"total_tokens\": 133, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
6) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc774\\uc804\\uc758 \\ub2f5\\ubcc0\\uc744 \\ud55c\\uae00\\ub85c \\ubc88\\uc5ed\\ud574 \\uc8fc\\uc138\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
7) "{\"type\": \"ai\", \"data\": {\"content\": \"Cosine represents the ratio of the adjacent side to the hypotenuse in a right triangle.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 19, \"prompt_tokens\": 47, \"total_tokens\": 66, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-146afcbf-7301-4673-b3d4-0bf6ae2c9191-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 47, \"output_tokens\": 19, \"total_tokens\": 66, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
8) "{\"type\": \"human\", \"data\": {\"content\": \"What does cosine mean?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"

프로젝트에 Redis 저장 및 로드 코드 추가

아래와 같이 Redis 설정을 추가하고

1
2
3
4
5
6
7
# Redis 설정
redis_client = redis.StrictRedis(
host=os.getenv('REDIS_HOST', 'localhost'),
port=int(os.getenv('REDIS_PORT', 6379)),
db=0,
decode_responses=True
)

대화 히스토리 저장용 함수를 추가하고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 대화 히스토리 저장용 함수
def save_chat_history_to_redis(user_id: str, message: str, response: str):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
user_chat_data = {
"timestamp": timestamp,
"role": "user",
"content": message
}
assistant_chat_data = {
"timestamp": timestamp,
"role": "assistant",
"content": response
}
# 사용자 메시지와 응답을 각각 저장
redis_client.rpush(f"chat_history:{user_id}", json.dumps(user_chat_data))
redis_client.rpush(f"chat_history:{user_id}", json.dumps(assistant_chat_data))
print("대화 내용이 저장되었습니다")

저장된 히스토리를 불러오는 함수도 추가하였다.

1
2
3
4
5
def get_session_history(user_id: str) -> List[ChatFormat]:
# Redis에서 user_id에 해당하는 대화 내역을 가져오는 로직 작성
history_data = redis_client.lrange(f"chat_history:{user_id}", 0, -1)
history = [ChatFormat(**json.loads(item)) for item in history_data]
return history

그리고 Agent 호출 로직에 history를 load 하여 호출하도록 추가하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Redis에서 사용자 대화 기록 가져와 history 대체하기
history = get_session_history(user_id)

# GIS AGENT 호출
response = call_gis_agent(message, history, summary)

# Redis에 대화 내용 저장
save_chat_history_to_redis(user_id, message, response)

# 대화 내역 출력 (optional)
print(print_session_history(history))

return response

그리고 비슷하게 summary를 저장하는 로직도 추가하였다.

실행하게 되면 Redis에서 아래와 같이 두 가지 Redis key가 사용자 별로 생성된다.

1
2
3
127.0.0.1:6379> keys *
1) "summary_list:default_user"
2) "chat_history:default_user"

한글로 채팅이 오고 가기 때문에 redis에서 조회해도 다 깨져있고, 로그에서 잘 저장되고 불러오는지 확인해보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
대화 내용이 저장되었습니다
저장된 대화 내용:
Role: user
Content: 효창공원역 맛집 추천해줘
----------
Role: assistant
Content: 효창공원역 주변의 맛집을 추천해드릴게요:

1. **창성옥**
- **주소**: 서울특별시 용산구 새창로 124-10 (용문동 25-16)
- **전화**: 02-718-2878
- **카테고리**: 한식
- **메뉴**:
- 뼈전골 (소): 27,000원
- 뼈전골 (중): 36,000원
- 해장국: 10,000원
- **영업시간**: 24시간 운영
- **예약 가능 여부**: 가능

2. **효창동짜장우동**
- **주소**: 서울특별시 용산구 백범로 283 (효창동 81-1)
- **전화**: 02-703-5287
- **카테고리**: 중식
- **메뉴**:
- 짜장: 4,000원
- 김치우동: 4,000원
- 우동: 4,000원

3. **박명도봉평메밀막국수**
- **주소**: 서울특별시 용산구 원효로 184-1 (원효로2가 43)
- **전화**: 02-717-7711
- **카테고리**: 국수전문
- **메뉴**:
- 들기름막국수: 11,000원
- 물막국수: 11,000원
- 비빔막국수: 11,000원
- **영업시간**: 10:00 - 22:00

4. **용문해장국**
- **주소**: 서울특별시 용산구 효창원로 110 (용문동 8-95)
- **전화**: 02-712-6290
- **카테고리**: 한식
- **메뉴**:
- 해장국: 7,000원
- 해장국 (2인분): 14,000원
- 뼈전골 (중): 30,000원
- **예약 가능 여부**: 가능, 주차 가능

5. **아성녹두빈대떡**
- **주소**: 서울특별시 용산구 효창원로48길 3 (용문동 8-113)
- **전화**: 02-706-8238
- **카테고리**: 기타
- **메뉴**:
- 녹두빈대떡: 15,000원
- 해물파전: 12,000원
- 감자전: 12,000원

이 맛집들이 효창공원역 근처에서 좋은 선택이 될 것입니다!
----------
Role: user
Content: 우리집은 반포자이 114동인데, 우리집에서 창성옥까지 얼마나걸려?
----------
Role: assistant
Content: 반포자이 114동에서 창성옥까지의 소요 시간은 약 24분 30초이며, 거리는 약 12,291 미터입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!
----------
Role: user
Content: 우리집에서 서울역까지 얼마나걸려?
----------
Role: assistant
Content: 반포자이 114동에서 서울역까지의 소요 시간은 약 33분이며, 거리는 약 9.5킬로미터입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!
----------
Role: user
Content: 아까 알려준 효창공원 맛집 중 첫번째가 어디였지?
----------
Role: assistant
Content: 아까 알려드린 효창공원역 맛집 중 첫 번째는 **창성옥**입니다. 주소는 서울특별시 용산구 새창로 124-10 (용문동 25-16)이고, 전화번호는 02-718-2878입니다. 한식을 제공하며, 뼈전골과 해장국이 주요 메뉴입니다. 24시간 운영하며 예약도 가능합니다.
----------
Role: user
Content: 아까 추천해준 효창공원 맛집 중 4번째는 어디였지?
----------
Role: assistant
Content: 아까 추천해드린 효창공원역 맛집 중 네 번째는 **용문해장국**입니다. 주소는 서울특별시 용산구 효창원로 110 (용문동 8-95)이고, 전화번호는 02-712-6290입니다. 한식을 제공하며, 해장국과 뼈전골이 주요 메뉴입니다. 예약이 가능하고 주차도 가능합니다.
----------
Role: user
Content: 우리 부모님집은 도원삼성래미안 아파트야
----------
Role: assistant
Content: 도원삼성래미안 아파트에서 서울역까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 17분 (1074초)
- **거리**: 약 3.965킬로미터 (3965미터)

안전한 운전 되세요!
----------
Role: user
Content: 우리집에서 우리부모님집까지 얼마나 걸려
----------
Role: assistant
Content: 반포자이 114동에서 도원삼성래미안 아파트까지의 소요 시간은 약 28분이며, 거리는 약 13,026미터입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!
----------
Role: user
Content: 우리집에서 KT 광화문 West 빌딩까지 얼마나 걸려?
----------
Role: assistant
Content: 반포자이 114동에서 KT 광화문 West 빌딩까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 29분 (1766초)
- **거리**: 약 9.3킬로미터 (9307미터)

안전한 운전 되세요!
----------
Role: user
Content: 우리집에서 KT 판교빌딩까지 얼마나 걸려?
----------
Role: assistant
Content: 반포자이 114동에서 KT 판교빌딩까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 19분 (1144초)
- **거리**: 약 14.47킬로미터 (14470미터)

안전 운전하세요!
----------
Role: user
Content: 우리집에서 KT 판교빌딩까지 얼마나 걸려?
----------
Role: assistant
Content: 반포자이 114동에서 KT 판교빌딩까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간:** 약 19분 17초 (1157초)
- **거리:** 약 14,464 미터

안전 운전하시기 바랍니다!
----------
Role: user
Content: 우리부모님집에서 KT 대관령 수련관 얼마나 걸려?
----------
Role: assistant
Content: 도원삼성래미안 아파트에서 KT 대관령 수련관까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 2시간 34분 (9280초)
- **거리**: 약 197.6킬로미터

이 정보를 바탕으로 여행을 계획하실 수 있습니다. 추가로 도움이 필요하시면 말씀해 주세요!
----------
None
/Users/leehamin/app/GIS-Agent/gis-ai-agent-be/agent_api/models/chat_models.py:22: LangChainDeprecationWarning: The class `LLMChain` was deprecated in LangChain 0.1.17 and will be removed in 1.0. Use :meth:`~RunnableSequence, e.g., `prompt | llm`` instead.
chain = LLMChain(prompt=get_summary_prompt_template(), llm=llm)
Summary list가 저장되었습니다.
answer_data: {'answer': '반포자이 114동에서 KT위즈파크까지의 경로 안내는 이미 완료되었습니다. 경로는 반포자이 114동에서 출발하여 신반포로를 따라 남쪽으로 이동한 후, 반포대교를 건너 수원 방향으로 계속 이동하여 경수대로를 따라 KT위즈파크에 도착하는 것입니다. 안전한 여행 되세요!', 'chat_status': 'C1001'}
answer_summary_list : ['사용자가 효창공원역 근처 맛집을 추천받고, 그 중 첫 번째와 네 번째 맛집에 대한 정보를 확인했습니다. 또한, 반포자이 114동에서 창성옥, 서울역, 도원삼성래미안 아파트, KT 광화문 West 빌딩, 그리고 KT 판교빌딩까지의 소요 시간과 거리를 문의하여 답변을 받았습니다.', '사용자가 효창공원역 근처 맛집을 추천받고, 첫 번째와 네 번째 맛집에 대한 정보를 확인했습니다. 반포자이 114동에서 창성옥, 서울역, 도원삼성래미안 아파트, KT 광화문 West 빌딩, KT 판교빌딩, 그리고 KT 대관령 수련관까지의 소요 시간과 거리를 문의하여 답변을 받았습니다. 또한, 도원삼성래미안 아파트에서 KT 대관령 수련관까지의 경로 정보를 확인했습니다.', '사용자가 효창공원역 근처 맛집을 추천받고, 그 중 첫 번째와 네 번째 맛집에 대한 정보를 확인했습니다. 또한, 반포자이 114동에서 창성옥, 서울역, 도원삼성래미안 아파트, KT 광화문 West 빌딩, KT 판교빌딩, 그리고 KT 대관령 수련관까지의 소요 시간과 거리를 문의하여 답변을 받았습니다. 사용자는 효창공원역 맛집 추천을 다시 요청했고, 첫 번째와 네 번째 맛집의 정보를 다시 확인했습니다. 도원삼성래미안 아파트에서 KT 대관령 수련관까지의 경로 정보도 확인했으며, 반포자이 114동에서 KT위즈파크까지의 소요 시간과 경로를 안내받았습니다.']
INFO: 127.0.0.1:51812 - "POST /api/v1/chat HTTP/1.1" 200 OK

위와 같이 대화 내용과 summary_list가 모두 잘 저장되고 load되며 멀티턴 또한 잘 진행된다.

User 별로 대화가 저장되도록 user_uuid 적용

더미로 default_user라는 user_id로 통일하던 것을
유저별로 대화 history와 summary가 적용될 수 있도록, 식별자인 user_uuid를 적용하였다.

헤더에 X-User-UUID를 달고 가는 방식이다.

1
2
3
4
5
6
7
8
def generate_or_get_uuid(user_uuid: Optional[str]) -> UUID:
if user_uuid:
try: # UUID가 유효한지 검사
return UUID(user_uuid)
except ValueError:
raise_error("E1006")
else: # UUID가 없으면 새 UUID 생성
return uuid4()

유저 아이디에 맞게 redis key가 생성된다.

1
2
3
4
5
127.0.0.1:6379> keys *
1) "summary_list:6b622f61-fd80-4906-bc50-e9aee6c72e77"
2) "summary_list:default_user"
3) "chat_history:6b622f61-fd80-4906-bc50-e9aee6c72e77"
4) "chat_history:default_user"

테스트 세션1

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "우리집은 판교 그랑블 아파트야, 우리집 좌표 알고 있니?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
{
"chat": {
"answer": "네, 판교 그랑블 아파트의 좌표는 다음과 같습니다:\n- 위도: 37.3933\n- 경도: 127.1144\n\n더 필요한 정보가 있으면 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다."
]
}

테스트 세션2

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "효창공원 맛집 알려줘",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"chat": {
"answer": "효창공원 주변의 맛집 정보는 다음과 같습니다:\n\n1. **효창원** \n - **주소**: 서울 용산구 효창원로 97\n - **특징**: 전통 한식과 다양한 메뉴를 제공하는 식당입니다.\n\n2. **카페 드 파리** \n - **주소**: 서울 용산구 효창원로 85\n - **특징**: 디저트와 커피를 전문으로 하는 아늑한 카페입니다.\n\n3. **고기리 막창** \n - **주소**: 서울 용산구 효창원로 95\n - **특징**: 고기 요리를 전문으로 하는 맛집으로, 막창이 유명합니다.\n\n이 외에도 더 많은 맛집이 있으니, 필요하시면 추가 정보를 요청해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다."
]
}

테스트 세션3

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "첫번째 것으로 할래! 우리집에서 거기까지 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"chat": {
"answer": "판교 그랑블 아파트에서 효창원까지의 소요 시간은 약 49분입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다."
]
}

테스트 세션4

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그럼 집에서 출발해서 거기서 밥먹고, 김포공항 가면 얼마나 걸릴까?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"chat": {
"answer": "판교 그랑블 아파트에서 효창원으로 이동한 후, 효창원에서 김포공항까지 이동하는 데 걸리는 총 소요 시간은 다음과 같습니다:\n\n- **판교 그랑블 아파트 → 효창원**: 약 41분 38초\n- **효창원 → 김포공항**: 약 56분 4초\n\n따라서, 총 소요 시간은 약 1시간 37분 42초입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다. 사용자가 효창원에서 식사를 마치고 김포공항으로 가는 시간을 묻자, 어시스턴트는 판교 그랑블 아파트에서 효창원까지 약 41분 38초, 효창원에서 김포공항까지 약 56분 4초로 총 소요 시간이 약 1시간 37분 42초라고 안내합니다."
]
}

테스트 세션5

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그러면 김포공항 대신 인천공항으로 가면 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"chat": {
"answer": "판교 그랑블 아파트에서 효창원까지의 소요 시간은 약 1시간 10분이며, 효창원에서 인천공항까지의 소요 시간은 약 47분입니다. 따라서, 총 소요 시간은 약 1시간 57분입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다. 사용자가 효창원에서 식사를 마치고 김포공항으로 가는 시간을 묻자, 어시스턴트는 판교 그랑블 아파트에서 효창원까지 약 41분 38초, 효창원에서 김포공항까지 약 56분 4초로 총 소요 시간이 약 1시간 37분 42초라고 안내합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다. 사용자가 효창원에서 식사를 마치고 김포공항으로 가는 시간을 묻자, 어시스턴트는 판교 그랑블 아파트에서 효창원까지 약 41분 38초, 효창원에서 김포공항까지 약 56분 4초로 총 소요 시간이 약 1시간 37분 42초라고 안내합니다. 사용자가 인천공항으로 가는 시간을 묻자, 어시스턴트는 총 소요 시간이 약 1시간 57분이라고 안내합니다."
]
}

서버에 배포

프로젝트에서 requirements.txt 파일을 사용하고 있기 때문에, 이 파일에 redis 패키지를 추가했다.

1
redis

Redis가 실행되고 있는 호스트의 IP로 바꾸었다

1
2
# host=os.getenv('REDIS_HOST', 'localhost'),
host=os.getenv('REDIS_HOST', '10.71.176.93'),
1
2
3
4
# EC2 환경
redis_url = os.getenv("REDIS_URL", "redis://10.71.176.93:6379/0")
# 로컬 환경
# redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")

Redis 서버가 외부 연결을 허용하는지 확인

Redis 설정파일 위치 확인

1
2
# Redis 설정 파일 위치 확인
find / -name "redis.conf"

Redis 설정 파일 수정

1
2
3
4
5
6
# 이렇게 있던 것을
bind 127.0.0.1 -::1

# 이렇게
bind 0.0.0.0
protected-mode no

Redis 재시작

1
2
3
4
5
6
7
8
9
redis-cli shutdown
# Redis 서버의 프로세스 ID 확인
ps aux | grep redis-server

# 프로세스 ID를 이용해 종료 (예: PID가 1234인 경우)
kill 1234

# Redis 서버 재시작
redis-server /path/to/redis.conf

여전히 protected mode가 실행중으로 나타난다

1
2
3
4
[root@ec2-ct01-dev-slm-app-01 ~]# curl -v telnet://10.71.176.93:6379
* Trying 10.71.176.93:6379...
* Connected to 10.71.176.93 (10.71.176.93) port 6379
-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE:You only need to do one of the above things in order for the server to start accepting connections from the outside.
  • Redis CLI를 통해 임시로 protected-mode 비활성화
    Redis 서버에 접근 가능한 경우, Redis CLI를 사용하여 protected-mode를 비활성화할 수 있다.
1
2
3
4
5
6
7
8
9
# Redis CLI 접속
redis-cli

# protected-mode 비활성화
CONFIG SET protected-mode no

[root@ec2-ct01-dev-slm-app-01 ~]# curl -v telnet://10.71.176.93:6379
* Trying 10.71.176.93:6379...
* Connected to 10.71.176.93 (10.71.176.93) port 6379

이번엔 연결이 되는 것 같다

서버 테스트

테스트 1

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "우리집은 판교 그랑블 아파트야, 우리집 좌표 알고 있니?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
{
"chat": {
"answer": "판교 그랑블 아파트의 좌표는 다음과 같습니다:\n\n- **위도**: 37.393299557105\n- **경도**: 127.11436915566647\n\n이 정보가 도움이 되길 바랍니다! 추가적인 질문이 있으면 언제든지 말씀해 주세요.",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다."
]
}

테스트 2

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "포천 맛집 알려줘",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"chat": {
"answer": "포천의 맛집 정보를 제공해드렸습니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다."
]
}

테스트 3

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "영종도 맛집 알려줘",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"chat": {
"answer": "영종도에서 추천하는 맛집 목록입니다:\n\n1. **선녀풍**\n - 주소: 인천광역시 중구 을왕동 용유서로 272, 689-1\n - 전화: 032-751-2121\n - 주메뉴: 해물파전(15,000원), 선녀물회(20,000원), 낙지물회(28,000원)\n - 영업시간: 매일 12:00 - 22:00\n - 주차: 가능\n - 좌표: [37.4444, 126.3787]\n\n2. **황해해물칼국수**\n - 주소: 인천광역시 중구 덕교동 마시란로 37, 128-56\n - 전화: 032-752-3017\n - 주메뉴: 산낙지(15,000원), 전복 (4마리)(16,000원), 해물칼국수(10,000원)\n - 영업시간: 매일 10:00 - 20:00\n - 주차: 가능\n - 좌표: [37.4262, 126.4212]\n\n3. **동해막국수**\n - 주소: 인천광역시 중구 을왕동 용유서로479번길 16, 859-3\n - 전화: 032-746-5522\n - 영업시간: 매일 11:00 - 21:00\n - 주차: 가능\n - 좌표: [37.4616, 126.3705]\n\n4. **미애네칼국수**\n - 주소: 인천광역시 중구 덕교동 용유로21번길 51, 80-14\n - 전화: 032-746-3838\n - 주메뉴: 산낙지(18,000원), 전복회(15,000원), 바다속칼국수 (소)(35,000원)\n - 영업시간: 매일 09:00 - 21:00\n - 주차: 가능\n - 좌표: [37.4300, 126.4242]\n\n5. **을항**\n - 주소: 인천광역시 중구 을왕동 선녀바위로55번길 39, 686-5\n - 전화: 032-752-2227\n - 주메뉴: 물회(1인)(25,000원), 물회(대)(75,000원), 물회(중)(55,000원)\n - 좌표: [37.4436, 126.3782]\n\n맛집 선택에 도움이 되길 바랍니다!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다."
]
}---
title: LangChain의 RunnableWithMessage와 Redis 활용하여 대화내용 저장하기
date: 2024-11-03 22:37:01
tags:
- LangChain
- LLM
- Redis
- [LLM, LangChain]
cover: /gallery/langChain.png
---

LangChain의 RunnableWithMessage는 LangChain에서 제공하는 유틸리티 클래스로, 주로 챗봇 애플리케이션 개발 시 이전의 대화 히스토리와 상호작용을 관리하기 위해 사용된다.
이 클래스는 주로 대화 기록을 저장하고 관리하여 대화 흐름을 유지하거나 개인화된 응답을 생성하는데 유용하다.

기본적으로 RunnableWithHistory 클래스는 대화 세션 내에서 사용자와 AI간의 대화 히스토리를 지속적으로 기록할 수 있도록 해주며, 필요한 경우 특정 조건에 따라 히스토리를 요약하거나 일부 삭제할 수 있는 기능도 제공한다.
이를통해 챗봇이 각 세션마다 대화 컨텍스트를 유지할 수 있어 더욱 자연스러운 대화 경험을 제공한다.

<!--more-->

~~~python
from dotenv import load_dotenv

load_dotenv(dotenv_path="../.env")
1
2
3
4
5
6
# LangSmith 추적을 설정합니다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력합니다.
logging.langsmith("langchain-Memory")
1
2
3
4
5
6
# LangSmith 추적을 설정합니다. https://smith.langchain.com
# !pip install langchain-teddynote
from langchain_teddynote import logging

# 프로젝트 이름을 입력합니다.
logging.langsmith("langchain-Memory")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI

model = ChatOpenAI()
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"당신은 {ability} 에 능숙한 어시스턴트입니다. 20자 이내로 응답하세요",
),
# 대화 기록을 변수로 사용, history 가 MessageHistory 의 key 가 됨
MessagesPlaceholder(variable_name="history"),
("human", "{input}"), # 사용자 입력을 변수로 사용
]
)
runnable = prompt | model # 프롬프트와 모델을 연결하여 runnable 객체 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

store = {} # 세션 기록을 저장할 딕셔너리


# 세션 ID를 기반으로 세션 기록을 가져오는 함수
def get_session_history(session_ids: str) -> BaseChatMessageHistory:
print(session_ids)
if session_ids not in store: # 세션 ID가 store에 없는 경우
# 새로운 ChatMessageHistory 객체를 생성하여 store에 저장
store[session_ids] = ChatMessageHistory()
return store[session_ids] # 해당 세션 ID에 대한 세션 기록 반환


with_message_history = (
RunnableWithMessageHistory( # RunnableWithMessageHistory 객체 생성
runnable, # 실행할 Runnable 객체
get_session_history, # 세션 기록을 가져오는 함수
input_messages_key="input", # 입력 메시지의 키
history_messages_key="history", # 기록 메시지의 키
)
)

영구 저장소 (Persistent Storage)

영구 저장소(Persistent Storage)는 프로그램이 종료되거나 시스템이 재부팅되더라도 데이터를 유지하는 저장 메커니즘을 말한다.
이는 데이터베이스, 파일시스템, 또는 기타 비휘발성 저장 장치를 통해 구현될 수 있다.

영구 저장소는 애플리케이션의 상태를 저장하고, 사용자 설정을 유지하며, 장기간 데이터를 보존하는 데 필수적이다.
이를 통해 프로그램은 이전 실행에서 중단된 지점부터 다시 시작할 수 있으며, 사용자는 데이터 손실 없이 작업을 계속 할 수 있다.

  • RunnableWithMessageHistory는 get_session_history 호출 가능 객체가 채팅 메시지 기록을 어떻게 검색하는지에 대해 독립적이다.

Redis 설치

Redis가 설치되어 있지 않다면 먼저 설치해야 한다.

폐쇄망 EC2이기 때문에, 인터넷이 연결된 로컬 컴퓨터에서 EC2 서버에 설치할 패키지를 다운로드 한다.

1
2
3
4
5
6
% pip download redis
Collecting redis
Using cached redis-5.2.0-py3-none-any.whl.metadata (9.1 kB)
Using cached redis-5.2.0-py3-none-any.whl (261 kB)
Saved ./redis-5.2.0-py3-none-any.whl
Successfully downloaded redis

S3에 redis whl 파일을 업로드한다.
그리고 EC2에 S3에서 whl 파일을 가져온다.

1
2
3
4
5
# aws configure
# export AWS_ACCESS_KEY_ID=...
...
# aws s3 cp s3://aipin-bucket/redis-5.2.0-py3-none-any.whl /root
download: s3://aipin-bucket/redis-5.2.0-py3-none-any.whl to ./redis-5.2.0-py3-none-any.whl

다운로드한 파일들을 EC2에서 설치한다.

1
2
3
4
5
# pip3 install redis-5.2.0-py3-none-any.whl --no-index --find-links .
WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Looking in links: .
Processing ./redis-5.2.0-py3-none-any.whl
ERROR: Package 'redis' requires a different Python: 3.7.16 not in '>=3.8'
  • –no-index 옵션을 사용하여 외부 PyPI에 접속하지 않고, 지정된 디렉토리(–find-links .)에서만 패키지를 찾도록 한다.
  • redis 패키지가 Python3.8 이상의 버전을 요구하고 현재 시스템에 설치된 Python 버전이 3.7.16이기 때문에, Python 3.8이상으로 업그레이드 해야한다.

Python 버전 업그레이드 방법 (Amazon Linux 환경)

  1. Python 3.8 설치
    1
    2
    3
    sudo yum install -y amazon-linux-extras
    sudo amazon-linux-extras enable python3.8
    sudo yum install -y python3.8
  2. 기본 Python 버전 변경
  • python 3.8을 python3로 대체하려면 아래와 같이 심볼릭 링크를 업데이트 할 수 있음
    1
    sudo ln -sf /usr/bin/python3.8 /usr/bin/python3
  • pip3 역시 Python 3.8에 맞는 버전으로 변경
    1
    sudo ln -sf /usr/bin/pip3.8 /usr/bin/pip3
  1. Python 버전 확인
    1
    python3 --version

이제 Redis 패키지 설치 재시도를 한다.

1
2
3
4
5
6
7
# pip3 install redis-5.2.0-py3-none-any.whl --no-index --find-links .

WARNING: Running pip install with root privileges is generally not a good idea. Try `pip3 install --user` instead.
Looking in links: .
Processing ./redis-5.2.0-py3-none-any.whl
ERROR: Could not find a version that satisfies the requirement async-timeout>=4.0.3; python_full_version < "3.11.3" (from redis)
ERROR: No matching distribution found for async-timeout>=4.0.3; python_full_version < "3.11.3"

이번엔 이런 에러가 나는데, redis 패키지가 async-timeout 패키지의 특정 버전 (>=4.0.3)을 필요로 하는데, 현재 환경에서는 해당 버전을 찾을 수 없어서 발생한 것이다.
pip download가 기본적으로 상위 패키지만 다운로드하고, 해당 패키지의 모든 의존성을 포함하지 않기 때문이다.
모든 의존성 패키지를 포함해 다운로드 하려면 pip에 추가 옵션을 지정해주어야 한다.

1
pip download --no-binary=:all: redis
  • 위와 같이 –no-binary 옵션을 사용해 의존성 패키지까지 함께 다운로드 할 수 있다.
  • redis와 모든 의존성 패키지들이 .whl 또는 .tar.gz 파일로 함께 다운로드 될 것이다.

tar.gz 파일을 S3를 통해 EC2로 전송하여 설치를 진행한다.

1
2
3
4
# aws s3 cp s3://aipin-bucket/redis-5.2.0.tar.gz /root
# tar -xzf async-timeout-4.0.3.tar.gz
# cd redis-5.2.0
# python3 setup.py install

그런데 또 에러가 난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# python3 setup.py install
/usr/lib64/python3.8/distutils/dist.py:274: UserWarning: Unknown distribution option: 'long_description_content_type'
warnings.warn(msg)
running install
error: can't create or remove files in install directory

The following error occurred while trying to add or remove files in the
installation directory:

[Errno 2] No such file or directory: '/usr/local/lib/python3.8/site-packages/test-easy-install-2904.write-test'

The installation directory you specified (via --install-dir, --prefix, or
the distutils default setting) was:

/usr/local/lib/python3.8/site-packages/

This directory does not currently exist. Please create it and try again, or
choose a different installation directory (using the -d or --install-dir
option).

이 오류는 /usr/local/lib/python3.8/site-packages/ 디렉토리가 존재하지 않아서 발생한 것이다.

-> 디렉토리 생성 후 설치 시도

1
2
sudo mkdir -p /usr/local/lib/python3.8/site-packages/
sudo python3 setup.py install

이번엔 setup.py를 사용하여 설치할 때 발생하는 marshal 모듈 관련 에러가 난다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Traceback (most recent call last):
File "setup.py", line 4, in <module>
setup(
File "/usr/lib/python3.8/site-packages/setuptools/__init__.py", line 129, in setup
return distutils.core.setup(**attrs)
File "/usr/lib64/python3.8/distutils/core.py", line 148, in setup
dist.run_commands()
File "/usr/lib64/python3.8/distutils/dist.py", line 966, in run_commands
self.run_command(cmd)
File "/usr/lib64/python3.8/distutils/dist.py", line 985, in run_command
cmd_obj.run()
File "/usr/lib/python3.8/site-packages/setuptools/command/install.py", line 67, in run
self.do_egg_install()
File "/usr/lib/python3.8/site-packages/setuptools/command/install.py", line 109, in do_egg_install
self.run_command('bdist_egg')
File "/usr/lib64/python3.8/distutils/cmd.py", line 313, in run_command
self.distribution.run_command(command)
File "/usr/lib64/python3.8/distutils/dist.py", line 985, in run_command
cmd_obj.run()
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 218, in run
os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 269, in zip_safe
return analyze_egg(self.bdist_dir, self.stubs)
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 379, in analyze_egg
safe = scan_module(egg_dir, base, name, stubs) and safe
File "/usr/lib/python3.8/site-packages/setuptools/command/bdist_egg.py", line 416, in scan_module
code = marshal.load(f)
ValueError: bad marshal data (unknown type code)

아예 방법을 바꿔서 Redis 서버를 설치하겠다.

Redis 서버 설치

Redis 서버를 설치하여 EC2 인스턴스에서 직접 Redis 서버를 운영하겠다.

  1. 인터넷이 연결된 환경에서 Redis 소스파일을 다운로드 한다.
    1
    wget http://download.redis.io/redis-stable.tar.gz
  2. 다운로드한 redis-stable.tar.gz 파일을 EC2 인스턴스로 전송한다.
  3. EC2에서 Redis 압축을 푼다.
    1
    2
    tar -xzf redis-stable.tar.gz
    cd redis-stable
  4. Redis를 컴파일하고 설치한다.
    1
    2
    make
    sudo make install
  5. Redis 서버를 실행하여 설치가 완료되었는지 확인한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    redis-server
    11366:C 07 Nov 2024 18:11:49.254 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then rebootor run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
    11366:C 07 Nov 2024 18:11:49.254 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    11366:C 07 Nov 2024 18:11:49.254 * Redis version=7.4.1, bits=64, commit=00000000, modified=0, pid=11366, just started
    11366:C 07 Nov 2024 18:11:49.254 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
    11366:M 07 Nov 2024 18:11:49.254 * monotonic clock: POSIX clock_gettime
    _._
    _.-``__ ''-._
    _.-`` `. `_. ''-._ Redis Community Edition
    .-`` .-```. ```\/ _.,_ ''-._ 7.4.1 (00000000/0) 64 bit
    ( ' , .-` | `, ) Running in standalone mode
    |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
    | `-._ `._ / _.-' | PID: 11366
    `-._ `-._ `-./ _.-' _.-'
    |`-._`-._ `-.__.-' _.-'_.-'|
    | `-._`-._ _.-'_.-' | https://redis.io
    `-._ `-._`-.__.-'_.-' _.-'
    |`-._`-._ `-.__.-' _.-'_.-'|
    | `-._`-._ _.-'_.-' |
    `-._ `-._`-.__.-'_.-' _.-'
    `-._ `-.__.-' _.-'
    `-._ _.-'
    `-.__.-'

    11366:M 07 Nov 2024 18:11:49.255 * Server initialized
    11366:M 07 Nov 2024 18:11:49.255 * Ready to accept connections tcp
1
2
# Redis 서버의 URL을 지정합니다.
REDIS_URL = "redis://localhost:6379/0"
1
2
3
4
5
6
7
8
9
from dotenv import load_dotenv
import os

load_dotenv()

# LANGCHAIN_TRACING_V2 환경 변수를 "true"로 설정합니다.
os.environ["LANGCHAIN_TRACING_V2"] = "true"
# LANGCHAIN_PROJECT 설정
os.environ["LANGCHAIN_PROJECT"] = "RunnableWithMessageHistory"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from langchain_community.chat_message_histories import RedisChatMessageHistory


def get_message_history(session_id: str) -> RedisChatMessageHistory:
# 세션 ID를 기반으로 RedisChatMessageHistory 객체를 반환합니다.
return RedisChatMessageHistory(session_id, url=REDIS_URL)


with_message_history = RunnableWithMessageHistory(
runnable, # 실행 가능한 객체
get_message_history, # 메시지 기록을 가져오는 함수
input_messages_key="input", # 입력 메시지의 키
history_messages_key="history", # 기록 메시지의 키
)
1
2
3
4
5
6
with_message_history.invoke(
# 수학 관련 질문 "코사인의 의미는 무엇인가요?"를 입력으로 전달합니다.
{"ability": "math", "input": "What does cosine mean?"},
# 설정 옵션으로 세션 ID를 "redis123" 로 지정합니다.
config={"configurable": {"session_id": "redis123"}},
)
1
2
3
4
5
6
with_message_history.invoke(
# 이전 답변에 대한 한글 번역을 요청합니다.
{"ability": "math", "input": "이전의 답변을 한글로 번역해 주세요."},
# 설정 값으로 세션 ID를 "foobar"로 지정합니다.
config={"configurable": {"session_id": "redis123"}},
)
1
2
3
4
5
6
with_message_history.invoke(
# 이전 답변에 대한 한글 번역을 요청합니다.
{"ability": "math", "input": "이전의 답변을 한글로 번역해 주세요."},
# 설정 값으로 세션 ID를 "redis456"로 지정합니다.
config={"configurable": {"session_id": "redis456"}},
)

Redis 컨테이너에서…

1
2
3
docker exec -it redis-container sh
redis-cli
keys *
1
2
3
4
5
6
7
8
9
10
11
get message_store:redis456
get message_store:redis123

LRANGE message_store:redis456
LRANGE message_store:redis123

hgetall message_store:redis456
hgetall message_store:redis123

type message_store:redis456
type message_store:redis123

Redis 조회 시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> keys *
1) "message_store:redis456"
2) "message_store:redis123"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> lrange message_store:redis456 0 -1
1) "{\"type\": \"ai\", \"data\": {\"content\": \"\\uc218\\ud559\\uc5d0 \\ub2a5\\uc219\\ud55c \\ub3c4\\uc6b0\\ubbf8\\uc785\\ub2c8\\ub2e4.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 17, \"prompt_tokens\": 103, \"total_tokens\": 120, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-1507d5ba-7bce-458d-b9ff-b6c93252dab3-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 103, \"output_tokens\": 17, \"total_tokens\": 120, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
2) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc774\\uc804\\uc758 \\ub2f5\\ubcc0\\uc744 \\ud55c\\uae00\\ub85c \\ubc88\\uc5ed\\ud574 \\uc8fc\\uc138\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
3) "{\"type\": \"ai\", \"data\": {\"content\": \"\\uc218\\ud559\\uc5d0 \\ub2a5\\uc219\\ud55c \\ub3c4\\uc6b0\\ubbf8\\uc785\\ub2c8\\ub2e4.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 17, \"prompt_tokens\": 60, \"total_tokens\": 77, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-c7ce147f-2ac9-4068-8405-6bc91669a52e-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 60, \"output_tokens\": 17, \"total_tokens\": 77, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
4) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc774\\uc804\\uc758 \\ub2f5\\ubcc0\\uc744 \\ud55c\\uae00\\ub85c \\ubc88\\uc5ed\\ud574 \\uc8fc\\uc138\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379>
127.0.0.1:6379> lrange message_store:redis123 0 -1
1) "{\"type\": \"ai\", \"data\": {\"content\": \"Shohei Ohtani is a Japanese professional baseball player who plays for the Los Angeles Angels in Major League Baseball (MLB). He is known for his exceptional skills as both a pitcher and a hitter.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 42, \"prompt_tokens\": 185, \"total_tokens\": 227, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-8ba62e18-27de-438b-8a88-a85b399951a6-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 185, \"output_tokens\": 42, \"total_tokens\": 227, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
2) "{\"type\": \"human\", \"data\": {\"content\": \"Who ohtani.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
3) "{\"type\": \"ai\", \"data\": {\"content\": \"Cosine is a trigonometric function that represents the ratio of the adjacent side to the hypotenuse in a right triangle.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 26, \"prompt_tokens\": 146, \"total_tokens\": 172, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-56f41ea1-25d2-4071-aa8e-24693cc3dead-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 146, \"output_tokens\": 26, \"total_tokens\": 172, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
4) "{\"type\": \"human\", \"data\": {\"content\": \"What does cosine mean?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
5) "{\"type\": \"ai\", \"data\": {\"content\": \"\\ucf54\\uc0ac\\uc778\\uc740 \\uc9c1\\uac01 \\uc0bc\\uac01\\ud615\\uc5d0\\uc11c \\uc778\\uc811\\ubcc0\\uacfc \\ube57\\ubcc0\\uc758 \\ube44\\uc728\\uc744 \\ub098\\ud0c0\\ub0c5\\ub2c8\\ub2e4.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 41, \"prompt_tokens\": 92, \"total_tokens\": 133, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-986633f9-22a6-4c16-89d9-3918780b585d-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 92, \"output_tokens\": 41, \"total_tokens\": 133, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
6) "{\"type\": \"human\", \"data\": {\"content\": \"\\uc774\\uc804\\uc758 \\ub2f5\\ubcc0\\uc744 \\ud55c\\uae00\\ub85c \\ubc88\\uc5ed\\ud574 \\uc8fc\\uc138\\uc694.\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"
7) "{\"type\": \"ai\", \"data\": {\"content\": \"Cosine represents the ratio of the adjacent side to the hypotenuse in a right triangle.\", \"additional_kwargs\": {\"refusal\": null}, \"response_metadata\": {\"token_usage\": {\"completion_tokens\": 19, \"prompt_tokens\": 47, \"total_tokens\": 66, \"completion_tokens_details\": {\"audio_tokens\": null, \"reasoning_tokens\": 0}, \"prompt_tokens_details\": {\"audio_tokens\": null, \"cached_tokens\": 0}}, \"model_name\": \"gpt-3.5-turbo-0125\", \"system_fingerprint\": null, \"finish_reason\": \"stop\", \"logprobs\": null}, \"type\": \"ai\", \"name\": null, \"id\": \"run-146afcbf-7301-4673-b3d4-0bf6ae2c9191-0\", \"example\": false, \"tool_calls\": [], \"invalid_tool_calls\": [], \"usage_metadata\": {\"input_tokens\": 47, \"output_tokens\": 19, \"total_tokens\": 66, \"input_token_details\": {\"cache_read\": 0}, \"output_token_details\": {\"reasoning\": 0}}}}"
8) "{\"type\": \"human\", \"data\": {\"content\": \"What does cosine mean?\", \"additional_kwargs\": {}, \"response_metadata\": {}, \"type\": \"human\", \"name\": null, \"id\": null, \"example\": false}}"

프로젝트에 Redis 저장 및 로드 코드 추가

아래와 같이 Redis 설정을 추가하고

1
2
3
4
5
6
7
# Redis 설정
redis_client = redis.StrictRedis(
host=os.getenv('REDIS_HOST', 'localhost'),
port=int(os.getenv('REDIS_PORT', 6379)),
db=0,
decode_responses=True
)

대화 히스토리 저장용 함수를 추가하고

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 대화 히스토리 저장용 함수
def save_chat_history_to_redis(user_id: str, message: str, response: str):
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
user_chat_data = {
"timestamp": timestamp,
"role": "user",
"content": message
}
assistant_chat_data = {
"timestamp": timestamp,
"role": "assistant",
"content": response
}
# 사용자 메시지와 응답을 각각 저장
redis_client.rpush(f"chat_history:{user_id}", json.dumps(user_chat_data))
redis_client.rpush(f"chat_history:{user_id}", json.dumps(assistant_chat_data))
print("대화 내용이 저장되었습니다")

저장된 히스토리를 불러오는 함수도 추가하였다.

1
2
3
4
5
def get_session_history(user_id: str) -> List[ChatFormat]:
# Redis에서 user_id에 해당하는 대화 내역을 가져오는 로직 작성
history_data = redis_client.lrange(f"chat_history:{user_id}", 0, -1)
history = [ChatFormat(**json.loads(item)) for item in history_data]
return history

그리고 Agent 호출 로직에 history를 load 하여 호출하도록 추가하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
# Redis에서 사용자 대화 기록 가져와 history 대체하기
history = get_session_history(user_id)

# GIS AGENT 호출
response = call_gis_agent(message, history, summary)

# Redis에 대화 내용 저장
save_chat_history_to_redis(user_id, message, response)

# 대화 내역 출력 (optional)
print(print_session_history(history))

return response

그리고 비슷하게 summary를 저장하는 로직도 추가하였다.

실행하게 되면 Redis에서 아래와 같이 두 가지 Redis key가 사용자 별로 생성된다.

1
2
3
127.0.0.1:6379> keys *
1) "summary_list:default_user"
2) "chat_history:default_user"

한글로 채팅이 오고 가기 때문에 redis에서 조회해도 다 깨져있고, 로그에서 잘 저장되고 불러오는지 확인해보겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
대화 내용이 저장되었습니다
저장된 대화 내용:
Role: user
Content: 효창공원역 맛집 추천해줘
----------
Role: assistant
Content: 효창공원역 주변의 맛집을 추천해드릴게요:

1. **창성옥**
- **주소**: 서울특별시 용산구 새창로 124-10 (용문동 25-16)
- **전화**: 02-718-2878
- **카테고리**: 한식
- **메뉴**:
- 뼈전골 (소): 27,000원
- 뼈전골 (중): 36,000원
- 해장국: 10,000원
- **영업시간**: 24시간 운영
- **예약 가능 여부**: 가능

2. **효창동짜장우동**
- **주소**: 서울특별시 용산구 백범로 283 (효창동 81-1)
- **전화**: 02-703-5287
- **카테고리**: 중식
- **메뉴**:
- 짜장: 4,000원
- 김치우동: 4,000원
- 우동: 4,000원

3. **박명도봉평메밀막국수**
- **주소**: 서울특별시 용산구 원효로 184-1 (원효로2가 43)
- **전화**: 02-717-7711
- **카테고리**: 국수전문
- **메뉴**:
- 들기름막국수: 11,000원
- 물막국수: 11,000원
- 비빔막국수: 11,000원
- **영업시간**: 10:00 - 22:00

4. **용문해장국**
- **주소**: 서울특별시 용산구 효창원로 110 (용문동 8-95)
- **전화**: 02-712-6290
- **카테고리**: 한식
- **메뉴**:
- 해장국: 7,000원
- 해장국 (2인분): 14,000원
- 뼈전골 (중): 30,000원
- **예약 가능 여부**: 가능, 주차 가능

5. **아성녹두빈대떡**
- **주소**: 서울특별시 용산구 효창원로48길 3 (용문동 8-113)
- **전화**: 02-706-8238
- **카테고리**: 기타
- **메뉴**:
- 녹두빈대떡: 15,000원
- 해물파전: 12,000원
- 감자전: 12,000원

이 맛집들이 효창공원역 근처에서 좋은 선택이 될 것입니다!
----------
Role: user
Content: 우리집은 반포자이 114동인데, 우리집에서 창성옥까지 얼마나걸려?
----------
Role: assistant
Content: 반포자이 114동에서 창성옥까지의 소요 시간은 약 24분 30초이며, 거리는 약 12,291 미터입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!
----------
Role: user
Content: 우리집에서 서울역까지 얼마나걸려?
----------
Role: assistant
Content: 반포자이 114동에서 서울역까지의 소요 시간은 약 33분이며, 거리는 약 9.5킬로미터입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!
----------
Role: user
Content: 아까 알려준 효창공원 맛집 중 첫번째가 어디였지?
----------
Role: assistant
Content: 아까 알려드린 효창공원역 맛집 중 첫 번째는 **창성옥**입니다. 주소는 서울특별시 용산구 새창로 124-10 (용문동 25-16)이고, 전화번호는 02-718-2878입니다. 한식을 제공하며, 뼈전골과 해장국이 주요 메뉴입니다. 24시간 운영하며 예약도 가능합니다.
----------
Role: user
Content: 아까 추천해준 효창공원 맛집 중 4번째는 어디였지?
----------
Role: assistant
Content: 아까 추천해드린 효창공원역 맛집 중 네 번째는 **용문해장국**입니다. 주소는 서울특별시 용산구 효창원로 110 (용문동 8-95)이고, 전화번호는 02-712-6290입니다. 한식을 제공하며, 해장국과 뼈전골이 주요 메뉴입니다. 예약이 가능하고 주차도 가능합니다.
----------
Role: user
Content: 우리 부모님집은 도원삼성래미안 아파트야
----------
Role: assistant
Content: 도원삼성래미안 아파트에서 서울역까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 17분 (1074초)
- **거리**: 약 3.965킬로미터 (3965미터)

안전한 운전 되세요!
----------
Role: user
Content: 우리집에서 우리부모님집까지 얼마나 걸려
----------
Role: assistant
Content: 반포자이 114동에서 도원삼성래미안 아파트까지의 소요 시간은 약 28분이며, 거리는 약 13,026미터입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!
----------
Role: user
Content: 우리집에서 KT 광화문 West 빌딩까지 얼마나 걸려?
----------
Role: assistant
Content: 반포자이 114동에서 KT 광화문 West 빌딩까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 29분 (1766초)
- **거리**: 약 9.3킬로미터 (9307미터)

안전한 운전 되세요!
----------
Role: user
Content: 우리집에서 KT 판교빌딩까지 얼마나 걸려?
----------
Role: assistant
Content: 반포자이 114동에서 KT 판교빌딩까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 19분 (1144초)
- **거리**: 약 14.47킬로미터 (14470미터)

안전 운전하세요!
----------
Role: user
Content: 우리집에서 KT 판교빌딩까지 얼마나 걸려?
----------
Role: assistant
Content: 반포자이 114동에서 KT 판교빌딩까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간:** 약 19분 17초 (1157초)
- **거리:** 약 14,464 미터

안전 운전하시기 바랍니다!
----------
Role: user
Content: 우리부모님집에서 KT 대관령 수련관 얼마나 걸려?
----------
Role: assistant
Content: 도원삼성래미안 아파트에서 KT 대관령 수련관까지의 경로 안내 결과는 다음과 같습니다:

- **소요 시간**: 약 2시간 34분 (9280초)
- **거리**: 약 197.6킬로미터

이 정보를 바탕으로 여행을 계획하실 수 있습니다. 추가로 도움이 필요하시면 말씀해 주세요!
----------
None
/Users/leehamin/app/GIS-Agent/gis-ai-agent-be/agent_api/models/chat_models.py:22: LangChainDeprecationWarning: The class `LLMChain` was deprecated in LangChain 0.1.17 and will be removed in 1.0. Use :meth:`~RunnableSequence, e.g., `prompt | llm`` instead.
chain = LLMChain(prompt=get_summary_prompt_template(), llm=llm)
Summary list가 저장되었습니다.
answer_data: {'answer': '반포자이 114동에서 KT위즈파크까지의 경로 안내는 이미 완료되었습니다. 경로는 반포자이 114동에서 출발하여 신반포로를 따라 남쪽으로 이동한 후, 반포대교를 건너 수원 방향으로 계속 이동하여 경수대로를 따라 KT위즈파크에 도착하는 것입니다. 안전한 여행 되세요!', 'chat_status': 'C1001'}
answer_summary_list : ['사용자가 효창공원역 근처 맛집을 추천받고, 그 중 첫 번째와 네 번째 맛집에 대한 정보를 확인했습니다. 또한, 반포자이 114동에서 창성옥, 서울역, 도원삼성래미안 아파트, KT 광화문 West 빌딩, 그리고 KT 판교빌딩까지의 소요 시간과 거리를 문의하여 답변을 받았습니다.', '사용자가 효창공원역 근처 맛집을 추천받고, 첫 번째와 네 번째 맛집에 대한 정보를 확인했습니다. 반포자이 114동에서 창성옥, 서울역, 도원삼성래미안 아파트, KT 광화문 West 빌딩, KT 판교빌딩, 그리고 KT 대관령 수련관까지의 소요 시간과 거리를 문의하여 답변을 받았습니다. 또한, 도원삼성래미안 아파트에서 KT 대관령 수련관까지의 경로 정보를 확인했습니다.', '사용자가 효창공원역 근처 맛집을 추천받고, 그 중 첫 번째와 네 번째 맛집에 대한 정보를 확인했습니다. 또한, 반포자이 114동에서 창성옥, 서울역, 도원삼성래미안 아파트, KT 광화문 West 빌딩, KT 판교빌딩, 그리고 KT 대관령 수련관까지의 소요 시간과 거리를 문의하여 답변을 받았습니다. 사용자는 효창공원역 맛집 추천을 다시 요청했고, 첫 번째와 네 번째 맛집의 정보를 다시 확인했습니다. 도원삼성래미안 아파트에서 KT 대관령 수련관까지의 경로 정보도 확인했으며, 반포자이 114동에서 KT위즈파크까지의 소요 시간과 경로를 안내받았습니다.']
INFO: 127.0.0.1:51812 - "POST /api/v1/chat HTTP/1.1" 200 OK

위와 같이 대화 내용과 summary_list가 모두 잘 저장되고 load되며 멀티턴 또한 잘 진행된다.

User 별로 대화가 저장되도록 user_uuid 적용

더미로 default_user라는 user_id로 통일하던 것을
유저별로 대화 history와 summary가 적용될 수 있도록, 식별자인 user_uuid를 적용하였다.

헤더에 X-User-UUID를 달고 가는 방식이다.

1
2
3
4
5
6
7
8
def generate_or_get_uuid(user_uuid: Optional[str]) -> UUID:
if user_uuid:
try: # UUID가 유효한지 검사
return UUID(user_uuid)
except ValueError:
raise_error("E1006")
else: # UUID가 없으면 새 UUID 생성
return uuid4()

유저 아이디에 맞게 redis key가 생성된다.

1
2
3
4
5
127.0.0.1:6379> keys *
1) "summary_list:6b622f61-fd80-4906-bc50-e9aee6c72e77"
2) "summary_list:default_user"
3) "chat_history:6b622f61-fd80-4906-bc50-e9aee6c72e77"
4) "chat_history:default_user"

테스트 세션1

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "우리집은 판교 그랑블 아파트야, 우리집 좌표 알고 있니?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
{
"chat": {
"answer": "네, 판교 그랑블 아파트의 좌표는 다음과 같습니다:\n- 위도: 37.3933\n- 경도: 127.1144\n\n더 필요한 정보가 있으면 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다."
]
}

테스트 세션2

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "효창공원 맛집 알려줘",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"chat": {
"answer": "효창공원 주변의 맛집 정보는 다음과 같습니다:\n\n1. **효창원** \n - **주소**: 서울 용산구 효창원로 97\n - **특징**: 전통 한식과 다양한 메뉴를 제공하는 식당입니다.\n\n2. **카페 드 파리** \n - **주소**: 서울 용산구 효창원로 85\n - **특징**: 디저트와 커피를 전문으로 하는 아늑한 카페입니다.\n\n3. **고기리 막창** \n - **주소**: 서울 용산구 효창원로 95\n - **특징**: 고기 요리를 전문으로 하는 맛집으로, 막창이 유명합니다.\n\n이 외에도 더 많은 맛집이 있으니, 필요하시면 추가 정보를 요청해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다."
]
}

테스트 세션3

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "첫번째 것으로 할래! 우리집에서 거기까지 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"chat": {
"answer": "판교 그랑블 아파트에서 효창원까지의 소요 시간은 약 49분입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다."
]
}

테스트 세션4

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그럼 집에서 출발해서 거기서 밥먹고, 김포공항 가면 얼마나 걸릴까?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"chat": {
"answer": "판교 그랑블 아파트에서 효창원으로 이동한 후, 효창원에서 김포공항까지 이동하는 데 걸리는 총 소요 시간은 다음과 같습니다:\n\n- **판교 그랑블 아파트 → 효창원**: 약 41분 38초\n- **효창원 → 김포공항**: 약 56분 4초\n\n따라서, 총 소요 시간은 약 1시간 37분 42초입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다. 사용자가 효창원에서 식사를 마치고 김포공항으로 가는 시간을 묻자, 어시스턴트는 판교 그랑블 아파트에서 효창원까지 약 41분 38초, 효창원에서 김포공항까지 약 56분 4초로 총 소요 시간이 약 1시간 37분 42초라고 안내합니다."
]
}

테스트 세션5

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그러면 김포공항 대신 인천공항으로 가면 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"chat": {
"answer": "판교 그랑블 아파트에서 효창원까지의 소요 시간은 약 1시간 10분이며, 효창원에서 인천공항까지의 소요 시간은 약 47분입니다. 따라서, 총 소요 시간은 약 1시간 57분입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 더 필요한 정보가 있는지 묻습니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다. 사용자가 효창원에서 식사를 마치고 김포공항으로 가는 시간을 묻자, 어시스턴트는 판교 그랑블 아파트에서 효창원까지 약 41분 38초, 효창원에서 김포공항까지 약 56분 4초로 총 소요 시간이 약 1시간 37분 42초라고 안내합니다.",
"사용자가 자신의 집이 판교 그랑블 아파트라고 말하며, 집의 좌표를 알고 있는지 물어봅니다. 어시스턴트는 위도 37.3933, 경도 127.1144의 좌표를 제공합니다. 사용자가 효창공원 맛집을 추천해달라고 하자, 어시스턴트는 효창원, 카페 드 파리, 고기리 막창 등의 맛집 정보를 제공합니다. 사용자가 효창원을 선택하며, 집에서 그곳까지 얼마나 걸리는지 물어보자, 어시스턴트는 약 49분이 걸린다고 답변합니다. 사용자가 효창원에서 식사를 마치고 김포공항으로 가는 시간을 묻자, 어시스턴트는 판교 그랑블 아파트에서 효창원까지 약 41분 38초, 효창원에서 김포공항까지 약 56분 4초로 총 소요 시간이 약 1시간 37분 42초라고 안내합니다. 사용자가 인천공항으로 가는 시간을 묻자, 어시스턴트는 총 소요 시간이 약 1시간 57분이라고 안내합니다."
]
}

서버에 배포

프로젝트에서 requirements.txt 파일을 사용하고 있기 때문에, 이 파일에 redis 패키지를 추가했다.

1
redis

Redis가 실행되고 있는 호스트의 IP로 바꾸었다

1
2
# host=os.getenv('REDIS_HOST', 'localhost'),
host=os.getenv('REDIS_HOST', '10.71.176.93'),
1
2
3
4
# EC2 환경
redis_url = os.getenv("REDIS_URL", "redis://10.71.176.93:6379/0")
# 로컬 환경
# redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")

Redis 서버가 외부 연결을 허용하는지 확인

Redis 설정파일 위치 확인

1
2
# Redis 설정 파일 위치 확인
find / -name "redis.conf"

Redis 설정 파일 수정

1
2
3
4
5
6
# 이렇게 있던 것을
bind 127.0.0.1 -::1

# 이렇게
bind 0.0.0.0
protected-mode no

Redis 재시작

1
2
3
4
5
6
7
8
9
redis-cli shutdown
# Redis 서버의 프로세스 ID 확인
ps aux | grep redis-server

# 프로세스 ID를 이용해 종료 (예: PID가 1234인 경우)
kill 1234

# Redis 서버 재시작
redis-server /path/to/redis.conf

여전히 protected mode가 실행중으로 나타난다

1
2
3
4
[root@ec2-ct01-dev-slm-app-01 ~]# curl -v telnet://10.71.176.93:6379
* Trying 10.71.176.93:6379...
* Connected to 10.71.176.93 (10.71.176.93) port 6379
-DENIED Redis is running in protected mode because protected mode is enabled and no password is set for the default user. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Set up an authentication password for the default user. NOTE:You only need to do one of the above things in order for the server to start accepting connections from the outside.
  • Redis CLI를 통해 임시로 protected-mode 비활성화
    Redis 서버에 접근 가능한 경우, Redis CLI를 사용하여 protected-mode를 비활성화할 수 있다.
1
2
3
4
5
6
7
8
9
# Redis CLI 접속
redis-cli

# protected-mode 비활성화
CONFIG SET protected-mode no

[root@ec2-ct01-dev-slm-app-01 ~]# curl -v telnet://10.71.176.93:6379
* Trying 10.71.176.93:6379...
* Connected to 10.71.176.93 (10.71.176.93) port 6379

이번엔 연결이 되는 것 같다

서버 테스트

테스트 1

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "우리집은 판교 그랑블 아파트야, 우리집 좌표 알고 있니?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
{
"chat": {
"answer": "판교 그랑블 아파트의 좌표는 다음과 같습니다:\n\n- **위도**: 37.393299557105\n- **경도**: 127.11436915566647\n\n이 정보가 도움이 되길 바랍니다! 추가적인 질문이 있으면 언제든지 말씀해 주세요.",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다."
]
}

테스트 2

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "포천 맛집 알려줘",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"chat": {
"answer": "포천의 맛집 정보를 제공해드렸습니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다."
]
}

테스트 3

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "영종도 맛집 알려줘",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"chat": {
"answer": "영종도에서 추천하는 맛집 목록입니다:\n\n1. **선녀풍**\n - 주소: 인천광역시 중구 을왕동 용유서로 272, 689-1\n - 전화: 032-751-2121\n - 주메뉴: 해물파전(15,000원), 선녀물회(20,000원), 낙지물회(28,000원)\n - 영업시간: 매일 12:00 - 22:00\n - 주차: 가능\n - 좌표: [37.4444, 126.3787]\n\n2. **황해해물칼국수**\n - 주소: 인천광역시 중구 덕교동 마시란로 37, 128-56\n - 전화: 032-752-3017\n - 주메뉴: 산낙지(15,000원), 전복 (4마리)(16,000원), 해물칼국수(10,000원)\n - 영업시간: 매일 10:00 - 20:00\n - 주차: 가능\n - 좌표: [37.4262, 126.4212]\n\n3. **동해막국수**\n - 주소: 인천광역시 중구 을왕동 용유서로479번길 16, 859-3\n - 전화: 032-746-5522\n - 영업시간: 매일 11:00 - 21:00\n - 주차: 가능\n - 좌표: [37.4616, 126.3705]\n\n4. **미애네칼국수**\n - 주소: 인천광역시 중구 덕교동 용유로21번길 51, 80-14\n - 전화: 032-746-3838\n - 주메뉴: 산낙지(18,000원), 전복회(15,000원), 바다속칼국수 (소)(35,000원)\n - 영업시간: 매일 09:00 - 21:00\n - 주차: 가능\n - 좌표: [37.4300, 126.4242]\n\n5. **을항**\n - 주소: 인천광역시 중구 을왕동 선녀바위로55번길 39, 686-5\n - 전화: 032-752-2227\n - 주메뉴: 물회(1인)(25,000원), 물회(대)(75,000원), 물회(중)(55,000원)\n - 좌표: [37.4436, 126.3782]\n\n맛집 선택에 도움이 되길 바랍니다!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다."
]
}

테스트 4

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "세번째 것으로 할래! 우리집에서 거기까지 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"chat": {
"answer": "판교 그랑블 아파트에서 영종도의 세 번째 맛집인 동해막국수까지의 경로는 약 1시간 5분이 소요되며, 거리는 약 78.54 km입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다."
]
}

테스트 5

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그럼 집에서 출발해서 거기서 밥먹고, 김포공항 가면 얼마나 걸릴까?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"chat": {
"answer": "판교 그랑블 아파트에서 출발하여 동해막국수에서 식사를 하고, 김포국제공항까지 가는 전체 경로는 다음과 같습니다:\n\n1. **판교 그랑블 아파트 → 동해막국수 (인천)**\n - 소요 시간: 약 1시간 5분\n - 거리: 약 78.54 km\n\n2. **동해막국수 (인천) → 김포국제공항**\n - 소요 시간: 약 40분\n - 거리: 약 45.51 km\n\n따라서 전체 소요 시간은 약 1시간 45분이며, 총 거리는 약 124.05 km입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다. 이어서 사용자가 동해막국수에서 식사를 하고 김포국제공항까지의 경로를 묻자, AI는 전체 소요 시간과 거리를 상세히 안내하였다."
]
}

테스트 6

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그러면 김포공항 대신 인천공항으로 가면 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"chat": {
"answer": "판교 그랑블 아파트에서 출발하여 동해막국수에서 식사를 하고, 인천국제공항까지 가는 전체 경로는 다음과 같습니다:\n\n1. **판교 그랑블 아파트 → 동해막국수 (인천)**\n - 소요 시간: 약 1시간 5분\n - 거리: 약 78.54 km\n\n2. **동해막국수 (인천) → 인천국제공항**\n - 소요 시간: 약 19분\n - 거리: 약 13.88 km\n\n따라서 전체 소요 시간은 약 1시간 24분이며, 총 거리는 약 92.42 km입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다. 이어서 사용자가 동해막국수에서 식사를 하고 김포국제공항까지의 경로를 묻자, AI는 전체 소요 시간과 거리를 상세히 안내하였다.",
"사용자가 판교 그랑블 아파트, 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다. 이어서 사용자가 동해막국수에서 식사를 하고 김포공항까지의 경로를 묻자, AI는 전체 소요 시간과 거리를 상세히 안내하였다. 마지막으로 사용자가 인천공항으로 가는 경로를 묻자, AI가 소요 시간과 거리를 안내하였다."
]
}

Redis에서도 해당 사용자 id에 맞게 history와 summary가 잘 저장된다.

1
2
3
4
5
6
7
8
9
[root@ec2-ct01-dev-slm-app-01 ~]# redis-cli
127.0.0.1:6379> keys *
1) "summary_list:b4d4e144-b8af-47d9-953b-ca80e65a474b"
2) "chat_history:b4d4e144-b8af-47d9-953b-ca80e65a474b"
3) "chat_history:6b622f61-fd80-4906-bc50-e9aee6c72e77"
4) "summary_list:6b622f61-fd80-4906-bc50-e9aee6c72e77"
127.0.0.1:6379> lrange chat_history:6b622f61-fd80-4906-bc50-e9aee6c72e77 0 -1
1) "{\"timestamp\": \"2024-11-11 08:36:48\", \"role\": \"user\", \"content\": \"\\uc6b0\\ub9ac\\uc9d1\\uc740 \\ud310\\uad50 \\uadf8\\ub791\\ube14 \\uc544\\ud30c\\ud2b8\\uc57c, \\uc6b0\\ub9ac\\uc9d1 \\uc88c\\ud45c \\uc54c\\uace0 \\uc788\\ub2c8?\"}"
2) "{\"timestamp\": \"2024-11-11 08:36:48\", \"role\": \"assistant\", \"content\": \"\\ud310\\uad50 \\uadf8\\ub791\\ube14 \\uc544\\ud30c\\ud2b8\\uc758 \\uc88c\\ud45c\\ub294 \\ub2e4\\uc74c\\uacfc \\uac19\\uc2b5\\ub2c8\\ub2e4:\\n\\n- **\\uc704\\ub3c4**: 37.393299557105\\n- **\\uacbd\\ub3c4**: 127.11436915566647\\n\\n\\uc774 \\uc815\\ubcf4\\uac00 \\ub3c4\\uc6c0\\uc774 \\ub418\\uae38 \\ubc14\\ub78d\\ub2c8\\ub2e4! \\ucd94\\uac00\\uc801\\uc778 \\uc9c8\\ubb38\\uc774 \\uc788\\uc73c\\uba74 \\uc5b8\\uc81c\\ub4e0\\uc9c0 \\ub9d0\\uc500\\ud574 \\uc8fc\\uc138\\uc694.\"}"
1
2
3
4
5
6
7
8
9
10
11
12

## 테스트 4

### Request
~~~json
{
"chat": {
"message": "세번째 것으로 할래! 우리집에서 거기까지 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"chat": {
"answer": "판교 그랑블 아파트에서 영종도의 세 번째 맛집인 동해막국수까지의 경로는 약 1시간 5분이 소요되며, 거리는 약 78.54 km입니다. 추가로 궁금한 점이 있으면 언제든지 말씀해 주세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다."
]
}

테스트 5

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그럼 집에서 출발해서 거기서 밥먹고, 김포공항 가면 얼마나 걸릴까?",
"history": []
},
"summary_list":[]
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"chat": {
"answer": "판교 그랑블 아파트에서 출발하여 동해막국수에서 식사를 하고, 김포국제공항까지 가는 전체 경로는 다음과 같습니다:\n\n1. **판교 그랑블 아파트 → 동해막국수 (인천)**\n - 소요 시간: 약 1시간 5분\n - 거리: 약 78.54 km\n\n2. **동해막국수 (인천) → 김포국제공항**\n - 소요 시간: 약 40분\n - 거리: 약 45.51 km\n\n따라서 전체 소요 시간은 약 1시간 45분이며, 총 거리는 약 124.05 km입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다. 이어서 사용자가 동해막국수에서 식사를 하고 김포국제공항까지의 경로를 묻자, AI는 전체 소요 시간과 거리를 상세히 안내하였다."
]
}

테스트 6

Request

1
2
3
4
5
6
7
{
"chat": {
"message": "그러면 김포공항 대신 인천공항으로 가면 얼마나 걸려?",
"history": []
},
"summary_list":[]
}

Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"chat": {
"answer": "판교 그랑블 아파트에서 출발하여 동해막국수에서 식사를 하고, 인천국제공항까지 가는 전체 경로는 다음과 같습니다:\n\n1. **판교 그랑블 아파트 → 동해막국수 (인천)**\n - 소요 시간: 약 1시간 5분\n - 거리: 약 78.54 km\n\n2. **동해막국수 (인천) → 인천국제공항**\n - 소요 시간: 약 19분\n - 거리: 약 13.88 km\n\n따라서 전체 소요 시간은 약 1시간 24분이며, 총 거리는 약 92.42 km입니다. 안전하고 즐거운 여행 되세요!",
"status_info": {
"chat_status": "C1001",
"summary_status": "S1001"
}
},
"summary_list": [
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다.",
"사용자가 판교 그랑블 아파트에 대해 묻자, AI가 아파트의 좌표를 제공하며 도움이 되길 바란다고 답변했다. 사용자가 포천 맛집을 묻자, AI가 포천의 맛집 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 영종도 맛집을 묻자, AI는 영종도에서 추천하는 다양한 맛집 목록과 상세 정보를 제공하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다.",
"사용자가 판교 그랑블 아파트와 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한, 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다. 이어서 사용자가 동해막국수에서 식사를 하고 김포국제공항까지의 경로를 묻자, AI는 전체 소요 시간과 거리를 상세히 안내하였다.",
"사용자가 판교 그랑블 아파트, 포천 및 영종도의 맛집에 대해 묻자, AI가 각각의 좌표와 맛집 정보를 제공하며 도움이 되길 바란다고 답변했다. 이후 사용자가 자신의 집인 판교 그랑블 아파트의 좌표를 알고 있는지 묻자, AI는 정확한 좌표를 제공하였다. 또한 사용자가 영종도의 맛집 중 세 번째 맛집인 동해막국수에 가고 싶다고 하자, AI는 판교 그랑블 아파트에서 해당 맛집까지의 소요 시간과 거리를 안내하였다. 이어서 사용자가 동해막국수에서 식사를 하고 김포공항까지의 경로를 묻자, AI는 전체 소요 시간과 거리를 상세히 안내하였다. 마지막으로 사용자가 인천공항으로 가는 경로를 묻자, AI가 소요 시간과 거리를 안내하였다."
]
}

Redis에서도 해당 사용자 id에 맞게 history와 summary가 잘 저장된다.

1
2
3
4
5
6
7
8
9
[root@ec2-ct01-dev-slm-app-01 ~]# redis-cli
127.0.0.1:6379> keys *
1) "summary_list:b4d4e144-b8af-47d9-953b-ca80e65a474b"
2) "chat_history:b4d4e144-b8af-47d9-953b-ca80e65a474b"
3) "chat_history:6b622f61-fd80-4906-bc50-e9aee6c72e77"
4) "summary_list:6b622f61-fd80-4906-bc50-e9aee6c72e77"
127.0.0.1:6379> lrange chat_history:6b622f61-fd80-4906-bc50-e9aee6c72e77 0 -1
1) "{\"timestamp\": \"2024-11-11 08:36:48\", \"role\": \"user\", \"content\": \"\\uc6b0\\ub9ac\\uc9d1\\uc740 \\ud310\\uad50 \\uadf8\\ub791\\ube14 \\uc544\\ud30c\\ud2b8\\uc57c, \\uc6b0\\ub9ac\\uc9d1 \\uc88c\\ud45c \\uc54c\\uace0 \\uc788\\ub2c8?\"}"
2) "{\"timestamp\": \"2024-11-11 08:36:48\", \"role\": \"assistant\", \"content\": \"\\ud310\\uad50 \\uadf8\\ub791\\ube14 \\uc544\\ud30c\\ud2b8\\uc758 \\uc88c\\ud45c\\ub294 \\ub2e4\\uc74c\\uacfc \\uac19\\uc2b5\\ub2c8\\ub2e4:\\n\\n- **\\uc704\\ub3c4**: 37.393299557105\\n- **\\uacbd\\ub3c4**: 127.11436915566647\\n\\n\\uc774 \\uc815\\ubcf4\\uac00 \\ub3c4\\uc6c0\\uc774 \\ub418\\uae38 \\ubc14\\ub78d\\ub2c8\\ub2e4! \\ucd94\\uac00\\uc801\\uc778 \\uc9c8\\ubb38\\uc774 \\uc788\\uc73c\\uba74 \\uc5b8\\uc81c\\ub4e0\\uc9c0 \\ub9d0\\uc500\\ud574 \\uc8fc\\uc138\\uc694.\"}"

LangChain의 RunnableWithMessage와 Redis 활용하여 대화내용 저장하기

https://hamin7.github.io/2024/11/03/LangChain-RunnableWithMessage/

Author

hamin

Posted on

2024-11-03

Updated on

2025-01-21

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.
You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.