从零上手 LangChain:用 JavaScript 打造强大 AI 应用的全流程指南

从零上手 LangChain:用 JavaScript 打造强大 AI 应用的全流程指南

2022 年 10 月,一个名为 LangChain 的开源框架悄然诞生,比 ChatGPT 正式发布早了一个月。它不是一个聊天机器人,而是一个AI 应用开发框架 ,专门帮助开发者把大语言模型(LLM)从"黑盒"变成可工程化、可组合的生产力工具。到 2025 年底,LangChain 已演进到 1.x 稳定版,成为构建 Agent、RAG、复杂工作流的事实标准,GitHub Star 数遥遥领先。

LangChain 的核心理念可以用一个词概括:Chain(链)。就像乐高积木一样,把 Prompt、Model、内存、工具、解析器等模块"链"起来,形成一个自动执行的工作流。这让 AI 应用从简单的"一问一答"跃升到多步骤推理、工具调用、长期记忆的智能代理。

本文基于实际代码示例(DeepSeek 模型 + JavaScript),带你从最基础的 Prompt 模板开始,一步步构建复杂链条。

一、环境配置

5 分钟快速上手环境配置

LangChain 的 JavaScript 版(也叫 LangChain.js)完全基于现代 ESM(ES Modules),所以我们要用 Node.js 18+。

  1. 创建项目文件夹并初始化

    mkdir langchain-demo && cd langchain-demo npm init -y

  2. 修改 package.json 支持 ESM(关键!)

    把这一行改成: "type": "module"

  3. 安装核心依赖

npm i @langchain/core @langchain/deepseek dotenv

  1. 创建 .env 文件存放 API Key(强烈推荐)

platform.deepseek.com/api-keys

申请 DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

易错提醒

  • 忘记加 "type": "module" 会报错 Cannot use import statement outside a module。
  • 不要直接把 API Key 写死在代码里,用 dotenv 读取更安全。

(dotenv 的作用就是把 .env 文件里的环境变量读取并加载到 Node.js 的全局对象 process.env 上)

  • DeepSeek 的包是 @langchain/deepseek,不是 langchain-deepseek。

二、为什么需要 LangChain?LLM 的"黑盒"如何被打开

大语言模型(如 DeepSeek、GPT、Claude)强大但原始:你给一个字符串,它吐出一个字符串。想做点复杂的事?比如:

  • 动态插入变量(Prompt 工程)
  • 多次调用模型完成多步任务
  • 记住对话历史
  • 调用外部工具(搜索、计算器、数据库)

直接用 API 就能实现,但代码会迅速变成"意大利面":嵌套回调、重复的错误处理、模型切换麻烦......

LangChain 就是那个"工程化工具箱":

  • 统一接口:不管用 DeepSeek、OpenAI 还是 Anthropic,一个抽象层搞定切换。
  • 模块化组件:PromptTemplate、ChatModel、OutputParser、Memory 等。
  • 可组合性 :用 pipe()RunnableSequence 把组件串成链,数据自动流动。
  • 生产级特性:流式输出、异步、调试(LangSmith)、监控。

用个比喻:原生 LLM API 像一辆法拉利引擎------动力强劲但裸露;LangChain 像整车工厂,把引擎装进车身,加方向盘、刹车、导航,让你安心上路。

三、LangChain 的适配器(Provider)

在 LangChain 中,安装了 dotenv 后,你可以不用显式传 apiKey 和 baseURL ,这正是 LangChain 的适配器(Provider)设计哲学的核心魅力之一:统一接口 + 智能默认配置。

我们可以直接这样创建大模型实例:

JavaScript

javascript 复制代码
import { ChatDeepSeek } from '@langchain/deepseek';

const model = new ChatDeepSeek({
  model: 'deepseek-reasoner',
  temperature: 0.7
  // 不用写 apiKey 和 baseURL!
});

而之前的老方式(不靠 dotenv)必须这样写:

JavaScript

