普通视图

发现新文章,点击刷新页面。
昨天 — 2026年2月9日首页

AI 应用工程化实战:使用 LangChain.js 编排 DeepSeek 复杂工作流

作者 NEXT06
2026年2月9日 20:31

在 2024 年至 2025 年的技术浪潮中,大语言模型(LLM)的应用开发已经从“尝鲜”阶段迈向了“工程化”阶段。对于开发者而言,仅仅调用 fetch 接口获取模型回复是远远不够的。在构建复杂的生产级应用时,我们面临着提示词管理混乱、模型切换成本高、上下文处理复杂以及任务编排困难等诸多痛点。

LangChain 的出现,正是为了解决这些工程化难题。它不是一个模型,而是一个框架,旨在将 LLM 的能力封装成可维护、可复用的组件。

本文将通过四个循序渐进的代码示例,演示如何利用 LangChain.js 结合当下热门的 DeepSeek(深度求索)模型,完成从基础调用到复杂工作流编排的进阶之路。

第一阶段:标准化的开始——适配器模式的应用

在没有任何框架之前,调用 LLM 通常意味着处理各种非标准化的 HTTP 请求。OpenAI、DeepSeek、Claude 的 API 格式各不相同。LangChain 的第一个核心价值在于标准化

以下是基于 main.js 的基础调用示例:

JavaScript

// main.js
import 'dotenv/config'; // 加载环境变量
import { ChatDeepSeek } from '@langchain/deepseek';

// 1. 实例化模型
const model = new ChatDeepSeek({
    model: 'deepseek-reasoner', // 使用 DeepSeek 的推理模型
    temperature: 0, // 设定温度,0 代表最确定性的输出
    // apiKey 自动从 process.env.DEEPSEEK_API_KEY 读取
});

// 2. 执行调用
const res = await model.invoke('用一句话解释什么是RAG?');
console.log(res.content);

深度解析:适配器模式 (Adapter Pattern)

这段代码看似简单,却蕴含了 AI 工程化的第一块基石:适配器模式

在软件工程中,适配器模式用于屏蔽底层接口的差异。ChatDeepSeek 类就是一个适配器(Provider)。

  • 统一接口:无论底层使用的是 DeepSeek、OpenAI 还是 Google Gemini,在 LangChain 中我们都统一调用 .invoke() 方法,invoke(英文:调用)。
  • 配置解耦:开发者无需关心 baseURL 配置、鉴权头部的拼接或请求体格式。
  • 参数控制:temperature: 0 是一个关键参数。在开发代码生成或逻辑推理(如使用 deepseek-reasoner)应用时,我们将温度设为 0 以减少随机性;而在创意写作场景,通常设为 0.7 或更高,这是决定你的大模型输出的内容严谨还是天马行空的关键因素之一。

通过这种方式,我们实现了业务逻辑与模型实现的解耦。如果未来需要更换模型,只需修改实例化部分,业务代码无需变动。

第二阶段:提示词工程化——数据与逻辑分离

直接在 .invoke() 中传入字符串(Hardcoding)在 Demo 阶段可行,但在实际项目中是反模式。因为提示词(Prompt)往往包含静态的指令和动态的用户输入。

下面这段代码展示了如何使用 PromptTemplate(对prompt设计一个模板,只需要提供关键的参数) 进行管理:

JavaScript

// 1.js
import { PromptTemplate } from '@langchain/core/prompts';
import { ChatDeepSeek } from '@langchain/deepseek';

// 1. 定义模板:静态结构与动态变量分离
const prompt = PromptTemplate.fromTemplate(`
你是一个{role}。
请用不超过{limit}字回答以下问题:
{question}
`);

// 2. 格式化:注入数据
const promptStr = await prompt.format({
    role: '前端面试官',
    limit: '50',
    question: '什么是闭包'
});

// 3. 调用模型
const model = new ChatDeepSeek({
    model: 'deepseek-reasoner',
    temperature: 0.7
});

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

深度解析:提示词模板的意义

