Skip to main content
AI 애플리케이션은 여러 상호작용에서 컨텍스트를 공유하기 위해 메모리가 필요합니다. LangGraph에서는 두 가지 유형의 메모리를 추가할 수 있습니다:

단기 메모리 추가

단기 메모리(스레드 수준 지속성)를 사용하면 에이전트가 다중 턴 대화를 추적할 수 있습니다. 단기 메모리를 추가하는 방법은 다음과 같습니다:
import { MemorySaver, StateGraph } from "@langchain/langgraph";

const checkpointer = new MemorySaver();

const builder = new StateGraph(...);
const graph = builder.compile({ checkpointer });

await graph.invoke(
  { messages: [{ role: "user", content: "hi! i am Bob" }] },
  { configurable: { thread_id: "1" } }
);

프로덕션 환경에서 사용하기

프로덕션 환경에서는 데이터베이스를 기반으로 하는 체크포인터를 사용하세요:
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";

const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const checkpointer = PostgresSaver.fromConnString(DB_URI);

const builder = new StateGraph(...);
const graph = builder.compile({ checkpointer });
npm install @langchain/langgraph-checkpoint-postgres
Postgres 체크포인터를 처음 사용할 때는 checkpointer.setup()을 호출해야 합니다
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, MessagesZodMeta, START } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const model = new ChatAnthropic({ model: "claude-3-5-haiku-20241022" });

const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const checkpointer = PostgresSaver.fromConnString(DB_URI);
// await checkpointer.setup();

const builder = new StateGraph(MessagesZodState)
  .addNode("call_model", async (state) => {
    const response = await model.invoke(state.messages);
    return { messages: [response] };
  })
  .addEdge(START, "call_model");

const graph = builder.compile({ checkpointer });

const config = {
  configurable: {
    thread_id: "1"
  }
};

for await (const chunk of await graph.stream(
  { messages: [{ role: "user", content: "hi! I'm bob" }] },
  { ...config, streamMode: "values" }
)) {
  console.log(chunk.messages.at(-1)?.content);
}

for await (const chunk of await graph.stream(
  { messages: [{ role: "user", content: "what's my name?" }] },
  { ...config, streamMode: "values" }
)) {
  console.log(chunk.messages.at(-1)?.content);
}

서브그래프에서 사용하기

그래프에 서브그래프가 포함되어 있는 경우, 부모 그래프를 컴파일할 때만 체크포인터를 제공하면 됩니다. LangGraph가 자동으로 체크포인터를 자식 서브그래프에 전파합니다.
import { StateGraph, START, MemorySaver } from "@langchain/langgraph";
import * as z from "zod";

const State = z.object({ foo: z.string() });

const subgraphBuilder = new StateGraph(State)
  .addNode("subgraph_node_1", (state) => {
    return { foo: state.foo + "bar" };
  })
  .addEdge(START, "subgraph_node_1");
const subgraph = subgraphBuilder.compile();

const builder = new StateGraph(State)
  .addNode("node_1", subgraph)
  .addEdge(START, "node_1");

const checkpointer = new MemorySaver();
const graph = builder.compile({ checkpointer });
서브그래프가 자체 메모리를 가지도록 하려면 적절한 체크포인터 옵션으로 컴파일할 수 있습니다. 이는 멀티 에이전트 시스템에서 에이전트가 자신의 내부 메시지 히스토리를 추적하도록 하려는 경우에 유용합니다.
const subgraphBuilder = new StateGraph(...);
const subgraph = subgraphBuilder.compile({ checkpointer: true });

장기 메모리 추가

장기 메모리를 사용하여 대화 간에 사용자별 또는 애플리케이션별 데이터를 저장하세요.
import { InMemoryStore, StateGraph } from "@langchain/langgraph";

const store = new InMemoryStore();

const builder = new StateGraph(...);
const graph = builder.compile({ store });

프로덕션 환경에서 사용하기

프로덕션 환경에서는 데이터베이스를 기반으로 하는 스토어를 사용하세요:
import { PostgresStore } from "@langchain/langgraph-checkpoint-postgres";

const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";
const store = PostgresStore.fromConnString(DB_URI);

const builder = new StateGraph(...);
const graph = builder.compile({ store });
npm install @langchain/langgraph-checkpoint-postgres
Postgres 스토어를 처음 사용할 때는 store.setup()을 호출해야 합니다
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, MessagesZodMeta, START, LangGraphRunnableConfig } from "@langchain/langgraph";
import { PostgresSaver, PostgresStore } from "@langchain/langgraph-checkpoint-postgres";
import { BaseMessage } from "@langchain/core/messages";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";
import { v4 as uuidv4 } from "uuid";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const model = new ChatAnthropic({ model: "claude-3-5-haiku-20241022" });

