iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
AI & Data

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

30-5: [實作] 我們 AI 工具人的第一步 - 基本查詢知識功能

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250919/200893586GCyVuMMHF.png

同步至 medium

前面幾篇我們學習了一些 AI Application 的基本知識以後,接下來我們就來開始實作我們的第一個需求 :

AI 工具人: 我會和你交談來幫助你查詢知識

接下來就開始吧。

🚀 Step 1. 套件安裝與環境變數

這裡我們總共需要這幾個核心的套件:

  • @langchain/core: 主要是核心抽象的部份。
  • @langchain/langgraph: langGraph 實際使用運行的部份。
  • @langchain/openai: LangChain OpenAI 的部份。
  • langchain: langchain 實際使用運行的部份。

https://www.npmjs.com/package/langchain

然後接下來是環境變數的設定。

OPENAI_API_KEY=你的 OPENAI Key

🚀 Step 2. 簡單的前端聊天室

前端我主要是用 vue 來進行簡單的畫面制作,但我自已並不是前端專業,所以這裡我是用 AI 產出來的,大概就是長的如下,然後程式碼很爛,所以可以不用看 :

https://ithelp.ithome.com.tw/upload/images/20250919/20089358F4J7KlOnc0.png

🚀 Step 3. AI Server 架構

然後接下來是我們的重點,整個 server 這的架構

├── server
│   └── src
│       ├── index.ts
│       ├── routes
│       │   └── chat.ts
│       └── workflows
│           ├── agents
│           │   └── base.agent.ts
│           └── chat.workflow.ts

整個的架構流程如下 :

routes -> worflows -> agents

  1. routes 這裡會處理 SSE 的連線,然後將內容送到 workflow 中。
  2. workflow 這裡就是 LangGraph 的 Workflow,然後裡面目前就是最簡單的一個 chat 機器人。
  3. agents 裡面目前就只有一個最簡單的 AI Agent。

🤔 什麼是 SSE 呢 ?

Server-Sent Events 事實上他的概念很簡單,它就是一種基於 HTTP 的單向推送機制,server 透過一條長連線,持續把事件流 ( text/event-stream ) 推給 client 端。

這個在現今 AI Application 很多情況下,它都是一種標配流程,它整個流程如下圖:

https://ithelp.ithome.com.tw/upload/images/20250919/20089358X0Dn2MTtS1.png

🤔 為什麼不用 WebSocket ?

主要在於以下幾個問題 :

  • AI 的使用場境很常是 Server -> Client 這種單向的訊息推送,像我們常在用 AI 就會是,我們問 1 句,AI 回 10 句的概念。
  • WebSocket 與 SSE 準確來說都是長連線,但 WebSocket 在大部份的情況下,都是 1 個連線佔 1 條 TCP,而 SSE 就是多個請求同時多工在同一條 TCP,所以相比之下,SSE 在 AI 的使用場境吃的效能比較小。

但是 SSE 的硬傷就是只支援純文字,所以如果要送圖片、音訊等,可能將要轉成 Base64 之類的,容量會大上不少。

但看 OpenAI 也都可以上傳圖片啊 ~ 它們不是用 SSE 嗎 ? 對他們是用 SSE,但他們的處理手法是先打一隻 api 上傳圖片後,取得編號,然後再用 SSE 送到後端。

🚀 Step 4. Routes SSE 處理

🤔 routes/chat.ts 的程式碼

幾個重點 :

  • 如果要讓這個連線進行 SSE,那就需要設定相對應的 Header,如下程式碼中 res.writeHead。
  • chatWorkflow.processMessage 這裡每回傳一個訊息就會送到前端。
  • 最後結束時 res.write("data: [DONE]\n\n"); 會送一個結束的字串給前端,讓他們知道這個 SSE 要結束了。
import express from "express";
import { ChatWorkflow } from "../workflows/chat.workflow";
import { randomUUID } from "node:crypto";

const router = express.Router();

router.get("/chat", async (req, res) => {
  const { message } = req.query;

  if (!message || typeof message !== 'string') {
    return res.status(400).json({ error: "Message is required" });
  }

  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Headers", "Cache-Control");

  const threadId = randomUUID();
  const chatWorkflow = new ChatWorkflow();
  await chatWorkflow.initialize(threadId);

  try {
    for await (const chunk of chatWorkflow.processMessage(message)) {
      res.write(`data: ${JSON.stringify({ chunk })}\n\n`);
    }
    res.write("data: [DONE]\n\n");

    res.end();
  } catch (error) {
    console.error("Chat error:", error);
    res.write(
      `data: ${JSON.stringify({ error: "Internal server error" })}\n\n`
    );
    res.end();
  }
});

export default router;

🚀 Step 5. Workflow 的處理

下面就是我們使用 LangGraph 建構出來的 Chat Workflow,其中幾個重點:

  • 最大的重點就是 buildGraph,它定義了整個 Workflow 的流程
  • 使用 MessagesAnnotation 會自動 append 每一個 Node 回傳出來的 message ( 這個之後會用來做上下文用 )。
  • processMessage 就是用來處理 message 進來的地方。
import {
  StateGraph,
  START,
  END,
  Annotation,
  MessagesAnnotation,
} from "@langchain/langgraph";

import { BaseChatAI } from "./agents/base.agent";

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;

  constructor() {}

  public async initialize(threadId: string) {
    this.baseChatAI = new BaseChatAI();
    this.graph = this.buildGraph();
    this.threadId = 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);

    return workflow.compile();
  }

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

    const result: ChatState = await this.graph.invoke(initialState);

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

https://ithelp.ithome.com.tw/upload/images/20250919/20089358j9c5Msd3GP.png

🚀 Step 6. Agents 的處理

這個事實上就上蠻簡單的,就是使用 OpenAI,然後叫他回話,你就可以和他討論知識 ~~~
事實上這裡的還不算 Agent 的概念,某些方面只是呼叫 LLM 而以,不過後面幾篇會慢慢的進化的。

import { ChatOpenAI } from "@langchain/openai";
import { BaseMessage, SystemMessage, HumanMessage } from "langchain";

/**
 * 基礎 Chat AI 服務,他可以做任何事情,不會做任何限制
 */
export class BaseChatAI {
  private model: ChatOpenAI;

  constructor() {
    this.model = new ChatOpenAI({
      modelName: "gpt-5-mini",
    });
  }

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

    return [response];
  }
}

🚀 Step 7. 執行結果

npm run dev

https://ithelp.ithome.com.tw/upload/images/20250919/20089358Er5cLvhaOx.png

🚀 小總結

事實上如果是以今天這個功能開發來說,事實上是可以完全不太需要用到 LangGraph 與 LangChain 相關的東西,只要一個 OpenAI SDK + SSE 處理就可以搞定掉一切。

但在接下來繼續增加需求後,應該就慢慢的可以看出更多功用了,不過今天真的沒有什麼太多的技術點,比較像是我們實作的第一版架構說明。

本日的程式碼 GitHub 連結


上一篇
30-4: [知識] LangChain X LangGraph 之要如何記得你 ? ( Memory )
系列文
30 天從 0 至 1 建立一個自已的 AI 學習工具人5
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言