这里体现了关注点分离(Separation of Concerns)的设计原则。

  1. 复用性:同一个 prompt 对象可以生成“前端面试官”、“后端面试官”甚至“测试工程师”的问答场景,只需改变 format 的入参。
  2. 维护性:当需要优化 Prompt(例如增加“请使用中文回答”的系统指令)时,只需修改模板定义,而不用在代码库的各个角落查找字符串拼接逻辑。
  3. 类型安全:虽然 JavaScript 是弱类型,但在 LangChain 的 TypeScript 定义中,模板的输入变量(Variables)是可以被静态分析和校验的。

然而,上述代码仍显得有些“命令式”:我们需要手动格式化,拿到字符串,再手动传给模型。这依然是两步操作。

第三阶段:链式流转——LCEL 与声明式编程

LangChain 的核心精髓在于 Chain(链) 。通过 LangChain 表达式语言(LCEL),我们可以通过管道(Pipe)将组件连接起来,形成自动化的工作流。

下面的这段代码展示了这一范式转变:

JavaScript

// 2.js
import { ChatDeepSeek } from '@langchain/deepseek';
import { PromptTemplate } from '@langchain/core/prompts';

const model = new ChatDeepSeek({
    model: 'deepseek-reasoner',
    temperature: 0.7
});

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

// 核心变化:构建 Chain
// prompt (模板节点) -> model (LLM 节点)
const chain = prompt.pipe(model);

// 执行 Chain
const response = await chain.invoke({
    topic: '闭包'
});
console.log(response.content);

深度解析:LCEL 与声明式编程

这段代码引入了 .pipe() 方法,它深受 Unix 管道思想的影响。

  1. 声明式编程 (Declarative)
    我们不再编写“如何做”(先格式化,再调用),而是定义“是什么”(链条是 Prompt 流向 Model)。LangChain 运行时会自动处理数据的传递。
  2. Runnable 接口
    在 LangChain 中,Prompt、Model、OutputParser 甚至整个 Chain 都实现了 Runnable 接口。这意味着它们具有统一的调用方式(invoke, stream, batch)。
  3. 自动化数据流
    当我们调用 chain.invoke({ topic: '闭包' }) 时,对象 { topic: '闭包' } 首先进入 Prompt,Prompt 将其转化为完整的提示词字符串,然后该字符串自动流入 Model,最终输出结果。

这是构建 Agent(智能体)的基础单元。

第四阶段:编排复杂工作流——任务拆解与序列化

在真实业务中,单一的 Prompt 往往难以完美解决复杂问题。例如,我们希望 AI 既能“详细解释原理”,又能“精简总结要点”。如果试图在一个 Prompt 中完成,模型往往会顾此失彼。

更好的工程化思路是任务拆解。下面的这段代码展示了如何使用 RunnableSequence 串联多个任务:

JavaScript

// 3.js
import { ChatDeepSeek } from '@langchain/deepseek';
import { PromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence } from '@langchain/core/runnables';

const model = new ChatDeepSeek({
    model: 'deepseek-reasoner',
    temperature: 0.7
});

// 任务 A:详细解释
const explainPrompt = PromptTemplate.fromTemplate(`
    你是一个前端专家,请详细介绍以下概念: {topic}
    要求:覆盖定义、原理、使用方式,不超过300字。
`);

// 任务 B:总结核心点
const summaryPrompt = PromptTemplate.fromTemplate(`
    请将以下前端概念总结为3个核心要点 (每点不超过20字):
    {explanation}
`);

// 创建两个独立的子链
const explainChain = explainPrompt.pipe(model);
const summaryChain = summaryPrompt.pipe(model);

// 核心逻辑:编排序列
const fullChain = RunnableSequence.from([
    // 第一步:输入 topic -> 获取详细解释 text
    (input) => explainChain.invoke({ topic: input.topic }).then(res => res.content),
    
    // 第二步:接收 explanation -> 生成总结 -> 组合最终结果
    (explanation) => summaryChain.invoke({ explanation }).then(res => 
        `知识点详情:\n${explanation}\n\n精简总结:\n${res.content}`
    )
]);

