Skip to content

LangChain 的理解与实战 #140

Description

@peng-yin

如果做个能查公司内部文档的客服机器人, 可能会遇到

  • RAG 检索永远答非所问,上下文驴唇不对马嘴
  • 多轮对话聊个七八轮就崩,token 直接爆仓
  • 想换个性价比更高的模型,代码要全量重写
  • 线上一限流、API 一超时,服务直接原地升天

一、重新认识 LangChain:你之前对它的理解可能全错了

很多人对 LangChain 的认知还停留在「Lang = 语言模型,Chain = 把步骤串起来」,格局小了家人们!咱们先把底层逻辑掰扯清楚。

1.1 它不是胶水代码,是 AI 应用界的「React」

咱们前端 er 都懂:原生 JS 能写页面,但是为什么大家都用 React?因为 React 把 DOM 操作、状态管理、组件复用、生命周期这些脏活累活全给你封装好了,让你能专注写业务逻辑,不用天天跟浏览器兼容性对线。

LangChain 就是干了一模一样的事:

  • 原生 LLM 接口 = 原生 JS:能实现基础功能,但是每加一个需求就要写一堆重复代码,换个环境直接不兼容
  • LangChain = React:把提示词工程、模型适配、数据流转、工具调用、内存管理、异常处理这些 AI 应用的通用脏活全给你封装了,提供了一套标准化的开发范式,让你不用天天跟不同模型的 API 文档对线

它的核心从来不是「把接口串起来」,而是一套可组合、可扩展、生产级可用的 AI 原生应用开发框架。

1.2 JS/TS 生态才是全栈开发的王炸

很多人有个误区:「LangChain 是 Python 的,JS 版本就是个玩具」。大错特错!

现在 LangChain 的 JS/TS 版本已经完全成熟,生产级可用,而且对咱们前端全栈开发者来说,它简直是天选之子:

  • 完全基于 Node.js 开发,原生支持 ESM 规范,跟你天天写的 Next.js、NestJS、Express 无缝衔接,不用额外学一门 Python
  • 完美兼容前端生态,你熟悉的 npm、yarn、pnpm 直接用,dotenv、axios 这些常用库随便接
  • 类型提示拉满!TypeScript 原生支持,写代码的时候 IDE 直接给你提示参数、报错,不用对着文档瞎猜,这一点直接吊打 Python 版本的体验

别再被 Python 教程劝退了,用你最熟悉的 JS/TS,照样能写出顶级的 AI 应用。

1.3 适配器模式的终极奥义:不止是换模型,是给应用上了「双保险」

很多人看完适配器模式只记住了「能随便换模型」,但它的价值远不止于此。

咱们用前端最熟悉的 Axios 来类比:Axios 为什么好用?因为它封装了浏览器和 Node.js 的 http 请求差异,不管你在什么环境,都是一套 get/post API,不用管底层是 XMLHttpRequest 还是 http 模块。

LangChain 的适配器就是干了这件事:

  • 不管你用的是 DeepSeek、OpenAI、Anthropic,还是本地部署的 Ollama 模型,全都是一套标准的 invoke/stream 接口
  • 不用去研究每个模型的请求格式、参数差异、鉴权方式,适配器全给你处理好了
  • 你写的业务逻辑完全和底层模型解耦,真正做到了「面向接口编程,而不是面向实现编程」

这带来的生产级价值,可比「换模型方便」大多了:

  • 模型 A/B 测试:同一个业务逻辑,同时测试 DeepSeek 和 GPT-4o 的效果,只需要改一行模型名,业务代码一行不用动
  • 灰度发布:新模型上线,先给 10% 的流量用,出问题一键切回旧模型,零成本
  • 降级容灾:主模型 API 挂了、限流了,自动切换到备用模型,服务完全不中断,用户根本感知不到
  • 成本优化:简单问题用便宜的小模型,复杂问题用强大的大模型

二、LCEL 进阶:声明式编排的艺术

管道式链式调用

import { ChatDeepSeek } from '@langchain/deepseek';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';

