단기 기억

단기 기억 (Short-term memory)

에이전트를 만들다 보면 "방금 전에 내가 뭐라고 했지?"를 에이전트가 기억해 주길 바라는 순간이 반드시 와요. 그게 바로 메모리(memory)의 역할이에요. 메모리는 이전 상호작용에 대한 정보를 기억하는 시스템이죠. AI 에이전트에게 메모리는 이전 상호작용을 기억하고, 피드백에서 배우고, 사용자 선호에 적응하게 해주는 핵심입니다. 에이전트가 많은 사용자 상호작용을 가진 복잡한 작업을 다룰수록 이 능력은 효율성과 사용자 만족 모두에서 필수적이 돼요. 이 페이지에서는 그중 단기 기억(short-term memory), 즉 단일 스레드나 대화 안에서 이전 상호작용을 기억하게 해주는 기능을 다룰게요.

출처: LangChain 공식 문서 — Short-term memory

개요 (Overview)

스레드(thread)는 세션 안에서 여러 상호작용을 조직화하는 단위로, 이메일이 하나의 대화에 메시지를 묶는 것과 비슷해요.

대화 기록(conversation history)은 가장 흔한 단기 기억의 형태입니다. 긴 대화는 오늘날 LLM에게 도전 과제가 돼요. 전체 기록이 LLM의 컨텍스트 윈도우에 맞지 않으면 컨텍스트 손실이나 오류가 발생할 수 있죠. 설령 모델이 전체 컨텍스트 길이를 지원하더라도, 대부분의 LLM은 긴 컨텍스트에서 성능이 떨어져요. 낡았거나 주제를 벗어난 내용에 "산만해지고", 응답 시간이 느려지고 비용도 올라가죠.

채팅 모델은 메시지(messages)를 통해 컨텍스트를 받아요. 메시지에는 지시(시스템 메시지)와 입력(휴먼 메시지)이 포함되죠. 채팅 애플리케이션에서 메시지는 휴먼 입력과 모델 응답이 번갈아 가며 리스트로 계속 길어져요. 컨텍스트 윈도우가 제한적이므로 많은 애플리케이션은 낡은 정보를 제거하거나 "잊는" 기법을 쓰는 게 유리합니다.

대화를 가로질러 정보를 기억해야 하나요? 그렇다면 장기 기억(long-term memory)을 사용해 여러 스레드와 세션에 걸친 사용자별·애플리케이션별 데이터를 저장하고 불러오세요.

사용법 (Usage)

에이전트에 단기 기억(스레드 수준 영속성)을 추가하려면 에이전트를 만들 때 checkpointer를 지정해야 해요.

LangChain 에이전트는 단기 기억을 에이전트 상태의 일부로 관리합니다. 그래프의 state에 저장하면 에이전트가 특정 대화의 전체 컨텍스트에 접근하면서도 서로 다른 스레드 사이의 분리를 유지할 수 있죠. state는 checkpointer를 통해 데이터베이스(또는 메모리)에 영속화되어 스레드를 언제든 재개할 수 있어요. 단기 기억은 에이전트가 호출되거나 (도구 호출 같은) 한 단계가 완료될 때 업데이트되고, 각 단계 시작 시 state가 읽힙니다.

예를 들어 Google 모델 기준:

from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver

def get_user_info() -> str:
    """Look up information about the current user."""
    return "No user profile on file."

agent = create_agent(
    model="google_genai:gemini-3.6-flash",
    tools=[get_user_info],
    checkpointer=InMemorySaver(),
)

thread_config = {"configurable": {"thread_id": "1"}}
response = agent.invoke(
    {"messages": [{"role": "user", "content": "Hi! My name is Bob."}]},
    thread_config,
)["messages"][-1].content

print(response)  # "Hi Bob! Nice to see you here. How are you doing?"

response = agent.invoke(
    {"messages": [{"role": "user", "content": "What's my name?"}]},
    thread_config,
)["messages"][-1].content

print(response)  # "You are Bob!"