const response = await fullChain.invoke({
    topic: '闭包'
});
console.log(response);

深度解析:序列化工作流

这是一个典型的 Sequential Chain(顺序链)  模式。

  1. 输入/输出对齐
    第一步的输出(详细解释)通过函数传递,直接成为了第二步的输入变量 { explanation }。这种数据流的自动衔接是复杂 AI 应用的关键。
  2. DeepSeek Reasoner 的优势
    在这个场景中,我们使用了 deepseek-reasoner。对于解释原理和归纳总结这类需要逻辑分析(Reasoning)的任务,DeepSeek 的 R1 系列模型表现优异。通过拆解任务,我们让模型在每个步骤都专注于单一目标,从而大幅提升了输出质量。
  3. 可观测性与调试
    将长任务拆分为短链,使得我们在调试时可以单独检查 explainChain 的输出是否准确,而不必在一个巨大的黑盒 Prompt 中盲目尝试。

总结

到此为止我们见证了 AI 代码从“脚本”到“工程”的进化:

  1. 适配器模式:解决了模型接口碎片化问题。
  2. 提示词模板:实现了数据与逻辑的分离。
  3. LCEL 管道:将原子能力组装成自动化流程。
  4. 序列化编排:通过任务拆解解决复杂业务逻辑。
  5. **要想拿到大模型输出的结果,别忘了配置APIKEY和环境变量

LangChain.js 结合 DeepSeek,不仅仅是调用了一个 API,更是为您提供了一套构建可扩展、可维护 AI 系统的脚手架。作为前端开发者,掌握这种“搭积木”的思维方式,是在 AI 时代保持竞争力的关键。

前端算法:从 O(n²) 到 O(n),列表转树的极致优化

作者 NEXT06
2026年2月8日 21:34

1. 引言与业务场景

在前端开发中,数据结构的转换是一项基础且高频的技能。后端数据库通常以扁平化(Flat List)的形式存储层级数据,每条记录仅保留 id 和 parentId 来标识父子关系。然而,前端组件(如 Ant Design 的 Tree、Cascader,或 Element UI 的 Table 树形模式)往往需要嵌套的树形结构(Tree Structure)来渲染视图。

常见的业务场景包括但不限于:

  • RBAC 权限系统:后台管理系统的侧边栏菜单。
  • 组织架构图:展示公司部门与员工的层级关系。
  • 行政区划联动:省、市、区/县的三级联动选择器。
  • 评论盖楼:社交平台的多级回复机制。

输入数据通常如下所示:

JavaScript

const flatList = [
  { id: 1, parentId: 0, name: '系统管理' },
  { id: 2, parentId: 1, name: '用户管理' },
  { id: 3, parentId: 1, name: '权限配置' },
  { id: 4, parentId: 2, name: '用户列表' },
  // ... 可能有成百上千条数据
];

目标是将其转换为如下的树形结构:

JavaScript

[
  {
    id: 1,
    name: '系统管理',
    children: [
      {
        id: 2,
        name: '用户管理',
        children: [
          { id: 4, name: '用户列表', children: [] }
        ]
      },
      { id: 3, name: '权限配置', children: [] }
    ]
  }
]

本文将从面试官的角度,分析两种主流的实现方案,探讨从递归到哈希映射的思维跃迁,以及如何通过利用 JavaScript 的对象引用(Object Reference)特性实现性能的极致优化。


2. 基础方案:递归实现 (Recursion)

递归是处理树形结构最直观的思维方式。其核心逻辑是:对于每一个节点,遍历整个列表,找出所有 parentId 等于当前节点 id 的项,作为其子节点。

代码实现

利用 ES6 的数组方法,我们可以写出非常简洁的代码:

JavaScript

/**
 * 递归查找,构建树形结构
 * @param {Array} list 原始列表
 * @param {Number} parentId 当前节点的父节点ID,默认为根节点ID 0
 * @return {Array} 树形结构
 */
function listToTreeRecursive(list, parentId = 0) {
  return list
    .filter(item => item.parentId === parentId)
    .map(item => ({
      ...item,
      children: listToTreeRecursive(list, item.id)
    }));
}

