iT邦幫忙

2025 iThome 鐵人賽

DAY 4
0
AI & Data

30 天從 0 至 1 建立一個自已的 AI 學習工具人系列 第 4

30-4: [知識] LangChain X LangGraph 之要如何記得你 ? ( Memory )

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250918/20089358ahVCYmb654.png

同步至 medium

在前兩篇文章中,咱們大概理解了 LangChain 與 LangGraph 的功用,但我們他們的文件中,發現兩個都有所謂的 Memory 功能,所以這篇文章將會來談談以下三個主題 :

  • LangChain 的 Memory
  • LangGraph 的 Memory
  • 兩者的分工是如何呢 ?

🚀 LangChain 的 Memory

在 LangChain 1.0.0 總共有兩篇文件專門在討論這個主題 :

🤔 在 LangChain 中如何讓 AI Agent 記得事情呢 ?

就像如下的範例一樣,主要就是會用 checkpointer 這個概念,來儲放,然後有個重點 :

它是以 thread_id 為記憶單位

所以我們在實務上時,會根據需求來決定如何產生這個 thread_id,例如是要記得這個人今天過的話,那就會是以『 user_id + date 』來產生個 thread_id 之類的,反正就是以需求為主。

下面的範例我們就是簡單用記憶功能,來讓他記得我是誰。

import { createAgent, MemorySaver } from "langchain";

const checkpointer = new MemorySaver();

const agent = createAgent({
    model: "openai:gpt-5-nano",
    tools: [],
    checkpointer,
});
const systemMessage = { role: "system", content: "你是一個助理,請用簡單的方式回答問題,用繁體中文回答" };
const threadId = "1";

const result1 = await agent.invoke(
    { messages: [systemMessage, { role: "user", content: "你好我是馬克" }] },
    { configurable: { thread_id: threadId } }
);

const result2 = await agent.invoke(
    { messages: [systemMessage, { role: "user", content: "我是誰" }] },
    { configurable: { thread_id: threadId } }
);


// 1. "嗨,馬克!很高興認識你。需要我幫忙做什麼呢?告訴我你想做的事就好。
// 2. "你是馬克。很高興認識你。需要我幫忙做什麼嗎?"

🤔 要注意上面的範例不適合一些情況,因為是儲在 Memory 中

大部份的工程師應該都知道這樣會發生幾個問題 :

  1. 如果是儲放服務的 memory 中,只要服務重啟後,那這些記憶都會消失。
  2. 如果你是有多個 process 服務,或是你是用 k8s 有多個 pod 情況下,你第一次聊天是記在第 1 台,除非你能確保你下 1 次可以走到第 1 台,不然記憶也會消失。

所以當然 LangChain 也有提到儲放在資料庫中的方案 :

例如 postgres 如下範例 :

import { createAgent } from "langchain";
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 agent = createAgent({
    model: "openai:gpt-5-nano",
    tools: [],
    checkpointer,
});

它還有以下幾種 Saver 然後我是在這下面找到的,你在 LangChain 的文件找不到,看起來它是寫在 LangGraph 但好像也有缺,然後直接去 LangGraph 的 repo 看有以下幾個:

https://github.com/langchain-ai/langgraphjs/tree/main/libs

  • checkpoint-mongodb
  • checkpoint-postgres
  • checkpoint-redis
  • checkpoint-sqlite

然後 LangGraph 的文件連結在這裡,但是它只列了以下幾個 :
https://docs.langchain.com/oss/javascript/langgraph/persistence#checkpointer-libraries

  • @langchain/langgraph-checkpoint-sqlite
  • @langchain/langgraph-checkpoint-postgres

看起來 repo 中有 mongodb 與 redis,但是文件也沒有,真有點搞不太懂它們的文件同步機制,不過也有可能這兩個的版本還在早期 0.1 版所以才沒更新到文件上,大概。

🤔 奇怪,看起來上面就有長期記憶的機制了,那 LangChain 文件的長期記憶章節是在說啥

https://docs.langchain.com/oss/javascript/langchain/long-term-memory

嗯嗯嗯… 我完全看不太懂他在做什麼,只知道以下幾件事情 :

  • 它是用 LangGraph 提供的東西來處理。
  • 長出了一個叫 Store 的東西,但看起來就是將記憶功能抽象成 Store 然後可以像操作 ?

這個 Store 主要是用在跨 thread_id 上,但我看到的時後在想,那這個用我們平常常用的資料庫不就行了 ? 我現在也還沒悟出他的真實用法。

