前言
这个时代开发产品一定会被反复要求「我们的产品能不能整合 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: "你是一位乐于助人且友善的助手。" });
// 3. 模拟对话历史(Prompt API 会在同一个 session 中自动维持 context) // 第一轮对话(User) const response1 = await session.prompt("你能写一首关于编程的短诗吗?"); console.log("助手:", response1); // 输出: 代码是机器的诗篇,逻辑在屏幕与缝隙间编织。
// 4. 如果你想在创建 session 时就直接注入「过去的对话历史」(历史记录 payload) // 可以使用 initialPrompts 参数: const sessionWithHistory = await ai.languageModel.create({ systemPrompt: "你是一位乐于助人且友善的助手。", initialPrompts: [ { role: 'user', content: '你能写一首关于编程的短诗吗?' }, { role: 'assistant', content: '代码是机器的诗篇,逻辑在屏幕与缝隙间编织。' } ] });
// 之后继续对话,它就会记得这段历史 const response2 = await sessionWithHistory.prompt("你能解释第一行吗?"); console.log("助手:", response2);
// 记得不用时要销毁 session 释放内存 session.destroy(); sessionWithHistory.destroy();}如果你接过 OpenAI 的模型,那一定很熟悉 Chat ML (Chat Markup Language) 格式感觉的载荷,这套 System / User / Assistant 的 Payload 在 Prompt API 中也一样,其实很多供应商的模式都差不多:
<|im_start|>system你是一位乐于助人且友善的助手。<|im_end|><|im_start|>user你能写一首关于编程的短诗吗?<|im_end|><|im_start|>assistant代码是机器的诗篇,逻辑在屏幕与缝隙间编织。<|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", // 可以使用 "nomic-ai/nomic-embed-text-v1" 获得更强但更慢的 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 的使用场景与真实流程。