CLOVA Studio 운영자6 Posted November 13 공유하기 Posted November 13 들어가며 (1부) MCP 실전 쿡북: LangChain에서 네이버 검색 도구 연결하기에 이어서, 이번 2부에서는 MCP로 Stateful 대화 기능을 구현하고 흐름을 관리하는 방법을 다룹니다. 1부에서 만든 MCP 서버를 기반으로 LangChain과 HyperCLOVA X 모델을 연결하고, 세션 ID를 활용해 대화 흐름을 지속적으로 이어가는 과정을 살펴봅니다. 또한 LangGraph의 Checkpointer를 사용해 세션 상태를 저장·재사용하며, SQLite 기반 저장소를 통해 멀티턴 문맥을 자연스럽게 유지하는 방법을 소개합니다. 마지막으로, 제한된 토큰 수 내에서 모델의 컨텍스트를 효율적으로 관리하는 팁도 함께 다뤄보겠습니다. MCP 실전 쿡북 4부작 시리즈 ✔︎ (1부) LangChain에서 네이버 검색 도구 연결하기 링크 ✔︎ (2부) 세션 관리와 컨텍스트 유지 전략 ✔︎ (3부) OAuth 인증이 통합된 MCP 서버 구축하기 링크 ✔︎ (4부) 운영과 확장을 위한 다양한 팁 링크 1. 사전 준비 사항 이 예제를 실행하려면 Python 3.10 이상의 개발 환경이 필요합니다. 가상환경을 구성하고 .env 파일에 API 키를 등록한 뒤, 필요한 패키지를 설치합니다. MCP 서버(server.py)는 1부에서 생성한 서버 파일을 그대로 사용합니다. 프로젝트 구성 프로젝트의 전체 파일 구조는 다음과 같습니다. Python 버전은 3.10 이상, 3.13 미만입니다. mcp_cookbook_part2/ ├── server.py # mcp_cookbook_part1에서 생성한 파일을 사용합니다. ├── client/ │ ├── stateful_client.py │ ├── stateful_client_trim.py │ └── stateful_client_sum.py ├── init_db.py ├── checkpoint.db # init_db.py 실행 시 생성됩니다. ├── requirements.txt ├── .venv └── .env 환경 변수 설정 루트 디렉터리에 .env 파일을 생성한 뒤, 앞서 발급받은 API Key를 다음과 같이 입력하고 저장합니다. 이때 따옴표 없이 값을 작성해야 하며, VS Code에서 실행할 경우 설정에서 Use Env File 옵션이 활성화되어 있는지 확인하세요. CLOVA_STUDIO_API_KEY=YOUR_API_KEY NAVER_CLIENT_ID=YOUR_CLIENT_ID NAVER_CLIENT_SECRET=YOUR_CLIENT_SECRET 패키지 설치 프로젝트에 필요한 패키지 목록은 아래 다운로드 링크에서 확인할 수 있습니다. 해당 내용을 복사해 루트 디렉터리에 requirements.txt 파일로 저장하세요. requirements.txt 다운로드 루트 디렉터리에서 터미널을 실행하여 다음과 같이 예제 실행에 필요한 패키지를 설치합니다. 가상환경 설치를 권장합니다. # 1. 파이썬 가상환경 생성 python -m venv .venv # 2. 가상환경 활성화 (macOS/Linux) source .venv/bin/activate # (Windows) # .venv/Scripts/activate.ps1 # 3. 패키지 설치 pip install -r requirements.txt 2. Stateless와 Stateful 개념 대화 시스템은 크게 Stateless와 Stateful 두 가지 방식으로 동작할 수 있습니다. Stateless: 각 요청이 이전 맥락과 완전히 분리되어 독립적으로 처리됩니다. Stateful: 대화 상태를 별도의 저장소에 보관하고, 동일한 세션 ID를 가진 후속 요청에서 자동으로 이 상태를 불러옵니다. 이렇게 하면 모델이 이전 대화 내용을 자연스럽게 이어받아 연속적이고 일관된 대화 흐름을 유지할 수 있습니다. 장기 대화나 도구 호출이 섞이는 복합 시나리오에서는 Stateful 방식을 사용하는 것이 사용자의 의도와 맥락을 안정적으로 추적하는 데 도움이 됩니다. 아래 도식은 클라이언트가 사용자 입력을 받아 세션 저장소에서 상태를 조회하거나 초기 컨텍스트를 구성한 뒤, LLM과 상호작용하여 응답하고, 마지막에 상태를 저장하는 Stateful 대화 흐름을 단계별로 보여줍니다. 3. Stateful 대화 구현 앞서 제시한 도식 흐름을 실제 코드로 구현하는 방법을 설명합니다. 세션 저장소 초기화 LangGraph에서 대화 상태를 지속적으로 저장・조회하려면 세션 저장소가 필요합니다. 여기에서는 SQLAlchemy를 이용해 SQLite 데이터베이스 파일(checkpoint.db)을 생성하고, 이후 Checkpointer가 사용할 수 있는 기본 테이블 메타데이터를 초기화합니다. 아래 스크립트를 실행하면 checkpoint.db 파일이 생성되며, 최초 1회만 실행하면 됩니다. 그 이후부터는 이 데이터베이스가 세션별 대화 상태를 보관하는 저장소로 사용됩니다. from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base, sessionmaker engine = create_engine("sqlite:///checkpoint.db", connect_args={"check_same_thread": False}) SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False) Base = declarative_base() Base.metadata.create_all(bind=engine) Stateful 대화 실행 다음은 사용자가 입력한 세션 ID를 기준으로 대화 상태를 관리하는 클라이언트입니다. 특정 ID가 입력되면 해당 세션의 히스토리를 불러와 맥락을 이어가고, 새로운 ID를 입력하면 시스템 프롬프트와 함께 새로운 세션을 시작합니다. 각 턴이 종료될 때마다 LangGraph가 Checkpointer에 상태를 저장하기 때문에, 이후 세션을 재실행하면 직전까지의 대화 흐름을 그대로 이어받아 자연스럽고 일관된 멀티턴 대화를 이어갈 수 있습니다. 모델은 HCX-005를 사용하였고, 만약 HCX-007 모델을 도구와 함께 사용할 경우에는 reasoning_effort="none"을 반드시 설정해야 합니다. import os import asyncio import uuid from dotenv import load_dotenv from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain_naver import ChatClovaX from langchain.agents import create_agent from langchain.messages import SystemMessage, HumanMessage from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver async def main(clova_api_key: str, server_url: str, checkpoint_path: str): """ CLOVA Studio 모델을 LangChain을 통해 호출하고, MCP 서버에 등록된 도구를 연동하여 최종 응답을 생성합니다. 사용자 입력에 따라 새 세션을 시작하거나 이전 세션을 이어갑니다. Args: clova_api_key (str): CLOVA Studio에서 발급받은 API Key server_url (str): 연결할 MCP 서버의 엔드포인트 URL checkpoint_path (str): Checkpoint 저장 경로 """ model = ChatClovaX(model="HCX-005", api_key=clova_api_key) async with streamablehttp_client(server_url) as (read, write, _,): async with ClientSession(read, write) as session: # MCP 서버를 초기화하고 도구 목록을 불러옵니다. await session.initialize() tools = await load_mcp_tools(session) # Checkpointer를 생성합니다. async with AsyncSqliteSaver.from_conn_string(checkpoint_path) as checkpointer: # 에이전트를 생성하고 Checkpointer를 연결합니다. agent = create_agent(model, tools, checkpointer=checkpointer) # 참조할 세션 ID를 입력받습니다. thread_id = input("세션 ID를 입력하세요(새 세션 시작은 Enter):").strip() # 새 세션이면 신규 세션 ID를 생성합니다. if not thread_id: thread_id = str(uuid.uuid4()) config = {"configurable": {"thread_id": thread_id}} print(f"현재 세션 ID: {thread_id}\n") print("안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)") system_message = [ SystemMessage(content=( "당신은 친절한 AI 어시스턴트입니다." "사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요." )) ] while True: user_input = input("\n\nUser: ") if user_input.lower() in ["종료", "exit"]: print("대화를 종료합니다. 이용해 주셔서 감사합니다.") break current_state = {"messages": [HumanMessage(content=user_input)]} try: existing_checkpoint = await checkpointer.aget_tuple(config) if existing_checkpoint is not None: # 기존 스레드가 있으면 현재 메시지만 추가합니다. state = current_state else: # 새 스레드라면 SystemMessage와 현재 메시지를 모두 추가합니다. state = {"messages": system_message + current_state["messages"]} # astream_events를 사용하여 스트리밍으로 응답을 처리합니다. async for event in agent.astream_events(state, config=config, version="v1"): kind = event["event"] if kind == "on_chat_model_stream": chunk = event["data"]["chunk"] if chunk.content: print(chunk.content, end="", flush=True) elif kind == "on_tool_start": print(f"\n[도구 선택]: {event['name']}\n[도구 호출]: {event['data'].get('input')}") elif kind == "on_tool_end": print(f"[도구 응답]: {event['data'].get('output')}\n") except Exception as e: print(f"\n요청을 처리하는 중에 오류가 발생했습니다. 오류: {e}") pass if __name__ == "__main__": """ .env 파일에서 CLOVA Studio API Key를 로드하고, MCP 서버의 엔드포인트 URL을 설정한 후 클라이언트를 실행합니다. """ load_dotenv() CLOVA_STUDIO_API_KEY = os.getenv("CLOVA_STUDIO_API_KEY") SERVER_URL = "http://127.0.0.1:8000/mcp/" CHECKPOINT_PATH = "checkpoint.db" asyncio.run(main(CLOVA_STUDIO_API_KEY, SERVER_URL, CHECKPOINT_PATH)) 위 스크립트를 실행하면, CLOVA Studio 모델(HCX-005)이 MCP 서버를 통해 등록된 도구와 연동되어 사용자의 질문에 응답합니다. 특정 세션에서 멀티턴 대화를 이어가면 대화 상태가 자동으로 저장되며, 이후 세션을 종료했다가 다시 실행하더라도 직전까지의 맥락을 불러와 자연스럽게 대화를 지속할 수 있습니다. 또한 이러한 대화 기록은 checkpoint.db의 checkpoints 테이블 내 checkpoint 컬럼에 직렬화된 형태로 저장되며, 실제 파일을 열어보면 해당 값에서 세션별 상태를 직접 확인할 수 있습니다. Quote 현재 세션 ID: 87aa13f0-074f-4b19-a804-c423e3e02da2 안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.) User: 요즘 날씨에 가기 좋은 국내 여행지 세 곳 알려줘 [도구 선택]: web_search [도구 호출]: {'query': '가을 날씨에 가기 좋은 국내 여행지', 'display': 20} [도구 응답]: content='{"query":"가을 날씨에 가기 좋은 국내 여행지","total":2629216,"items":[...}' name='web_search' tool_call_id='call_aU2RBXFVcomyyT8PBygupAM8' 현재 날씨에 가기 좋은 국내 여행지 몇 곳을 다음과 같이 추천드립니다. - **경기도 가평 아침고요수목원**: 가을에는 다채로운 색깔의 국화와 단풍이 어우러진 모습을 볼 수 있고, 넓은 정원을 산책하며 자연을 만끽할 수 있습니다. - **제주도 새별오름**: 억새풀이 아름답게 펼쳐진 오름으로 가을의 정취를 느끼기에 아주 좋습니다. 또한 정상에서의 전망이 탁 트여 있어 가슴이 뻥 뚫리는 듯한 느낌을 받을 수 있습니다. - **강원도 정선 민둥산**: 해발 1119미터의 산으로 정상에는 나무가 없고 참억새 밭이 흐드러지게 피어있어 장관을 이룹니다. 특히 가을에는 억새꽃 축제가 열려 더욱 볼거리가 많습니다. 위의 장소들은 모두 가을의 아름다움을 대표하는 곳으로 개인의 취향에 맞게 선택하시면 됩니다. 하지만 여행 전에 해당 지역의 날씨와 교통상황 등 자세한 정보를 먼저 파악하시기를 권해드립니다. 안전하고 즐거운 여행 되시길 바랍니다! User: 해외는? [도구 선택]: web_search [도구 호출]: {'query': '가을 날씨에 가기 좋은 해외 여행지', 'display': 20} [도구 응답]: content='{"query":"가을 날씨에 가기 좋은 해외 여행지","total":2492973,"items":[...]}' name='web_search' tool_call_id='call_kUgCWjOEuX7A2MHd88P9DNAY' 가을 날씨에 가기 좋은 해외 여행지를 다음과 같이 소개합니다. 1. **일본 교토**: 전통적인 일본 문화를 체험하고 아름다운 단풍 구경을 할 수 있습니다. 또한, 선선한 가을 날씨는 축제와 야외 활동을 즐기기에도 좋습니다. 2. **스페인 바르셀로나**: 11월에는 여름철 관광객 수가 줄어들어 더욱 여유로운 관광이 가능합니다. 특히, 가우디 건축물이나 캄프 누 경기장 등 대표적인 관광 명소를 방문하기 좋은 때입니다. 3. **캐나다 밴쿠버**: 캐나다 로키산맥의 가을은 황금빛으로 물든 풍경을 자랑하며, 하이킹과 자연 탐험을 즐기기에 이상적인 장소입니다. 또한, 온화한 기후로 인해 야외 활동하기에 적합한 환경을 제공합니다. 위의 여행지들은 모두 대중적으로 인기있는 여행지로 여행객들에게 많은 볼거리와 즐길 거리를 제공합니다. 하지만 개인의 취향과 선호도에 따라 다른 여행지가 더 적합할 수도 있으니 참고하시기 바랍니다. 또한, 각 국가의 입국 규정과 현지 상황을 미리 확인하여 계획을 세우는 것이 중요합니다. 다음과 같이 새로운 세션을 시작하면, 기존에 저장된 대화 맥락은 참조되지 않으며 시스템 프롬프트와 함께 처음부터 대화를 이어가게 됩니다. Quote 현재 세션 ID: cba6a7da-e4ca-43b9-9972-a42bcac9db3c User: 내가 맨 처음 물어본 질문 기억해? 죄송하지만, 저는 대화형 인공지능 모델이기 때문에 이전 대화를 저장하거나 기억하는 기능이 없습니다. 따라서 사용자가 처음으로 저에게 무엇을 물어봤는지 알 수가 없습니다. 하지만 현재의 질문에 대해서는 최대한 정확하고 유용한 답변을 제공하기 위해 노력하겠습니다. 어떤 주제에 대해 궁금하신 부분이 있으시면 말씀해 주세요! 제가 알고 있는 지식과 정보를 바탕으로 최선을 다해 도와드리겠습니다. 다음과 같이 기존 세션을 재시작하면, 직전까지 저장된 대화 맥락을 불러와 이전 흐름을 그대로 이어갈 수 있습니다. Quote 현재 세션 ID: 87aa13f0-074f-4b19-a804-c423e3e02da2 User: 내가 맨 처음 물어본 질문 기억해? 네, 제가 처음으로 받았던 질문은 "요즘 날씨에 가기 좋은 국내 여행지 세 곳"에 대한 내용이었습니다. 이후 사용자님께서 해외 여행지도 추가로 문의하셨습니다. 4. 컨텍스트 관리 전략 Stateful 대화는 맥락을 자동으로 이어받는 장점이 있지만, 저장된 히스토리를 모두 모델 입력에 넣다 보면 토큰 수 초과 오류를 마주하게 됩니다. 이때 맥락 관리 및 토큰 최적화 전략이 중요합니다. 다음 전략은 OpenAI Cookbook: Context Engineering를 참고하였습니다. 서비스 성격과 요구 사항에 맞게 전략을 적절히 선택하거나 새롭게 조합해 보시길 바랍니다. 컨텍스트 트리밍(Context Trimming) 최근 N개 턴의 메시지만 유지하는 방식으로, 구현이 단순하고 추가 지연이 발생하지 않는다는 장점이 있습니다. 하지만 N개 이전의 맥락은 모두 잊히며, 최근 턴이라 하더라도 도구 호출 결과가 지나치게 길면 여전히 토큰 수 초과 오류가 발생할 수 있습니다. 여기에서 한 턴(Turn)은 사용자 메시지 1개와 그에 대한 처리 과정(도구 호출, 도구 응답, 최종 답변)을 모두 포함합니다. 다음 이미지는 컨텍스트 트리밍의 동작을 설명합니다. 예를 들어 N=2로 설정하면, 매번 새로운 요청이 들어올 때마다 오래된 대화가 순차적으로 제거되어, 모델은 항상 최근 2개의 턴만 참조하게 됩니다. 다음은 세션별로 턴의 개수를 카운트해 관리하는 예시 코드입니다. import os import asyncio import uuid from dotenv import load_dotenv from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain_naver import ChatClovaX from langchain.agents import create_agent from langchain.messages import SystemMessage, HumanMessage from langchain_core.messages import BaseMessage from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver MAX_TURNS_TO_KEEP = 5 # 최대 유지할 턴(Turn)의 개수 def trim_messages_by_turn(messages: list[BaseMessage], max_turns: int) -> list[BaseMessage]: """ 대화 기록을 턴(Turn) 단위로 트리밍합니다. SystemMessage를 제외한 최대 max_turns 개의 턴만 유지합니다. 턴은 HumanMessage로 시작하는 묶음으로 간주합니다. Args: messages (list[BaseMessage]): 대화 기록 메시지 리스트 max_turns (int): 유지할 최대 턴 수 """ if not messages: return [] # SystemMessage와 대화 기록을 분리합니다. system_message = messages[0] if isinstance(messages[0], SystemMessage) else None history_messages = messages[1:] if system_message else messages # 턴 시작 인덱스를 기준으로 현재 턴 개수를 계산합니다. turn_start_indices = [ i for i, msg in enumerate(history_messages) if isinstance(msg, HumanMessage) ] current_turn_count = len(turn_start_indices) if current_turn_count <= max_turns: # 현재 턴 개수가 최대 유지할 턴 수 이하이면 그대로 반환합니다. return messages # 유지할 턴의 시작 인덱스를 계산합니다. trim_index = current_turn_count - max_turns keep_messages_start_index = turn_start_indices[trim_index] # 메시지 리스트를 트리밍합니다. trimmed_history = history_messages[keep_messages_start_index:] print(f"\n{max_turns}턴을 초과하여서 트리밍을 수행합니다.\n") # SystemMessage와 트리밍된 대화 기록을 합쳐 반환합니다. return [system_message] + trimmed_history async def main(clova_api_key: str, server_url: str, checkpoint_path: str): """ CLOVA Studio 모델을 LangChain을 통해 호출하고, MCP 서버에 등록된 도구를 연동하여 최종 응답을 생성합니다. 사용자 입력에 따라 새 세션을 시작하거나 이전 세션을 이어갑니다. Args: clova_api_key (str): CLOVA Studio에서 발급받은 API Key server_url (str): 연결할 MCP 서버의 엔드포인트 URL checkpoint_path (str): Checkpoint 저장 경로 """ model = ChatClovaX(model="HCX-005", api_key=clova_api_key) async with streamablehttp_client(server_url) as (read, write, _,): async with ClientSession(read, write) as session: # MCP 서버를 초기화하고 도구 목록을 불러옵니다. await session.initialize() tools = await load_mcp_tools(session) # Checkpointer를 생성합니다. async with AsyncSqliteSaver.from_conn_string(checkpoint_path) as checkpointer: # 에이전트를 생성하고 Checkpointer를 연결합니다. agent = create_agent(model, tools, checkpointer=checkpointer) # 참조할 세션 ID를 입력받습니다. thread_id = input("세션 ID를 입력하세요(새 세션 시작은 Enter):").strip() # 새 세션이면 신규 세션 ID를 생성합니다. if not thread_id: thread_id = str(uuid.uuid4()) config = {"configurable": {"thread_id": thread_id}} print(f"현재 세션 ID: {thread_id}\n") print("안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)") system_message = [ SystemMessage(content=( "당신은 친절한 AI 어시스턴트입니다." "사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요." )) ] while True: user_input = input("\n\nUser: ") if user_input.lower() in ["종료", "exit"]: print("대화를 종료합니다. 이용해 주셔서 감사합니다.") break current_state = {"messages": [HumanMessage(content=user_input)]} try: existing_checkpoint = await checkpointer.aget_tuple(config) if existing_checkpoint is not None: # 기존 대화 기록을 불러옵니다. messages = existing_checkpoint.checkpoint["channel_values"]["messages"] # 대화 기록을 턴 단위로 트리밍합니다. trimmed_messages = trim_messages_by_turn(messages, MAX_TURNS_TO_KEEP) # 트리밍된 메시지에 현재 사용자 메시지를 추가합니다. state = {"messages": trimmed_messages + current_state["messages"]} else: # 새 스레드라면 SystemMessage와 현재 메시지를 모두 추가합니다. state = {"messages": system_message + [HumanMessage(content=user_input)]} # astream_events를 사용하여 스트리밍으로 응답을 처리합니다. async for event in agent.astream_events(state, config=config, version="v1"): kind = event["event"] if kind == "on_chat_model_stream": chunk = event["data"]["chunk"] if chunk.content: print(chunk.content, end="", flush=True) elif kind == "on_tool_start": print(f"\n[도구 선택]: {event['name']}\n[도구 호출]: {event['data'].get('input')}") elif kind == "on_tool_end": print(f"[도구 응답]: {event['data'].get('output')}\n") except Exception as e: print(f"\n요청을 처리하는 중에 오류가 발생했습니다. 오류: {e}") pass if __name__ == "__main__": """ .env 파일에서 CLOVA Studio API Key를 로드하고, MCP 서버의 엔드포인트 URL을 설정한 후 클라이언트를 실행합니다. """ load_dotenv() CLOVA_STUDIO_API_KEY = os.getenv("CLOVA_STUDIO_API_KEY") SERVER_URL = "http://127.0.0.1:8000/mcp/" CHECKPOINT_PATH = "checkpoint.db" asyncio.run(main(CLOVA_STUDIO_API_KEY, SERVER_URL, CHECKPOINT_PATH)) 위 스크립트를 실행하면, CLOVA Studio 모델(HCX-005)이 MCP 서버를 통해 등록된 도구와 연동되어 사용자의 질문에 응답합니다. 세션 단위로 대화 상태가 유지되므로 멀티턴 대화를 지속할 수 있으며, 설정한 최대 턴 수를 기준으로 컨텍스트 트리밍이 적용됩니다. 다음은 사용자가 질의를 이어갈 때 트리밍이 발생하는 시점을 보여줍니다. 예를 들어, 최대 턴 수를 5로 설정한 경우, 여섯 번째 요청까지는 직전 5개 턴이 모두 유지되어 트리밍이 발생하지 않습니다. 일곱 번째 요청부터는 가장 오래된 턴이 제거되어, 항상 최근 5개 턴만 참조하도록 트리밍이 수행됩니다. Quote 현재 세션 ID: 77d0d044-5983-4b56-ab1e-da633276186a 안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.) User: 안녕 안녕하세요! 저는 CLOVA X입니다. 궁금하신 부분이나 도움이 필요한 사항이 있으시면 말씀해 주세요. 제가 알고 있는 지식과 능력으로 최대한 도움을 드리겠습니다. 좋은 하루 보내세요! User: 요즘 날씨에 가기 좋은 국내 여행지 세 곳 알려줘 [도구 선택]: web_search [도구 호출]: {'query': '가을 날씨에 가기 좋은 국내 여행지', 'display': 20} [도구 응답]: content='{"query":"가을 날씨에 가기 좋은 국내 여행지","total":2629216,"items":[...]}' name='web_search' tool_call_id='call_aU2RBXFVcomyyT8PBygupAM8' 현재 날씨에 가기 좋은 국내 여행지 몇 곳을 다음과 같이 추천드립니다. - **경기도 가평 아침고요수목원**: 가을에는 다채로운 색깔의 국화와 단풍이 어우러진 모습을 볼 수 있고, 넓은 정원을 산책하며 자연을 만끽할 수 있습니다. - **제주도 새별오름**: 억새풀이 아름답게 펼쳐진 오름으로 가을의 정취를 느끼기에 아주 좋습니다. 또한 정상에서의 전망이 탁 트여 있어 가슴이 뻥 뚫리는 듯한 느낌을 받을 수 있습니다. - **강원도 정선 민둥산**: 해발 1119미터의 산으로 정상에는 나무가 없고 참억새 밭이 흐드러지게 피어있어 장관을 이룹니다. 특히 가을에는 억새꽃 축제가 열려 더욱 볼거리가 많습니다. 위의 장소들은 모두 가을의 아름다움을 대표하는 곳으로 개인의 취향에 맞게 선택하시면 됩니다. 하지만 여행 전에 해당 지역의 날씨와 교통상황 등 자세한 정보를 먼저 파악하시기를 권해드립니다. 안전하고 즐거운 여행 되시길 바랍니다! User: 가을로 이행시 해줘 **가을** 가: 가슴이 설레는 계절 을: 을왕리로 떠나볼까요? 가을은 짧아서 아쉽기만 하지만 그만큼 더욱 소중한 시간이기도 합니다. 아름다운 단풍잎처럼 여러분들의 마음속에도 예쁜 추억이 가득 쌓이는 가을이 되셨으면 좋겠습니다. User: 가을은 왜 가을이야? 가을이라는 이름은 어떻게 생겨났을까요? 가을이란 명칭의 유래는 크게 두 가지로 나뉩니다. - 첫 번째는 고대 순우리말 '갈'에서 비롯되었다는 것입니다. 여기서 '갈'은 농작물을 수확할 때 사용하는 '낫'처럼 구부러진 모습을 의미한다고 합니다. 이는 가을이 곡식과 과일을 거두는 시기이기 때문이라고 해석됩니다. - 두 번째는 한자 문화권에서 유래했다는 설입니다. 가을의 옛 표현인 '추'(秋) 자는 원래 '벼 화'와 '열매가 많다는 뜻'을 나타내는 것으로 풀이되는 '리'(离) 자를 조합한 단어라고 합니다. 현재 우리가 사용하고 있는 '가을'이라는 말은 고려 H에 등장한 것으로 보아, 이전부터 쓰이던 우리 고유어였던 것으로 보입니다. 이처럼 가을은 풍성한 수확의 시기와 연결되며 자연스럽게 해당 이름이 붙여진 것으로 추측됩니다. User: 가을은 언제부터 언제까지야? 일반적으로 가을은 9월부터 11월까지라고 이야기하지만, 정확한 기간은 기상청마다 조금씩 다를 수 있습니다. 그러나 대부분의 기상청은 다음과 같이 구분합니다. * **대한민국 기상청**: 9월 1일부터 11월 30일까지 * **미국 국립해양대기청(NOAA)**: 9월 22일부터 12월 21일까지 * **일본 기상청**: 9월 1일부터 11월 30일까지 이와 같이 국가별로 약간의 차이가 있지만 대체로 비슷한 기간에 해당합니다. 또한 일반적으로 가을의 시작은 서리가 내리기 시작하는 늦여름부터로 간주되며, 끝은 첫눈이 내리는 초겨울까지로 여겨집니다. 따라서 위 기간보다 더 넓게 가을을 보는 경우도 많습니다. 한편, 천문학적으로는 춘분(3월 21일경)부터 하지(6월 21일경)까지를 봄, 하지를 중심으로 15도 이상 떨어진 기간을 가을이라 하여 9월 23일경부터 12월 21일경까지를 말합니다. 이 때는 태양 황경이 135°부터 225°사이의 중간에 위치하여 가을이라고 합니다. 결국, 가을의 정의는 지역과 문화, 기후 등에 따라 다르게 해석될 수 있다는 점을 기억하시는 게 좋습니다. User: 아하 고마워 별말씀을요! 저는 사용자님께 도움이 될 수 있도록 항상 최선을 다하겠습니다. 언제든지 필요하신 부분이 있으면 말씀해 주세요. 즐거운 하루 보내세요! User: 그나저나 내가 맨 처음 너한테 보낸 메시지 기억해? 5턴을 초과하여서 트리밍을 수행합니다. 네, 제가 받은 첫 번째 메시지는 "요즘 날씨에 가기 좋은 국내 여행지 세 곳"에 대한 질문이었습니다. 이에 대한 답변을 드리기 위해 웹 검색을 통해 관련 정보를 수집하였고, 이를 바탕으로 다음과 같은 여행지를 추천드렸습니다. - 경기도 가평 아침고요수목원 - 제주도 새별오름 - 강원도 정선 민둥산 위의 내용을 참고하여 사용자님께서 즐거운 여행을 하실 수 있기를 바랍니다. 추가적인 질문이나 요청사항이 있으시면 언제든지 말씀해주세요. 컨텍스트 요약(Context Summarization) 모델을 이용해 이전 대화를 간결한 요약으로 변환하고, 해당 요약 메시지를 히스토리에 삽입하는 방식입니다. 이 방법은 장기 기억을 압축해 보존할 수 있다는 장점이 있지만, 요약 과정에서 세부 정보가 누락될 수 있고, 요약을 갱신할 때마다 모델 호출이 필요하다는 유의 사항이 있습니다. 요약이 실행되는 로직은 다음과 같습니다. 여기에서 한 턴(Turn)은 사용자 메시지 1개와 그에 대한 처리 과정(도구 호출, 도구 응답, 최종 답변)을 모두 포함합니다. CONTEXT_TURNS_LIMIT(N): 요약을 실행하는 기준이 되는 값입니다. 누적된 턴의 개수가 이 값보다 클 때 요약을 실행합니다. MAX_TURNS_TO_KEEP(M): 요약이 실행된 뒤에도 원문 그대로 유지할 최근 턴 개수입니다. 그 이전 구간은 요약문으로 대체됩니다. 즉, 누적된 턴의 개수가 N을 초과하면 요약이 실행되고, 그 시점에서 최근 M개의 턴만 상태에 유지하며 그 이전 대화는 두 개의 메시지(요약 요청·요약 결과)로 치환됩니다. HumanMessage: 지금까지의 대화를 요약해 주세요. AIMessage: {요약문} 다음 이미지는 컨텍스트 요약이 동작하는 기준과 흐름을 설명합니다. 요약 기준 턴 수(Max N)를 초과하면 모델은 오래된 대화 중 요약 대상 구간(N − M + 1개의 턴)을 압축해 하나의 요약문으로 변환합니다. 이때 요약 요청(HumanMessage)과 요약 결과(AIMessage) 두 메시지가 새로 삽입되고, 그 뒤로는 보존 대상 구간(최근 M개의 사용자 턴)이 원문 그대로 유지됩니다. 이때, 요약이 요청되는 시점에 새로 들어오는 사용자 입력이 함께 포함되므로, 실제 요약 대상 범위는 N − M + 1개의 턴으로 계산됩니다. 다음은 세션별로 컨텍스트를 요약해 관리하는 예시 코드입니다. import os import asyncio import uuid from dotenv import load_dotenv from typing import List, Optional, Tuple from mcp import ClientSession from mcp.client.streamable_http import streamablehttp_client from langchain_mcp_adapters.tools import load_mcp_tools from langchain_naver import ChatClovaX from langchain.agents import create_agent from langchain.messages import SystemMessage, HumanMessage, AIMessage from langchain_core.messages import BaseMessage from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver CONTEXT_TURNS_LIMIT = 5 # 요약을 시작할 기준이 되는 턴(Turn)의 개수 MAX_TURNS_TO_KEEP = 2 # 최대 유지할 턴(Turn)의 개수 SUMMARY_REQUEST_CONTENT = "지금까지의 대화를 요약해 주세요." SUMMARY_PROMPT = ( "당신은 친절한 AI 어시스턴트입니다. 지금까지의 대화를 간결하게 요약하세요.\n" "요약에는 중요한 정보, 사용자 요청, 도구 호출 내역 단계가 포함되어야 합니다.\n" ) async def summarize_messages_by_turn(messages: List[BaseMessage], max_turns: int, context_limit: int, model: ChatClovaX, summary_prompt: str = SUMMARY_PROMPT,) -> Tuple[List[BaseMessage], bool]: # 반환 타입: (메시지 리스트, 요약 발생 여부) """ 대화 기록을 턴(Turn) 단위로 요약합니다. SystemMessage를 제외한 최대 max_turns 개의 턴만 유지합니다. 턴은 HumanMessage로 시작하는 묶음으로 간주합니다. 단, 요약은 현재 대화 기록의 사용자 턴 수가 context_limit를 초과할 때만 수행합니다. Args: messages (List[BaseMessage]): 대화 기록 메시지 리스트 max_turns (int): 유지할 최대 턴 수 context_limit (int): 요약을 시작할 기준이 되는 턴 수 model (ChatClovaX): 요약에 사용할 LLM 모델 summary_prompt (str): 요약에 사용할 프롬프트 """ if not messages: return [], False # SystemMessage와 대화 기록을 분리합니다. system_message: Optional[BaseMessage] = ( messages[0] if isinstance(messages[0], SystemMessage) else None ) # SystemMessage는 복사본을 만들어 메타데이터를 초기화합니다. if system_message: system_message = system_message.model_copy(deep=True) system_message.response_metadata = {} history_messages = messages[1:] if system_message else messages # 마지막 요약 지점을 찾습니다. last_synthetic_idx = -1 for idx, msg in enumerate(history_messages): if isinstance(msg, HumanMessage) and ( msg.additional_kwargs.get("synthetic") or getattr(msg, "content", "").strip() == SUMMARY_REQUEST_CONTENT ): last_synthetic_idx = idx # 요약 대상 범위를 결정합니다. if last_synthetic_idx != -1: target_messages = history_messages[last_synthetic_idx + 2 :] # 요청+응답 2개 이후 else: target_messages = history_messages # 실제 사용자 턴 개수를 계산합니다. turn_start_indices = [] for idx, msg in enumerate(target_messages): if isinstance(msg, HumanMessage) and not msg.additional_kwargs.get("synthetic"): turn_start_indices.append(idx) current_turn_count = len(turn_start_indices) # 요약 필요하지 않으면 원본 메시지를 그대로 반환합니다. if current_turn_count <= context_limit: return messages, False # 요약 경계 인덱스를 계산합니다. trim_index = current_turn_count - max_turns summary_boundary_in_target = turn_start_indices[trim_index] # 요약 대상 메시지를 준비합니다. summarize_messages_for_input = target_messages[:summary_boundary_in_target] conversation_snippets = [] if last_synthetic_idx != -1: old_synthetic_summary = history_messages[last_synthetic_idx + 1] prev_summary_text = getattr(old_synthetic_summary, "content", "").strip() if prev_summary_text: conversation_snippets.append( f"Previous Context Summary: {prev_summary_text}" ) for m in summarize_messages_for_input: content = (getattr(m, "content", "") or "").strip() if not content: continue role_label = "User" if isinstance(m, HumanMessage) else "Assistant" conversation_snippets.append(f"{role_label}: {content}") if not conversation_snippets: return messages, False summary_input_messages = [ SystemMessage(content=summary_prompt), HumanMessage(content="\n\n".join(conversation_snippets)), ] try: summary_response: BaseMessage = await model.ainvoke(summary_input_messages) summary_text = getattr(summary_response, "content", "").strip() except Exception as e: print(f"\n요약 호출 중 오류가 발생했습니다. 전체 기록을 유지합니다. 오류: {e}\n") return messages, False # 요약본과 함께 새로운 대화 상태를 구성합니다. shadow_user = HumanMessage( content=SUMMARY_REQUEST_CONTENT, additional_kwargs={"synthetic": True} ) synthetic_summary = AIMessage( content=summary_text, additional_kwargs={"synthetic": True} ) suffix_messages = target_messages[summary_boundary_in_target:] cleaned_suffix_messages = [] for msg in suffix_messages: new_msg = msg.model_copy(deep=True) new_msg.response_metadata = {} cleaned_suffix_messages.append(new_msg) new_history = [shadow_user, synthetic_summary] + cleaned_suffix_messages final_messages = ([system_message] + new_history) if system_message else new_history return final_messages, True def prune_to_last_summary(messages: List[BaseMessage]) -> List[BaseMessage]: """ Checkpointer에서 hydrate한 전체 메시지 중 마지막 요약 이후만 남겨 과거 히스토리 재유입을 차단합니다. Args: messages (List[BaseMessage]): 전체 메시지 리스트 """ if not messages: return messages system = messages[0] if isinstance(messages[0], SystemMessage) else None history = messages[1:] if system else messages base = 1 if system else 0 last_pair_start = -1 for i in range(len(history) - 1): h = history[i] a = history[i + 1] if isinstance(h, HumanMessage) and isinstance(a, AIMessage): if ( h.additional_kwargs.get("synthetic") or (getattr(h, "content", "").strip() == SUMMARY_REQUEST_CONTENT) ) and a.additional_kwargs.get("synthetic"): last_pair_start = i if last_pair_start == -1: return messages cut_pos = base + last_pair_start pruned = ([system] if system else []) + messages[cut_pos:] return pruned async def main(clova_api_key: str, server_url: str, checkpoint_path: str): """ CLOVA Studio 모델을 LangChain을 통해 호출하고, MCP 서버에 등록된 도구를 연동하여 최종 응답을 생성합니다. 사용자 입력에 따라 새 세션을 시작하거나 이전 세션을 이어갑니다. Args: clova_api_key (str): CLOVA Studio에서 발급받은 API Key server_url (str): 연결할 MCP 서버의 엔드포인트 URL checkpoint_path (str): Checkpoint 저장 경로 """ model = ChatClovaX(model="HCX-005", api_key=clova_api_key) async with streamablehttp_client(server_url) as (read, write, _,): async with ClientSession(read, write) as session: # MCP 서버를 초기화하고 도구 목록을 불러옵니다. await session.initialize() tools = await load_mcp_tools(session) # Checkpointer를 생성합니다. async with AsyncSqliteSaver.from_conn_string(checkpoint_path) as checkpointer: # 에이전트를 생성하고 Checkpointer를 연결합니다. agent = create_agent(model, tools, checkpointer=checkpointer) # 참조할 세션 ID를 입력받습니다. thread_id = input("세션 ID를 입력하세요(새 세션 시작은 Enter):").strip() # 새 세션이면 신규 세션 ID를 생성합니다. if not thread_id: thread_id = str(uuid.uuid4()) config = {"configurable": {"thread_id": thread_id}} print(f"현재 세션 ID: {thread_id}\n") print( "안녕하세요. 저는 AI 어시스턴트입니다. 원하시는 요청을 입력해 주세요. (종료하려면 '종료'를 입력하세요.)" ) system_message = [ SystemMessage(content=( "당신은 친절한 AI 어시스턴트입니다." "사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요." )) ] while True: user_input = input("\n\nUser: ") if user_input.lower() in ["종료", "exit"]: print("대화를 종료합니다. 이용해 주셔서 감사합니다.") break current_state = {"messages": [HumanMessage(content=user_input)]} try: existing_checkpoint = await checkpointer.aget_tuple(config) if existing_checkpoint is not None: # 기존 대화 기록을 불러옵니다. messages: List[BaseMessage] = existing_checkpoint.checkpoint["channel_values"]["messages"] # 과거 히스토리 재유입 차단을 위해 마지막 요약 이후만 남깁니다. messages = prune_to_last_summary(messages) # 턴 단위로 컨텍스트 요약을 적용합니다. summarized_messages, is_summarized = await summarize_messages_by_turn( messages, MAX_TURNS_TO_KEEP, CONTEXT_TURNS_LIMIT, model ) if is_summarized: # 요약이 발생하면 요약본과 함께 현재 사용자 메시지를 추가합니다. state = {"messages": summarized_messages + current_state["messages"]} else: # 요약이 없으면 프루닝된 메시지에 현재 사용자 메시지만 추가합니다. state = {"messages": messages + current_state["messages"]} else: # 새 스레드라면 SystemMessage와 재 메시지를 모두 추가합니다. state = {"messages": system_message + [HumanMessage(content=user_input)]} # print(f"[state 확인] {state['messages']}\n") # astream_events를 사용하여 스트리밍으로 응답을 처리합니다. async for event in agent.astream_events(state, config=config, version="v1"): kind = event["event"] if kind == "on_chat_model_stream": chunk = event["data"]["chunk"] if chunk.content: print(chunk.content, end="", flush=True) elif kind == "on_tool_start": print( f"\n[도구 선택]: {event['name']}\n[도구 호출]: {event['data'].get('input')}" ) elif kind == "on_tool_end": print(f"[도구 응답]: {event['data'].get('output')}\n") except Exception as e: print(f"\n요청을 처리하는 중에 오류가 발생했습니다. 오류: {e}") pass if __name__ == "__main__": """ .env 파일에서 CLOVA Studio API Key를 로드하고, MCP 서버의 엔드포인트 URL을 설정한 후 클라이언트를 실행합니다. """ load_dotenv() CLOVA_STUDIO_API_KEY = os.getenv("CLOVA_STUDIO_API_KEY") SERVER_URL = "http://127.0.0.1:8000/mcp/" CHECKPOINT_PATH = "checkpoint.db" asyncio.run(main(CLOVA_STUDIO_API_KEY, SERVER_URL, CHECKPOINT_PATH)) 위 스크립트를 실행하면, CLOVA Studio 모델(HCX-005)이 MCP 서버를 통해 등록된 도구와 연동되어 사용자의 질문에 응답합니다. 세션 단위로 대화 상태가 유지되므로 멀티턴 대화를 지속할 수 있으며, 설정된 요약 실행 임계치(N)와 최대 턴 수(M)를 기준으로 컨텍스트 요약이 자동 적용됩니다. 다음은 누적된 대화 상태를 보여줍니다. 예를 들어, N=5, M=2로 설정한 경우 여섯 번째 요청까지는 이전 5개 턴이 모두 유지되어 요약이 실행되지 않습니다. Quote [SystemMessage(content='당신은 친절한 AI 어시스턴트입니다.사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요.', additional_kwargs={}, response_metadata={}, id='597ef94e-fab0-479f-bf7d-6377e98ec8de'), HumanMessage(content='안녕', additional_kwargs={}, response_metadata={}, id='6616cce9-a8a0-42dd-8e33-4065183c55c4'), AIMessage(content='안녕하세요! 저는 CLOVA X입니다.\n\n궁금하신 부분이나 도움이 필요하시면 언제든지 말씀해 주세요. 제가 알고 있는 지식과 능력으로 최대한 도움을 드리겠습니다. \n\n좋은 하루 보내세요!', additional_kwargs={'thinking_content': ''}, response_metadata={'finish_reason': 'stop', 'model_name': 'HCX-005'}, id='run--f5eab89f-80d4-4953-8f0c-2c73ca7f325c', usage_metadata={'input_tokens': 34, 'output_tokens': 41, 'total_tokens': 75, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='요즘 날씨에 가기 좋은 국내 여행지 세 곳 알려줘', additional_kwargs={}, response_metadata={}, id='f39488db-614c-4c96-aae0-25abf3466ffc'), AIMessage(content='', additional_kwargs={'tool_calls': [{'index': 0, 'id': 'call_xup7wnfp3VcnM9eLpKg5GzI5', 'function': {'arguments': '{"query": "가을 날씨에 가기 좋은 국내 여행지", "display": 20}', 'name': 'web_search'}, 'type': 'function'}], 'thinking_content': ''}, response_metadata={'finish_reason': 'tool_calls', 'model_name': 'HCX-005'}, id='run--b692aefd-8f87-4ca5-8710-c6d1288fe021', tool_calls=[{'name': 'web_search', 'args': {'query': '가을 날씨에 가기 좋은 국내 여행지', 'display': 20}, 'id': 'call_xup7wnfp3VcnM9eLpKg5GzI5', 'type': 'tool_call'}], usage_metadata={'input_tokens': 106, 'output_tokens': 44, 'total_tokens': 150, 'input_token_details': {}, 'output_token_details': {}}), ToolMessage(content='{"query":"가을 날씨에 가기 좋은 국내 여행지","total":2641945,"items":[...]}', name='web_search', id='cad16903-d644-4b2b-ac6e-e342c0808938', tool_call_id='call_xup7wnfp3VcnM9eLpKg5GzI5'), AIMessage(content='가을 날씨에 가기 좋은 국내 여행지 세 곳을 다음과 같이 추천드립니다.\n\n\n1. 제주도: 제주는 가을에도 따뜻한 날씨와 아름다운 경치를 제공합니다. 특히 가을에는 억새풀이 유명한 산굼부리와 섭지코스를 방문해 보시는 것을 추천드립니다. 또한 우도와 마라도 등 섬 여행도 인기가 많습니다.\n\n2. 경주: 경주는 역사와 문화가 살아 숨쉬는 도시로, 가을에는 노란 은행나무와 붉은 단풍이 아름다운 경관을 자아냅니다. 첨성대, 불국사 등의 역사 유적지와 함께 동궁과 월지에서 산책을 즐겨보세요.\n\n3. 강원도 설악산 국립공원: 가을철 단풍이 유명한 곳으로 케이블카를 타고 권금성을 올라가면 울산바위와 멋진 가을 풍경을 감상하실 수 있습니다. 신흥사와 흔들바위 코스는 비교적 쉬운 트래킹 코스로 남녀노소 가볍게 걸으며 단풍을 즐기기에 좋습니다.\n\n위의 여행지들은 가을의 아름다운 자연과 문화를 경험할 수 있는 대표적인 장소들입니다. 하지만 개인의 취향에 따라 선호하는 여행지가 다를 수 있으므로, 자신에게 맞는 여행지를 선택하여 즐거운 여행을 즐기시길 바랍니다. 또한 여행 전에는 해당 지역의 날씨와 교통 상황 등을 미리 확인하시는 것이 좋습니다.', additional_kwargs={'thinking_content': ''}, response_metadata={'finish_reason': 'stop', 'model_name': 'HCX-005'}, id='run--69f86fd3-e219-4543-9816-9095b90c4124', usage_metadata={'input_tokens': 3102, 'output_tokens': 279, 'total_tokens': 3381, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='가을로 이행시 해줘', additional_kwargs={}, response_metadata={}, id='e3d8abd3-7b45-4347-9fb3-0c272ba69165'), AIMessage(content='가을로 이행시를 지어드리겠습니다.\n\n**가**: 가슴이 뛰는 계절,\n**을**: 을왕리로 떠나요!\n\n아름다운 가을 바다와 맛있는 조개구이를 먹으며 행복한 시간을 보낼 수 있어 많은 사람들에게 사랑받는 여행지 입니다. ', additional_kwargs={'thinking_content': ''}, response_metadata={'finish_reason': 'stop', 'model_name': 'HCX-005'}, id='run--82e7f174-4818-427a-ab57-3af50381910f', usage_metadata={'input_tokens': 3399, 'output_tokens': 61, 'total_tokens': 3460, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='가을은 왜 가을이야?', additional_kwargs={}, response_metadata={}, id='bf7fdcf1-a7ed-4522-9a23-86dfe316e374'), AIMessage(content="'가을'이라는 이름은 어떻게 생겨났을까요?\n\n가을의 명칭 유래에 대한 여러 가지 설이 있지만 대표적으로는 다음 두 가지 이야기가 널리 알려져 있습니다.\n\n- **농사의 마지막 단계**: 농작물을 수확하며 한 해 농사를 마무리 짓는 시기로 접어들면서 ‘갓길’ 또는 ‘갋다’라는 단어로부터 파생되었다는 설입니다.\n\n- **추수한 곡식 저장**: 추수한 곡식을 저장한다는 의미의 '곶감'에서 음운 변화가 일어나 ‘가을’이 되었다는 주장입니다.\n\n이러한 유래 외에도 다양한 학설이 있으나 위 두 가지 설이 가장 많이 인용되고 있습니다. ", additional_kwargs={'thinking_content': ''}, response_metadata={'finish_reason': 'stop', 'model_name': 'HCX-005'}, id='run--55e431fe-fc8e-48ff-b482-90f60342bc80', usage_metadata={'input_tokens': 3476, 'output_tokens': 138, 'total_tokens': 3614, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='가을은 언제부터 언제까지야?', additional_kwargs={}, response_metadata={}, id='957d8048-4110-4b22-934f-3115d997eccf'), AIMessage(content='일반적으로 가을은 다음과 같이 구분됩니다.\n\n - 봄(3월~5월): 따뜻한 날씨와 함께 꽃이 피고 새싹이 돋아나는 계절입니다.\n - 여름(6월~8월): 더운 날씨가 지속되면서 휴가를 즐기기 좋은 계절입니다.\n - 가을(9월~11월): 선선한 날씨와 함께 단풍이 들고 수확의 계절이라고도 합니다.\n - 겨울(12월~2월): 추운 날씨와 눈이 내리는 계절입니다.\n\n우리나라 기상청에서도 이와 비슷하게 9월부터 11월까지를 가을로 보고 있으며, 이 기간 동안 기온이 점차 내려가고 단풍이 드는 모습을 볼 수 있습니다. 따라서 이러한 특징을 고려하면 대략적으로 9월 초부터 11월 말까지를 가을로 생각하시면 됩니다. ', additional_kwargs={'thinking_content': ''}, response_metadata={'finish_reason': 'stop', 'model_name': 'HCX-005'}, id='run--03c0dede-649d-4bf6-8cee-ccdd6032e60c', usage_metadata={'input_tokens': 3632, 'output_tokens': 162, 'total_tokens': 3794, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='아하 고마워', additional_kwargs={}, response_metadata={})] 일곱 번째 요청부터는 컨텍스트 요약이 실행되어, 이전 대화의 맥락이 두 개의 메시지(요약 요청·요약 결과)로 대체되고, 최근 두 개의 사용자 턴만 원문으로 유지됩니다. Quote [SystemMessage(content='당신은 친절한 AI 어시스턴트입니다.사용자의 질문에 대해 신뢰할 수 있는 정보만 근거로 삼아 답변하세요.', additional_kwargs={}, response_metadata={}, id='597ef94e-fab0-479f-bf7d-6377e98ec8de'), HumanMessage(content='지금까지의 대화를 요약해 주세요.', additional_kwargs={'synthetic': True}, response_metadata={}), AIMessage(content="요약:\n1. Assistant는 가을에 가기 좋은 국내 여행지 세 곳으로 제주, 경주, 설악산을 추천함.\n2. User의 요청으로 '가을로 이행시'를 제공함.\n3. 마지막으로 '가을'이라는 이름의 유래에 대해 설명함.", additional_kwargs={'synthetic': True}, response_metadata={}), HumanMessage(content='가을은 언제부터 언제까지야?', additional_kwargs={}, response_metadata={}, id='957d8048-4110-4b22-934f-3115d997eccf'), AIMessage(content='일반적으로 가을은 다음과 같이 구분됩니다.\n\n - 봄(3월~5월): 따뜻한 날씨와 함께 꽃이 피고 새싹이 돋아나는 계절입니다.\n - 여름(6월~8월): 더운 날씨가 지속되면서 휴가를 즐기기 좋은 계절입니다.\n - 가을(9월~11월): 선선한 날씨와 함께 단풍이 들고 수확의 계절이라고도 합니다.\n - 겨울(12월~2월): 추운 날씨와 눈이 내리는 계절입니다.\n\n우리나라 기상청에서도 이와 비슷하게 9월부터 11월까지를 가을로 보고 있으며, 이 기간 동안 기온이 점차 내려가고 단풍이 드는 모습을 볼 수 있습니다. 따라서 이러한 특징을 고려하면 대략적으로 9월 초부터 11월 말까지를 가을로 생각하시면 됩니다. ', additional_kwargs={'thinking_content': ''}, response_metadata={}, id='run--03c0dede-649d-4bf6-8cee-ccdd6032e60c', usage_metadata={'input_tokens': 3632, 'output_tokens': 162, 'total_tokens': 3794, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='아하 고마워', additional_kwargs={}, response_metadata={}, id='00a3058f-c0b2-4d92-99a4-ec644a529d09'), AIMessage(content='별말씀을요!\n\n언제든지 궁금한 점이나 도움이 필요한 사항이 있으시면 편하게 문의해주세요. 최선을 다해 도와드리겠습니다.\n\n즐거운 하루 보내세요!', additional_kwargs={'thinking_content': ''}, response_metadata={}, id='run--24b80839-4297-4530-a4d9-3eecb0014c36', usage_metadata={'input_tokens': 3808, 'output_tokens': 39, 'total_tokens': 3847, 'input_token_details': {}, 'output_token_details': {}}), HumanMessage(content='환절기 건강 지키기 팁 알려줘', additional_kwargs={}, response_metadata={})] 마무리 2부에서는 세션 기반의 Stateful 대화 흐름을 구현하는 과정을 살펴보았습니다. 대화 상태를 저장하고 이어받는 방식, 그리고 컨텍스트를 효율적으로 관리하기 위한 전략을 실제 코드와 함께 구현해 보았는데요. 이를 통해 MCP와 HyperCLOVA X 모델을 함께 활용하면 대화의 맥락을 지속적으로 이해하고 기억하는 대화형 시스템을 설계할 수 있음을 확인했습니다. 다음 3부에서는 인증 구조를 통합하여 HyperCLOVA X 모델과 안전하게 연동되는 개인화 MCP 서버를 구현해 보겠습니다. MCP 실전 쿡북 4부작 시리즈 ✔︎ (1부) LangChain에서 네이버 검색 도구 연결하기 링크 ✔︎ (2부) 세션 관리와 컨텍스트 유지 전략 ✔︎ (3부) OAuth 인증이 통합된 MCP 서버 구축하기 링크 ✔︎ (4부) 운영과 확장을 위한 다양한 팁 링크 링크 복사 다른 사이트에 공유하기 More sharing options...
Recommended Posts
게시글 및 댓글을 작성하려면 로그인 해주세요.
로그인