const DB_URI = "postgresql://postgres:postgres@localhost:5442/postgres?sslmode=disable";

const store = PostgresStore.fromConnString(DB_URI);
const checkpointer = PostgresSaver.fromConnString(DB_URI);
// await store.setup();
// await checkpointer.setup();

const callModel = async (
  state: z.infer<typeof MessagesZodState>,
  config: LangGraphRunnableConfig,
) => {
  const userId = config.configurable?.userId;
  const namespace = ["memories", userId];
  const memories = await config.store?.search(namespace, { query: state.messages.at(-1)?.content });
  const info = memories?.map(d => d.value.data).join("\n") || "";
  const systemMsg = `You are a helpful assistant talking to the user. User info: ${info}`;

  // 사용자가 모델에 기억하도록 요청하면 새로운 메모리를 저장합니다
  const lastMessage = state.messages.at(-1);
  if (lastMessage?.content?.toLowerCase().includes("remember")) {
    const memory = "User name is Bob";
    await config.store?.put(namespace, uuidv4(), { data: memory });
  }

  const response = await model.invoke([
    { role: "system", content: systemMsg },
    ...state.messages
  ]);
  return { messages: [response] };
};

const builder = new StateGraph(MessagesZodState)
  .addNode("call_model", callModel)
  .addEdge(START, "call_model");

const graph = builder.compile({
  checkpointer,
  store,
});

const config = {
  configurable: {
    thread_id: "1",
    userId: "1",
  }
};

for await (const chunk of await graph.stream(
  { messages: [{ role: "user", content: "Hi! Remember: my name is Bob" }] },
  { ...config, streamMode: "values" }
)) {
  console.log(chunk.messages.at(-1)?.content);
}

const config2 = {
  configurable: {
    thread_id: "2",
    userId: "1",
  }
};

for await (const chunk of await graph.stream(
  { messages: [{ role: "user", content: "what is my name?" }] },
  { ...config2, streamMode: "values" }
)) {
  console.log(chunk.messages.at(-1)?.content);
}

시맨틱 검색 사용하기

그래프의 메모리 스토어에서 시맨틱 검색을 활성화하여 그래프 에이전트가 스토어의 항목을 의미적 유사성으로 검색할 수 있도록 하세요.
import { OpenAIEmbeddings } from "@langchain/openai";
import { InMemoryStore } from "@langchain/langgraph";

// 시맨틱 검색이 활성화된 스토어 생성
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });
const store = new InMemoryStore({
  index: {
    embeddings,
    dims: 1536,
  },
});

await store.put(["user_123", "memories"], "1", { text: "I love pizza" });
await store.put(["user_123", "memories"], "2", { text: "I am a plumber" });

const items = await store.search(["user_123", "memories"], {
  query: "I'm hungry",
  limit: 1,
});
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";
import { StateGraph, START, MessagesZodMeta, InMemoryStore } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const llm = new ChatOpenAI({ model: "gpt-4o-mini" });

// 시맨틱 검색이 활성화된 스토어 생성
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });
const store = new InMemoryStore({
  index: {
    embeddings,
    dims: 1536,
  }
});

await store.put(["user_123", "memories"], "1", { text: "I love pizza" });
await store.put(["user_123", "memories"], "2", { text: "I am a plumber" });

const chat = async (state: z.infer<typeof MessagesZodState>, config) => {
  // 사용자의 마지막 메시지를 기반으로 검색
  const items = await config.store.search(
    ["user_123", "memories"],
    { query: state.messages.at(-1)?.content, limit: 2 }
  );
  const memories = items.map(item => item.value.text).join("\n");
  const memoriesText = memories ? `## Memories of user\n${memories}` : "";

  const response = await llm.invoke([
    { role: "system", content: `You are a helpful assistant.\n${memoriesText}` },
    ...state.messages,
  ]);

  return { messages: [response] };
};

const builder = new StateGraph(MessagesZodState)
  .addNode("chat", chat)
  .addEdge(START, "chat");
const graph = builder.compile({ store });

for await (const [message, metadata] of await graph.stream(
  { messages: [{ role: "user", content: "I'm hungry" }] },
  { streamMode: "messages" }
)) {
  if (message.content) {
    console.log(message.content);
  }
}