深度解析与瓶颈

这段代码在面试中通常作为“及格”的答案。它逻辑清晰,代码量少,但在工程实践中存在明显的性能隐患。

时间复杂度分析:O(n²)

假设列表长度为 n。

  1. 函数 listToTreeRecursive 会被调用多次。
  2. 每一次调用,filter 都会遍历整个列表(长度为 n)来寻找子节点。
  3. 随着递归深度的增加,虽然总调用次数取决于节点数量,但从宏观算法角度来看,这是一个典型的嵌套遍历模型。其时间复杂度接近 O(n²)

性能风险

  • CPU 阻塞:当数据量达到几千条(例如全国省市区数据)时,计算量将呈指数级增长,可能导致主线程阻塞,页面卡顿。
  • 栈溢出:虽然在 DOM 树场景下层级通常不会太深,但如果数据层级极深,递归调用栈可能超出浏览器限制(Stack Overflow)。

3. 进阶方案:Map 映射优化 (Iterative Approach)

为了解决递归带来的性能问题,我们需要打破“每次查找子节点都要遍历整个列表”的限制。

优化思路:空间换时间

通过引入一个哈希表(Hash Map),我们可以将节点的查找时间复杂度从 O(n)  降低到 O(1) 。在 JavaScript 中,我们可以利用 Map 或原生 Object 来实现。

核心原理:利用对象引用

这是面试中的加分项,也是容易写错的地方。
核心在于:JavaScript 中的对象是引用传递(Pass by Reference) 。当我们修改 Map 中存储的对象的 children 属性时,所有指向该对象的引用都会同步感知到变化。

代码实现

JavaScript

/**
 * 利用 Map 映射,非递归构建树形结构
 * 时间复杂度 O(n)
 * @param {Array} list 原始列表
 * @return {Array} 树形结构
 */
function listToTreeMap(list) {
  const nodeMap = new Map();
  const tree = [];

  // 第一步:初始化 Map,将所有节点以 id 为键存入 Map
  // 关键点:不仅存入,还必须为每个节点初始化 children 数组
  list.forEach(item => {
    nodeMap.set(item.id, { ...item, children: [] });
  });

  // 第二步:再次遍历,建立父子关系
  list.forEach(item => {
    // 必须获取 Map 中的引用(reference),而不是原始 list 中的 item
    // 只有修改 Map 中的对象,才能通过引用机制同步到 tree 数组中
    const node = nodeMap.get(item.id);
    
    // 如果是根节点,直接放入结果数组
    if (item.parentId === 0) {
      tree.push(node);
    } else {
      // 在 Map 中查找父节点
      const parentNode = nodeMap.get(item.parentId);
      // 如果父节点存在,将当前节点(的引用)推入父节点的 children
      if (parentNode) {
        parentNode.children.push(node);
      }
    }
  });

  return tree;
}

关键逻辑解析

  1. Map 初始化:我们首先遍历一次列表,将所有数据转换为 { id: node } 的映射结构。这一步使得后续查找任意节点的操作变为 O(1)。

  2. 引用传递的妙用

    • 当 tree.push(node) 执行时,tree 数组持有的是节点的内存地址引用
    • 当 parentNode.children.push(node) 执行时,parentNode 的 children 数组持有的也是同一个内存地址引用
    • 因此,无论节点层级多深,我们只需要两层平级的遍历即可完成所有连接。

时间复杂度分析:O(n)

  • 第一次遍历构建 Map:O(n)。
  • 第二次遍历构建关系:O(n)。
  • 总复杂度:O(2n),即 O(n)

4. 方案对比与选型建议

从面试官的角度来看,能够清晰分析出两种方案的优劣,并根据场景选择合适的方案,是高级工程师具备的素质。

维度 递归方案 (Recursion) Map 映射方案 (Iteration)
时间复杂度 O(n²)  (性能较差) O(n)  (性能极佳)
空间复杂度 O(n) (递归栈开销) O(n) (Map 存储开销)
代码可读性 高,逻辑符合直觉 中,需要理解引用关系
适用场景 数据量小 (<100条),快速开发 数据量大 (>1000条),追求性能
健壮性 深度过大可能导致栈溢出 无栈溢出风险