arduino 复制代码
const model = new ChatDeepSeek({
  apiKey: 'sk-xxx',
  baseURL: 'https://api.deepseek.com',  // 某些老版本需要手动指定
  model: 'deepseek-reasoner'
});
背后的核心机制是什么?

这是 LangChain(尤其是 JS 版)在 ChatModel 适配器内部 实现的智能配置优先级链,大致逻辑如下(优先级从高到低):

  1. 构造函数显式传入的参数(最高优先级) 你手动传了 apiKey 或 baseURL,就无条件用你的。

  2. 环境变量(process.env)中的标准名称 (这就是 dotenv 发挥作用的地方) LangChain 的每个 Provider(DeepSeek、OpenAI、Anthropic 等)都定义了约定的环境变量名

    • DeepSeek:DEEPSEEK_API_KEY
    • OpenAI:OPENAI_API_KEY(+ OPENAI_BASE_URL 可选)
    • Anthropic:ANTHROPIC_API_KEY
    • Groq:GROQ_API_KEY
    • ...

    当你没有显式传 apiKey 时,ChatDeepSeek 构造函数内部会自动去 process.env.DEEPSEEK_API_KEY 里取!

  3. baseURL 的智能默认 每个 Provider 包内部都硬编码了官方 API 地址:

    • @langchain/deepseek 默认 api.deepseek.com
    • @langchain/openai 默认 api.openai.com/v1 所以你根本不用手动指定,除非你要用代理或自定义 endpoint。
  4. 如果以上都没找到 → 抛错 防止你默默跑出"密钥为空"的诡异行为。

统一接口的魅力

LangChain 的统一接口,就是不管你用哪个大模型提供商(DeepSeek、OpenAI、Anthropic、Groq、Claude、Gemini 等),创建和调用模型的方式、链的构建方式、输入输出格式都完全一致,你只需要换个 import 包和环境变量,就能无缝切换模型。

具体体现在哪些统一接口?
  1. 模型创建与调用接口统一(最常用)

    所有聊天模型都继承自同一个基类 ChatModel,提供相同的构造函数和方法:

    JavaScript

    javascript 复制代码
    // DeepSeek
    import { ChatDeepSeek } from '@langchain/deepseek';
    const model = new ChatDeepSeek({ model: 'deepseek-reasoner' });
    
    // OpenAI(切换只需改这一行 + .env)
    import { ChatOpenAI } from '@langchain/openai';
    const model = new ChatOpenAI({ model: 'gpt-4o' });
    
    // Anthropic
    import { ChatAnthropic } from '@langchain/anthropic';
    const model = new ChatAnthropic({ model: 'claude-3-5-sonnet' });
    
    // 调用方式完全一样!
    const res = await model.invoke(messages);  // 所有模型都支持 .invoke()
    console.log(res.content);

    输出也统一为 AIMessage 对象,内容在 .content。

  2. Chain 构建接口统一(pipe 和 Runnable)

    不管底层模型是谁,链的写法一模一样:

    JavaScript

    arduino 复制代码
    const chain = prompt.pipe(model).pipe(parser);  // 所有模型都支持 pipe
    await chain.invoke({ topic: '闭包' });
  3. 输入输出格式统一

    • 输入:通常是 { key: value } 对象或消息数组
    • 输出:标准化消息对象(HumanMessage、AIMessage、SystemMessage)
    • 解析器:StringOutputParser、JsonOutputParser 等通用
  4. 环境变量命名统一

    每个提供商都有约定的环境变量名:

    • DEEPSEEK_API_KEY
    • OPENAI_API_KEY
    • ANTHROPIC_API_KEY
    • GROQ_API_KEY

    配合 dotenv,一换就灵。

为什么这个统一接口这么重要?