단기 메모리 관리

단기 메모리가 활성화되면 긴 대화가 LLM의 컨텍스트 윈도우를 초과할 수 있습니다. 일반적인 해결 방법은 다음과 같습니다: 이를 통해 에이전트는 LLM의 컨텍스트 윈도우를 초과하지 않으면서 대화를 추적할 수 있습니다.

메시지 잘라내기

대부분의 LLM에는 지원되는 최대 컨텍스트 윈도우(토큰 단위)가 있습니다. 메시지를 자를 시점을 결정하는 한 가지 방법은 메시지 히스토리의 토큰 수를 세어 해당 제한에 근접할 때마다 잘라내는 것입니다. LangChain을 사용하는 경우, trim messages 유틸리티를 사용하여 목록에서 유지할 토큰 수와 경계를 처리하기 위한 strategy(예: 마지막 maxTokens 유지)를 지정할 수 있습니다. 메시지 히스토리를 자르려면 trimMessages 함수를 사용하세요:
import { trimMessages } from "@langchain/core/messages";

const callModel = async (state: z.infer<typeof MessagesZodState>) => {
  const messages = trimMessages(state.messages, {
    strategy: "last",
    maxTokens: 128,
    startOn: "human",
    endOn: ["human", "tool"],
  });
  const response = await model.invoke(messages);
  return { messages: [response] };
};

const builder = new StateGraph(MessagesZodState)
  .addNode("call_model", callModel);
// ...
import { trimMessages, BaseMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, START, MessagesZodMeta, MemorySaver } from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" });

const callModel = async (state: z.infer<typeof MessagesZodState>) => {
  const messages = trimMessages(state.messages, {
    strategy: "last",
    maxTokens: 128,
    startOn: "human",
    endOn: ["human", "tool"],
    tokenCounter: model,
  });
  const response = await model.invoke(messages);
  return { messages: [response] };
};

const checkpointer = new MemorySaver();
const builder = new StateGraph(MessagesZodState)
  .addNode("call_model", callModel)
  .addEdge(START, "call_model");
const graph = builder.compile({ checkpointer });

const config = { configurable: { thread_id: "1" } };
await graph.invoke({ messages: [{ role: "user", content: "hi, my name is bob" }] }, config);
await graph.invoke({ messages: [{ role: "user", content: "write a short poem about cats" }] }, config);
await graph.invoke({ messages: [{ role: "user", content: "now do the same but for dogs" }] }, config);
const finalResponse = await graph.invoke({ messages: [{ role: "user", content: "what's my name?" }] }, config);

console.log(finalResponse.messages.at(-1)?.content);
Your name is Bob, as you mentioned when you first introduced yourself.

메시지 삭제하기

그래프 상태에서 메시지를 삭제하여 메시지 히스토리를 관리할 수 있습니다. 특정 메시지를 제거하거나 전체 메시지 히스토리를 지우고 싶을 때 유용합니다. 그래프 상태에서 메시지를 삭제하려면 RemoveMessage를 사용할 수 있습니다. RemoveMessage가 작동하려면 MessagesZodState와 같이 messagesStateReducer 리듀서가 있는 상태 키를 사용해야 합니다. 특정 메시지를 제거하려면:
import { RemoveMessage } from "@langchain/core/messages";

const deleteMessages = (state) => {
  const messages = state.messages;
  if (messages.length > 2) {
    // 가장 초기의 두 메시지 제거
    return {
      messages: messages
        .slice(0, 2)
        .map((m) => new RemoveMessage({ id: m.id })),
    };
  }
};
메시지를 삭제할 때는 결과로 나오는 메시지 히스토리가 유효한지 반드시 확인하세요. 사용 중인 LLM 제공자의 제한 사항을 확인하세요. 예를 들어:
  • 일부 제공자는 메시지 히스토리가 user 메시지로 시작할 것을 기대합니다
  • 대부분의 제공자는 도구 호출이 있는 assistant 메시지 다음에 해당하는 tool 결과 메시지가 와야 합니다.
import { RemoveMessage, BaseMessage } from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";
import { StateGraph, START, MemorySaver, MessagesZodMeta } from "@langchain/langgraph";
import * as z from "zod";
import { registry } from "@langchain/langgraph/zod";

const MessagesZodState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
});

const model = new ChatAnthropic({ model: "claude-3-5-sonnet-20241022" });

const deleteMessages = (state: z.infer<typeof MessagesZodState>) => {
  const messages = state.messages;
  if (messages.length > 2) {
    // 가장 초기의 두 메시지 제거
    return { messages: messages.slice(0, 2).map(m => new RemoveMessage({ id: m.id })) };
  }
  return {};
};

