- 단기 메모리 추가: 에이전트의 상태의 일부로 단기 메모리를 추가하여 다중 턴 대화를 가능하게 합니다.
- 장기 메모리 추가: 세션 간에 사용자별 또는 애플리케이션 수준 데이터를 저장합니다.
단기 메모리 추가
단기 메모리(스레드 수준 지속성)를 사용하면 에이전트가 다중 턴 대화를 추적할 수 있습니다. 단기 메모리를 추가하는 방법은 다음과 같습니다:Copy
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" } }
);
프로덕션 환경에서 사용하기
프로덕션 환경에서는 데이터베이스를 기반으로 하는 체크포인터를 사용하세요:Copy
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 });
예제: Postgres 체크포인터 사용하기
예제: Postgres 체크포인터 사용하기
Copy
npm install @langchain/langgraph-checkpoint-postgres
Postgres 체크포인터를 처음 사용할 때는
checkpointer.setup()을 호출해야 합니다Copy
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가 자동으로 체크포인터를 자식 서브그래프에 전파합니다.Copy
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 });
Copy
const subgraphBuilder = new StateGraph(...);
const subgraph = subgraphBuilder.compile({ checkpointer: true });
장기 메모리 추가
장기 메모리를 사용하여 대화 간에 사용자별 또는 애플리케이션별 데이터를 저장하세요.Copy
import { InMemoryStore, StateGraph } from "@langchain/langgraph";
const store = new InMemoryStore();
const builder = new StateGraph(...);
const graph = builder.compile({ store });
프로덕션 환경에서 사용하기
프로덕션 환경에서는 데이터베이스를 기반으로 하는 스토어를 사용하세요:Copy
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 });
예제: Postgres 스토어 사용하기
예제: Postgres 스토어 사용하기
Copy
npm install @langchain/langgraph-checkpoint-postgres
Postgres 스토어를 처음 사용할 때는
store.setup()을 호출해야 합니다Copy
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);
}
시맨틱 검색 사용하기
그래프의 메모리 스토어에서 시맨틱 검색을 활성화하여 그래프 에이전트가 스토어의 항목을 의미적 유사성으로 검색할 수 있도록 하세요.Copy
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,
});
시맨틱 검색을 사용한 장기 메모리
시맨틱 검색을 사용한 장기 메모리
Copy
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의 컨텍스트 윈도우를 초과할 수 있습니다. 일반적인 해결 방법은 다음과 같습니다:- 메시지 잘라내기: 처음 또는 마지막 N개의 메시지 제거하기 (LLM 호출 전)
- 메시지 삭제하기: LangGraph 상태에서 메시지 영구 삭제하기
- 메시지 요약하기: 히스토리의 초기 메시지들을 요약하여 요약본으로 대체하기
- 체크포인트 관리하기: 메시지 히스토리 저장 및 검색하기
- 사용자 정의 전략 (예: 메시지 필터링 등)
메시지 잘라내기
대부분의 LLM에는 지원되는 최대 컨텍스트 윈도우(토큰 단위)가 있습니다. 메시지를 자를 시점을 결정하는 한 가지 방법은 메시지 히스토리의 토큰 수를 세어 해당 제한에 근접할 때마다 잘라내는 것입니다. LangChain을 사용하는 경우, trim messages 유틸리티를 사용하여 목록에서 유지할 토큰 수와 경계를 처리하기 위한strategy(예: 마지막 maxTokens 유지)를 지정할 수 있습니다.
메시지 히스토리를 자르려면 trimMessages 함수를 사용하세요:
Copy
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);
// ...
전체 예제: 메시지 잘라내기
전체 예제: 메시지 잘라내기
Copy
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);
Copy
Your name is Bob, as you mentioned when you first introduced yourself.
메시지 삭제하기
그래프 상태에서 메시지를 삭제하여 메시지 히스토리를 관리할 수 있습니다. 특정 메시지를 제거하거나 전체 메시지 히스토리를 지우고 싶을 때 유용합니다. 그래프 상태에서 메시지를 삭제하려면RemoveMessage를 사용할 수 있습니다. RemoveMessage가 작동하려면 MessagesZodState와 같이 messagesStateReducer 리듀서가 있는 상태 키를 사용해야 합니다.
특정 메시지를 제거하려면:
Copy
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결과 메시지가 와야 합니다.
전체 예제: 메시지 삭제하기
전체 예제: 메시지 삭제하기
Copy
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]));
}
Copy
[['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.']]
메시지 요약하기
위에 표시된 것처럼 메시지를 자르거나 제거하는 것의 문제점은 메시지 큐를 정리함으로써 정보를 잃을 수 있다는 것입니다. 이 때문에 일부 애플리케이션은 채팅 모델을 사용하여 메시지 히스토리를 요약하는 보다 정교한 접근 방식의 이점을 얻습니다.
messages 키와 함께 상태에 summary 키를 포함할 수 있습니다:
Copy
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 상태 키에 일정 수의 메시지가 누적된 후에 호출될 수 있습니다.
Copy
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
};
};
전체 예제: 메시지 요약하기
전체 예제: 메시지 요약하기
Copy
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 });
체크포인트 관리하기
체크포인터가 저장한 정보를 보고 삭제할 수 있습니다.스레드 상태 보기
Copy
const config = {
configurable: {
thread_id: "1",
// 선택적으로 특정 체크포인트의 ID를 제공합니다,
// 그렇지 않으면 최신 체크포인트가 표시됩니다
// checkpoint_id: "1f029ca3-1f5b-6704-8004-820c16b69a5a"
},
};
await graph.getState(config);
Copy
{
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: []
}
스레드의 히스토리 보기
Copy
const config = {
configurable: {
thread_id: "1",
},
};
const history = [];
for await (const state of graph.getStateHistory(config)) {
history.push(state);
}
스레드의 모든 체크포인트 삭제하기
Copy
const threadId = "1";
await checkpointer.deleteThread(threadId);
Connect these docs programmatically to Claude, VSCode, and more via MCP for real-time answers.