Build a local LLM agent inside product with LangChain

研究通过 LangChain 为产品导入本地模型与配套 Harness

前言

这个时代开发产品一定会被反复要求「我们的产品能不能整合 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)
EmbeddingTransformers.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 的价值在于:WebPDFLoaderRecursiveCharacterTextSplitterVoyVectorStore 这些组件都是标准化的接口,你不需要自己去处理 PDF 解析或 Chunk 切割的边界条件。

第二步:RAG 对话流程

当用户提问时,项目用 LangGraph 建了一个三节点的状态图来处理 RAG 流程:

多轮对话

首轮对话

start

rephraseQuestion

retrieveSourceDocuments

generateResponse

对应到代码:

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();

三个节点各自负责:

  1. rephraseQuestion — 如果已经是多轮对话,先用 LLM 将最新的问题结合上下文改写成独立的搜索查询,提升检索质量(例如用户问「那第二章呢?」→ 改写成「文档第二章的内容是什么?」)
  2. retrieveSourceDocuments — 拿改写后的查询去向量数据库做相似度搜索,找出最相关的文档片段
  3. 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 的使用场景与真实流程。

延伸阅读