const callModel = async (state: z.infer<typeof MessagesZodState>) => {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
};

const builder = new StateGraph(MessagesZodState)
  .addNode("call_model", callModel)
  .addNode("delete_messages", deleteMessages)
  .addEdge(START, "call_model")
  .addEdge("call_model", "delete_messages");

const checkpointer = new MemorySaver();
const app = builder.compile({ checkpointer });

const config = { configurable: { thread_id: "1" } };

for await (const event of await app.stream(
  { messages: [{ role: "user", content: "hi! I'm bob" }] },
  { ...config, streamMode: "values" }
)) {
  console.log(event.messages.map(message => [message.getType(), message.content]));
}

for await (const event of await app.stream(
  { messages: [{ role: "user", content: "what's my name?" }] },
  { ...config, streamMode: "values" }
)) {
  console.log(event.messages.map(message => [message.getType(), message.content]));
}
[['human', "hi! I'm bob"]]
[['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?']]
[['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"]]
[['human', "hi! I'm bob"], ['ai', 'Hi Bob! How are you doing today? Is there anything I can help you with?'], ['human', "what's my name?"], ['ai', 'Your name is Bob.']]
[['human', "what's my name?"], ['ai', 'Your name is Bob.']]

메시지 요약하기

위에 표시된 것처럼 메시지를 자르거나 제거하는 것의 문제점은 메시지 큐를 정리함으로써 정보를 잃을 수 있다는 것입니다. 이 때문에 일부 애플리케이션은 채팅 모델을 사용하여 메시지 히스토리를 요약하는 보다 정교한 접근 방식의 이점을 얻습니다. 프롬프팅 및 오케스트레이션 로직을 사용하여 메시지 히스토리를 요약할 수 있습니다. 예를 들어, LangGraph에서는 messages 키와 함께 상태에 summary 키를 포함할 수 있습니다:
import { BaseMessage } from "@langchain/core/messages";
import { MessagesZodMeta } from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";

const State = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
  summary: z.string().optional(),
});
그런 다음, 기존 요약을 다음 요약의 컨텍스트로 사용하여 채팅 히스토리의 요약을 생성할 수 있습니다. 이 summarizeConversation 노드는 messages 상태 키에 일정 수의 메시지가 누적된 후에 호출될 수 있습니다.
import { RemoveMessage, HumanMessage } from "@langchain/core/messages";

const summarizeConversation = async (state: z.infer<typeof State>) => {
  // 먼저, 기존 요약을 가져옵니다
  const summary = state.summary || "";

  // 요약 프롬프트를 생성합니다
  let summaryMessage: string;
  if (summary) {
    // 요약이 이미 존재합니다
    summaryMessage =
      `This is a summary of the conversation to date: ${summary}\n\n` +
      "Extend the summary by taking into account the new messages above:";
  } else {
    summaryMessage = "Create a summary of the conversation above:";
  }

  // 히스토리에 프롬프트를 추가합니다
  const messages = [
    ...state.messages,
    new HumanMessage({ content: summaryMessage })
  ];
  const response = await model.invoke(messages);

  // 가장 최근 2개를 제외한 모든 메시지를 삭제합니다
  const deleteMessages = state.messages
    .slice(0, -2)
    .map(m => new RemoveMessage({ id: m.id }));

  return {
    summary: response.content,
    messages: deleteMessages
  };
};
import { ChatAnthropic } from "@langchain/anthropic";
import {
  SystemMessage,
  HumanMessage,
  RemoveMessage,
  type BaseMessage
} from "@langchain/core/messages";
import {
  MessagesZodMeta,
  StateGraph,
  START,
  END,
  MemorySaver,
} from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";
import { v4 as uuidv4 } from "uuid";

const memory = new MemorySaver();

// `messages` 키 외에 `summary` 속성을 추가합니다
// (MessagesZodState에 이미 있음)
const GraphState = z.object({
  messages: z
    .array(z.custom<BaseMessage>())
    .register(registry, MessagesZodMeta),
  summary: z.string().default(""),
});

// 대화와 요약 모두에 이 모델을 사용합니다
const model = new ChatAnthropic({ model: "claude-3-haiku-20240307" });

