POST · AI · 文章
LangChain 从入门到进阶(Node.js + TypeScript)
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 3366 字 · 预计阅读 9 分钟
本文整理一 LangChain 学习路径:从生态定位与核心概念起步,逐层深入 LCEL 管道、Agent 与中间件、RAG 检索增强,最后给出生产化要点、版本迁移与上线检查清单。所有代码片段使用 Node.js + TypeScript。
- 适用场景:LLM 应用原型、RAG 系统、Agent 工作流、Node 后端 AI 集成
- 技术栈:LangChain 1.x(JavaScript/TypeScript 端),需 Node.js 22+
- 目标:应用能跑、能控、能观测、能生产
一、先明确它在解决什么问题
LangChain 的核心定位是LLM 应用开发框架:提供统一抽象,让开发者用一致的 API 组合提示词、模型、工具、检索器,构建从简单链到复杂 Agent 的应用。
| 问题 | LangChain 的答案 |
|---|---|
| 换模型要重写代码? | 统一接口,一行切换 OpenAI / Anthropic / DeepSeek |
| 提示词、工具、检索器拼装麻烦? | LCEL 管道 prompt | model | parser |
| 需要多步推理、调用工具? | createAgent 快速构建 Agent |
| 需要接入私有数据? | RAG 组件:加载 → 分割 → 向量化 → 检索 |
| 生产环境难调试? | LangSmith 追踪每一次 LLM 调用与工具调用 |
必须理解的边界:
| 维度 | LangChain 的表现 |
|---|---|
| 简单线性流水线 | 极强,LCEL 一行组合 |
| 复杂有状态多智能体 | 需配合 LangGraph,LangChain 本身不擅长循环与分支 |
| 生产可观测性 | 需配合 LangSmith |
| 学习曲线 | 抽象层多,但 v1 已大幅收敛 |
三个原则:
- LangChain 管“构建块”,LangGraph 管“编排”,LangSmith 管“看见”——三者分工明确,不要混用。
- 锁版本:LangChain 迭代极快,不锁版本的代码三个月后可能跑不通。
- 密钥永不进代码:
.env+dotenv是最低要求。
二、生态全景:四个工具各管什么
这是最容易混淆的部分,必须先分清。
| 工具 | 角色 | 解决什么 | 什么时候用 |
|---|---|---|---|
| LangChain | 构建块 | 提示词、模型、工具、检索器的统一抽象与组合 | 原型开发、线性 RAG、简单工具调用 |
| LangGraph | 编排器 | 有状态、可循环、可分支的多步骤工作流 | 复杂 Agent、多智能体、需要重试与检查点 |
| LangSmith | 观察者 | 追踪、评估、监控 LLM 应用的每一次调用 | 调试、回归测试、生产质量跟踪 |
| LangFlow | 可视化构建器 | 拖拽式快速原型 | 团队协作、非编程人员 |
决策规则:先用 LangChain 起步;工作流出现循环/分支时迁移到 LangGraph;为可观测性加入 LangSmith。LangGraph 不是 LangChain 的替代品,而是上层编排层,节点内部仍然可以调用 LCEL 链。
三、入门:环境与最小 Agent
3.1 环境要求
LangChain.js 1.x 要求 Node.js 22+。
node -v
3.2 安装依赖(锁版本)
npm init -y
npm install langchain @langchain/openai zod dotenv
npm install -D typescript @types/node ts-node
| 包 | 作用 |
|---|---|
langchain |
框架本体:createAgent、中间件、消息类型 |
@langchain/openai |
OpenAI 模型适配层(换 DeepSeek 装对应包) |
zod |
工具参数 schema 定义 |
dotenv |
从 .env 加载密钥 |
tsconfig.json 关键配置:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"outDir": "./dist",
"rootDir": "./src",
"esModuleInterop": true
},
"include": ["src/**/*"]
}
3.3 密钥配置
.env 文件:
OPENAI_API_KEY=sk-xxxx
src/env.ts:
import dotenv from "dotenv";
dotenv.config();
export const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
if (!OPENAI_API_KEY) {
throw new Error("缺少 OPENAI_API_KEY,请检查 .env 文件");
}
密钥不进代码的三个理由:泄漏风险(Git 历史不可逆)、环境切换(测试/生产 Key 不同)、团队协作(同事拿到代码跑不起来)。
3.4 最小 Agent
createAgent 是 v1 的标准入口,替代了旧版的 createReactAgent 。
import { createAgent } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import { OPENAI_API_KEY } from "./env.js";
const model = new ChatOpenAI({
model: "gpt-4o-mini",
apiKey: OPENAI_API_KEY,
});
const agent = createAgent({
model,
tools: [],
systemPrompt: "你是一个简洁的助手,回答控制在两句话以内。",
});
async function main() {
const result = await agent.invoke({
messages: [{ role: "user", content: "用一句话解释什么是 TypeScript" }],
});
const lastMessage = result.messages[result.messages.length - 1];
const textBlock = lastMessage.contentBlocks?.find(
(block) => block.type === "text"
);
console.log(textBlock?.text ?? lastMessage.content);
}
main().catch(console.error);
createAgent 底层是一个基础 Agent 循环:调用模型 → 让模型选择工具 → 执行工具 → 再次调用模型,直到模型不再调用工具为止 。
四、核心抽象:Runnable 与 LCEL
4.1 Runnable 协议
LangChain 1.x 把 prompt 模板、聊天模型、检索器、输出解析器全部统一为 Runnable 。只要实现了 Runnable 契约,就能放进管道。
Runnable 的核心能力:
| 方法 | 作用 |
|---|---|
.invoke(input) |
单次调用 |
.stream(input) |
流式输出 |
.batch([inputs]) |
批量并行 |
.ainvoke() / .astream() |
异步版本 |
这套协议是框架运行时真正依赖的类型边界,不是文档约定。编排器拿到任何 Runnable,直接调 .invoke(),不需要判断“这是模型还是解析器” 。
4.2 LCEL 管道
LCEL 用 | 运算符把 Runnable 串联,返回一个 RunnableSequence(它本身也是 Runnable)。
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatOpenAI } from "@langchain/openai";
const prompt = ChatPromptTemplate.fromTemplate("用一句话解释:{topic}");
const model = new ChatOpenAI({ model: "gpt-4o-mini" });
const parser = new StringOutputParser();
const chain = prompt.pipe(model).pipe(parser);
const result = await chain.invoke({ topic: "量子纠缠" });
4.3 LCEL 的适用边界
LCEL 适合线性流水线:RAG、结构化抽取、简单工具调用。一旦需要“根据结果决定下一步、可能循环多次”,LCEL 就力不从心了——这是 LangGraph 的领域 。
五、Agent:从链到自主决策
5.1 Agent 的本质
Agent = 模型 + 工具 + 循环。模型决定调用哪个工具,拿到结果后决定下一步,直到任务完成。
import { createAgent, tool } from "langchain";
import { ChatOpenAI } from "@langchain/openai";
import * as z from "zod";
import { OPENAI_API_KEY } from "./env.js";
const model = new ChatOpenAI({ model: "gpt-4o-mini", apiKey: OPENAI_API_KEY });
const getWeather = tool(
async ({ city }: { city: string }) => {
const mockData: Record<string, string> = {
"北京": "晴,12~22°C",
"上海": "多云,15~20°C",
};
return mockData[city] ?? `暂无 ${city} 的天气数据`;
},
{
name: "get_weather",
description: "查询指定城市的当前天气情况。当用户询问天气时调用此工具。",
schema: z.object({
city: z.string().describe("城市名称,例如:北京、上海"),
}),
}
);
const agent = createAgent({
model,
tools: [getWeather],
systemPrompt: "你是一个天气助手。用户询问天气时,必须调用 get_weather 工具。",
});
const result = await agent.invoke({
messages: [{ role: "user", content: "北京今天天气怎么样?" }],
});
5.2 工具定义要点
- 用
tool()函数定义 - description 是给模型看的,必须清晰描述“什么时候调用”
- schema 用 Zod 定义,类型完整
createAgent的tools接受@tool函数、BaseTool实例、provider 内置工具 dict;不再接受ToolNode实例
5.3 模型配置
v1 的 createAgent 不再接受预绑定工具的模型:
// ❌ 不再支持
const modelWithTools = model.bindTools([someTool]);
createAgent({ model: modelWithTools, tools: [] });
// ✅ 正确
createAgent({ model, tools: [someTool] });
动态模型选择迁移到中间件实现 。
六、中间件:v1 的核心能力
中间件是 createAgent 的定义性特征,用于控制动态提示、对话总结、选择性工具访问、状态管理和护栏 。
6.1 预置中间件
| 中间件 | 作用 |
|---|---|
PIIMiddleware |
发送给模型前脱敏敏感信息 |
SummarizationMiddleware |
对话过长时压缩历史 |
HumanInTheLoopMiddleware |
敏感工具调用前要求人工审批 |
6.2 人在回路
import { createAgent, tool, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const deleteFile = tool(
async ({ path }: { path: string }) => `已删除:${path}`,
{
name: "delete_file",
description: "删除指定路径的文件。危险操作。",
schema: z.object({ path: z.string() }),
}
);
const agent = createAgent({
model,
tools: [deleteFile],
middleware: [
humanInTheLoopMiddleware({
interruptOn: { delete_file: true },
interruptMode: "per_call",
}),
],
checkpointer: new MemorySaver(),
});
interruptMode: "per_call" 让每次受控工具调用产生独立中断,便于精确审批。checkpointer 是必需的,用于保存中断状态以便恢复。
6.3 自定义中间件钩子
| 钩子 | 时机 | 用途 |
|---|---|---|
before_agent |
调用 Agent 前 | 加载记忆、校验输入 |
before_model |
每次 LLM 调用前 | 更新提示词、裁剪消息 |
wrap_model_call |
包裹 LLM 调用 | 拦截/修改请求响应 |
wrap_tool_call |
包裹工具调用 | 拦截/修改工具执行、处理错误 |
after_model |
每次 LLM 响应后 | 校验输出、应用护栏 |
after_agent |
Agent 完成后 | 保存结果、清理 |
七、RAG:检索增强生成
7.1 标准流程
加载文档 → 分割 chunk → 向量化 → 存入向量库 → 检索 → 拼入 prompt → 模型生成
7.2 最小 RAG 实现
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
const docs = [
"公司成立于 2020 年,总部位于北京。",
"主要产品有智能客服系统(9999 元/年)和数据分析平台(19999 元/年)。",
"联系电话:400-123-4567。",
];
const splitter = new RecursiveCharacterTextSplitter({
chunkSize: 200,
chunkOverlap: 20,
});
const chunks = await splitter.createDocuments(docs);
const vectorStore = await MemoryVectorStore.fromDocuments(
chunks,
new OpenAIEmbeddings({ model: "text-embedding-3-small" })
);
const retriever = vectorStore.asRetriever({ k: 2 });
const prompt = ChatPromptTemplate.fromTemplate(
"根据以下上下文回答问题:\n{context}\n\n问题:{question}"
);
const ragChain = prompt.pipe(new ChatOpenAI({ model: "gpt-4o-mini" })).pipe(new StringOutputParser());
const relevantDocs = await retriever.invoke("产品价格是多少");
const context = relevantDocs.map((d) => d.pageContent).join("\n");
const answer = await ragChain.invoke({ context, question: "产品价格是多少" });
7.3 向量库选型
| 库 | 特点 | 适用 |
|---|---|---|
| MemoryVectorStore | 内存、零依赖 | 原型、测试 |
| Chroma | 轻量、可持久化 | 小规模生产 |
| Pinecone / Weaviate | 托管服务、可扩展 | 大规模生产 |
八、生产化:观测、评估与部署
8.1 LangSmith 追踪
LangChain 通过 LangSmith SDK 发出 OTel 兼容的 span 。用 traceable 包裹函数即可追踪:
import { traceable } from "langsmith/traceable";
const tracedChain = traceable(
async (question: string) => {
return await chain.invoke({ topic: question });
},
{ name: "my-chain" }
);
8.2 生产化检查项
| 项 | 做法 |
|---|---|
| 密钥管理 | .env + 环境变量,绝不进代码 |
| 版本锁定 | package-lock.json 提交,精确到 patch |
| 超时与重试 | 模型调用设 timeout,工具调用设 retry |
| 成本控制 | 监控 token 消耗,动态模型选择 |
| 错误处理 | 用 wrap_tool_call 中间件统一处理工具错误 |
| 可观测性 | LangSmith 或自建 tracing |
8.3 标准消息内容
v1 引入 message.contentBlocks,将不同 provider 的响应统一为标准类型(text、reasoning、tool_call、image 等)。这是“写一次,跑在任何 provider 上”的关键:
for (const block of message.contentBlocks) {
if (block.type === "text") console.log(block.text);
if (block.type === "reasoning") console.log(block.reasoning);
}
旧版 .content 保持向后兼容。
九、版本迁移要点(v0 → v1)
| 变化 | 处理 |
|---|---|
createAgent 替代旧入口 |
从 langchain 导入 createAgent |
| state 只支持 TypedDict | Pydantic/dataclass 改为 TypedDict,校验放中间件 |
| 不再接受预绑定模型 | 模型和工具分开传 |
| 工具不再接受 ToolNode | 用 tool() 函数或 BaseTool |
| 结构化输出在主循环生成 | 不再单独节点,降低成本与延迟 |
| Python 3.9 支持移除 | Node 端对应 Node 18 移除,升级到 Node 22+ |
.text() 方法改为属性 |
message.text 替代 message.text() |
十、方案对比与选型
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| LangChain | 抽象统一、生态丰富、上手快 | 复杂编排弱、抽象层多 | 原型、线性 RAG、简单 Agent |
| LangGraph | 循环/分支/状态/检查点 | 学习曲线陡 | 复杂 Agent、多智能体、生产编排 |
| LlamaIndex | RAG 专精、检索策略丰富 | Agent 编排弱于 LangGraph | 检索密集型应用 |
| 直接调用 SDK | 零抽象、完全可控 | 无统一接口、重复代码 | 极简场景、单一模型 |
决策建议:先用 LangChain 快速验证;工作流复杂化后迁移到 LangGraph;RAG 需求重时评估 LlamaIndex。
十一、常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
createAgent 报错“不支持预绑定模型” |
传了 model.bindTools() |
模型和工具分开传 |
| 工具调用失败 | description 不清晰或 schema 缺类型 | 完善 description 和 Zod schema |
| 中断后无法恢复 | 未配置 checkpointer | 加 MemorySaver 或其他 checkpointer |
| 换模型后解析出错 | 直接读 .content 原生格式 |
改用 contentBlocks |
| 构建产物跑不起来 | 未锁版本或 Node 版本过低 | 锁版本、Node 22+ |
| RAG 检索不准 | chunk 过大或 k 值不合理 | 调小 chunkSize,调整 k |
| 工具错误导致 Agent 崩溃 | 未处理工具异常 | 用 wrap_tool_call 中间件捕获 |
十二、FAQ
Q1:LangChain 和 LangGraph 怎么选?
线性流水线用 LangChain(LCEL + createAgent);需要循环、分支、持久化状态、多智能体协作用 LangGraph。两者是叠加关系,不是替代 。
Q2:为什么 createAgent 不接受预绑定模型?
为了更好地支持结构化输出和动态模型选择。v1 要求模型和工具分开传入,动态选择通过中间件实现 。
Q3:中间件和传统 hook 有什么区别?
中间件是可组合的抽象,可以在 Agent 执行的每个阶段插入逻辑,且可以叠加多个。旧版 hook 是单点拦截,扩展性差 。
Q4:contentBlocks 和 .content 有什么区别?
.content 返回 provider 原生格式,换模型可能要改解析代码。contentBlocks 返回标准化的块数组(text/reasoning/tool_call 等),跨 provider 一致 。
Q5:如何控制 Agent 的成本?
用 SummarizationMiddleware 压缩长对话,用动态模型选择中间件让简单任务走小模型,监控 token 消耗。
Q6:人在回路怎么实现?
用 HumanInTheLoopMiddleware,配置 interruptOn 指定哪些工具需要审批,checkpointer 保存中断状态,恢复时传审批结果。
Q7:工具 description 有多重要?
极其重要。模型靠 description 决定是否调用工具、如何传参。写“查询天气”和写“当用户询问天气、温度、气候时调用此工具,参数为城市名”,效果差别很大。
Q8:RAG 的 chunkSize 怎么定?
没有通用答案。从 500~1000 字符起步,根据检索效果调整。块太大检索不精准,太小丢失上下文。
附录:项目结构
推荐项目结构
src/
├─ env.ts 环境变量加载
├─ agents/
│ ├─ weather.ts 天气 Agent
│ └─ file-manager.ts 文件管理 Agent(带 HITL)
├─ tools/
│ ├─ weather.ts 工具定义
│ └─ file.ts
├─ rag/
│ ├─ index.ts RAG 链
│ └─ retriever.ts 检索器配置
└─ index.ts 入口