做了几年前端,页面写了不少,组件也封装了一堆,但说实话,很多时候都是在消费别人封装好的能力。真正让我想转AI开发的契机,是发现自己写业务代码时,技术栈的成长曲线已经拉平了。AI应用开发不一样,它需要你把模型能力、提示词管理、工具调用、上下文记忆这些环节自己串起来,这恰好是前端工程师不太陌生、又需要重新学习的部分。这个系列的第一章,我决定直接从LangChain入手——前端转AI,第一步不是去啃深度学习,而是先把一个能回答问题的模型接进自己的Node.js项目里,让流程先跑起来。这一章的内容,适合有JS/TS基础、没接触过AI框架、想快速做出第一个AI原型的开发者。
1. 为什么前端开发者要折腾 LangChain
1.1 前端的积累到底哪里能用得上
很多前端朋友一提到AI,第一反应是要学Python、要懂矩阵、要看论文,还没开始就劝退了自己。实际上做AI应用开发,每天打交道最多的是数据格式转换、接口调用、状态管理、异步流程控制——这些恰恰是前端日常工作的核心能力。
以调用大模型为例,本质上就是发一个请求过去、拿到一段文本回来,这不就是一次异步数据获取吗?前端写惯了 fetch,对这个流程毫无压力。真正的门槛在别处:当你需要多次调用模型、需要把上一轮的对话内容拼进下一次请求、需要让模型在回答问题的同时去查资料或者执行工具,这些"组合动作"才是AI开发的难点。
LangChain解决的就是这个组合问题。它不给模型增加任何能力,而是给开发者提供了一套标准的"组装流水线":Prompt模板、模型调用、输出解析、记忆管理、Agent工具调度,每个环节都是可替换的积木。前端工程师对组件化组合太熟了,这套思维方式迁移过来非常顺。
1.2 裸调模型接口和用LangChain有什么区别
最原始的调用方式是这样的:你写一个函数,把用户说的话拼成一个请求体,调模型接口,拿回结果返回给页面。看起来没问题,但用着用着就会遇到一堆重复劳动。
比如第二版你想给AI加一点系统人设,那每次请求的body里都要多拼一段固定的角色描述;第三版你想让模型记住上下文,你得自己维护一个数组,把历史消息手动塞进去;第四版你想让模型能调用外部工具查天气,你就得自己写调度逻辑,判断模型什么时候返回工具调用、什么时候返回普通文本。每一版都在"造轮子+改轮子"。
LangChain的做法是:把每一次和模型的交互抽象成一条"链"。链上有模板节点、模型节点、输出解析节点,数据从左往右流。你配好一次,后面换人设、换模型、加记忆,都只需要替换链上的某个节点,不用把整段逻辑推倒重来。
生活化类比就是:裸调API相当于你想吃一顿饭,每次从买菜、洗菜、切菜、开火做起;LangChain则是一间配置好的厨房,冰箱里有处理好的半成品,灶台有定时的火候,你只需要把菜按顺序下锅。至于这顿饭是炒饭还是炖汤(不同场景、不同模型),换菜谱就行,不用换厨房。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手之前,先把这几个概念吃透
2.1 本地环境准备:一个Node.js项目就够了
接入LangChain不需要独立的后端服务,一个普通的Node.js项目就能跑。前端开发者日常肯定装过Node,版本建议在18以上,我本机用的是Node 20,整个实践过程很顺畅。
项目初始化的命令很简单:
bash复制mkdir langchain-demo
cd langchain-demo
npm init -y
然后安装核心依赖:
bash复制npm install langchain @langchain/core dotenv
这里有两个容易忽略的点需要提前说明。
第一,LangChain的JavaScript版本和Python版本是两套独立实现,API有不少区别。网上搜到的教程很多是Python版,直接照搬到JS里会报各种奇怪的错,下面遇到的坑我一并记录。
第二,@langchain/core 是LangChain 0.2版本开始拆出来的核心库。早期版本所有东西都集中在 langchain 包里,现在拆包之后,PromptTemplate、OutputParser 这类基础组件都在 @langchain/core 里。两个包都装上,后面按示例导入才不会缺东西。
2.2 五个绕不开的核心抽象
LangChain 的概念体系不算多,核心就集中在五个词上:模型、提示词模板、链、记忆、代理。
先看模型(Model)。这是整个链路的"发动机"。LangChain 本身不含模型能力,它通过适配器对接各种大模型服务商。对前端来说,模型就是接口那头的黑盒,你传字符串进prompt,它返回字符串。LangChain只是把它包装成了统一的调用接口,后续你切换服务商,业务代码不需要大改。
再看提示词模板(Prompt Template)。这是把固定文案和动态变量分离的工具。比如你每次问模型都想要它扮演一个"技术面试官",固定部分就是角色设定,动态部分是你的问题。模板负责把这两段拼成最终发出去的prompt,和前端模板字符串里 ${name} 的替换逻辑是同一回事。
然后是链(Chain)。链是一系列处理步骤的串接,输入进链,经过模板、经过模型、经过解析器,最后输出结果。在0.2版本里,链的构造方式从旧的类继承风格改成了 pipe 管道风格,用起来更直观。
记忆(Memory)解决的是多轮对话的上下文问题。模型本身没记性,上一轮说了什么,这一轮它全忘了。记忆模块负责存历史消息、在构造prompt时把历史拼回去,让模型看起来"记得"你们聊过什么。
代理(Agent)是更高级的玩法。它让模型不只是"回答问题",而是能根据问题内容决定要不要调用外部工具。比如用户问"现在几点",模型可以调用时间工具拿到结果,再组织语言回答。第一章先不深入,知道它的定位就够了。
2.3 前端思维到LangChain思维的一张对照表
接触新框架时,我最喜欢做的一件事是把新概念映射到已有经验上。前端工程师理解LangChain,用这张表格就够了:
| 前端概念 | LangChain 对应概念 | 关系说明 |
|---|---|---|
| React/Vue 组件 | 链上的节点 | 每个节点只干一件事,节点之间传数据 |
| props 传参 | template 里的变量注入 | 数据从外部流入模板,渲染出完整prompt |
| 状态管理(Pinia/Redux) | Memory 模块 | 跨轮次保留对话信息 |
| 事件回调 | Tool/Agent 回调 | 满足条件时触发额外动作 |
| 异步函数 async/await | 链的 invoke 调用 | 模型调用是异步的,等待返回后再继续 |
这张对照表的意义在于,你不必觉得自己是零基础进入一个新领域。前端工作里天天在练的组合、传参、状态同步,到了LangChain里全部用得上,只是换了套名词和API。
3. 第一个能跑的程序:让模型说句话
3.1 环境变量:API Key不放代码里
按照第二章的步骤准备好项目后,接下来要配置大模型服务商的信息。这里有个必须养成的工程习惯——密钥永远不要写死在代码里。
新建一个 .env 文件:
bash复制MODEL_API_KEY=你的密钥
MODEL_NAME=你的模型名称
然后在入口文件最顶部加上环境变量加载。我用 dotenv 来做这个事:
javascript复制import dotenv from "dotenv";
dotenv.config();
这里有个前端转AI最容易栽的坑:dotenv.config() 必须在你读取任何环境变量之前调用。JS模块里如果顶部还导入了其他模块,而这些模块在初始化时就访问了 process.env.MODEL_API_KEY,顺序错了拿到的就是 undefined。下面的章节我会专门展开讲。
3.2 最小可运行脚本:用本地模拟模型跑通链路
为了让新手不被服务商配置干扰,我决定先用一个不需要密钥的本地模拟模型,把完整链路跑通。LangChain 包里内置了测试用的 FakeListChatModel,它会按照预设的列表返回答案,非常适合验证代码逻辑。
新建 index.js,输入以下内容:
javascript复制import dotenv from "dotenv";
dotenv.config();
import { FakeListChatModel } from "langchain/chat_models/fake";
import { PromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";
const model = new FakeListChatModel({
responses: ["我是模拟模型返回的第一条回答"],
});
const prompt = PromptTemplate.fromTemplate("用户说:{input}");
const parser = new StringOutputParser();
const chain = prompt.pipe(model).pipe(parser);
const response = await chain.invoke({ input: "你好,请做个自我介绍" });
console.log(response);
在 package.json 里加上 "type": "module",然后运行:
bash复制node index.js
终端会打印出 我是模拟模型返回的第一条回答。
整个脚本把LangChain的三大核心组件都用上了:
PromptTemplate.fromTemplate定义了一条模板,里面的{input}是动态插槽。FakeListChatModel扮演模型角色,处理prompt并输出结果。StringOutputParser把模型的原始输出转成纯文本字符串。pipe把它们串联成一条链,数据像自来水一样从左流到右。
也许有读者会问:直接手写 "用户说:" + input 不就行了,何必用模板?这里的关键在于,模板不仅管拼接,它还承担着"约定输入结构"的作用。当链复杂起来之后,模板的变量清单就等于这条链对外暴露的接口文档,后面接入Agent、接入记忆时,这个约定能避免很多传参问题。
3.3 把真实模型替换进来
模拟模型跑通后,把真实模型替换进来的步骤,思路是固定的:
第一步,去你选定的模型服务商平台申请API Key,拿到模型名称字符串。
第二步,在项目里安装对应的适配包。LangChain官方把每家服务商封装成了独立的包,命名规律一般是 @langchain/服务商缩写。打开你所用服务商的LangChain集成文档,找到安装命令装到项目里即可。
第三步,把代码里的 FakeListChatModel 换成对应服务商的模型类,构造参数里的 responses 替换成 apiKey 和 model,环境变量正好用上。
javascript复制import { ChatYourProvider } from "@langchain/你的服务商适配包";
const model = new ChatYourProvider({
apiKey: process.env.MODEL_API_KEY,
model: process.env.MODEL_NAME,
});
推荐第一次用真实模型时,也先跑这个最简单的链,确认模型能正常返回,再去叠加其他功能。这样做的好处是出错范围小,密钥配错了、模型名写错了、适配包装错了,都会立刻暴露在只涉及这一环节的报错里,不用在几十行链路代码里做排查。
3.4 链路的数据流,拆开看更清晰
这段最小代码值得从"数据流"的角度再品一遍。
用户输入进 invoke 时是一个对象 { input: "你好" },这个对象就是链的输入契约。它首先被传给 PromptTemplate,模板把对象里的 input 值填进 {input} 插槽,产出一段完整的文本。接着文本传给模型,模型返回原始输出。最后 StringOutputParser 把输出清洗成字符串,返回给调用方。
要注意的是,每一处节点只做自己该做的事。模板不关心模型是什么,模型不关心输出怎么解析,解析器不关心prompt怎么生成的。这种职责单一的设计,让替换和扩展都变得极简单:今天用模拟模型,明天换真实模型,后天真要加一个输出格式校验,也只是在链路上插一个新节点的事。
4. 进阶一点:组装一个带记忆的问答机器人
4.1 场景需求拆解
第一段代码跑通后,可以挑战一个小场景:做一个能连续对话的问答机器人。要求是它能记住你上一轮说过的话,不会出现"你刚才才告诉我名字,下一轮又问你是谁"的尴尬。
直接裸调模型你会发现,多轮对话的逻辑需要自己维护:准备一个数组存历史消息,每次请求前把历史拼接进messages,请求后再把最新一轮追加进数组。这个逻辑写起来不难,但工程上很重复,换项目又得重写。LangChain 提供了标准的记忆模块,只需一行很短的配置就能让链自动拼接历史。
4.2 Prompt Template + Memory + Chain 的组合写法
这里我用偏传统但直观的 ConversationChain 写法,初学者一眼就能看懂记忆是怎么介入的:
javascript复制import dotenv from "dotenv";
dotenv.config();
import { FakeListChatModel } from "langchain/chat_models/fake";
import { BufferMemory } from "langchain/memory";
import { ConversationChain } from "langchain/chains";
const model = new FakeListChatModel({
responses: ["第一轮回复", "第二轮回复"],
});
const memory = new BufferMemory();
const chain = new ConversationChain({ llm: model, memory: memory });
const response1 = await chain.run("我的名字是小明");
console.log(response1);
const response2 = await chain.run("我叫什么名字?");
console.log(response2);
运行之后,第二次问题虽然模型是模拟的,但从传入 model 的prompt里就能看到完整的历史被拼了进去。
关键在这一行:new BufferMemory()。它默认会把所有历史对话按 Human: ...\nAI: ... 的格式累积在内部,然后每次调用链时把这段历史自动拼到prompt里。ConversationChain 内部解决了两件事:负责调用模型、负责注入记忆。这就是为什么用裸调API时,你要手动做的那两步拼接,在这里全部被框架接管了。
用真实模型替换 FakeListChatModel 后,试一下连续对话,你会明显感觉到模型的"记性"变好了。对话内容自己给自己挖个坑再填坑,比如先告诉它"我最喜欢的技术是TypeScript",绕几轮话题后问它"我喜欢的语言是什么",它能从历史里找到答案。
4.3 记忆的边界和容易忽略的细节
这个示例里有一个不容易注意到的细节:ConversationChain 的默认 prompt 模板里带了 {history} 插槽和 {input} 插槽,BufferMemory 注入的是 history 变量。如果你自定义了模板,却忘了在模板中声明 {history},你会惊讶地发现记忆不生效。这不是记忆模块坏了,而是你没有给记忆留出口。
另一个细节是 BufferMemory 不裁剪历史。对话超过几十轮后,每次请求携带的prompt会越来越长,既增加费用又可能超出模型输入上限。真实场景里建议用裁剪策略,比如只保留最近N轮对话,LangChain提供了一些带窗口的Memory实现,后续章节可以专项展开。
记忆模块的核心职责可以概括为一句话:它在链的运行前后自动从上下文对象里读取历史、更新历史。理解这一点,你就掌握了大部分LangChain组件的协作方式——它们本质上都是在这个"上下文"里读写数据。
5. 前端转AI最容易踩的坑,我替你趟过一遍
5.1 模型服务商识别失败、API Key无效
现象:安装适配包后,实例化模型类报错,或者调用时报401。原因通常是以下两种之一:适配包装错了包名,或API Key没正确加载。
排查顺序建议:
- 检查是否安装了对应的适配包。
- 打印
process.env.MODEL_API_KEY是否跟.env里一致,如果显示undefined,问题出在环境变量加载。 - 确认key本身没过期,以及模型名称字符串是否和文档一致。模型名称填错时,报错信息里通常会有 "model not found" 之类的关键词。
5.2 dotenv加载顺序的坑
前端写习惯后,习惯性把所有 import 放在文件最顶部,dotenv.config() 也顺理成章放在所有导入之后。可当某个模块在导入阶段就读取了环境变量,这时候执行顺序已经晚了,读取到的值自然是 undefined。
JavaScrip 模块的 import 有提升机制,所有导入都会先于普通代码执行。稳妥的写法是:把 dotenv.config() 放进一个单独的文件,比如 env.js,然后在入口文件第一行先导入它:
javascript复制import "./env.js";
import { ChatYourProvider } from "@langchain/你的服务商适配包";
这样能保证任何模块在读取环境变量前,配置已经加载完毕。
5.3 LangChain 版本之间的兼容问题
LangChain JS 迭代速度很快,0.1到0.2之间做了大量拆包,很多类名的导入路径都变了。网上教程和AI工具生成的代码,经常带着几个月前的写法,装的是最新版包,于是出现 "module not found"。
遇到这种报错,先别急着怀疑自己写错,去查一下对应模块在当前版本里的官方导入路径。比如早期直接从 langchain 导入 PromptTemplate,现在应该从 @langchain/core/prompts 导入。解决办法并不复杂:或者根据报错信息调整导入方式,或者把LangChain相关包的版本降到教程对应的版本。个人建议跟随新版本学习,毕竟后续Agent、RAG的很多特性依赖新版API。
5.4 异步思维没转换过来
前端写惯了 await fetch(),理论上不该在异步上栽跟头,但实际转化到LangChain时,还是经常看到两类错误。
第一类错误是忘了 await。链调用返回的是一个Promise,直接 console.log(chain.invoke(...)) 打出来的是 Promise { <pending> }。这个好解决,记住一条:所有的 invoke 都返回Promise。
第二类错误是试图用同步的方式拿结果,比如在非async函数里直接调用链。遇到这种情况,把外层函数声明成 async,内部正常 await 即可。动手写AI应用时可以把"链路调用都是异步"这句话贴在显示器旁边,能少踩一半的坑。
5.5 常见问题速查表
整理了一下这个阶段高频出现的问题,方便你对照:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 密钥读取为undefined | dotenv加载顺序错误 | 在入口文件最顶部导入env配置 |
| 模型调用报401 | API Key过期或错误 | 核对平台密钥和 process.env 输出 |
| module not found | 版本拆分后导入路径变化 | 按当前版本官方文档调整导入路径 |
| 链调用打印出Promise | 缺少await | 在async函数内使用await |
| 多轮对话不记得历史 | 自定义模板缺少history插槽 | 模板中声明 {history} 变量 |
| 长对话越来越慢/贵 | 记忆不裁剪导致prompt过长 | 换用带窗口的Memory实现 |
5.6 一个从长期来看很重要的习惯
前端写页面时,每次改动浏览器刷新立刻看到结果,反馈非常及时。转AI开发后,模型返回是异步且有一定延迟的,调试节奏会明显变慢。
我后来习惯了这样一个调试方法:每加一个环节,先用模拟模型或临时的 fake 输入跑一遍,确认链路通畅,再切回真实模型。把"能跑"和"能用对"分两步做。这样可以最大程度缩短单次调试的反馈周期,也避免每次报错都混进模型输出变量的干扰。
另外建议第一次接入真实模型时,准备一个只有几十个token的超短prompt,比如 "hi",先把网络链路、密钥、模型实例都验证通,再做复杂场景,这会节省大量无效等待时间。
最后再说两句踩出来的体会
接入LangChain这件事,技术上并不难,难的是思维层面的切换。前端开发的思维方式是"数据进、组件出";AI应用开发的思维方式是"链路组装、上下文流转"。这两种思维不是对立的,前端积累的异步处理、模块化拆分、状态管理能力,在AI应用里全是底子。
这个系列的第一章到这里收尾。下一步值得投入的方向是按顺序搞懂Agent、向量数据库和RAG,把"能对话"升级成"能调用工具、能根据文档回答"。我个人最有收获的一段经历,其实是用UI组件之间通过props单向传数据来类比链的上下文流转——当你把"组件树"想成"链图",前端经验就不再是转型的包袱了。