场景 没统一接口(原始方式) 有统一接口(LangChain)
切换模型 大改代码、改 URL、改参数格式 改一行 import + .env 的 key
团队协作 每个人用不同模型,代码不兼容 统一标准,代码可复用
成本优化 想用更便宜的模型?重写适配代码 直接换 Gpt 或 DeepSeek,零成本切换
生产稳定性 某个提供商挂了,应用瘫痪 快速切换备用模型,故障切换秒级完成

实际案例:一键从 DeepSeek 切换到 GPT-4o

Bash

ini 复制代码
# .env 原版
DEEPSEEK_API_KEY=sk-xxx

# 改成
OPENAI_API_KEY=sk-xxx

JavaScript

javascript 复制代码
// 代码只改一行
// import { ChatDeepSeek } from '@langchain/deepseek';
import { ChatOpenAI } from '@langchain/openai';

const model = new ChatOpenAI({  // 构造函数参数基本一致
  model: 'gpt-4o',
  temperature: 0.7
});

// 下面所有链、invoke 代码完全不动!
const chain = prompt.pipe(model);

运行结果:行为几乎一致,但可能更聪明或更贵😂

总结

LangChain 的"统一接口"本质就是:

一套标准化的抽象层(基类 + 协议 + 约定),让所有大模型提供商看起来"长得一样",用起来"感觉一样"。

这才是 LangChain 能爆火的真正原因------它不只是工具,更是AI 应用的工程化基础设施

四、入门:PromptTemplate + Model 的第一次"链"

我们从最简单的例子开始。

javascript 复制代码
import { ChatDeepSeek } from '@langchain/deepseek';
import { PromptTemplate } from '@langchain/core/prompts';

const prompt = PromptTemplate.fromTemplate(`
你是一个{role},
请用不超过{limit}字回答以下问题:
{question}
`);

const model = new ChatDeepSeek({
  model: 'deepseek-reasoner',  // 2025 年 DeepSeek 的旗舰推理模型,擅长长链思考
  temperature: 0
});

const promptStr = await prompt.format({
  role: '前端面试官',
  limit: '50',
  question: '什么是闭包'
});

const res = await model.invoke(promptStr);
console.log(res.content);

底层逻辑

PromptTemplate 就是一个带占位符的字符串工厂。format() 方法把变量注入,生成完整提示词。然后直接扔给模型调用。

扩展知识点

  • 为什么用模板? Prompt 工程是 AI 应用的核心,模板让你复用、动态调整,避免硬编码。
  • DeepSeek-reasoner 模型亮点:2025 年版本内置 Chain of Thought(CoT),在回答前会自动生成推理过程(reasoning_content),准确率大幅提升,尤其适合复杂问题。

易错提醒

  • 别忘了 await prompt.format() ------ 它是异步的(虽然简单模板看起来同步)。
  • temperature=0 时输出最确定,适合知识问答;0.7~1.0 更有创意。

五、真正意义上的 Chain:pipe() 与 RunnableSequence

单纯的 Prompt + Model 还不是链。真正的链是用 pipe() 连接。

javascript 复制代码
const prompt = PromptTemplate.fromTemplate(`
你是一个前端专家,用一句话解释:{topic}
`);

const chain = prompt.pipe(model);  // 这里诞生了链!

const res = await chain.invoke({ topic: '闭包' });
console.log(res.content);

底层逻辑揭秘
pipe() 返回一个 RunnableSequence 对象(可运行序列)。内部结构是:

  • first:PromptTemplate
  • middle:[](空)
  • last:ChatModel

输入 {topic: '闭包'} → Prompt 格式化 → 完整提示词 → Model 调用 → AIMessage 输出。

扩展:RunnableSequence 的 first/middle/last

  • first:链的起点
  • middle:中间组件列表(可多个)
  • last:终点
  • 数据像水流一样,从 first 流到 last,自动传递。