처음 인사에서 에이전트는 "제 이름은 Bob이에요"를 기억하고, 다음 호출에서 "What's my name?"에 "You are Bob!"이라고 답해요. 같은 thread_id를 넘겼기 때문이죠. OpenAI, Anthropic, OpenRouter, Fireworks, Baseten, Ollama 등 다른 제공자도 모델 문자열만 다르고 동일한 패턴을 사용합니다.

운영 환경에서 (In production)

운영에서는 데이터베이스로 백업된 checkpointer를 쓰세요.

pip install -U langgraph-checkpoint-postgres "psycopg[binary]"

기본적으로 langgraph-checkpoint-postgres는 extras 없이 psycopg(Psycopg 3)를 설치해요. 위 설치 명령은 대부분의 사용자에게 권장되는 psycopg[binary]를 추가합니다. 다른 옵션은 Psycopg 설치 문서를 참고하세요.

from langchain.agents import create_agent
from langgraph.checkpoint.postgres import PostgresSaver

def get_user_info() -> str:
    """Look up information about the current user."""
    return "No user profile on file."

DB_URI = "postgresql://postgres:postgres@localhost:5432/postgres?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
    checkpointer.setup() # auto create tables in PostgreSQL
    agent = create_agent(
        "gpt-5.5",
        tools=[get_user_info],
        checkpointer=checkpointer,
    )

SQLite, Postgres, Azure Cosmos DB를 포함한 더 많은 checkpointer 옵션은 영속성(Persistence) 문서의 checkpointer 라이브러리 목록을 참고하세요.

에이전트 메모리 커스터마이즈 (Customizing agent memory)

기본적으로 에이전트는 AgentState로 단기 기억을 관리하며, 구체적으로 messages 키를 통해 대화 기록을 다뤄요. AgentState를 확장해 추가 필드를 넣을 수 있습니다. 커스텀 state 스키마는 state_schema 파라미터로 create_agent에 전달해요.

from langchain.agents import create_agent, AgentState
from langgraph.checkpoint.memory import InMemorySaver

class CustomAgentState(AgentState):
    user_id: str
    preferences: dict

agent = create_agent(
    "gpt-5.5",
    tools=[get_user_info],
    state_schema=CustomAgentState,
    checkpointer=InMemorySaver(),
)

# Custom state can be passed in invoke
result = agent.invoke(
    {
        "messages": [{"role": "user", "content": "Hello"}],
        "user_id": "user_123",
        "preferences": {"theme": "dark"}
    },
    {"configurable": {"thread_id": "1"}})

일반적인 패턴 (Common patterns)

단기 기억을 켜면 긴 대화가 LLM의 컨텍스트 윈도우를 넘어설 수 있어요. 흔한 해법은 세 가지입니다.

  • 메시지 자르기 (Trim messages): LLM 호출 전에 처음/마지막 N개 메시지를 제거
  • 메시지 삭제 (Delete messages): LangGraph state에서 메시지를 영구히 삭제
  • 메시지 요약 (Summarize messages): 기록의 이전 메시지를 요약으로 대체

메시지 자르기 (Trim messages)

대부분의 LLM은 토큰 기준 최대 컨텍스트 윈도우를 가져요. 메시지를 언제 자를지 정하는 방법 중 하나는 메시지 기록의 토큰 수를 세어 한계에 가까워지면 자르는 거예요. 에이전트의 메시지 기록을 자르려면 @before_model 미들웨어 데코레이터를 쓰세요.

from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
from langgraph.runtime import Runtime
from langchain_core.runnables import RunnableConfig
from typing import Any

@before_model
def trim_messages(state: AgentState, runtime: Runtime) -> dict[str, Any] | None:
    """Keep only the last few messages to fit context window."""
    messages = state["messages"]

    if len(messages) <= 3:
        return None  # No changes needed

    first_msg = messages[0]
    recent_messages = messages[-3:] if len(messages) % 2 == 0 else messages[-4:]
    new_messages = [first_msg] + recent_messages

    return {
        "messages": [\
            RemoveMessage(id=REMOVE_ALL_MESSAGES),\
            *new_messages\
        ]
    }

