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 已大幅收敛

三个原则:

  1. LangChain 管“构建块”,LangGraph 管“编排”,LangSmith 管“看见”——三者分工明确,不要混用。
  2. 锁版本:LangChain 迭代极快,不锁版本的代码三个月后可能跑不通。
  3. 密钥永不进代码:.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            入口
← 返回帖子 更多AI内容 →