const prompt = ChatPromptTemplate.fromTemplate('用一句话解释什么是{concept}');
const model = new ChatDeepSeek({ model: 'deepseek-reasoner' });
const outputParser = new StringOutputParser();

// 用管道操作符串联,声明式写法
const chain = prompt.pipe(model).pipe(outputParser);
const result = await chain.invoke({ concept: 'LCEL' });

RunnableSequence:串行执行多步骤

import { RunnableSequence } from '@langchain/core/runnables';

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

const result = await chain.invoke({ concept: '函数式编程' });

RunnableParallel:并行执行多条链

import { RunnableParallel, RunnablePassthrough } from '@langchain/core/runnables';

const parallelChain = RunnableParallel({
  title: titleChain,
  content: contentChain,
  end: endChain,
  // 把原始输入也透传下去
  topic: new RunnablePassthrough(),
});

// 一次调用,同时拿到三个结果,响应速度直接拉满
const res = await parallelChain.invoke('前端进阶学习指南');
console.log(res);

三、核心场景进阶实战:从玩具 Demo 到生产级应用

3.1 提示词工程进阶:告别瞎写 Prompt,用模板体系拿捏模型输出

进阶 1:用 ChatPromptTemplate 替代 PromptTemplate,适配对话模型

import { ChatPromptTemplate } from '@langchain/core/prompts';

// 精准定义系统提示、用户消息,角色分离,模型更听话
const prompt = ChatPromptTemplate.fromMessages([
  // 系统提示词:给模型定规矩
  ["system", "你是一个资深的前端架构师,回答必须简洁专业,只讲干货,不写废话,每一条回答不超过3条要点"],
  // 用户消息:用占位符接收用户输入
  ["human", "用户的问题:{question},相关技术栈:{techStack}"],
]);

// 格式化提示词
const formattedPrompt = await prompt.formatMessages({
  question: 'React项目性能优化怎么做?',
  techStack: 'React 18 + TypeScript'
});

console.log(formattedPrompt);

进阶 2:少样本学习 FewShotPromptTemplate,让模型秒懂你的要求

import { ChatPromptTemplate, FewShotChatMessagePromptTemplate } from '@langchain/core/prompts';

// 1. 定义示例:告诉模型你想要的输入输出格式
const examples = [
  {
    input: "Vue和React的区别",
    output: "核心差异:1. 核心理念:Vue 渐进式框架,React函数式UI;2. 响应式:Vue双向绑定,React单向数据流;3. 上手难度:Vue更低,React对JS要求更高"
  },
  {
    input: "var和let的区别",
    output: "核心差异:1. 作用域:var函数作用域,let块级作用域;2. 提升:var存在变量提升,let存在暂时性死区;3. 重复声明:var允许,let不允许"
  }
];

// 2. 定义单个示例的模板
const examplePrompt = ChatPromptTemplate.fromMessages([
  ["human", "{input}"],
  ["ai", "{output}"],
]);

// 3. 创建少样本提示词模板
const fewShotPrompt = new FewShotChatMessagePromptTemplate({
  examplePrompt,
  examples,
  inputVariables: ["input"],
});

// 4. 拼接成最终的提示词
const finalPrompt = ChatPromptTemplate.fromMessages([
  ["system", "你是一个资深前端讲师,回答问题必须简洁明了,只列核心差异,不写废话"],
  fewShotPrompt,
  ["human", "{input}"],
]);

// 调用模型
const chain = finalPrompt | new ChatDeepSeek({ model: 'deepseek-reasoner' }) | new StringOutputParser();
const res = await chain.invoke({ input: "Promise和async/await的区别" });
console.log(res);

3.2 RAG 系统进阶:解决答非所问,从「能用」到「好用」

生产级 RAG 完整代码

import 'dotenv/config';
// 文档加载与分割
import { PDFLoader } from '@langchain/community/document_loaders/fs/pdf';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
// 嵌入与向量数据库
import { OpenAIEmbeddings } from '@langchain/openai';
import { Chroma } from '@langchain/community/vectorstores/chroma';
// LCEL核心组件
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { ChatDeepSeek } from '@langchain/deepseek';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnablePassthrough, RunnableSequence } from '@langchain/core/runnables';

