iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
AI & Data

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

30-6: [實作-2] 讓我們的 AI 工具人有記憶與匯總訊息的能力 ( RedisSaver 版本 )

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250920/20089358ubMkEnIV4T.png

同步至 medium

在這一篇文章中,我們說明了要如何讓 AI 記得我們說過的話,接下來我們就來實作它。

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

然後今天我們要實作的架構大約如下,主要是會用 Redis 來儲放資訊 :

https://ithelp.ithome.com.tw/upload/images/20250920/200893588P9LaiCMXb.png

🚀 Step 1. 安裝 Redis 相關的東西

🤔 套件

npm install @langchain/langgraph-checkpoint-redis

但正常來說你執行下去會看到以下的錯誤,主要的原因是 LangChain 我們的版本是 1.0.0 以上,而這個官方的套件只支援到 0.4.0。

$ npm install @langchain/langgraph-checkpoint-redis
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error
npm error While resolving: 2025-ai-mark@1.0.0
npm error Found: @langchain/core@1.0.0-alpha.5
npm error node_modules/@langchain/core
npm error   @langchain/core@"^1.0.0-alpha.5" from the root project
npm error
npm error Could not resolve dependency:
npm error peer @langchain/core@">=0.2.31 <0.4.0" from @langchain/langgraph-checkpoint-redis@0.0.1
npm error node_modules/@langchain/langgraph-checkpoint-redis
npm error   @langchain/langgraph-checkpoint-redis@"*" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry

但我實際上測試使用後,發現事實上只是它們的 pageage.json 還沒升級,實際上還是可以動,不過這個建議只是先當實驗品,正式環境還是乖乖等他們,或是你自已送 PR 過去。

然後我這裡是指先在 package.json 加上這段,然後再執行 npm i 來安裝。

  "dependencies": {
    "@langchain/langgraph-checkpoint-redis": "^0.0.1"
  },
  "overrides": {
    "@langchain/langgraph-checkpoint-redis": {
      "@langchain/core": "1.0.0-alpha.5"
    }
  }

🤔 docker-compose

version: '2.1'

services:
  redis:
    image: redis:8.2.0-alpine
    ports: ['6379:6379']
    volumes:
      - './docker-data/redis-data:/data'
volumes:
  redis-data:

🤔 環境變數

REDIS_URL=redis://localhost:6379

🚀 Step 2. LangGraph 中使用 Redis Checkpointer

🤔 Workflow 的修改

主要有以下幾個地方有修改,下面程式碼可以用列表號來看到:

  1. 新增了 checkpointSaver 然後它是透過 RedisSaver 來實作。
  2. 在 workflow.compile 丟入這個 checkpointSaver。
  3. 這個 workflow 執行時 ( 就是要處理訊息時 ),會代入 checkpoint 所需要 configurable,主要是個 thread_id
import {
  StateGraph,
  START,
  END,
  Annotation,
  MessagesAnnotation,
} from "@langchain/langgraph";
import { BaseChatAI } from "./agents/base.agent";
import { RedisSaver } from "@langchain/langgraph-checkpoint-redis";
import { BaseCheckpointSaver } from "@langchain/langgraph";

enum Steps {
  INITIAL = "initial",
  CALL_CHAT_AI = "call_chat_ai",
}

const ChatStateAnnotation = Annotation.Root({
  // 它可以自動 append messages
  ...MessagesAnnotation.spec,
  query: Annotation<string>,
  step: Annotation<Steps>,
});

type ChatState = typeof ChatStateAnnotation.State;

export class ChatWorkflow {
  private baseChatAI: BaseChatAI | null = null;
  private graph: ReturnType<typeof this.buildGraph>;
  private threadId: string | null = null;
  private checkpointSaver: BaseCheckpointSaver | null = null;

  constructor() {}

  public async initialize(threadId: string) {  
    this.threadId = threadId; 
    // --------------------------------------------------- ( 1 ) 的修改
    this.checkpointSaver = await RedisSaver.fromUrl(process.env.REDIS_URL!, {
      defaultTTL: 5, // TTL in minutes
      refreshOnRead: true,
    });
    this.baseChatAI = new BaseChatAI(this.checkpointSaver, {
      threadId: this.threadId,
    });
    this.graph = this.buildGraph();
  }