当你用 prompt.pipe(model) 或 RunnableSequence.from([...]) 创建链时,LangChain 内部会自动把组件组织成以下结构:

  • first :永远是链的第一个组件(通常是 PromptTemplate 或 RunnablePassthrough 等)
  • middle从第二个到倒数第二个的所有组件,形成一个数组(可以是空 [],也可以有多个)
  • last :永远是链的最后一个组件(通常是 ChatModel、OutputParser 或自定义 RunnableLambda)

数据流向:输入 → first → middle[0] → middle[1] → ... → middle[n] → last → 输出 每一步的输出自动成为下一步的输入。

举例说明(假设用 pipe 连了 5 个组件):

JavaScript

scss 复制代码
const chain = prompt          // 1. PromptTemplate
  .pipe(model)               // 2. ChatModel
  .pipe(parser)              // 3. StringOutputParser
  .pipe(runnableLambda)      // 4. 自定义函数
  .pipe(finalFormatter);     // 5. 最终格式化

// 内部结构相当于:
{
  first: prompt,
  middle: [model, parser, runnableLambda],
  last: finalFormatter
}

易错提醒

  • chain.invoke() 的输入必须匹配第一个组件的输入变量(这里是 {topic})。

    核心规则: chain.invoke(input) 的 input 只看链的第一个组件(即 first)需要什么变量。

  • 输出是 AIMessage 对象,内容在 .content.text(不同模型略有差异)。

六、进阶:多步链 + 自动数据传递

手动链:

JavaScript

javascript 复制代码
const fullChain = RunnableSequence.from([
  (input) => explainChain.invoke({topic: input.topic}).then(res => res.text),
  (explanation) => summaryChain.invoke({explanation}).then(res => `知识点:${explanation} 总结:${res.text}`)
]);

它能跑,但只是"伪链"------所有 invoke 和拼接都是你手动写的,LangChain 只负责笨笨地传返回值。

纯线性自动链(真链,但丢失中间值):

JavaScript

javascript 复制代码
import { StringOutputParser } from "@langchain/core/output_parsers";

const explainChain = explainPrompt.pipe(model);          // Prompt + Model
const summaryChain = summaryPrompt.pipe(model);          // Prompt + Model

// 真正的自动链:全都是 Runnable 对象
const fullChain = RunnableSequence.from([
  explainChain.pipe(new StringOutputParser()),  // 第1步:输出详细解释字符串
  summaryChain.pipe(new StringOutputParser()),  // 第2步:自动收到上一步字符串,输出总结字符串
]);

const result = await fullChain.invoke({ topic: "闭包" });
console.log(result);  
// 输出:只有"三点总结"的字符串,详细解释被"吃掉"了

为什么是真自动?

  • 没有一行 invoke()
  • 没有 .then()
  • 数据完全由 LangChain 自动传递
  • 支持流式、批处理、LangSmith 追踪

缺点:最终只能拿到总结,详细解释丢失了(线性链的特性)。

终极优雅版:对象语法 + RunnablePassthrough(LCEL 精髓):

JavaScript

javascript 复制代码
import { RunnablePassthrough, StringOutputParser } from '@langchain/core/runnables';

const explainChain = explainPrompt.pipe(model).pipe(new StringOutputParser());
const summaryChain = summaryPrompt.pipe(model).pipe(new StringOutputParser());

const fullChain = RunnableSequence.from([
  { explanation: explainChain },  // 生成详细解释

  {
    explanation: new RunnablePassthrough(),  // 保留不吃掉
    summary: summaryChain,
  },

  ({ explanation, summary }) => `
【前端知识点详解】
${explanation}

【三点核心总结】
${summary}
  `.trim()
]);

await fullChain.invoke({ topic: '闭包' });

底层逻辑: new RunnablePassthrough() 的作用是把上一步的整个输出(返回值)原封不动地传递到下一步,但它通常和对象语法结合使用,才发挥最大威力。

