前面幾篇我們學習了一些 AI Application 的基本知識以後,接下來我們就來開始實作我們的第一個需求 :
AI 工具人: 我會和你交談來幫助你查詢知識
接下來就開始吧。
這裡我們總共需要這幾個核心的套件:
https://www.npmjs.com/package/langchain
然後接下來是環境變數的設定。
OPENAI_API_KEY=你的 OPENAI Key
前端我主要是用 vue 來進行簡單的畫面制作,但我自已並不是前端專業,所以這裡我是用 AI 產出來的,大概就是長的如下,然後程式碼很爛,所以可以不用看 :
然後接下來是我們的重點,整個 server 這的架構
├── server
│ └── src
│ ├── index.ts
│ ├── routes
│ │ └── chat.ts
│ └── workflows
│ ├── agents
│ │ └── base.agent.ts
│ └── chat.workflow.ts
整個的架構流程如下 :
routes -> worflows -> agents
🤔 什麼是 SSE 呢 ?
Server-Sent Events 事實上他的概念很簡單,它就是一種基於 HTTP 的單向推送機制,server 透過一條長連線,持續把事件流 ( text/event-stream ) 推給 client 端。
這個在現今 AI Application 很多情況下,它都是一種標配流程,它整個流程如下圖:
🤔 為什麼不用 WebSocket ?
主要在於以下幾個問題 :
但是 SSE 的硬傷就是只支援純文字,所以如果要送圖片、音訊等,可能將要轉成 Base64 之類的,容量會大上不少。
但看 OpenAI 也都可以上傳圖片啊 ~ 它們不是用 SSE 嗎 ? 對他們是用 SSE,但他們的處理手法是先打一隻 api 上傳圖片後,取得編號,然後再用 SSE 送到後端。
🤔 routes/chat.ts 的程式碼
幾個重點 :
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;
下面就是我們使用 LangGraph 建構出來的 Chat Workflow,其中幾個重點:
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;
}
}
這個事實上就上蠻簡單的,就是使用 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];
}
}
npm run dev
事實上如果是以今天這個功能開發來說,事實上是可以完全不太需要用到 LangGraph 與 LangChain 相關的東西,只要一個 OpenAI SDK + SSE 處理就可以搞定掉一切。
但在接下來繼續增加需求後,應該就慢慢的可以看出更多功用了,不過今天真的沒有什麼太多的技術點,比較像是我們實作的第一版架構說明。