// ========== 第一步:文档处理与向量库构建 ==========
// 1. 加载PDF文档
const loader = new PDFLoader('./前端开发规范.pdf');
const docs = await loader.load();

// 2. 文本分块(核心优化点!)
const textSplitter = new RecursiveCharacterTextSplitter({
  chunkSize: 500, // 块大小:技术文档推荐300-800
  chunkOverlap: 100, // 块重叠:保留上下文,避免关键信息被截断
  separators: ["\n\n", "\n", "。", "!", "?", " ", ""], // 按语义分割
});
const splitDocs = await textSplitter.splitDocuments(docs);

// 3. 生成嵌入向量,存入向量数据库
const embeddings = new OpenAIEmbeddings({
  model: 'text-embedding-3-small',
});
const vectorStore = await Chroma.fromDocuments(splitDocs, embeddings, {
  collectionName: 'frontend_docs',
});

// 4. 创建检索器
const retriever = vectorStore.asRetriever({
  k: 4, // 只返回最相关的4条内容
  searchType: 'similarity',
});

// ========== 第二步:构建RAG问答Chain ==========
// 1. 构建RAG专属提示词模板
const ragPrompt = ChatPromptTemplate.fromTemplate(`
你是一个专业的文档问答助手,只能基于下面的参考文档回答用户的问题,绝对不能编造文档里没有的信息。
如果参考文档里没有相关内容,直接回答"抱歉,参考文档中没有相关内容",不要自己瞎编。

参考文档:
{context}

用户的问题:{question}
`);

// 2. 格式化检索到的文档内容
const formatDocs = (docs) => docs.map(doc => doc.pageContent).join('\n\n');

// 3. 用LCEL构建RAG Chain
const ragChain = RunnableSequence.from([
  {
    context: (input) => retriever.invoke(input.question).then(formatDocs),
    question: new RunnablePassthrough(),
  },
  ragPrompt,
  new ChatDeepSeek({ model: 'deepseek-reasoner', temperature: 0 }),
  new StringOutputParser(),
]);

// ========== 第三步:调用问答 ==========
const answer = await ragChain.invoke({
  question: '公司的Git提交规范是什么?'
});

console.log(answer);

RAG 核心优化点

  1. 文本分块是重中之重:不要一刀切用 1000 甚至 2000 的 chunkSize,技术文档推荐 300-800,一定要加 chunkOverlap,按语义分割
  2. 嵌入模型要选对:用专门的嵌入模型(text-embedding-3-small、text-embedding-v2),不要用大模型的嵌入能力
  3. 检索结果不是越多越好:太多无关内容会污染模型上下文,一般 k 设 3-5 就够
  4. 提示词一定要加边界限制:明确告诉模型只能用参考文档回答,避免幻觉问题

3.3 多轮对话进阶:聊一百轮也不崩的上下文管理

import 'dotenv/config';
import { ChatDeepSeek } from '@langchain/deepseek';
import { ChatPromptTemplate, MessagesPlaceholder } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
import { ChatMessageHistory } from '@langchain/community/stores/message/in_memory';

// 1. 构建带历史消息的提示词模板
const chatPrompt = ChatPromptTemplate.fromMessages([
  ["system", "你是一个友好的前端技术助手,专业解答前端相关问题,回答简洁易懂"],
  // 关键:MessagesPlaceholder用来存放历史对话消息
  new MessagesPlaceholder("chat_history"),
  ["human", "{input}"],
]);

// 2. 初始化模型和Chain
const model = new ChatDeepSeek({ model: 'deepseek-reasoner', temperature: 0.7 });
const outputParser = new StringOutputParser();
const chatChain = chatPrompt | model | outputParser;

// 3. 给对话加上历史消息管理
const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chatChain,
  // 按sessionId存储不同用户的对话历史
  getMessageHistory: (sessionId) => new ChatMessageHistory(),
  inputMessagesKey: "input",
  historyMessagesKey: "chat_history",
});