agent = create_agent(
    "gpt-5.5",
    tools=[...],
    middleware=[trim_messages],
    checkpointer=InMemorySaver(),
)

config: RunnableConfig = {"configurable": {"thread_id": "1"}}

agent.invoke({"messages": "hi, my name is bob"}, config)
agent.invoke({"messages": "write a short poem about cats"}, config)
agent.invoke({"messages": "now do the same but for dogs"}, config)
final_response = agent.invoke({"messages": "what's my name?"}, config)

final_response["messages"][-1].pretty_print()
"""
================================== Ai Message ==================================

Your name is Bob. You told me that earlier.
If you'd like me to call you a nickname or use a different name, just say the word.
"""

자른 후에도 첫 메시지와 최근 메시지를 남기기 때문에 에이전트가 Bob의 이름을 여전히 기억할 수 있어요.

메시지 삭제 (Delete messages)

그래프 state에서 메시지를 삭제해 메시지 기록을 관리할 수 있어요. 특정 메시지를 제거하거나 전체 기록을 비울 때 유용하죠. RemoveMessage를 사용합니다. RemoveMessage가 동작하려면 add_messages 리듀서(reducer)가 있는 state 키를 써야 해요. 기본 AgentState가 이를 제공합니다.

특정 메시지 제거:

from langchain.messages import RemoveMessage

def delete_messages(state):
    messages = state["messages"]
    if len(messages) > 2:
        # remove the earliest two messages
        return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]}

모든 메시지 제거:

from langgraph.graph.message import REMOVE_ALL_MESSAGES

def delete_messages(state):
    return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES)]}

메시지를 삭제할 때는 결과 메시지 기록이 유효한지 반드시 확인하세요. 사용 중인 LLM 제공자의 제약을 확인해야 해요. 예를 들면:

  • 일부 제공자는 메시지 기록이 user 메시지로 시작하길 기대해요.
  • 대부분의 제공자는 도구 호출이 딸린 assistant 메시지 뒤에 대응하는 tool 결과 메시지가 오길 요구해요.

@after_model 미들웨어에서도 삭제를 수행할 수 있어요.

from langchain.messages import RemoveMessage
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import after_model
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.runtime import Runtime
from langchain_core.runnables import RunnableConfig

@after_model
def delete_old_messages(state: AgentState, runtime: Runtime) -> dict | None:
    """Remove old messages to keep conversation manageable."""
    messages = state["messages"]
    if len(messages) > 2:
        # remove the earliest two messages
        return {"messages": [RemoveMessage(id=m.id) for m in messages[:2]]}
    return None

agent = create_agent(
    "gpt-5-nano",
    tools=[...],
    system_prompt="Please be concise and to the point.",
    middleware=[delete_old_messages],
    checkpointer=InMemorySaver(),
)

메시지 요약 (Summarize messages)

위에서 본 자르기나 삭제의 문제는 메시지 대기열에서 정보를 버리면서 정보를 잃을 수 있다는 거예요. 그래서 일부 애플리케이션은 채팅 모델로 메시지 기록을 요약하는 더 정교한 접근이 유리합니다. 에이전트에서 메시지 기록을 요약하려면 내장 SummarizationMiddleware를 쓰세요.

from langchain.agents import create_agent
from langchain.agents.middleware import SummarizationMiddleware
from langgraph.checkpoint.memory import InMemorySaver
from langchain_core.runnables import RunnableConfig

checkpointer = InMemorySaver()

agent = create_agent(
    model="gpt-5.5",
    tools=[...],
    middleware=[\
        SummarizationMiddleware(\
            model="gpt-5.4-mini",\
            trigger=("tokens", 4000),\
            keep=("messages", 20)\
        )\
    ],
    checkpointer=checkpointer,
)