🚀 LangGraph 的 Memory

https://docs.langchain.com/oss/javascript/langgraph/persistence

https://ithelp.ithome.com.tw/upload/images/20250918/20089358OLHVBSIr0B.png
圖片來源: LangGraph 官網

以下為官網的使用範例,事實上就和上面 LangChain 的範例有 90 % 像,就只是 :

  • LangChain 的範例的 checkoutpointer 是設在 createAgent 中。
  • LangGraph 的範例的 checkoutpointer 是設在 workflow ( grpah ) 中。

所以 LangGraph 整個 Memory 機制是以 workflow 為單位。

import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import { registry } from "@langchain/langgraph/zod";
import * as z from "zod";

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

const workflow = new StateGraph(State)
  .addNode("nodeA", (state) => {
    return { foo: "a", bar: ["a"] };
  })
  .addNode("nodeB", (state) => {
    return { foo: "b", bar: ["b"] };
  })
  .addEdge(START, "nodeA")
  .addEdge("nodeA", "nodeB")
  .addEdge("nodeB", END);

const checkpointer = new MemorySaver();
const graph = workflow.compile({ checkpointer });

const config = { configurable: { thread_id: "1" } };
const result = await graph.invoke({ foo: "" }, config);
console.log(result);

🤔 在 LangGraph 上用這個實際可以幹麻? LangChain 用的可以讓 Agent 記得上下文,那這個呢?

對 ~ LangGraph 這裡主要的目的不是為了 Agent,而是為了讓 Workflow 做到以下幾個事情 :

  • Human-in-the-loop : 就是我們上一篇有提到的,可以在整個 Workflow 加入人類的判斷確認。
  • Time Travel : 通常是為了 Debug 與分析讓你可以回過過去某個時間點的 Node 然後再開始走。
  • Fault-tolerance : 就是炸了,可以從炸的那個節點重跑。。

🤔 那它有分長期模式嗎 ?

當然,而且 LangGraph 的文件總於不像 LangChain 分開來還沒寫什麼。

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 });

🚀 在Agentic AI 中兩者的分工是如何呢 ?

簡單的說分類如下 :

🤔 LangChain(Agent 層)

  • 目標 : 讓一個 Agent 在一條對話(thread)中記得先前的對話。
  • 單位 : Agent
  • 工具 : checkpointer( MemorySaver / Postgres / Redis )、store ( 跨 thread 場合共享用 )

🤔 LangGraph( Workflow 層)

  • 目標 : 讓整條 workflo 能中斷、重播、失敗恢復、人類介入,並在跨 thread 場合共享關鍵知識。
  • 單位 : Workflow。
  • 武器 : checkpointer( MemorySaver / Postgres / Redis )、store ( 跨 thread 場合共享用 )

🤔 那適合所有包含 Workflow 與 Agent 都用同一個 checkpointer 嗎?

可以,要注意太長的問題,如果在 agent 用 checkpointer 會自動記得之前整個 workflow 說過的話喔,這也代表會送給 LLM。

如下範例,我的 createAgent 與 workflow 都是同一個 checkpointer 與相同的 thread_id,所以下面這個範例的執行結果就會記得我是誰。

import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import * as z from "zod";
import { createAgent } from "langchain";

const checkpointer = new MemorySaver();
const config = { configurable: { thread_id: "1" } };

const State = z.object({
  messages: z.array(z.object({ role: z.string(), content: z.string() })),
});

const agent = createAgent({
  model: "openai:gpt-5-nano", // 可換你實際可用的模型
  tools: [],
  checkpointer,
});

const workflow = new StateGraph(State)
  .addNode("nodeA", (state) => {
    return {
      messages: [
        ...(state.messages ?? []),
        { role: "user", content: "我是馬克" },
      ],
    };
  })
  .addNode("nodeB", async (state) => {
    const res = await agent.invoke(
      {
        messages: [
          { role: "system", content: "請用繁體中文、簡潔回答。" },
          {
            role: "user",
            content: `我是誰`,
          },
        ],
      },
      config
    );

    return {
      messages: res.messages,
    };
  })
  .addEdge(START, "nodeA")
  .addEdge("nodeA", "nodeB")
  .addEdge("nodeB", END);

const graph = workflow.compile({ checkpointer });
const result = await graph.invoke({ messages: [] }, config);
console.log(result);

這是 console 結果。