面试建议

  • 如果面试要求“写一个转换函数”,先询问数据量级。
  • 默认情况下,优先通过 Map 方案展示你对复杂度和引用的理解。
  • 在编写 Map 方案时,务必注意不要直接操作原始 list item,而是操作 Map 中存储的新对象引用,这是最常见的逻辑陷阱。

5. 结语

“扁平列表转树”不仅仅是一道算法题,它深刻体现了前端开发中对内存引用时间复杂度的理解。

  1. 基础层:理解树形结构,能写出递归。
  2. 进阶层:理解哈希表(Hash Map)在算法优化中的“空间换时间”思想。
  3. 专家层:熟练掌握 JavaScript 的对象引用机制,能够编写出无副作用、高性能的转换代码。

在实际业务开发中,面对复杂且庞大的组织架构或菜单数据,使用 O(n) 的 Map 映射方案应是你的首选。

昨天以前首页

Webpack 与 Vite:我究竟该选哪个

作者 NEXT06
2026年2月6日 20:30

在前端工程化的演进历程中,工具链的发展始终围绕着两个核心命题:构建的灵活性开发的即时性。Webpack 作为构建工具的集大成者,确立了“一切皆模块”的工程标准;而 Vite 则利用浏览器原生能力,掀起了从“构建驱动”向“体验驱动”的范式转移。

本文将结合底层原理,从构建机制、配置哲学、兼容性策略及热更新效率四个维度,深度解构这两者的核心差异。


一、 构建机制与冷启动:Bundle vs No-Bundle

Webpack 与 Vite 最根本的区别在于开发环境的启动模式。这直接决定了项目的冷启动速度与规模扩展性。

Webpack:全量构建 (Bundle-Based)

Webpack 是一个基于依赖图谱(Dependency Graph)的静态模块打包器。

  • 原理:在开发服务器启动前,Webpack 必须从入口文件(Entry)开始,递归解析所有的依赖模块(AST 分析),通过 Loader 转译代码,最终将所有模块打包进内存中的 Bundle 文件。

  • 瓶颈:启动时间 

    O(n)O(n)
    

     与项目复杂度成正比。随着应用规模扩大,依赖解析和打包的过程呈指数级增长。

Vite:按需编译 (Native ESM)

Vite 采用了 No-Bundle 的设计理念,将构建过程移交给了浏览器。

  • 原理:Vite 利用现代浏览器原生支持 ES Module(

  • 优势:启动时间接近 

    O(1)O(1)
    

    ,与项目总模块数无关,仅取决于页面当前需要的模块。

代码对比

Webpack (隐式逻辑)
需等待所有模块打包完成,终端才会显示 Compiled successfully,浏览器才能访问。

Vite (浏览器请求)

codeHtml

<!-- index.html -->
<script type="module" src="/src/main.js"></script>

浏览器发起 HTTP 请求 -> Vite Server 拦截 -> 编译 main.js -> 返回。

屏幕录制 2026-02-06 201827.gif


二、 开发体验与配置哲学:显式装配 vs 开箱即用

在配置层面,Webpack 倾向于提供原子化的控制权,而 Vite 倾向于提供最佳实践的默认配置。

Webpack:职责单一与链式调用

Webpack 默认只理解 JavaScript。处理其他资源必须显式配置 Loader,且对配置顺序有严格要求。

  • 痛点:Loader 的执行顺序是从右向左(或从下到上) 。若顺序颠倒,会导致解析失败。
  • 模块化规范:配置文件采用 CommonJS 规范 (module.exports),在编写复杂配置时缺乏类型提示。

Webpack 配置示例

JavaScript

// webpack.config.js
const path = require('path');

module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        // 必须严格遵守顺序:先 css-loader 解析 import,再 style-loader 挂载 DOM
        use: ['style-loader', 'css-loader'] 
      }
    ]
  }
};