// 모델을 호출하는 로직을 정의합니다
const callModel = async (state: z.infer<typeof GraphState>) => {
  // 요약이 존재하면 이를 시스템 메시지로 추가합니다
  const { summary } = state;
  let { messages } = state;
  if (summary) {
    const systemMessage = new SystemMessage({
      id: uuidv4(),
      content: `Summary of conversation earlier: ${summary}`,
    });
    messages = [systemMessage, ...messages];
  }
  const response = await model.invoke(messages);
  // 기존 상태에 추가될 것이므로 객체를 반환합니다
  return { messages: [response] };
};

// 이제 대화를 종료할지 요약할지 결정하는 로직을 정의합니다
const shouldContinue = (state: z.infer<typeof GraphState>) => {
  const messages = state.messages;
  // 메시지가 6개 이상이면 대화를 요약합니다
  if (messages.length > 6) {
    return "summarize_conversation";
  }
  // 그렇지 않으면 종료할 수 있습니다
  return END;
};

const summarizeConversation = async (state: z.infer<typeof GraphState>) => {
  // 먼저, 대화를 요약합니다
  const { summary, messages } = state;
  let summaryMessage: string;
  if (summary) {
    // 요약이 이미 존재하는 경우, 존재하지 않는 경우와 다른 시스템 프롬프트를 사용하여 요약합니다
    summaryMessage =
      `This is summary of the conversation to date: ${summary}\n\n` +
      "Extend the summary by taking into account the new messages above:";
  } else {
    summaryMessage = "Create a summary of the conversation above:";
  }

  const allMessages = [
    ...messages,
    new HumanMessage({ id: uuidv4(), content: summaryMessage }),
  ];

  const response = await model.invoke(allMessages);

  // 이제 더 이상 표시하지 않으려는 메시지를 삭제해야 합니다
  // 마지막 두 메시지를 제외한 모든 메시지를 삭제하겠지만, 이를 변경할 수 있습니다
  const deleteMessages = messages
    .slice(0, -2)
    .map((m) => new RemoveMessage({ id: m.id! }));

  if (typeof response.content !== "string") {
    throw new Error("Expected a string response from the model");
  }

  return { summary: response.content, messages: deleteMessages };
};

// 새로운 그래프를 정의합니다
const workflow = new StateGraph(GraphState)
  // conversation 노드와 summarize 노드를 정의합니다
  .addNode("conversation", callModel)
  .addNode("summarize_conversation", summarizeConversation)
  // 진입점을 conversation으로 설정합니다
  .addEdge(START, "conversation")
  // 이제 조건부 엣지를 추가합니다
  .addConditionalEdges(
    // 먼저, 시작 노드를 정의합니다. `conversation`을 사용합니다.
    // 이는 `conversation` 노드가 호출된 후 이 엣지들이 사용된다는 의미입니다.
    "conversation",
    // 다음으로, 어떤 노드가 다음에 호출될지 결정할 함수를 전달합니다.
    shouldContinue,
  )
  // 이제 `summarize_conversation`에서 END로의 일반 엣지를 추가합니다.
  // 이는 `summarize_conversation`이 호출된 후 종료한다는 의미입니다.
  .addEdge("summarize_conversation", END);

// 마지막으로, 컴파일합니다!
const app = workflow.compile({ checkpointer: memory });

체크포인트 관리하기

체크포인터가 저장한 정보를 보고 삭제할 수 있습니다.

스레드 상태 보기

const config = {
  configurable: {
    thread_id: "1",
    // 선택적으로 특정 체크포인트의 ID를 제공합니다,
    // 그렇지 않으면 최신 체크포인트가 표시됩니다
    // checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a"
  },
};
await graph.getState(config);
{
  values: { messages: [HumanMessage(...), AIMessage(...), HumanMessage(...), AIMessage(...)] },
  next: [],
  config: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1f5b-6704-8004-820c16b69a5a' } },
  metadata: {
    source: 'loop',
    writes: { call_model: { messages: AIMessage(...) } },
    step: 4,
    parents: {},
    thread_id: '1'
  },
  createdAt: '2025-05-05T16:01:24.680462+00:00',
  parentConfig: { configurable: { thread_id: '1', checkpoint_ns: '', checkpoint_id: '1f029ca3-1790-6b0a-8003-baf965b6a38f' } },
  tasks: [],
  interrupts: []
}

스레드의 히스토리 보기

const config = {
  configurable: {
    thread_id: "1",
  },
};

const history = [];
for await (const state of graph.getStateHistory(config)) {
  history.push(state);
}

스레드의 모든 체크포인트 삭제하기

const threadId = "1";
await checkpointer.deleteThread(threadId);

Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.
I