config: RunnableConfig = {"configurable": {"thread_id": "1"}}
agent.invoke({"messages": "hi, my name is bob"}, config)
agent.invoke({"messages": "write a short poem about cats"}, config)
agent.invoke({"messages": "now do the same but for dogs"}, config)
final_response = agent.invoke({"messages": "what's my name?"}, config)

final_response["messages"][-1].pretty_print()
"""
================================== Ai Message ==================================

Your name is Bob!

"""

여기서 trigger=("tokens", 4000)는 토큰이 4000개를 넘을 때 요약을 트리거하고, keep=("messages", 20)는 최근 20개 메시지를 남겨요. 더 많은 설정 옵션은 SummarizationMiddleware를 참고하세요.

메모리 접근 (Access memory)

에이전트의 단기 기억(state)에 접근하거나 수정하는 방법은 여러 가지예요.

도구에서 메모리 읽기 (Read short-term memory in a tool)

runtime 파라미터(ToolRuntime 타입)를 사용해 도구 안에서 단기 기억(state)에 접근해요. runtime 파라미터는 도구 시그니처에서 숨겨져서(모델이 보지 못함) 도구만 state를 통해 접근할 수 있습니다.

from langchain.agents import create_agent, AgentState
from langchain.tools import tool, ToolRuntime

class CustomState(AgentState):
    user_id: str

@tool
def get_user_info(
    runtime: ToolRuntime
) -> str:
    """Look up user info."""
    user_id = runtime.state["user_id"]
    return "User is John Smith" if user_id == "user_123" else "Unknown user"

agent = create_agent(
    model="gpt-5-nano",
    tools=[get_user_info],
    state_schema=CustomState,
)

result = agent.invoke({
    "messages": "look up user information",
    "user_id": "user_123"
})
print(result["messages"][-1].content)
# > User is John Smith.

도구에서 메모리 쓰기 (Write short-term memory from tools)

실행 중에 에이전트의 단기 기억(state)을 수정하려면 도구에서 state 업데이트를 직접 반환하면 돼요. 중간 결과를 저장하거나 이후 도구·프롬프트에서 접근할 수 있게 만드는 데 유용합니다.

from langchain.tools import tool, ToolRuntime
from langchain_core.runnables import RunnableConfig
from langchain.messages import ToolMessage
from langchain.agents import create_agent, AgentState
from langgraph.types import Command
from pydantic import BaseModel

class CustomState(AgentState):
    user_name: str

class CustomContext(BaseModel):
    user_id: str

@tool
def update_user_info(
    runtime: ToolRuntime[CustomContext, CustomState],
) -> Command:
    """Look up and update user info."""
    user_id = runtime.context.user_id
    name = "John Smith" if user_id == "user_123" else "Unknown user"
    return Command(update={
        "user_name": name,
        # update the message history
        "messages": [\
            ToolMessage(\
                "Successfully looked up user information",\
                tool_call_id=runtime.tool_call_id\
            )\
        ]
    })

미들웨어에서 메모리 접근

@before_model 미들웨어에서 단기 기억(state)에 접근해 모델 호출 전에 메시지를 처리할 수 있어요(위 trim 예시 참고). @after_model 미들웨어에서는 모델 호출 후 메시지를 처리합니다. 예를 들어 민감한 단어가 담긴 응답 메시지를 제거하는 검증을 넣을 수 있어요.

from langchain.messages import RemoveMessage
from langgraph.checkpoint.memory import InMemorySaver
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import after_model
from langgraph.runtime import Runtime

@after_model
def validate_response(state: AgentState, runtime: Runtime) -> dict | None:
    """Remove messages containing sensitive words."""
    STOP_WORDS = ["password", "secret"]
    last_message = state["messages"][-1]
    if any(word in last_message.content for word in STOP_WORDS):
        return {"messages": [RemoveMessage(id=last_message.id)]}
    return None

agent = create_agent(
    model="gpt-5-nano",
    tools=[],
    middleware=[validate_response],
    checkpointer=InMemorySaver(),
)

더 알아보기 (Learn more)