Vite:约定优于配置与类型友好

Vite 针对高频场景(CSS、TypeScript、JSX)内置了支持,无需额外配置 Loader。

  • 优势:原生支持 ESM 配置文件,配合 defineConfig 辅助函数,能获得完整的 TypeScript 类型推断与智能提示。
  • CSS处理:直接 import CSS 文件即可生效,且原生支持 CSS Modules 和 Pre-processors(只需安装对应的 sass/less 依赖)。

Vite 配置示例

JavaScript

// vite.config.js
import { defineConfig } from 'vite';

// 获得代码提示与类型检查
export default defineConfig({
  // CSS 预处理器等配置已内置,无需手动编写 Loader 规则
});

屏幕录制 2026-02-06 202147.gif


三、 生产构建与兼容性策略:统一降级 vs 分流加载

生产环境的构建策略体现了两者对“兼容性”与“性能”权衡的差异。

Webpack:Babel 统一转译

Webpack 通常结合 babel-loader 和 @babel/preset-env,将所有 ES6+ 代码转换为 ES5,以兼容目标浏览器(如 IE11)。

  • 代价:即使是支持现代特性的浏览器,也必须加载体积冗余、执行效率较低的 ES5 代码及 Polyfills。

Webpack 配置片段

JavaScript

// rule 配置
{
  test: /.m?js$/,
  exclude: /node_modules/,
  use: {
    loader: 'babel-loader',
    options: { presets: ['@babel/preset-env'] }
  }
}

Vite:Modern Mode + Legacy 分层策略

Vite 默认构建目标为现代浏览器(支持 Native ESM)。为了兼容旧版浏览器,Vite 提供了 @vitejs/plugin-legacy。

  • 机制:构建会生成两套代码。

    1. Modern Bundle:使用 
    2. Legacy Bundle:使用 SystemJS 加载,包含必要的 Polyfills,仅在不支持 ESM 的浏览器中通过 
  • Rollup:Vite 生产环境使用 Rollup 打包,而非 esbuild。这是因为 Rollup 在代码分割(Code Splitting)和 CSS 处理上更为成熟稳定。

Vite Legacy 配置

JavaScript

// vite.config.js
import legacy from '@vitejs/plugin-legacy';

export default defineConfig({
  plugins: [
    legacy({
      targets: ['ie >= 11'], // 自动生成 polyfills-legacy.js chunks
      additionalLegacyPolyfills: ['regenerator-runtime/runtime']
    })
  ]
});

四、 热更新 (HMR) 效率:重建 vs 精准替换

热更新(HMR)的速度直接影响开发者的心流体验。

Webpack:增量构建

当文件修改时,Webpack 需要重新构建包含该模块的依赖子树,计算 Patch,并通过 WebSocket 推送更新。虽然有缓存机制,但在大型项目中,重建依赖图的过程仍可能导致秒级延迟。

Vite:精准链式更新

Vite 的 HMR 是基于 ESM 的。

  • 原理:当模块编辑后,Vite 只需要让浏览器重新请求该模块(加上时间戳 query 防止缓存)。
  • 304 缓存:未变更的模块,浏览器直接利用 HTTP 缓存(304 Not Modified),无需服务器再次处理。
  • 效率:HMR 速度与应用总规模几乎无关,始终保持毫秒级响应。

五、 总结与选型建议

Webpack 与 Vite 并非简单的替代关系,而是不同工程化理念的产物。

  • Webpack 是一个编译器。它拥有庞大的插件生态和极致的定制能力,适合对构建产物有极高要求、需要深度定制 Loader 链、或必须兼容极低版本浏览器的存量巨型项目。
  • Vite 是一个开发服务器 + 生产打包器的组合。它通过标准化开发流程和利用现代浏览器特性,解决了“慢”的痛点。对于绝大多数现代 Web 应用(Vue 3 / React 18+),Vite 是首选方案。

从配置繁琐的“作坊式组装”到开箱即用的“工业化引擎”,Vite 的出现标志着前端工程化进入了追求极致开发体验的新阶段。

❌
❌