  private buildGraph() {
    const workflow = new StateGraph(ChatStateAnnotation)
      .addNode(Steps.INITIAL, async (state: ChatState): Promise<ChatState> => {
        return {
          step: Steps.INITIAL,
          query: state.query,
          messages: [],
        };
      })
      .addNode(
        Steps.CALL_CHAT_AI,
        async (state: ChatState): Promise<ChatState> => {
          const response = await this.baseChatAI!.callLLM(state.query);

          return {
            step: Steps.CALL_CHAT_AI,
            messages: [...response],
            query: state.query,
          };
        }
      )
      .addEdge(START, Steps.INITIAL)
      .addEdge(Steps.INITIAL, Steps.CALL_CHAT_AI)
      .addEdge(Steps.CALL_CHAT_AI, END);

    if (!this.checkpointSaver) {
      throw new Error("Checkpoint saver is not initialized");
    }
    
    // --------------------------------------------------- ( 2 ) 的修改
    return workflow.compile({
      checkpointer: this.checkpointSaver,
    });
  }

  async *processMessage(
    message: string
  ): AsyncGenerator<string, void, unknown> {
    const initialState: ChatState = {
      query: message,
      step: Steps.INITIAL,
      messages: [],
    };

    // --------------------------------------------------- ( 3 ) 的修改
    const result: ChatState = await this.graph.invoke(initialState, {
      configurable: {        
        thread_id: this.threadId
      },
    });

    yield result.messages[result.messages.length - 1].content as string;
  }
}

🤔 Agent 的修改

主要的修改以下幾個地方:

  1. 從原本的 ChatOpenAI 改成用 createAgent,因為主要是 agent 才有記憶功能,這個記得在之前有提到說這個是 agent 的功能之一。
  2. 在 agent 中注入 checkpoint。
  3. 在讓 agent 執行 invoke ( 就是處理訊息時 ),帶入 checkpointer 所需要的 configurable,與 workflow 相同,都是需要 thread_id

事實上他整體的修改和 workflow 幾乎是相同的。

import { BaseMessage, SystemMessage, HumanMessage } from "langchain";
import { createAgent } from "langchain";
import { Configurable } from "./interfaces/configurable";
import { BaseCheckpointSaver } from "@langchain/langgraph";

/**
 * 基礎 Chat AI 服務,他可以做任何事情,不會做任何限制
 */
export class BaseChatAI {
  private checkpointSaver: BaseCheckpointSaver;
  private configurable: Configurable;
  private agent: any;

  constructor(
    checkpointSaver: BaseCheckpointSaver,
    configurable: Configurable
  ) {
    this.checkpointSaver = checkpointSaver;
    this.configurable = configurable;
    
    // ------------------------------------------------- ( 1 ) ( 2 )的修改
    this.agent = createAgent({
      model: "openai:gpt-5-mini",
      tools: [],
      checkpointer: this.checkpointSaver,
    });
  }

  async callLLM(message: string): Promise<BaseMessage[]> {
    const messages = [
      new SystemMessage(
        "你是 AI 知識學習助理,會回答 AI 相關知識,回應不超過 300 個字"
      ),
      new HumanMessage(message),
    ];

    const response = await this.agent.invoke(
      {
        messages,
      },
      {
        configurable: {
          thread_id: this.configurable.threadId
        },
      }
    );

    return response.messages;
  }
}

🚀 Step 3. 整理一下我們實際上要記錄的 Context

我們上面的範例實際上中,有一個東西要注意一下 :

你的 createAgent 沒進行特別處理的話,送多送很多東西給 LLM,如下圖,因為 agent 是會直接將 LLM 回傳的 messages 放到 checkpoint。

https://ithelp.ithome.com.tw/upload/images/20250920/20089358ThvWpnQ55J.png

其中問題在於 SystemMessage 每次都相同,但是都會帶入。

事實上有幾個解法 :

  1. 不要用 createAgent 層級的 checkpointer,改成統一用 workflow 層的,然後再需要歷史訊息時,再從 workflow 帶入,而因為 workflow 的 checkpointer 是看 state,所以只要 state.messages 不回傳 system prompt,或是用另一個欄位儲就好。
  2. 使用 agent middleware,在 model 回傳訊息後,將 system message 移除。

