Nodejs也能写Agent - 10.LangChain篇 - 初识LangChain

我尝试了很多其它开发 Agent 的 Node.js 技术栈,兜兜转转一圈下来,我发现似乎只有 LangChain.js、LangGraph.js 才是最终的归途。 Mastra 很好,如果只是做独立的 Agent 确实不错,但如果要集成到自己的系统里面去,我觉得 Mastra 提前内置的那一堆东西会显得很臃肿,而且它自己就是一个独立的 Node.js 服务,很难塞进你已有的后端服务里面。LangChain.js、LangGraph.js 的自由度很高,意味着你可以攒出高度自定义的 AI 逻辑,还能方便地集成进自己的系统,所以这一期我们就来正式入门 LangChain.js、LangGraph.js。

这个系列我想解决的其实是一个很朴素的问题:只要你会 Node.js,就能自己写出一个 Agent。不需要先去啃一遍 Python,也不需要另起一个孤立的服务,用你最熟悉的 TypeScript,把 Agent 直接长在你现有的项目里。

本篇是入门篇,先把地基打牢------搞清楚 LangChain 到底是什么、它的三个核心概念(Runnable / LCEL / Message),以及怎么跑起第一个例子。下一章我们再深入模型与 Prompt ,把 ChatOllamaPromptTemplate / ChatPromptTemplate 以及流式输出这些日常最常打交道的东西讲透。

简介

说白了,LangChain.js 就是现代 LLM 应用开发里的那层「胶水」------它把模型、Prompt、工具、检索器这些零件统一抽象成一个可组合的 Runnable,让你用声明式的管道(LCEL)去拼装复杂的 AI 流程,而不是自己手写一堆 imperative(命令式)的胶水脚本。

你可以把它想象成 AI 世界里的 Express 中间件:每个环节都长一个样子,插上去就能用,想换一个环节也不用动其它代码。

官方文档现在的入口在这里(后面章节我会经常带你去翻,因为版本迭代很快,以官网为准最靠谱):

环境准备

正式写代码前,先把环境铺好。整个系列我会尽量用**本地模型(Ollama)**做演示,这样你不用花一分钱、不用申请 API Key 就能全程跟着跑;等真要上线了,换成 OpenAI / Anthropic 也就是改一行代码的事(这正是 LangChain 的价值所在)。

bash 复制代码
# 1. 核心包 + 主包(Node.js 22+)
npm install langchain @langchain/core

# 2. 本地模型集成(我们这期用 Ollama)
npm install @langchain/ollama

# 3. 如果你想直接用云端模型,装对应的集成包即可,二选一
# npm install @langchain/openai
# npm install @langchain/anthropic

Ollama 本地跑一个模型也很简单:

bash 复制代码
# 安装 Ollama 后,拉一个中文效果不错的小模型
ollama pull qwen2.5:7b

LangChain 的一个核心设计:每个模型厂商是一个独立的集成包@langchain/ollama@langchain/openai......),核心抽象都在 @langchain/core。所以换模型 = 换包 + 换一行 new,业务逻辑一行都不用改。

核心

Runnable、LCEL、Message 是 LangChain 的三根顶梁柱。只要把这三个概念吃透,后面所有花里胡哨的东西你都能一眼看穿它的本质。

一切皆 Runnable

在 LangChain.js 里,几乎所有组件都实现了 Runnable 接口,对外提供一套统一的调用方式:

方法 用途
invoke(input) 单次调用,返回完整结果
stream(input) 流式调用,逐块(chunk)返回
batch(inputs) 批量调用多个输入

以下这些类型全都是 Runnable:ChatOllama(模型)、ChatPromptTemplate(模板)、StringOutputParser(解析器),甚至你自己写的一个普通函数,用 RunnableLambda 包一下也能变成 Runnable。

这个设计的意义在哪? 无论底层是 Ollama、OpenAI 还是你自定义的一段逻辑,上层代码永远用同一种姿势 invoke / stream 去调它。这意味着替换、测试、Mock 都变得极其简单------这正是我认为 LangChain 比其它框架更适合「集成进现有系统」的根本原因。

LCEL:把零件串成管道

LCEL 全称 LangChain Expression Language ,是官方推荐的组合方式。核心思路一句话就能讲清楚:把上一个 Runnable 的输出,喂给下一个 Runnable 当输入,像流水线一样一节一节接起来:

复制代码
输入 → Prompt → Model → OutputParser → 输出

等价于函数式编程里的管道:outputParser(model(prompt(input))),只不过写起来顺眼得多。

在 JS 里,我们用 .pipe() 方法把它们串起来:

typescript 复制代码
const chain = prompt.pipe(model).pipe(outputParser);

⚠️ 这是一个大坑,很多从 Python 转过来的人会栽在这 : Python 版 LangChain 可以用 | 运算符写成 prompt | model | parser,非常优雅。但 JavaScript 不支持运算符重载 ,所以在 JS 里你不能prompt | model | parser| 在 JS 里是按位或,含义完全不同)。 JS 里请一律使用 .pipe() 记住这一点能帮你省下半小时的 debug。

和旧式 Chain 的区别

LangChain 早期提供过 LLMChainConversationChain 这些类,现在已经废弃了。新代码请一律用 LCEL:

typescript 复制代码
// 旧写法(已废弃,勿用)
// const chain = new LLMChain({ llm, prompt });

// 新写法(LCEL,JS 只能这么写)
const chain = prompt.pipe(model).pipe(outputParser);

