前言
這個時代開發產品絕對會被反覆要求「我們的產品能不能整合 AI?」、「AI 能帶來什麼新的商機?」,很多團隊包括我也是第一次遇到需要整合 AI 到實際產品的情況。
一個 AI 功能背後需要考慮到成本、可行性等多方面因素,而我在開發的產品性質又是需要斷網的嚴苛環境,因此這篇文章主要研究盡可能基於瀏覽器或本地 LLM 的 MVP,著重探討技術上的可能性。
聽說瀏覽器有 LLM?
很早就聽說過 Chrome 將 LLM 內建於 Browser API 當中,像是 Prompt API但它的普及率可以說是只有最新版的 Chrome 才能試用,API 看起來很像在哪看過:
// 1. 檢查瀏覽器是否支援,並確認模型已下載完畢const capabilities = await ai.languageModel.capabilities();if (capabilities.available === 'readily') {
// 2. 建立 session,並在建立時帶入 system prompt const session = await ai.languageModel.create({ systemPrompt: "You are a helpful and friendly assistant." });
// 3. 模擬對話歷史 (Prompt API 會在同一個 session 中自動維持 context) // 第一輪對話 (User) const response1 = await session.prompt("Can you write a short poem about coding?"); console.log("Assistant:", response1); // 輸出: Code is the poetry of machines, Where logic weaves through screens and seams.
// 4. 如果你想在建立 session 時就直接注入「過去的對話歷史」(歷史紀錄 payload) // 可以使用 initialPrompts 參數: const sessionWithHistory = await ai.languageModel.create({ systemPrompt: "You are a helpful and friendly assistant.", initialPrompts: [ { role: 'user', content: 'Can you write a short poem about coding?' }, { role: 'assistant', content: 'Code is the poetry of machines, Where logic weaves through screens and seams.' } ] });
// 之後繼續對話,它就會記得這段歷史 const response2 = await sessionWithHistory.prompt("Can you explain the first line?"); console.log("Assistant:", response2);
// 記得不用時要銷毀 session 釋放記憶體 session.destroy(); sessionWithHistory.destroy();}如果你有串過 OpenAI 的模型那一定很熟悉 Chat ML (Chat Markup Language) 格式感覺的酬載,這套 System / User / Assistant 的 Payload 在 Prompt API 當中也一樣,其實很多家供應商的模式都差不多:
<|im_start|>systemYou are a helpful and friendly assistant.<|im_end|><|im_start|>userCan you write a short poem about coding?<|im_end|><|im_start|>assistantCode is the poetry of machines,Where logic weaves through screens and seams.<|im_end|>根據以上模式建了一個運用 Prompt API 的 local-ai-assistance🔗 MVP 專案實驗
但從單純的接詞機器 LLM 到成為獨當一面的 Agent 還是不足的,可以利用額外的套件像是 LangChain 去做 LLM 的 Harness。
LangChain
像是前面的案例會需要自己手動管理不同 AI 供應商來源、長期記憶、記憶檢索(RAG、Embedding)、工作流搭建、結構化回應或工具調用⋯⋯等延伸問題,如果每個專案都要從頭刻這些基礎建設成本將非常高,LangChain就是為了解決這些痛點而生的框架。
LangChain vs. LangGraph vs. Deep Agents
到 LangChain 開始文件 會看到三種開始方式:
| 方式 | 定義與核心功能描述 |
|---|---|
| LangChain | 一個極簡、可配置的 Agent 框架。您可以根據需求,精準組合模型、工具、提示詞(Prompts)和中間件(Middleware)。 |
| LangGraph | 適用於具狀態、長期執行 Agent 的底層編排:提供持久化執行、序列流(Streaming)、記憶以及人機協同(Human-in-the-loop)功能。 |
| Deep Agent | 建立用於複雜、長期執行任務的 Agent。具備完整的 Agent 運作架構,內建規劃、子 Agent、虛擬檔案系統與長期記憶功能。這是最快上手的途徑。 |
- Deep Agents:最適合一開始就想要「功能齊全」Agent 的開發者。內建了自動上下文壓縮(automatic context compression)、虛擬檔案系統(virtual filesystem)以及子代理生成(subagent-spawning)等功能。Deep Agents 是建立在 LangChain 代理之上,你也可以選擇直接使用 LangChain 代理。
- LangChain:適合需要高度自訂化框架的場景,讓你能夠輕鬆地根據特定的使用案例和數據進行調整。
- LangGraph:底層流程協調框架,專為需要結合「確定性流程」與「自主代理流程」的進階需求而設計。
三者的關係可以理解為:LangGraph 是流程引擎,LangChain 是建立在它上面的框架,Deep Agent 是更高階的開箱即用方案。
LangChain 解決什麼?
產品對於 AI 的期望已經不止於回答問題,而是期望一個智能對象能解決問題,開發上麻煩的不是模型本身,而是周圍的一堆整合工作
假設今天用 OpenAI,明天改成 Ollama;需要替 prompt 加上固定的 System Message;要讀 PDF、切文件,再接上向量資料庫。這些事情每項都不難,但全部串在一起後,程式很容易變得瑣碎難維護,LangChain 做的事情,就是把這些常見需求抽象成一致的介面。
LangGraph 解決什麼?
使用有向圖管理 LLM 流程邏輯
如果 LangChain 解決的是「元件怎麼接」,那 LangGraph 解決的就是「整個流程怎麼跑」。如果全部自己寫,通常就是大量的 if...else 或巢狀函式呼叫,流程一變複雜開始會出現分支、重試、共享狀態等需求,閱讀和維護都會愈來愈困難。
收到問題 → 判斷是否需要改寫 → 檢索文件 → 生成答案 → 檢查結果是否符合預期 → 必要時重新執行某個步驟LangGraph 用有向圖的方式來描述這類流程。
- Node(節點):代表一個執行步驟,例如改寫問題、檢索文件或產生回答,本質上就是一個普通函式。
- Edge(邊):定義節點之間的執行順序。
- Conditional Edge(條件邊):根據目前的狀態決定下一個要執行哪個節點,例如第一輪對話直接檢索,後續對話才先改寫問題。
- State(狀態):使用 Annotation 定義共享狀態,每個節點都可以讀取或更新,資料會在節點之間自動傳遞。
流程大概會長這樣:
// 1. 定義狀態結構const MyState = Annotation.Root({ ...MessagesAnnotation.spec, rephrasedQuestion: Annotation<string>, sourceDocuments: Annotation<Document[]>,});// 2. 定義節點const nodeA = async (state) => { return { rephrasedQuestion: "改寫後的問題" };};// 3. 組裝流程const graph = new StateGraph(MyState) .addNode("nodeA", nodeA) .addNode("nodeB", nodeB) .addConditionalEdges("__start__", (state) => { return state.messages.length > 1 ? "nodeA" : "nodeB"; }) .addEdge("nodeA", "nodeB") .compile();拆解 fully-local-pdf-chatbot 案例
Effectively Building with LLMs in the Browser with Jacob - LangChain 影片中提到 fully-local-pdf-chatbot 專案運用 LangChain 串上 3 種本地方案達成對用戶上傳的 PDF 文件提問的功能,以下是我拆解這個專案的筆記。
技術棧
核心邏輯全部跑在 Web Worker 裡(worker.ts),避免阻塞主線程,重點是所有東西都可以在客戶端跑完:
| 職責 | 工具 |
|---|---|
| LLM 推論 | Ollama(本地桌面)/ WebLLM(瀏覽器 WebGPU)/ Chrome AI(瀏覽器內建 Gemini Nano) |
| Embedding | Transformers.js(Xenova/all-MiniLM-L6-v2) |
| 向量資料庫 | Voy(WASM,完全在瀏覽器中運作) |
| 流程編排 | LangChain.js + LangGraph.js |
第一步:PDF 嵌入
當用戶上傳 PDF 後,Worker 會執行以下流程:
import { WebPDFLoader } from "@langchain/community/document_loaders/web/pdf";import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";import { Voy as VoyClient } from "voy-search";import { HuggingFaceTransformersEmbeddings } from "@langchain/community/embeddings/hf_transformers";
const embeddings = new HuggingFaceTransformersEmbeddings({ modelName: "Xenova/all-MiniLM-L6-v2", // Can use "nomic-ai/nomic-embed-text-v1" for more powerful but slower embeddings // modelName: "nomic-ai/nomic-embed-text-v1",});
const voyClient = new VoyClient();const vectorstore = new VoyVectorStore(voyClient, embeddings);
const embedPDF = async (pdfBlob: Blob) => { // 1. 用 LangChain 的 WebPDFLoader 解析 PDF const pdfLoader = new WebPDFLoader(pdfBlob, { parsedItemSeparator: " " }); const docs = await pdfLoader.load();
// 2. 用 RecursiveCharacterTextSplitter 切成小塊 const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, chunkOverlap: 50, }); const splitDocs = await splitter.splitDocuments(docs);
// 3. 透過 Transformers.js Embedding 轉換後存入 Voy 向量資料庫 await vectorstore.addDocuments(splitDocs);};這裡 LangChain 的價值在於:WebPDFLoader、RecursiveCharacterTextSplitter、VoyVectorStore 這些元件都是標準化的接口,你不需要自己去處理 PDF 解析或 Chunk 切割的邊界條件。
第二步:RAG 對話流程
當用戶提問時,專案用 LangGraph 建了一個三節點的狀態圖來處理 RAG 流程:
對應到程式碼:
const graph = new StateGraph(RAGStateAnnotation) .addNode("rephraseQuestion", rephraseQuestion) .addNode("retrieveSourceDocuments", retrieveSourceDocuments) .addNode("generateResponse", generateResponse) .addConditionalEdges("__start__", async (state) => { // 首輪對話直接檢索;多輪對話先改寫問題再檢索 if (state.messages.length > 1) { return "rephraseQuestion"; } return "retrieveSourceDocuments"; }) .addEdge("rephraseQuestion", "retrieveSourceDocuments") .addEdge("retrieveSourceDocuments", "generateResponse") .compile();三個節點各自負責:
- rephraseQuestion — 如果已經是多輪對話,先用 LLM 將最新的問題結合上下文改寫成獨立的搜尋查詢,提升檢索品質(例如用戶問「那第二章呢?」→ 改寫成「文件第二章的內容是什麼?」)
- retrieveSourceDocuments — 拿改寫後的查詢去向量資料庫做相似度搜尋,找出最相關的文件片段
- generateResponse — 把檢索到的文件片段塞進 System Prompt 的
<context>區塊,讓 LLM 基於這些資料生成回答
第三步:三種本地 LLM 供應商的抽換
LangChain 最大的好處之一就是統一介面,專案中三種模型的切換只需要替換實例化的 class:
let model;if (modelProvider === "webllm") { model = new ChatWebLLM(modelConfig); // 瀏覽器內 WebGPU 推論} else if (modelProvider === "chrome_ai") { model = new ChromeAI(modelConfig); // Chrome 內建 Gemini Nano} else { model = new ChatOllama(modelConfig); // 本地 Ollama 伺服器}同一套 RAG Pipeline 完全不需要因為底層模型不同而改動邏輯,這就是框架抽象層帶來的好處。唯一需要注意的是 Chrome AI 目前是 text-in/text-out 的 LLM(非 Chat Model),所以在 Prompt 格式上需要特別處理:對話歷史必須手動拼接成字串。
小結
拆解這個專案後可以發現 LangChain 的核心價值:
- 文件處理標準化 —
WebPDFLoader+RecursiveCharacterTextSplitter幾行程式碼就能完成 PDF 到模型可檢索的資料格式 - 向量檢索抽象 — 不管底層用的是 Voy、Pinecone 還是其他向量資料庫,API 一致
- 模型供應商可抽換 — Ollama / WebLLM / Chrome AI 共用同一套流程
- LangGraph 狀態機 — 用聲明式的方式描述 RAG 流程中的節點與條件分支,比手動寫
if-else更易讀、易維護
透過拆解 fully-local-pdf-chatbot 專案了解整個 LangChain 的使用情境與真實流程。