{
  messages: [
    HumanMessage {
      "id": "4939039d-7141-4a6c-a974-035654d20897",
      "content": "我是馬克",
      "additional_kwargs": {},
      "response_metadata": {}
    },
    SystemMessage {
      "id": "39cde393-6fe4-4f5d-bdfa-2c72d3fc49bf",
      "content": "請用繁體中文、簡潔回答。",
      "additional_kwargs": {},
      "response_metadata": {}
    },
    HumanMessage {
      "id": "1981b111-4fcd-45c1-8402-a96ccc18dbaf",
      "content": "我是誰",
      "additional_kwargs": {},
      "response_metadata": {}
    },
    AIMessage {
      "id": "chatcmpl-CH3S8jYUCMPUSHYMoTeRTpzhopiM4",
      "content": "你是馬克。",
      "name": "model",
      "additional_kwargs": {},
      "response_metadata": {
        "tokenUsage": {
          "promptTokens": 29,
          "completionTokens": 270,
          "totalTokens": 299
        },
        "finish_reason": "stop",
        "model_provider": "openai",
        "model_name": "gpt-5-nano-2025-08-07"
      },
      "tool_calls": [],
      "invalid_tool_calls": [],
      "usage_metadata": {
        "output_tokens": 270,
        "input_tokens": 29,
        "total_tokens": 299,
        "input_token_details": {
          "audio": 0,
          "cache_read": 0
        },
        "output_token_details": {
          "audio": 0,
          "reasoning": 256
        }
      }
    }
  ]
}

🤔 由於 Token 要錢,所以理想上還是會希望將 worfklow 與 agent 的分開,然後走到 agent 時,再自已判斷要不要抓整個 workflow 的資料進入到自已的 agent

所以大概會改成如下,會將 checkpointer 分開來,然後在 nodeB 中自已判斷要不要從 state 抓整個 workflow 的資料後,再帶入到自已的 agent 中。執行後得到的結果和上面一樣。

import { StateGraph, START, END, MemorySaver } from "@langchain/langgraph";
import * as z from "zod";
import { createAgent } from "langchain";

const workflowCheckpointer = new MemorySaver();
const agentCheckpointer = new MemorySaver();
const config = { configurable: { thread_id: "1" } };

const State = z.object({
  messages: z.array(z.object({ role: z.string(), content: z.string() })),
});

const agent = createAgent({
  model: "openai:gpt-5-nano", // 可換你實際可用的模型
  tools: [],
  checkpointer: agentCheckpointer,  // <---------- 重點是這裡 !!!!!!
});

const workflow = new StateGraph(State)
  .addNode("nodeA", (state) => {
    return {
      messages: [
        ...(state.messages ?? []),
        { role: "user", content: "我是馬克" },
      ],
    };
  })
  .addNode("nodeB", async (state) => {
    const needMessages = state.messages ?? []; // <---------- 重點是這裡 !!!!!!

    const res = await agent.invoke(
      {
        messages: [
          ...needMessages,
          { role: "system", content: "請用繁體中文、簡潔回答。" },
          {
            role: "user",
            content: `我是誰`,
          },
        ],
      },
      config
    );

    return {
      messages: res.messages,
    };
  })
  .addEdge(START, "nodeA")
  .addEdge("nodeA", "nodeB")
  .addEdge("nodeB", END);

const graph = workflow.compile({ checkpointer: workflowCheckpointer }); // <---------- 重點是這裡 !!!!!!
const result = await graph.invoke({ messages: [] }, config);
console.log(result);

🚀 小總結

這個章節中,咱們 LangChain X LangGraph 兩個一起看的角度來看看要如何記得你,然後這兩個都有提到記憶功能差別在於 :

  • LangChain: 讓一個 Agent 在一條對話(thread)中記得先前的對話。
  • LangGraph: 讓整條 workflo 能中斷、重播、失敗恢復、人類介入,並在跨 thread 場合共享關鍵知識。

然後在同一個 thread 的情況下,都是用 checkpointer 來運行,然後跨 thread 的情況則是用 store,然後最後還有個建議是記得打理 checkpointer 如果裡面的東西越來越多,那你的 LLM 可能就會不知不覺得送很多沒用的 token 出去了。

🚀 參考資料


上一篇
30-3: [知識] LangChain 的好朋友之 LangGraph 概略
下一篇
30-5: [實作] 我們 AI 工具人的第一步 - 基本查詢知識功能
系列文
30 天從 0 至 1 建立一個自已的 AI 學習工具人5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言