用 LCEL 有什么好处?我总结下来是「白嫖」了一堆能力:每个环节可以独立测试;整条链自动支持流式(stream)、批量(batch)、异步;还天然接入 LangSmith 做可观测/追踪;再加上 TypeScript 的类型推断,前后环节的类型对不对,编辑器直接给你标红。

RunnableSequence 显式写法

除了 .pipe(),你还可以用 RunnableSequence.from 把顺序显式声明出来:

typescript 复制代码
import { RunnableSequence } from "@langchain/core/runnables";

const chain = RunnableSequence.from([prompt, model, outputParser]);

两种写法完全等价。.pipe() 更简洁,日常首选;RunnableSequence.from 则适合动态组装链路的场景(比如根据配置在数组里增删环节)。

Message 类型

对话模型(比如 ChatOllama)的输入和输出都不是纯字符串,而是一组 Message 对象数组。LangChain 定义了四种最常用的 Message:

类型 角色 典型用途
SystemMessage 系统 设定 AI 的角色、行为规则
HumanMessage 用户 用户的提问或指令
AIMessage 助手 模型的回复;Tool calling 时会带上 tool_calls
ToolMessage 工具 工具执行的结果,回传给模型

一次「多轮对话 + Tool calling」的完整消息流大概长这样:

scss 复制代码
SystemMessage → HumanMessage → AIMessage(tool_calls) → ToolMessage → AIMessage(最终回复)

这张图你现在可能还没完全体会到它的分量,但我先剧透一句:下一章的 Agent,本质上就是围绕这条消息流反复循环------模型说要调工具,我们执行工具、把结果塞回去,再让模型接着说,直到它给出最终答案。 Message 就是这一切的载体。

而且 Message 是 LangChain 和 OpenAI / Ollama 等各家 API 之间的标准中间格式,换模型的时候你的业务逻辑完全不用动。

与 Python 版 LangChain 的差异简述

如果你之前学过 Python 版的 LangChain,那恭喜,概念是完全通用的,主要差异集中在包结构类型系统上:

维度 Python JavaScript / TypeScript
包名 langchainlangchain_community @langchain/core@langchain/ollama
LCEL 语法 `prompt model
Schema 验证 Pydantic zod
类型 运行时 duck typing 编译期 TypeScript 类型

invoke / stream 的语义两边是一致的,所以学会 JS 版后你也能快速看懂 Python 项目,反之亦然。唯一要反复提醒自己的还是那句话:JS 里没有 |,用 .pipe()

代码示例

示例 1:最简 LCEL 管道

typescript 复制代码
import { ChatOllama } from "@langchain/ollama";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

// 1. 定义 Prompt 模板
const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你是一个简洁的技术助手,回答控制在三句话以内。"],
  ["human", "{question}"],
]);

// 2. 实例化本地模型
const model = new ChatOllama({
  model: "qwen2.5:7b",
  temperature: 0.3,
});

// 3. 输出解析器:从 AIMessage 中提取纯文本 content
const outputParser = new StringOutputParser();

// 4. LCEL 组合:注意 JS 用 .pipe(),不是 |
const chain = prompt.pipe(model).pipe(outputParser);

// 5. 调用
const answer = await chain.invoke({ question: "什么是 Runnable?" });
console.log(answer); // 纯字符串,而非 AIMessage 对象

示例 2:多 Message 对话

直接使用 Message 数组,适合需要精细控制对话历史的场景:

typescript 复制代码
import { ChatOllama } from "@langchain/ollama";
import {
  SystemMessage,
  HumanMessage,
  AIMessage,
} from "@langchain/core/messages";

const model = new ChatOllama({ model: "qwen2.5:7b" });

// 构造多轮对话上下文
const messages = [
  new SystemMessage("你是一位 Node.js 导师,用中文回答,举例时用 TypeScript。"),
  new HumanMessage("Promise 和 async/await 有什么区别?"),
  new AIMessage("Promise 是异步操作的容器,async/await 是基于 Promise 的语法糖......"),
  new HumanMessage("能给我一个 async/await 的代码例子吗?"),
];

const response = await model.invoke(messages);
console.log(response.content);

ChatPromptTemplate 适合模板化输入;直接传 Message 数组则适合 Agent、多轮记忆这类需要动态追加消息的场景。

到这里,你已经能跑起一条最基础的「一问一答」链路了。至于怎么把模型调得更听话(temperaturebaseUrl 等参数)、PromptTemplateChatPromptTemplate 到底怎么选、以及怎么做打字机式的流式输出------这些都是下一章「模型与 Prompt」的主场,我们下篇细聊。

相关推荐
HjhIron3 小时前
Node.js 内置模块之 path 与 fs:从路径拼接到异步演进
node.js·promise
触底反弹3 小时前
🚀 Node.js path 和 fs 模块,从回调地狱到 async/await 的进化之路!
javascript·后端·node.js
sugar__salt3 小时前
Node.js path 与 fs 核心模块完全指南 —— 从路径处理到异步流程控制
javascript·node.js
屑曦晨4 小时前
npm 全局包迁移指南:从系统目录到用户目录
npm·node.js
moMo5 小时前
Node.js 文件操作:path 路径与 fs 异步流程
node.js
拾年2755 小时前
Node.js 异步进化论:从 path 路径拼接到 fs 文件读取,我终于理解了回调地狱的救赎之路
前端·node.js
小粉粉hhh6 小时前
Node.js(五)——编写接口
前端·javascript·node.js
先吃饱再说14 小时前
别再手动拼接路径了!Node.js path 模块的 9 个核心 API 详解
node.js
烬羽1 天前
还在手动拼路径、写回调地狱?一文吃透 Node.js 的 path 和 fs
javascript·程序员·node.js