作为一名前端开发老程序员,这次给大家更新一个前端学AI的突破口:LangChain.js,希望能有所帮助。
背景
随着大语言模型(LLM)的快速发展,开发者需要高效的工具链来集成模型能力到实际应用中。
LangChain 是一个开源框架,旨在简化基于语言模型的应用程序开发,提供模块化的组件(如模型调用、记忆管理、工具集成等),可以简单类比Java界的Spring框架来理解,Nodejs界的express/chair等。
本文将通过一个示例项目,展示如何用 LangChain.js 实现一个对话机器人,结合 Ollama(本地运行开源模型)和 通义千问(国产大模型API),并提供完整的前后端代码。
LangChain.js 介绍
LainChain结构图:
其中:
- LainChain:提供底层的核心能力。LainGraph:提供流程编排能力。Integrations:提供扩展和集成能力。LangSmith:提供调试、监控、评测能力。LainGraph Platform:LangChain 的商业化大模型应用发布平台。
LangChain.js 是基于Langchain的 JavaScript/TypeScript 版本,支持在浏览器、Node.js 等环境中快速构建AI应用,除此之外还有Python版本。
LangChain.js 支持多种 LLM 提供商(如 OpenAI、Ollama 等),并提供了灵活的接口,使得开发者可以轻松集成不同的模型和服务,主要包括以下模块包:
- langchain-core: 提供基础抽象和核心运行时机制(聊天模型、向量存储、工具等)的抽象接口和组装方式。langchain: langchain的主包,包含了内置的通用的链(chains)、代理(agents)、检索策略(retrieval strategies),不包含第三方集成。langchain-community: 由LangChain社区维护的第三方集成包,包括 OpenAI、Anthropic 等 LLM,以及向量存储(如 Pinecone)、工具(如 Tavily 搜索)等。
LangChain.js 基础API详解
LangChain 的 API 设计以模块化和链式调用为核心,下面是一些基础 API 的简单介绍:
模型调用(Models)
LangChain 支持三类核心模型接口,满足不同场景需求:
LLM
基础文本生成模型(如 GPT-3.5 ):
- 核心方法:
const model = new OpenAI({ temperature: 0.7 }); await model.call("你好,介绍react");
ChatModals
对话式模型(如 GPT-4 ):
- 核心方法:predictMessages()
const chat = new ChatOpenAI();await chat.predictMessages([new HumanMessage("你好!")]);
Embeddings
文本向量化模型(用于语义检索、聚类)
- 核心方法:embedQuery() / embedDocuments()
const embeddings = new OpenAIEmbeddings();const vec = await embeddings.embedQuery("react技术");
关键参数说明:
- temperature(0-2):控制生成随机性,值越高结果越多样。maxTokens:限制生成文本的最大长度。streaming:启用流式传输(适合实时聊天场景)。
提示模板(Prompts)
通过模板动态生成提示词,支持结构化输入与输出控制:
基础模板
import { PromptTemplate } from"@langchain/core/prompts";// 单变量模板const template = "用{style}风格翻译以下文本:{text}";const prompt = new PromptTemplate({ template, inputVariables: ["style", "text"], });// 使用示例const formattedPrompt = await prompt.format({ style: "文言文", text: "Hello world",});// 输出:"用文言文风格翻译以下文本:Hello world"
Few-shot模板
嵌入示例提升模型表现:
import { FewShotPromptTemplate } from"langchain/prompts";const examples = [ { input: "高兴", output: "欣喜若狂" }, { input: "悲伤", output: "心如刀绞" }];const examplePrompt = new PromptTemplate({ template: "输入:{input}\n输出:{output}", inputVariables: ["input", "output"],});const fewShotPrompt = new FewShotPromptTemplate({ examples, examplePrompt, suffix: "输入:{adjective}\n输出:", inputVariables: ["adjective"],});await fewShotPrompt.format({ adjective: "愤怒" });/* 输出:输入:高兴输出:欣喜若狂输入:悲伤输出:心如刀绞输入:愤怒输出: */
文件模板加载
从外部文件读取模板:
import { PromptTemplate } from "@langchain/core/prompts";import fs from "fs";const template = fs.readFileSync("./prompts/email_template.txt", "utf-8");const prompt = new PromptTemplate({ template, inputVariables: ["userName", "product"],});
链式调用(Chains)(langchain核心)
通过组合多个组件构建复杂工作流,是langchain核心模块:
LLMChain(基础链)
import { LLMChain } from "langchain/chains";const chain = new LLMChain({ llm: new OpenAI(), prompt: new PromptTemplate({ template: "生成关于{topic}的{num}条冷知识", inputVariables: ["topic", "num"] }),});const res = await chain.call({ topic: "react", num: 3 });// 输出:模型生成的3条react冷知识
SequentialChain**(顺序链)
串联多个链实现分步处理:
import { SequentialChain } from "langchain/chains";const chain1 = new LLMChain({ ... }); // 生成文章大纲const chain2 = new LLMChain({ ... }); // 扩展章节内容const chain3 = new LLMChain({ ... }); // 优化语言风格const overallChain = new SequentialChain({ chains: [chain1, chain2, chain3], inputVariables: ["title"], outputVariables: ["outline", "content", "final"],});const result = await overallChain.call({ title: "前端已死,还有未来" });
TransformChain(转换链)
自定义数据处理逻辑:
import { TransformChain } from "langchain/chains";const transform = new TransformChain({ transform: async (inputs) => { // 自定义处理逻辑(如文本清洗) return { cleaned: inputs.text.replace(/\d+/g, "") }; }, inputVariables: ["text"], outputVariables: ["cleaned"],});
文档加载器(Document Loaders)
支持从多种来源加载结构化文档,可以用来RAG的知识库录入:
- 本地文件:加载 文本/PDF/Word 文档。
const loader = new TextLoader("example.txt");const docs = await loader.load();const loader2 = new PDFLoader("report.pdf");const docs2 = await loader2.load();console.log({ docs });console.log({ docs2 });
- 网页内容:抓取指定 URL 的 HTML 内容。
const loader = new CheerioWebBaseLoader("https://exampleurl.com");const docs = await loader.load();console.log({ docs });
- 数据库:从 MySQL/MongoDB 读取数据。
import { createClient } from"@supabase/supabase-js";import { OpenAIEmbeddings } from"@langchain/openai";import { SupabaseHybridSearch } from"@langchain/community/retrievers/supabase";// 初始化 Supabase 客户端const client = createClient( process.env.SUPABASE_URL, process.env.SUPABASE_PRIVATE_KEY);// 创建混合检索器const retriever = new SupabaseHybridSearch(new OpenAIEmbeddings(), { client, similarityK: 5, // 语义搜索返回结果数 keywordK: 3, // 关键词搜索返回结果数 tableName: "documents", // 数据库表名 similarityQueryName: "match_documents", // 语义搜索函数名 keywordQueryName: "kw_match_documents", // 关键词搜索函数名// 高级参数 reRanker: (results) => { // 自定义结果合并策略(如加权分数) return results.sort((a, b) => b.score - a.score); }});
- 云存储:从 AWS S3/GCS** 加载文件。
const loader = new S3Loader({ bucket: "my-document-bucket-123", key: "AccountingOverview.pdf", s3Config: { region: "us-east-1", credentials: { accessKeyId: "<YourAccessKeyId>", secretAccessKey: "<YourSecretAccessKey>", }, }, unstructuredAPIURL: "<YourUnstructuredAPIURL>", unstructuredAPIKey: "<YourUnstructuredAPIKey>",});const docs = await loader.load();
文档分块处理
可以用来做embeddings:
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, // 单块最大字符数 chunkOverlap: 50, // 块间重叠字符});const docs = await loader.load();const splitDocs = await splitter.splitDocuments(docs);
上下文管理(Memory)
管理对话或任务的上下文状态:
对话记忆
import { BufferMemory } from "langchain/memory";const memory = new BufferMemory({ memoryKey: "chat_history", // 存储对话历史的字段名});const chain = new ConversationChain({ llm: new ChatOpenAI(), memory });// 连续对话await chain.call({ input: "你好!" });await chain.call({ input: "刚才我们聊了什么?" }); // 模型能回忆历史
实体记忆
跟踪特定实体信息:
import { EntityMemory } from "langchain/memory";const memory = new EntityMemory({ llm: new OpenAI(), entities: ["userName", "preferences"], // 需要跟踪的实体});await memory.saveContext( { input: "我叫小明,喜欢研究react技术" }, { output: "已记录您的偏好" });const currentEntities = await memory.loadEntities();// 输出:{ userName: "小明", preferences: "react技术" }
输出解析(Output Parsers)
将模型返回的文本转换为结构化数据,实用功能:
基础解析器
import { StringOutputParser } from "@langchain/core/output_parsers";const chain = prompt.pipe(model).pipe(new StringOutputParser());const result = await chain.invoke({ topic: "react技术" });// result "React技术是一个用于构建用户界面的JavaScript库,其核心用途包括:1) 组件化开发,2) 虚拟DOM高效更新,3) 支持单页应用(SPA)开发。"
结构化解析,json
import { StructuredOutputParser } from"langchain/output_parsers";// 定义输出Schemaconst parser = StructuredOutputParser.fromZodSchema( z.object({ title: z.string().describe("生成的文章标题"), keywords: z.array(z.string()).describe("3-5个关键词"), content: z.string().describe("不少于500字的内容") }));const chain = new LLMChain({ llm: model, prompt: new PromptTemplate({ template: "根据主题{topic}写一篇文章\n{format_instructions}", inputVariables: ["topic"], partialVariables: { format_instructions: parser.getFormatInstructions() } }), outputParser: parser});const res = await chain.call({ topic: "react技术" });/*{ title: "React技术:现代前端开发的核心理念", keywords: ["组件化", "虚拟DOM", "Hooks", "SPA", "状态管理"], content: "React是由Facebook开发的一个用于构建用户界面的JavaScript库...(至少500字)"}*/
工具与代理(Tools & Agents)
集成外部API扩展模型能力:
预定义工具
使用langchain内置的工具:
import { Calculator, SerpAPI } from "langchain/tools";const tools = [ new Calculator(), // 数学计算 new SerpAPI(), // 实时网络搜索 new WolframAlphaTool(), // 科学计算];
自定义工具
自定义开发工具:
import { DynamicTool } from "langchain/tools";export const weatherTool = new DynamicTool({ name: "get_weather", description: "查询指定城市的天气", func: async (city) => { const apiUrl = `https://api.weather.com/${city}`; return await fetch(apiUrl).then(res => res.json()); }});
代理执行:
import { initializeAgentExecutorWithOptions } from "langchain/agents";import { weatherTool } from 'weatherToolconst executor = await initializeAgentExecutorWithOptions( tools:[weatherTool], new ChatOpenAI({ temperature: 0 }), { agentType: "structured-chat-zero-shot-react-description" });const res = await executor.invoke({ input: "上海当前温度是多少?比纽约高多少摄氏度?"});// 模型将自动调用天气查询工具和计算器
structured-chat-zero-shot-react-description是 LangChain 框架中一种 结构化对话代理(Structured Chat Agent) 的类型,专为让大模型(如 GPT-4)无需示例学习(Zero-Shot) 即可调用外部工具链而设计。
检索增强(Retrieval)RAG
结合向量数据库实现知识增强:
import { MemoryVectorStore } from"langchain/vectorstores/memory";import { OpenAIEmbeddings } from"@langchain/openai";// 1. 加载文档并向量化const vectorStore = await MemoryVectorStore.fromDocuments( splitDocs, new OpenAIEmbeddings());// 2. 语义检索const results = await vectorStore.similaritySearch("神经网络的发展历史", 3);// 3. 将检索结果注入提示词const chain = createRetrievalChain({ retriever: vectorStore.asRetriever(), combineDocsChain: new LLMChain(...)});
示例:对话机器人
后端实现
安装依赖
安装langchainjs相关模块:
"@langchain/community": "^0.3.40","@langchain/core": "^0.3.44","@langchain/ollama": "^0.2.0","langchain": "^0.3.21",
由于需要提供http服务托管前端页面,需要安装express:
"@types/express": "^5.0.1","express": "^5.1.0",
调用本地模型
一个简单的通过ollama调用本地模型的例子:
import { Ollama,ChatOllama } from"@langchain/ollama"asyncfunction main(): Promise<void> {const ollamaLlm = new Ollama({ baseUrl: "http://127.0.0.1:11434", model: "deepseek-r1:7b", });const stream = await ollamaLlm.stream( `你谁,擅长什么?` );forawait (const chunk of stream) { process.stdout.write(chunk); }}main().catch(error => {console.error("程序执行出错:");console.error(error);});
结合上下文,进行连续调用:
import { Ollama,ChatOllama } from"@langchain/ollama"import { SystemMessage, HumanMessage } from"@langchain/core/messages"; asyncfunction mainChat(): Promise<void> {const chatModel = new ChatOllama({ baseUrl: "http://127.0.0.1:11434", model: "deepseek-r1:7b", });const stream = await chatModel.stream([ new SystemMessage("角色:一个前端技术专家 擅长:擅长回答前端技术相关的问题。"), new HumanMessage("你谁,擅长什么?"), ]);forawait (const chunk of stream) { process.stdout.write(chunk.text); }}main().catch(error => {console.error("程序执行出错:");console.error(error);});
在Langchain中,Ollama,ChatOllama分别是对ollama工具的封装,Ollama用于基础文本生成,ChatOllama专为对话设计,支持多消息类型和上下文管理。需要将这些区别清晰地传达给用户。
调用外部模型-通义
- 前往 阿里云通义千问控制台创建API Key(需实名认证)获取模型服务地址(例如
qwen-turbo
或 qwen-plus
)根据文档,简单通过fetch实现对通义的调用:
/** * 流式响应处理函数 * @param apiEndpoint - 通义千问API端点地址 * @param apiKey - API认证密钥 * @param modelName - 使用的大模型名称(例:qwen-max) * @param prompt - 用户输入的提示词 * @param onCallback - 每收到一个token时的回调函数 * * 实现流程: * 1. 发送携带prompt的POST请求到API端点 * 2. 处理流式响应数据 * 3. 解析并提取每个数据块中的文本内容 * 4. 通过回调函数实时返回生成的文本 */exportconst streamResponseChunks = async ({ apiEndpoint, apiKey, modelName, prompt, onCallback}: { apiEndpoint: string, apiKey: string, modelName: string, prompt: string, onCallback: (text: string) =>void}) => { // 发送POST请求到通义千问API const response = await fetch(apiEndpoint, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}`// 使用Bearer Token认证 }, body: JSON.stringify({ model: modelName, messages: [{ // 构造消息体 role: "user", content: prompt }], stream: true// 启用流式传输 }) }); // 处理HTTP错误响应 if (!response.ok || !response.body) { const errorResponse = await response.json(); thrownewError(JSON.stringify(errorResponse)); } // 创建流式读取器 const reader = response.body.getReader(); const decoder = new TextDecoder(); // 用于解码二进制流数据 // 持续读取流数据 while (true) { const { done, value } = await reader.read(); if (done) break; // 流读取结束 const chunk = decoder.decode(value); const lines = chunk.split("\n"); // 按行分割数据块 // 处理每行数据 for (const line of lines) { if (line.trim() === "") continue; // 跳过空行 try { // 处理流结束标记 if (line === 'data: [DONE]') { console.log('流式响应结束'); continue; } // 验证数据格式 if (!line.startsWith('data: ')) { console.log('跳过非数据行:', line); continue; } // 解析JSON数据 const jsonStr = line.replace(/^data: /, ''); const data = JSON.parse(jsonStr); // 提取生成的文本内容 if (data.choices?.[0]?.delta?.content) { const text = data.choices[0].delta.content; onCallback(text); // 触发回调函数 } } catch (e) { console.log('解析错误:', e); console.log('错误数据:', line); } } }};
上面代码根据注释理解即可,主要是发起http调用,流式返回结果。
基于BaseLLM/BaseModal封装
上面代码中,只是简单使用fetch来调用通义模型提供的接口,但是如果要使用到langchain,则需要遵循BaseLLM/BaseModal来将上面的逻辑进行封装,以便符合langchain的规范:
import { BaseLLM, BaseLLMParams } from"@langchain/core/language_models/llms";import { CallbackManagerForLLMRun } from"@langchain/core/callbacks/manager";import { GenerationChunk } from"@langchain/core/outputs";import { LLMResult, Generation } from"@langchain/core/outputs";interface CustomLLMParams extends BaseLLMParams { apiKey: string; modelName: string; apiEndpoint: string;}exportclass CustomLLM extends BaseLLM { apiKey: string; modelName: string; apiEndpoint: string;constructor(params: CustomLLMParams) { super(params); this.apiKey = params.apiKey; this.modelName = params.modelName; this.apiEndpoint = params.apiEndpoint; } _llmType(): string { return"tongyi"; }async *_streamResponseChunks( prompt: string, options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun ): AsyncGenerator<GenerationChunk> { const response = await fetch(this.apiEndpoint, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${this.apiKey}` }, body: JSON.stringify({ model: this.modelName, messages: [ { role: "user", content: prompt } ], stream: true }) }); if (!response.ok || !response.body) { const errorResponse = await response.json(); thrownewError(JSON.stringify(errorResponse)); } const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split("\n"); for (const line of lines) { if (line.trim() === "") continue; try { // 检查是否是结束标记 if (line === 'data: [DONE]') { console.log('流式响应结束'); continue; } // 确保数据以 "data: " 开头 if (!line.startsWith('data: ')) { console.log('跳过非数据行:', line); continue; } const jsonStr = line.replace(/^data: /, ''); const data = JSON.parse(jsonStr); if (data.choices?.[0]?.delta?.content) { const text = data.choices[0].delta.content; const generationChunk = new GenerationChunk({ text: text, generationInfo: {} }); yield generationChunk; await runManager?.handleLLMNewToken(text); } } catch (e) { console.log('解析错误:', e); console.log('错误数据:', line); } } } } async _generate( prompts: string[], options: this["ParsedCallOptions"], runManager?: CallbackManagerForLLMRun ): Promise<LLMResult> { const prompt = prompts[0]; const chunks: Generation[] = []; forawait (const chunk of this._streamResponseChunks(prompt, options, runManager)) { const text = chunk.text; // 实时输出到控制台 process.stdout.write(text); chunks.push({ text }); } // 输出换行 process.stdout.write('\n'); return { generations: [chunks] }; } async streamResponse( prompt: string, options: this["ParsedCallOptions"], onToken: (token: string) =>void ): Promise<void> { forawait (const chunk of this._streamResponseChunks(prompt, options)) { onToken(chunk.text); } }}
上面代码中,我们封装了一个CustomLLM类继承自BaseLLM,然后将参数作为构造方法的参数传入,同时将之前的fetch逻辑移到了 _streamResponseChunks 这个方法中,然后实现了一个 _generate 方法。
其中 _streamResponseChunks 和 _generate是langchain使用中的固定需要实现的方法,这样有助于在后续复杂的链式中简单调用,如果不理解记着这属于langchain的规范就行了,就像mvc框架需要有controller,service一样。
例如调用本地模型时,我们可以直接使用Ollama的ChatModal,那是由于langchain社区对Ollama统一进行了封装,如果社区没有,就可以自己继承BaseLLM/BaseModal来实现。
除了BaseLLM外,langchain还提供了BaseChatModal,其中:
- 接口设计差异 :
- BaseLLM :面向纯文本输入( string[] )BaseChatModel :面向结构化消息( BaseMessage[] )
- 使用场景 :
- BaseLLM :适用于单轮问答场景BaseChatModel :适用于多轮对话场景
实例代码使用的是BaseLLM,当然也可以换成BaseChatModel。
express服务:
下面是express实现一个本地http服务,流式返回,3000端口:
import { Router } from'express';import express from'express';import { streamResponseChunks } from'./tongyi-chat.js';const router = Router();router.get('/streamChat', (req, res) => { // 设置 SSE 头 res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); const { prompt } = req.query; if (!prompt || typeof prompt !== 'string') { res.write(`data: ${JSON.stringify({ error: '缺少有效的prompt参数' })}\n\n`); res.end(); return; } // 处理客户端断开连接 req.on('close', () => { res.end(); }); // 开始流式响应 // 调用 streamResponseChunks});const app = express();const port = 3000;// 中间件app.use(express.json());app.use(express.static('public'));// 路由app.use('/api', router);// 启动服务器app.listen(port, () => { console.log(`服务器运行在 http://localhost:${port}`);});
前端实现
前端页面比较简单,只有一个输入框和问题/回答列表,直接用原生实现,不用框架,下面是前端页面的JavaScript代码:
<script> const chatbox = document.getElementById('chatbox');const userInput = document.getElementById('userInput');function appendMessage(text, isUser, element) { if (element) { element.innerHTML += text; // 使用innerHTML return element; } const div = document.createElement('div'); div.className = `message ${isUser ? 'user' : 'bot'}`; div.innerHTML = text; // 用户消息保持纯文本 chatbox.appendChild(div); chatbox.scrollTop = chatbox.scrollHeight; return div; }function sendRequest() { const input = userInput.value; userInput.value = ''; // 清空输入框 appendMessage(input, true); let botMessage = null; // 用于跟踪当前bot消息元素 const eventSource = new EventSource(`/api/streamChat?prompt=${encodeURIComponent(input)}`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.text) { // 首次创建bot消息元素,后续持续更新 botMessage = appendMessage(data.text, false, botMessage); } elseif (data.error) { appendMessage(`错误: ${data.error}`, false); eventSource.close(); } } catch (error) { } }; eventSource.onerror = () => { eventSource.close(); }; }</script>
需要注意的是,创建EventSource事件来接受后端的流式返回,同时前端在append的时候要注意累加然后整体替换,详细看appendMessage这个方法。
实现效果
运行:
npm run server
效果截图