执行流程详解

  1. 第1步输出一个对象:{ explanation: "详细文本..." }

  2. 第2步收到这个对象:

    • explanation: new RunnablePassthrough() → 直接返回收到的 explanation 值(不修改)
    • summary: summaryChain → 用收到的 explanation 作为输入,生成总结
  3. 第2步最终输出:{ explanation: "详细文本...", summary: "三点总结..." }

  4. 第3步拿到完整对象,自由组合

所以:RunnablePassthrough 就是那个"搬运工"------它确保上一步的 explanation 不会被 summaryChain 的输出覆盖,而是继续向下传递。

常见用法总结

用法场景 代码示例 作用
保留原始输入 { topic: new RunnablePassthrough() } 把 invoke 的 {topic} 一直传下去
保留中间结果 { explanation: new RunnablePassthrough() } 防止被下一步吃掉
合并多个并行结果 和其他字段一起用 构建复杂对象
跳过处理直接传递 单独用(少见) 输入 = 输出

核心点:对象语法,占位保留

先说"对象语法"是什么

对象语法就是:在 RunnableSequence.from() 里,每一步不是放单个 Runnable,而是放一个对象 { key: runnable }。

JavaScript

csharp 复制代码
RunnableSequence.from([
  { explanation: explainChain },     // 第一步:对象
  { 
    explanation: ...,               // 第二步:还是对象
    summary: ...
  }
])

每一步的输入/输出不再是单个字符串,而是一个对象(带有多个字段)。

这让数据流从"一维流水线"变成了"多维结构化对象流",你可以随意添加、保留、合并字段。

再来说"占位保留"是什么意思

在对象语法里,如果你不做任何处理,后一步的输出会完全覆盖前一步的输出(只保留新生成的字段)。

比如:

JavaScript

yaml 复制代码
RunnableSequence.from([
  { explanation: explainChain },  // 输出 { explanation: "详细文本" }

  { summary: summaryChain }       // 输入 { explanation: ... },但只输出 { summary: "三点总结" }
])
// 最终结果:只有 { summary: ... },explanation 没了!

explanation 被"吃掉"了------这就是线性思维的遗毒。

"占位保留" 的意思就是:我要在这一步故意"占个位置",把上一步的某个字段原样保留下来,不让它被吃掉

工具就是 new RunnablePassthrough()。

JavaScript

arduino 复制代码
{
  explanation: new RunnablePassthrough(),  // ← 这里!占位保留
  summary: summaryChain
}

它相当于在对象里说:"explanation 这个字段,你别动,上一步是什么我就输出什么(占位传下去)"。

优势:完全自动、保留所有中间值、支持并行/分支、LangSmith 追踪完美。

八、LangChain 的未来与最佳实践

到 2025 年,LangChain 已与 LangGraph(状态图工作流)深度融合,支持更复杂的 Agent。记住:

  • 用 LCEL(LangChain Expression Language)即 |pipe() 构建链,最简洁。
  • 生产环境加 LangSmith 调试、追踪。
  • 模型切换只需改一行:从 DeepSeek 换 OpenAI 零成本。

LangChain 不是终点,而是起点。它让你把 LLM 从玩具变成生产力武器。

相关推荐
坐吃山猪5 小时前
Google的A2A智能体群聊
python·llm·a2a
૮・ﻌ・5 小时前
小兔鲜电商项目(一):项目准备、Layout模块、Home模块
前端·javascript·vue
用户47949283569156 小时前
JavaScript 还有第三种注释?--> 竟然合法
javascript
zhougl9966 小时前
AJAX本质与核心概念
前端·javascript·ajax
hpz12236 小时前
对Element UI 组件的二次封装
javascript·vue.js·ui
GISer_Jing6 小时前
Taro跨端开发实战:核心原理与关键差异解析
前端·javascript·taro
布局呆星6 小时前
Vue 3 从创建项目到基础语法---01
javascript·vue.js
karshey6 小时前
【前端】sort:js按照固定顺序排序
开发语言·前端·javascript
AI大模型7 小时前
别再被割韭菜!真正免费的Prompt学习路径,0基础也能抄
程序员·llm·agent