這裡我們會選擇第 2 種來執行,因為這次需求我想嘗試走看看 Workflows + 多 Agent 情境。

🤔 先問一下,System Role Prompt 是啥 ?

這裡有個知識要先科普一下,那就是在 LLM 的世界中 message 通常會有以下幾種 :

  • system : 通常是開發者或是 AI Application 提到的系統詞,例如你是一個 XXX 專家。
  • user : 就是用戶問題問題。
  • model : 這個就是 AI Model 回傳的問題。

然後每個 AI 基本上就是分這三類,然後再長出一些,但是名詞都不同,可以看以下 OpenAI 的文件,它在裡面叫 developer、user、assistant,然後看 Google Gemini 的則是兩種 user、model,但 user 內好像又有分。

OpenAI-message-roles-and-instruction-following

然後 LangChain 這裡就統一抽象成以下幾個,你正你用了他,LangChain 會自動轉成對應的送給 LLM。

https://docs.langchain.com/oss/javascript/langchain/messages#message-types

  • SystemMessage
  • HumanMessage
  • AIMessage

然後因為我們每一次 SystemMessage 都是一樣,浪費資源,因為我們只會保留一個就好。

🤔 程式碼修改的地方

主要就是在 agent 那加上下面我寫的這個 cleanMessageMiddleware 這個 middleware,這樣你就會發現整個 agent 的 checkpoint 就只會儲放 HumanMessage 與 AIMessage。

import { BaseMessage, SystemMessage, HumanMessage, AIMessage } from "langchain";
import { createAgent, createMiddleware } from "langchain";
import { Configurable } from "./interfaces/configurable";
import { BaseCheckpointSaver } from "@langchain/langgraph";

const cleanMessageMiddleware = createMiddleware({
  name: "cleanMessageMiddleware",
  afterModel: (state: { messages: BaseMessage[] }) => {
    state.messages = state.messages.filter((message: BaseMessage) => {
      if (message instanceof HumanMessage || message instanceof AIMessage) {
        return true;
      }
      return false;
    });

    return state;
  },
});

/**
 * 基礎 Chat AI 服務,他可以做任何事情,不會做任何限制
 */
export class BaseChatAI {
  private checkpointSaver: BaseCheckpointSaver;
  private configurable: Configurable;
  private agent: any;

  constructor(
    checkpointSaver: BaseCheckpointSaver,
    configurable: Configurable
  ) {
    this.checkpointSaver = checkpointSaver;
    this.configurable = configurable;
    this.agent = createAgent({
      model: "openai:gpt-5-mini",
      tools: [],
      checkpointer: this.checkpointSaver,
      // ref: https://blog.langchain.com/agent-middleware/
      middleware: [cleanMessageMiddleware],  // <------------------- 修改了這裡 ~
    });
  }

  async callLLM(message: string): Promise<BaseMessage[]> {
    ...
  }
}

🚀 Step 4. 請 AI 工具人幫我總結我今天學習過的東西

最後接下來我們來試一下結果。

https://ithelp.ithome.com.tw/upload/images/20250920/20089358SBMf2Oa3K3.png
https://ithelp.ithome.com.tw/upload/images/20250920/20089358AwK0j9eKmy.png
https://ithelp.ithome.com.tw/upload/images/20250920/20089358EMopTGdbmO.png

🚀 小總結

實作完了今天的東西以後,我們大約學習到以下幾個東西 :

  • LangChain 的 Agent 的記憶功能。
  • LangChain 的 Agent 可以透過 middleware 來進行訊息整理。
  • LangGraph 的記憶功能。
  • 如何使用 Redis 來進行記憶功能。

不過今天實作完以後,比較讓我糾結的是,LangChain 的 Agent 要不要讓他也有 checkpointer,還是統一由 LangGraph 的 Workflow 來管理呢 ? 這個在接下來的需求在看看沒有沒新的發現,現在目前簡單的情況下,好像都可以。

備註: 我現在 Workflow 與 Agent 是用同一個 checkpointer 這個我知道,這個之後會優化,因為現在這還看不出問題。

範例程式碼連結

🚀 參考資料


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

尚未有邦友留言

立即登入留言