// 4. 多轮对话测试
const res1 = await chainWithHistory.invoke(
  { input: "我现在在学React,应该先学什么?" },
  { configurable: { sessionId: "user_001" } }
);
console.log("AI回答1:", res1);

const res2 = await chainWithHistory.invoke(
  { input: "那学完这些之后呢?" },
  { configurable: { sessionId: "user_001" } }
);
console.log("AI回答2:", res2);

进阶优化:解决 token 爆仓问题

import { ConversationSummaryMemory } from 'langchain/memory';

// 初始化摘要内存,自动总结对话历史
const memory = new ConversationSummaryMemory({
  llm: new ChatDeepSeek({ model: 'deepseek-reasoner' }),
  returnMessages: true,
  memoryKey: "chat_history",
});

// 每次对话结束,自动把新的对话内容更新到摘要里
// 不管聊多少轮,摘要都只会保留关键信息,token占用极低

四、LangChain 生产级避坑指南:90% 的人都踩过这些坑

坑 1:乱用 PromptTemplate,对话模型效果直接打对折

很多人不管什么场景都用 PromptTemplate,但对于对话模型,ChatPromptTemplate 的角色分离能力,能让模型的效果和可控性提升一个量级。记住:只要用的是 Chat 模型,优先用 ChatPromptTemplate。

坑 2:文本分块一刀切,检索永远答非所问

不要随便抄个 chunkSize=1000 就用,不同的文档类型,分块策略完全不一样:

  • 技术文档、合同条款:chunkSize 小一点,300-800,保证语义完整
  • 小说、长文:chunkSize 可以大一点,800-1500,保留上下文
  • 一定要加 chunkOverlap,一般是 chunkSize 的 10%-20%

坑 3:不做 token 管理,聊几句就爆上下文

永远不要把完整的历史消息全丢给模型,一定要做上下文管理:要么用滑动窗口只保留最近的几轮对话,要么用摘要内存压缩历史消息,不然 token 账单和报错会教你做人。

坑 4:没有错误处理和降级,线上一限流就崩

LLM 的 API 不是 100% 稳定的,超时、限流、报错是常有的事。生产环境一定要加重试机制和降级策略,LCEL 的 withRetrywithFallbacks 直接用,不要自己手写一堆 try/catch。

坑 5:不用 LCEL,硬写胶水代码,维护到哭

很多人还在用旧的 LLMChain、RetrievalQA,甚至自己手写 Promise 串流程,代码又臭又长,改一个需求就要重构一半。赶紧拥抱 LCEL,声明式的写法,可组合、可复用、好维护,谁用谁知道。

坑 6:硬编码 API 密钥,上线直接被刷爆欠费

永远不要把 API 密钥硬编码在代码里!一定要用环境变量加载,生产环境用云服务的密钥管理服务,不然代码一提交到 GitHub,密钥直接被爬虫爬走,一夜之间欠费几万块。


五、进阶玩法拓展:LangChain 生态的王炸组合

  • LangSmith:LangChain 官方的调试、监控、评估平台,能看到每一次调用的完整链路、token 消耗、耗时,还能给模型的输出做评分,调试 AI 应用跟调试前端代码一样简单。
  • LangGraph:基于 LangChain 的智能体工作流框架,能实现更复杂的循环、分支、多智能体协作,比如代码生成机器人(写代码→执行→调试→重写)、智能客服机器人(检索→判断→转人工),能实现普通 Chain 做不到的复杂业务逻辑。

结语

其实 LangChain 从来都不是什么高深的东西,它只是把 AI 应用开发里的通用能力做了封装,让我们这些开发者不用重复造轮子,能专注于业务逻辑本身。

对于咱们前端全栈开发者来说,JS/TS 版本的 LangChain,就是我们进入 AI 应用开发领域最好的入场券——不用学新的语言,不用换技术栈,用你最熟悉的代码,就能写出顶级的 AI 应用。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions