
LangChain.js 是面向 JavaScript 和 TypeScript 生态的 AI 应用开发框架。在实际项目里直接用 LLM API 可以跑通一次对话但一旦涉及提示词模板、多轮上下文、外部工具调用、RAG 检索这些环节代码就会迅速变得难以维护。LangChain.js 把这些常见能力封装成可组合的模块让开发者把精力放在业务逻辑而不是重复拼装请求。这篇文章适合对 Node.js 有一定基础、想进入 AI 应用开发但还没系统接触过框架的读者。读完你会发现LangChain.js 解决的不是“调用模型”这一件事而是“如何把模型调用组织成一个完整应用”这件事。1. LangChain.js 到底解决什么问题面向 AI 应用开发的组合层1.1 直接调用模型 API 为什么不够假设你已经用 npm 安装过 OpenAI 或其他大模型 SDK并且成功发送过一次请求。这种方式的优点是简单缺点是当需求复杂以后项目会变得很脆弱。举一个真实场景你做了一个问答机器人刚开始只需要把用户问题发给模型然后把返回文本显示到页面。这时候直接调用模型 API 没有问题。到了第二个迭代你发现模型不具备某个实时数据需要让它先查询订单系统于是你开始手动拼接函数定义、解析模型返回的 tool_calls、再执行本地函数、再把结果塞回消息列表。到了第三个迭代产品要求机器人记住用户会话你又得自己维护历史消息数组、切分长上下文、处理截断。这些问题不是某个模型特有的而是所有 LLM 应用都会遇到。LangChain.js 的定位就是把“与模型对话”“组织提示词”“管理上下文”“调用工具”“检索资料”这些通用动作抽象成标准化接口让业务代码不直接散落在各种 API 调用里。1.2 LangChain.js 核心抽象速览在进入代码之前先用一张表建立全局认识。后面每一节都会对应到其中一两个抽象。抽象概念作用典型场景ChatModels封装大模型对话接口普通问答、聊天机器人Messages表示系统、用户、助手等不同角色的消息对话历史、多轮交互Prompt Templates复用提示词模板动态填入变量固定指令、带参数生成的提问Runnable可执行单元支持链式组合把提示词、模型、输出解析串起来Memory / Message History保存和加载历史消息多轮上下文、会话管理Tools给模型提供外部能力查询天气、查数据库、做计算Agents由模型决定何时调用哪些工具复杂任务拆解、多工具协作Retrievers从外部知识源检索相关内容私有知识库问答、RAG这些抽象之间的关系可以这样理解Message 是对话中流转的最小数据单元Prompt Templates 负责把输入变成消息序列ChatModel 接收消息并返回新的消息Runnable 是统一执行入口工具和记忆是模型之外的系统能力。1.3 和 Python 版 LangChain 有什么差异如果你看过 LangChain 的 Python 文档再来写 LangChain.js会发现两个版本的设计理念基本一致但细节并不完全对齐。Python 版历史悠久生态更全很多第三方集成文档优先给 Python 示例。JavaScript/TypeScript 版更适合前端、Node.js 后端和全栈团队。API 风格上也有一点差异Python 版有很多类方法装饰器而 JS 版更依赖配置对象和函数式组合。另一个容易踩的点是包名。LangChain.js 在版本演进中拆分出了多个 npm 包。现在常见的包包括langchain、langchain/core、langchain/openai、langchain/community等。官方文档推荐按需安装不要只装一个大包。后面环境准备部分会给出一个最少依赖组合。注意LangChain.js 的版本更新很快小版本升级时可能会调整类的导出路径。看文档时要先确认文档版本和你安装的 npm 版本一致否则容易出现“文档能跑、本地报错”的情况。2. 搭建环境版本、依赖和最小可运行代码2.1 环境要求LangChain.js 是纯 JavaScript/TypeScript 库在 Node.js 环境中运行。当前发布的版本通常要求 Node.js 18 或更高具体要求以你安装版本的package.json中engines字段为准。建议使用 Node.js 20 LTS 或者更高的 LTS 版本。新版本对 Web Stream、fetch、异步迭代的支持更稳定跑流式输出时问题更少。我的建议是先确认三样东西Node.js 版本node -vnpm 版本npm -v网络环境确保本机可以访问你选择的模型服务商 API 地址这里的学习目标很明确先跑通一个最简单的模型调用再逐渐加入提示词模板、多轮记忆和工具调用。2.2 创建项目并安装依赖打开终端创建一个新目录并初始化项目。mkdir langchain-demo cd langchain-demo npm init -y然后安装本教程需要的最小依赖。npm install langchain langchain/core langchain/openai dotenv如果后面要写工具调用还需要安装 zod用来描述工具入参的 JSON Schema。npm install zod依赖装完后检查一下项目根目录应该有node_modules、package.json、package-lock.json。2.3 配置 API Key在项目根目录创建.env文件写入模型服务商的密钥。OPENAI_API_KEY你的密钥注意不要把真实密钥提交到 Git。如果项目已经初始化了 Git建议把.env加入.gitignore。node_modules/ .env这里用 OpenAI 作为示例并不是唯一选择。你可以替换成其他模型服务商只需安装对应的langchain/xxx包并调整模型类名和参数。2.4 第一个最小调用在项目根目录创建demo-simple.js。import dotenv/config; import { ChatOpenAI } from langchain/openai; const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0, }); const response await model.invoke(请用一句话介绍 LangChain.js); console.log(response);运行命令node demo-simple.js成功时控制台不会直接打印纯文本而会打印一个消息对象。你会看到类似这样的结构AIMessage { content: LangChain.js 是用于构建大语言模型应用的 JavaScript 框架。, ... }这里有两个容易让人困惑的地方。第一invoke返回的不是字符串而是一个消息对象。真实内容在content字段里。如果你只需要文本可以写response.content。第二temperature是模型采样参数设为 0 时输出更稳定、更接近确定性结果。学习阶段建议先用 0后面再按场景调整。这段最小代码跑通后你已经完成了 LangChain.js 的“最小闭环”。接下来可以把重点放到它真正的设计核心可组合的 Runnable 接口。3. 核心抽象之一消息、模板和 LCEL3.1 消息类型与 role大模型 API 的对话请求本质上是一个消息数组。每条消息都有角色常见角色有系统、用户、助手。在 LangChain.js 中对应的是SystemMessage、HumanMessage、AIMessage。你可以直接创建它们import { SystemMessage, HumanMessage, AIMessage } from langchain/core/messages; const messages [ new SystemMessage(你是一个资深前端工程师), new HumanMessage(请解释什么是闭包), new AIMessage(闭包是函数与其词法环境的组合。), ];但实际开发中你很少手动初始化一整批消息。更常见的做法是使用提示词模板让代码把用户输入动态填充到消息里。3.2 ChatPromptTemplate 动态拼接模板直接拼接字符串很容易出错而且不方便维护。LangChain.js 提供了ChatPromptTemplate让你以声明式的方式描述消息结构。import dotenv/config; import { ChatOpenAI } from langchain/openai; import { ChatPromptTemplate } from langchain/core/prompts; const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0, }); const prompt ChatPromptTemplate.fromMessages([ [system, 你是一位耐心、严谨的技术导师擅长用通俗语言解释前端概念。], [human, {question}], ]); const chain prompt.pipe(model); const result await chain.invoke({ question: 什么是 Event Loop }); console.log(result.content);这段代码的关键在于prompt.pipe(model)。它不是把字符串传给模型而是把整个“模板管线和模型”组合成一个新的执行单元。调用chain.invoke时参数对象里的question会填充到模板的{question}位置生成一条 HumanMessage再交给模型。这样做有三个好处提示词和业务代码分离后续改动文案不需要动主逻辑。模板可以复用不同页面传入不同变量即可。可以方便地在管道中插入输出解析器、校验器、日志器等组件。3.3 LCEL 与 Runnable 接口LCEL 是 LangChain Expression Language 的缩写它定义了一种组合 Runnable 对象的方式。只要一个对象实现了 Runnable 接口它就能和另一个 Runnable 通过pipe连接。Runnable 接口提供了几个统一方法方法作用典型返回值invoke单次调用返回完整结果最终输出对象stream流式调用按块返回结果每个输出块batch批量调用传入数组结果数组bind绑定额外参数不立即执行新的 Runnable流式输出是生产环境很常用的能力。改造上面的例子const stream await chain.stream({ question: 用一句话解释 Promise }); for await (const chunk of stream) { process.stdout.write(chunk.content || ); }当模型开始回复时控制台会像打字机一样逐个块输出内容。它背后的原理是模型接口本身支持流式返回LangChain.js 把流拆成小块并保证不管是单个模型还是整条链对外都提供同样的stream方法。理解 LCEL 是理解 LangChain.js 的关键。后续无论你写多复杂的 AI 应用本质上都是在把各种能响应invoke的模块串起来。4. 核心抽象之二多轮对话状态的保存方法4.1 RunnableWithMessageHistory 的用法单轮对话没有上下文机器人每次都是“第一次见你”。要支持多轮对话必须把历史消息传给模型。LangChain.js 提供了RunnableWithMessageHistory它会在每次调用前后自动读取和保存历史。看一个完整示例import dotenv/config; import { ChatOpenAI } from langchain/openai; import { ChatPromptTemplate } from langchain/core/prompts; import { RunnableWithMessageHistory } from langchain/core/runnables; import { ChatMessageHistory } from langchain/memory; const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0, }); const prompt ChatPromptTemplate.fromMessages([ [system, 你是用中文回答的 AI 助手。], [placeholder, {chat_history}], [human, {question}], ]); const chain prompt.pipe(model); const withHistory new RunnableWithMessageHistory({ runnable: chain, getMessageHistory: () new ChatMessageHistory(), inputMessagesKey: question, historyMessagesKey: chat_history, }); const config { configurable: { sessionId: session-1 } }; const first await withHistory.invoke({ question: 我的名字是小明 }, config); console.log(第一次回答, first.content); const second await withHistory.invoke({ question: 我叫什么名字 }, config); console.log(第二次回答, second.content);如果一切正常第二次模型应该能回答出“小明”。原因是RunnableWithMessageHistory在拿到用户输入前先从消息历史里加载了之前的对话并把历史拼接到了{chat_history}占位符所在的位置。这里特别要注意inputMessagesKey和historyMessagesKey的区别inputMessagesKey指定当前用户输入在 prompt 输入对象中的字段名。historyMessagesKey指定历史消息应该填充到模板中的哪个变量名。4.2 内存、Redis 与数据库存储的取舍上面的getMessageHistory每次都返回新的ChatMessageHistory这意味着历史只存在内存里。进程一退出对话记录就丢了。生产环境通常需要把历史保存到 Redis 或数据库。LangChain.js 社区提供了多种实现也可以自己实现一个BaseChatMessageHistory子类把读写逻辑接到 Redis 或 MySQL。选型时要考虑几个问题存储方式优点缺点适用场景内存 Map实现简单、速度快进程重启丢失、多实例无法共享本地调试、原型演示Redis读写快、支持过期时间需要额外部署 Redis聊天机器人、在线客服MySQL/PostgreSQL持久化可靠、易查询每次读写延迟较高需要审计对话记录的场景实现自定义存储时至少要实现三个方法getMessages()返回历史消息数组addMessage()写入新消息clear()清空会话。这样 LangChain.js 就能在适当的时候自动调用它们。4.3 多轮对话的坑多轮对话看似简单实际项目里有几个非常常见的坑。第一历史消息无限增长。每次对话都把整段历史传给模型很快会超过上下文窗口。解决方式是控制历史条数比如只保留最近 10 条或者先计算 token 数再截断。第二历史消息数据格式不一致。从数据库读出来的可能是 JSON 字符串必须转换成HumanMessage、AIMessage等对象再传给模板。直接拼字符串会导致消息角色丢失。第三会话隔离失效。如果多个用户共用同一个sessionId会出现串话。生产环境一定要保证sessionId与登录用户、业务会话严格绑定不能使用全局固定值。5. 让模型学会使用工具tool calling 与 agent 初探5.1 工具调用的运行流程大模型本身不能执行代码、不能查数据库、不能调用外部接口。工具调用的思路是开发者先声明“有哪些工具、每个工具需要什么参数”模型根据用户问题决定是否调用、调用哪个、传什么参数最后开发者在本地执行工具并把结果返回模型。整个过程分成五个阶段用户发起提问。模型判断这个问题需要工具返回一个tool_calls结构。开发者在代码里找到对应工具并执行。把工具执行结果作为新的消息返回给模型。模型根据工具结果生成最终回答。5.2 自定义工具示例这里写一个严格校验过的计算器工具。强调“严格校验”是因为工具调用是本地代码执行通道如果模型可以任意传字符串并被当成代码执行风险很高。import { tool } from langchain/core/tools; import { z } from zod; const calculator tool( async ({ expression }) { const allowed /^[0-9\-*/(). ]$/.test(expression); if (!allowed) { return 表达式包含不允许的字符只允许数字和四则运算符号; } try { const result Function(use strict; return (${expression});)(); return String(result); } catch (error) { return 表达式计算失败; } }, { name: calculator, description: 计算四则运算表达式例如 (35)*2, schema: z.object({ expression: z.string(), }), } );这个示例里zod负责描述参数结构description给模型提供了判断依据。模型看不到工具函数体只能看到名字、描述和参数结构所以描述写得越清楚模型越不容易乱用。注意示例为了演示思路使用了Function。即使经过白名单校验也建议在生产环境中改用表达式解析器比如 mathjs不要直接执行代码。如果工具本身会操作文件、发请求、执行命令必须叠加权限控制、审计日志和超时机制。5.3 从手动循环到 Agent你先手动演示一次工具调用理解底层逻辑。import { ChatOpenAI } from langchain/openai; import { tool } from langchain/core/tools; const model new ChatOpenAI({ model: gpt-4o-mini, temperature: 0, }); const modelWithTools model.bindTools([calculator]); const response await modelWithTools.invoke(计算 (35)*2 的结果); if (response.tool_calls) { for (const call of response.tool_calls) { const matchedTool [calculator].find((item) item.name call.name); const output await matchedTool.invoke(call.args); console.log(${call.name} 返回, output); } }控制台会看到类似输出calculator 返回 16这只是手动的一轮工具调用。真实场景里模型可能需要连续调用多个工具第一次调用的结果会影响第二次调用。这种“循环判断、继续调用、直到收敛”的逻辑就是 Agent 的职责。LangChain.js 在版本演进中把 Agent 的执行逻辑逐步过渡到了 LangGraph.js 生态。不同版本对 Agent 的入口和用法差异较大动手前一定要看你当前安装版本的官方文档。先理解上面手动循环的逻辑再去看 Agent 抽象会更清楚它到底为你省了哪些代码。5.4 工具使用的安全边界工具是 AI 应用中最容易出现安全问题的部分。模型如果被诱导可能会尝试调用不恰当的工具。建议遵守四条规则给工具的输入做类型和业务校验不能只看 JSON 结构合法就执行。执行外部命令、写文件、发请求的工具要有独立权限体系使用最小权限账号。对工具的调用次数、调用参数、执行结果全部记录日志方便事后审计。给工具设置超时时间避免模型生成的参数导致工具长时间卡住。6. 影响输出质量的参数到底怎么调6.1 重要参数速查表模型不是只传提示词就够了。同一个模型参数不同输出风格和稳定度差异很大。参数常见范围作用调大时的影响调小时的影响temperature0 到 2控制随机性回答更发散、更有创造力回答更确定、更保守maxTokens视模型而定限制生成最大 token 数可输出更长内容防止超长和成本失控topP0 到 1核采样控制候选词范围允许更多候选词更集中更保守frequencyPenalty-2 到 2惩罚重复出现的词减少重复内容允许更多重复presencePenalty-2 到 2鼓励谈论新话题增加话题覆盖面更专注当前主题很多初学者认为 temperature 越大越好其实不是。写代码、做翻译、抽取结构化数据时通常应该用低 temperature甚至直接调成 0保证结果稳定。做文案创意、头脑风暴、故事生成时才适合把 temperature 调高。6.2 参数调错的现场举例说明。假设你写了一个代码解释工具temperature 设为 1.2结果同一个问题每次回答都不一样有时甚至出现错误示例。这是因为高随机性让模型每次都选择了不同路径。反过来你做一个诗歌生成器temperature 设为 0结果生成的内容风格固定缺少变化用户会觉得“太死板”。正确的做法是先明确任务类型事实类任务低 temperature低 topP。逻辑类任务低 temperature同时给出明确的输出格式。创意类任务高 temperature可以结合较高 topP。长文本任务关注 maxTokens避免后半段被截断。参数调整要一次只动一个变量观察变化后再调下一个。同时把输入、参数、输出记录下来形成自己的参数实验表而不是凭感觉乱调。7. 从报错日志定位问题的一条排查链路7.1 先按调用层次拆解LangChain.js 项目报错时不要直接去翻最底层代码。先按调用层次拆开。完整的调用链路是环境层Node 版本、npm 包、环境变量。请求层API key、模型名称、网络、超时。应用层提示词模板、工具逻辑、历史消息。前端接入层接口返回格式、流式处理。排查时从最外层开始先确认环境没问题再看请求最后看应用代码。这能避免在错误层里绕圈。7.2 常见异常清单错误现象常见原因检查方式处理建议401 UnauthorizedAPI key 无效或加载失败检查.env是否存在打印process.env.OPENAI_API_KEY重新配置密钥确认没有提交错误密钥404 Model Not Found模型名称写错或当前账号不可用查看服务商模型列表换成可用的模型名429 Too Many Requests触发限流或余额不足查看服务商控制台的配额增加重试降低并发检查余额请求超时输出过长或网络不稳定打印错误堆栈打开流式输出设置更合理的超时时间使用 streamCannot find module缺少依赖包检查node_modules和 package.json安装对应包确认导入路径content不是预期字符串模型返回了多段内容打印整个响应对象使用result.content或解析结构文档能跑本地报错版本不一致或导入路径变了查看node_modules中的实际导出以当前版本的官方文档为准7.3 排查清单遇到问题可以按下面清单逐一核对node -v是否满足要求。npm ls langchain langchain/core langchain/openai是否安装成功。代码开头是否执行了import dotenv/config。.env是否在项目根目录变量名是否一致。API key 是否有效服务商控制台是否能看到调用记录。模型名称是否真实存在是否拼写错误。把请求改成最简单的单模型调用排除提示词和工具问题。打印错误对象重点看error.message和error.status。如果是流式问题先改成非流式确认结果正确后再排查流式管道。查看包版本锁定文件确认依赖没有被意外升级到不兼容版本。8. 学习环境和生产环境应该差在哪里8.1 学习阶段的取舍学习 LangChain.js 时目标不是做生产级系统而是快速理解抽象概念。学习阶段可以这样做直接用.env保存 key先不过度设计密钥管理。历史消息先放内存跑通多轮对话逻辑。错误处理先打印完整堆栈方便看内部流程。只写一个文件减少项目结构干扰。这些取舍只是为了降低入门门槛。到了生产环境每一条都要反过来重新设计。8.2 生产环境需要补上的能力如果要把项目部署给真实用户至少要补上这些能力能力学习阶段是否必须生产环境要求日志可选必须有结构化日志记录请求、响应、耗时、token 数监控可选接入 LLM 调用监控关注错误率和延迟密钥管理可选使用环境变量或密钥管理服务禁止明文入库权限控制可选工具和接口都要最小权限回滚方案不需要模型配置、prompt、依赖版本都要能快速回滚成本控制不需要统计每用户、每接口的 token 消耗设置预算压力测试不需要模拟并发请求确认限流和降级策略安全策略不需要防提示注入敏感信息脱敏输出内容审核学习中跑通功能只算完成 20%。剩下 80% 是异常处理、性能、成本和稳定性。这也是很多简历上写着“熟悉 LangChain.js”的开发者在真实项目里依然手忙脚乱的原因。9. 最佳实践与 AI 应用开发学习路线建议9.1 直接能落地的工程规范把这些规则写进团队规范比记住某个 API 更重要。第一提示词必须走模板。不要把业务输入直接拼进系统提示词。使用ChatPromptTemplate并保留模板的历史版本。第二所有模型调用封装在独立模块。不要把ChatOpenAI实例散落到各个业务文件里。统一配置超时、重试、日志和 token 统计后续换模型时才不用改全项目。第三模型输出要做结构和业务校验。如果要模型返回 JSON不要直接JSON.parse。先校验内容是否为合法 JSON再校验关键字段是否齐全最后再进入业务逻辑。第四工具调用要受控。工具列表要明确参数要定义 schema执行前要校验执行后要记录日志。第五重视可测试性。把 prompt 构建、工具逻辑、输出解析拆成纯函数模型调用封在外面测试时 mock 模型返回就可以快速验证业务分支。9.2 建议的 AI 应用开发学习路线如果从零开始进入 AI 应用开发建议按这条路径推进理解 LLM 基本概念token、上下文窗口、temperature、system prompt。直接用模型 SDK 写一个最小对话程序先不碰框架。学习 LangChain.js 的 Message、PromptTemplate、Runnable。用模板加流式输出做一个翻译或总结小工具。加入 RAG用向量数据库做私有知识库问答。练习工具调用做一个能查天气、算数学、查库存的助手。理解 Agent 循环跑通一个多工具协作任务。补上工程能力日志、监控、限流、安全、成本控制。不要一上来就学 Agent。Agent 里包含了很多状态流转逻辑如果连单轮调用、流式输出、模板填充都不熟悉出了问题根本不知道是哪一环错的。9.3 关于“AI 应用开发工程师考证”的建议近期“AI 应用开发工程师”“AI 大模型应用开发”这类关键词热度很高也出现了不少培训机构和证书项目。这里需要冷静看待。目前 AI 应用开发并没有像传统行业那样必须持证上岗的硬性门槛。招聘团队更看重的是候选人能不能把一个 AI 功能做成稳定、可上线、可维护的产品。如果你看到一个证书课程先确认发证单位、课程内容、项目实战占比再判断是否值得投入。对大多数人来说性价比更高的路线是用两周时间把 LangChain.js 基础跑通再做一个包含模型调用、多轮对话、工具调用或 RAG 的完整项目放到 GitHub 并写清楚 README。这份真实代码的说服力通常比一张证书更直接。回到 LangChain.js 本身。它真正值得投入时间的地方是那套“消息、模板、Runnable、工具、记忆”的抽象模型。一旦掌握了这套组合思路你会发现所有 LLM 应用都长得很像接受输入组织上下文调用模型处理工具结果输出结构化内容。后面的路就是在工程化层面把每一环打磨得足够稳。