掘金 人工智能 前天 15:46
前端学AI之LangChain.js入门教程:实现智能对话机器人
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文为前端开发者提供了LangChain.js的全面入门指南,介绍了LLM(大语言模型)背景下的开发需求,以及LangChain.js框架的核心概念和模块。通过一个对话机器人示例项目,展示了如何结合Ollama和通义千问等模型,实现前端AI应用的构建。文章深入讲解了LangChain.js的基础API,包括模型调用、提示模板、链式调用、文档加载器、上下文管理、输出解析、工具与代理、检索增强等,并提供了代码示例和实践指导,帮助开发者快速上手,构建基于AI的应用。

💡 LangChain.js 是一个基于 JavaScript/TypeScript 的开源框架,用于简化基于语言模型的应用程序开发,支持在浏览器、Node.js 等环境中快速构建AI应用,其核心功能包括模型调用、记忆管理、工具集成等。

✨ LangChain.js 的核心模块包括:langchain-core(提供基础抽象和核心运行时机制)、langchain(包含内置的通用链、代理、检索策略)、langchain-community(第三方集成包,如OpenAI、Ollama等),这些模块共同构建了LangChain.js的强大功能。

🛠️ LangChain.js 提供了丰富的 API,如模型调用(LLM、ChatModals、Embeddings)、提示模板(基础模板、Few-shot模板、文件模板加载)、链式调用(LLMChain、SequentialChain、TransformChain)、文档加载器、上下文管理、输出解析等,方便开发者构建复杂的AI应用。

作为一名前端开发老程序员,这次给大家更新一个前端学AI的突破口:LangChain.js,希望能有所帮助。

背景

随着大语言模型(LLM)的快速发展,开发者需要高效的工具链来集成模型能力到实际应用中。

LangChain 是一个开源框架,旨在简化基于语言模型的应用程序开发,提供模块化的组件(如模型调用、记忆管理、工具集成等),可以简单类比Java界的Spring框架来理解,Nodejs界的express/chair等。

本文将通过一个示例项目,展示如何用 LangChain.js 实现一个对话机器人,结合 Ollama(本地运行开源模型)和 通义千问(国产大模型API),并提供完整的前后端代码。

LangChain.js 介绍

LainChain结构图:

其中:

LangChain.js 是基于Langchain的 JavaScript/TypeScript 版本,支持在浏览器、Node.js 等环境中快速构建AI应用,除此之外还有Python版本。

LangChain.js 支持多种 LLM 提供商(如 OpenAI、Ollama 等),并提供了灵活的接口,使得开发者可以轻松集成不同的模型和服务,主要包括以下模块包:

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 ):

const chat = new ChatOpenAI();await chat.predictMessages([new HumanMessage("你好!")]);

Embeddings

文本向量化模型(用于语义检索、聚类)

const embeddings = new OpenAIEmbeddings();const vec = await embeddings.embedQuery("react技术");

关键参数说明:

提示模板(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 examplePromptnew PromptTemplate({  template"输入:{input}\n输出:{output}"inputVariables: ["input""output"],});const fewShotPromptnew 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 chainnew LLMChain({  llmnew OpenAI(),  promptnew PromptTemplate({    template"生成关于{topic}的{num}条冷知识",     inputVariables: ["topic""num"]  }),});const res = await chain.call({ topic"react", num3 });// 输出:模型生成的3条react冷知识

SequentialChain**(顺序链)

串联多个链实现分步处理:

import { SequentialChain } from "langchain/chains";const chain1new LLMChain({ ... }); // 生成文章大纲const chain2new LLMChain({ ... }); // 扩展章节内容const chain3new LLMChain({ ... }); // 优化语言风格const overallChainnew SequentialChain({  chains: [chain1, chain2, chain3],  inputVariables: ["title"],  outputVariables: ["outline""content""final"],});const result = await overallChain.call({ title"前端已死,还有未来" });

TransformChain(转换链)

自定义数据处理逻辑:

import { TransformChainfrom "langchain/chains";const transform = new TransformChain({  transformasync (inputs) => {    // 自定义处理逻辑(如文本清洗)    return { cleaned: inputs.text.replace(/\d+/g"") };  },  inputVariables: ["text"],  outputVariables: ["cleaned"],});

文档加载器(Document Loaders)

支持从多种来源加载结构化文档,可以用来RAG的知识库录入:

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 });
const loader = new CheerioWebBaseLoader("https://exampleurl.com");const docs = await loader.load();console.log({ docs });
import { createClient } from"@supabase/supabase-js";import { OpenAIEmbeddingsfrom"@langchain/openai";import { SupabaseHybridSearchfrom"@langchain/community/retrievers/supabase";// 初始化 Supabase 客户端const client = createClient(  process.env.SUPABASE_URL,  process.env.SUPABASE_PRIVATE_KEY);// 创建混合检索器const retriever = new SupabaseHybridSearch(new OpenAIEmbeddings(), {  client,  similarityK5,       // 语义搜索返回结果数  keywordK3,           // 关键词搜索返回结果数  tableName"documents"// 数据库表名  similarityQueryName"match_documents"// 语义搜索函数名  keywordQueryName"kw_match_documents"// 关键词搜索函数名// 高级参数  reRanker(results) => {    // 自定义结果合并策略(如加权分数)    return results.sort((a, b) => b.score - a.score);  }});
const loadernew 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 { RecursiveCharacterTextSplitterfrom "langchain/text_splitter";const splitter = new RecursiveCharacterTextSplitter({  chunkSize500,   // 单块最大字符数  chunkOverlap50// 块间重叠字符});const docs = await loader.load();const splitDocs = await splitter.splitDocuments(docs);

上下文管理(Memory)

管理对话或任务的上下文状态:

对话记忆

import { BufferMemoryfrom "langchain/memory";const memory = new BufferMemory({  memoryKey"chat_history"// 存储对话历史的字段名});const chain = new ConversationChain({   llmnew ChatOpenAI(),  memory });// 连续对话await chain.call({ input"你好!" });await chain.call({ input"刚才我们聊了什么?" }); // 模型能回忆历史

实体记忆

跟踪特定实体信息:

import { EntityMemoryfrom "langchain/memory";const memory = new EntityMemory({  llmnew OpenAI(),  entities: ["userName""preferences"], // 需要跟踪的实体});await memory.saveContext(  { input"我叫小明,喜欢研究react技术" },   { output"已记录您的偏好" });const currentEntities = await memory.loadEntities();// 输出:{ userName: "小明", preferences: "react技术" }

输出解析(Output Parsers)

将模型返回的文本转换为结构化数据,实用功能:

基础解析器

import { StringOutputParserfrom "@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 chainnew LLMChain({  llm: model,  promptnew 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, SerpAPIfrom "langchain/tools";const tools = [  new Calculator(),       // 数学计算  new SerpAPI(),          // 实时网络搜索  new WolframAlphaTool(), // 科学计算];

自定义工具

自定义开发工具:

import { DynamicToolfrom "langchain/tools";export const weatherTool = new DynamicTool({  name"get_weather"description"查询指定城市的天气"funcasync (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 { MemoryVectorStorefrom"langchain/vectorstores/memory";import { OpenAIEmbeddingsfrom"@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(),  combineDocsChainnew 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,ChatOllamafrom"@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,ChatOllamafrom"@langchain/ollama"import { SystemMessage, HumanMessagefrom"@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专为对话设计,支持多消息类型和上下文管理。需要将这些区别清晰地传达给用户。

调用外部模型-通义

根据文档,简单通过fetch实现对通义的调用:

/** * 流式响应处理函数 * @param apiEndpoint - 通义千问API端点地址 * @param apiKey - API认证密钥 * @param modelName - 使用的大模型名称(例:qwen-max) * @param prompt - 用户输入的提示词 * @param onCallback - 每收到一个token时的回调函数 *  * 实现流程: * 1. 发送携带prompt的POST请求到API端点 * 2. 处理流式响应数据 * 3. 解析并提取每个数据块中的文本内容 * 4. 通过回调函数实时返回生成的文本 */exportconst streamResponseChunksasync ({    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认证        },        bodyJSON.stringify({            model: modelName,            messages: [{  // 构造消息体                role"user",                content: prompt            }],            streamtrue// 启用流式传输        })    });    // 处理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, BaseLLMParamsfrom"@langchain/core/language_models/llms";import { CallbackManagerForLLMRunfrom"@langchain/core/callbacks/manager";import { GenerationChunkfrom"@langchain/core/outputs";import { LLMResult, Generationfrom"@langchain/core/outputs";interface CustomLLMParams extends BaseLLMParamsapiKeystringmodelNamestringapiEndpointstring;}exportclass CustomLLM extends BaseLLMapiKeystringmodelNamestringapiEndpointstring;constructor(params: CustomLLMParams) {    super(params);    this.apiKey = params.apiKey;    this.modelName = params.modelName;    this.apiEndpoint = params.apiEndpoint;  }  _llmType(): string {    return"tongyi";  }async *_streamResponseChunks(    promptstring,    optionsthis["ParsedCallOptions"],    runManager?: CallbackManagerForLLMRun  ): AsyncGenerator<GenerationChunk> {    const response = await fetch(this.apiEndpoint, {      method"POST",      headers: {        "Content-Type""application/json",        "Authorization"`Bearer ${this.apiKey}`      },      bodyJSON.stringify({        modelthis.modelName,        messages: [          {            role"user",            content: prompt          }        ],        streamtrue      })    });    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(        promptsstring[],        optionsthis["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(        promptstring,        optionsthis["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,当然也可以换成BaseChatModel。

express服务:

下面是express实现一个本地http服务,流式返回,3000端口:

import { Routerfrom'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

效果截图

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

LangChain.js 前端开发 LLM AI
相关文章