LangChain.js框架入门:从零构建LLM应用的组件化开发实践
2026/9/7 5:37:12 网站建设 项目流程

1. 项目概述:从“手搓”到“搭积木”的思维跃迁

如果你和我一样,是从零开始接触大语言模型应用开发的,那你大概率也经历过一个阶段:面对一个需求,比如“让AI根据我的文档回答问题”,第一反应就是打开代码编辑器,开始“手搓”代码。从调用OpenAI的API,到处理上下文长度,再到设计提示词模板,每一步都亲力亲为。这个过程很锻炼人,但也很容易陷入细节的泥潭,代码结构混乱,可维护性差,而且一旦需求稍微复杂一点,比如需要联网搜索或者调用工具,整个架构就得推倒重来。

“LangChain.js 初探:从手写代码到框架思维”这个标题,精准地捕捉到了这个关键的转折点。它描述的不仅仅是学习一个名为LangChain.js的JavaScript/TypeScript框架,更是一种开发范式的转变——从面向过程的、胶水式的代码编写,转向面向组件的、声明式的应用构建。LangChain.js不是一个魔法黑盒,它更像是一套精心设计的、标准化的“乐高积木”。它把LLM应用开发中那些重复、繁琐但又至关重要的环节(如模型调用、提示词管理、记忆、工具使用、数据检索等)抽象成了一个个可插拔的组件。我们的工作,从“从零制造每一个零件”,变成了“如何更优雅、更高效地组合这些现成的、经过验证的零件”。

这次初探的核心价值,就在于理解这套“积木”的搭建逻辑。我们将不再满足于仅仅跑通一个“Hello World”示例,而是要深入其设计哲学,弄明白为什么需要LLMChainAgentTool是如何协作的,RetrieverVector Store背后又隐藏着怎样的数据流。掌握了这种框架思维,你就能在面对“构建一个能联网、能查数据库、能进行多轮对话的智能客服”这类复杂需求时,不再感到无从下手,而是能清晰地将其拆解为链、代理、记忆、工具等模块,并快速组合实现。这对于任何希望将LLM能力产品化、工程化的开发者来说,都是一项必备的核心技能。

2. 框架思维解析:LangChain.js 的核心设计哲学

在开始写第一行LangChain.js代码之前,我们必须先理解它试图解决的根本问题,以及它为此提出的抽象模型。这决定了我们是用“框架的方式”还是用“旧脚本的方式”来使用它。

2.1 为何需要框架:手写代码的典型困境

让我们用一个具体的场景来对比。假设我们需要构建一个简单的“公司产品问答机器人”,它需要基于内部产品文档来回答用户问题。

手写代码的典型路径:

  1. 硬编码提示词:在代码里写死一个字符串,比如“请根据以下文档回答问题:{context}\n\n问题:{question}”
  2. 手动处理上下文:写一个函数,将用户问题和检索到的文档片段拼接起来,同时要小心翼翼地计算token数量,确保不超过模型限制。如果超了,还得自己写逻辑去截断或总结文档。
  3. 直接调用API:使用fetchaxios直接向OpenAI、Anthropic等服务的端点发送请求。
  4. 解析响应:手动解析返回的JSON,提取出content字段。
  5. 错误处理与重试:自己实现网络错误、速率限制、模型过载等情况的处理逻辑。

这段代码可能一开始只有几十行,看起来很简单。但问题很快就会接踵而至:

  • 需求变更:老板说,回答时语气要更友好一些。于是你不得不去修改那个硬编码的提示词字符串,并确保所有用到的地方都同步更新。
  • 增加功能:需要支持联网搜索。你不得不引入一个新的API,并重写整个请求流程,将搜索结果的整合逻辑硬塞进去。
  • 切换模型:想试试Claude或者本地部署的Ollama。你需要重写API调用部分,处理不同的请求格式和参数名。
  • 代码复用:另一个项目也需要类似的功能,你发现很难把这块逻辑干净地抽离出来复用。

你的代码库会迅速变成一个充满胶水代码、条件判断和重复逻辑的“泥球”。

2.2 LangChain.js 的抽象:组件化与声明式

LangChain.js的解决方案是提供一套高层次的抽象。它将LLM应用视为一个由标准化组件构成的数据流管道。核心抽象包括:

  • 模型 (Models):不仅仅是LLM,还包括聊天模型、嵌入模型。它封装了不同供应商(OpenAI, Anthropic, Google等)的API差异,提供统一的调用接口。你不再关心是发POST请求到https://api.openai.com/v1/chat/completions还是其他地址,你只关心model.invoke(prompt)
  • 提示词 (Prompts):将提示词从字符串升级为可模板化的对象。PromptTemplate允许你定义带有变量的模板(如{context}, {question}),并安全、方便地进行填充。还有更强大的ChatPromptTemplate,可以轻松构建包含系统消息、用户消息、历史消息的复杂对话提示。
  • 链 (Chains):这是LangChain的灵魂。一个链将多个组件(模型、提示词、工具,甚至其他链)按特定顺序组合起来,完成一个特定任务。最简单的LLMChain就是“提示词模板 + 模型”的组合。链将你的业务逻辑从具体的API调用中解耦出来。
  • 检索器 (Retrievers):专门负责从外部数据源(如向量数据库、Wikipedia)中,根据查询获取相关文档。它抽象了检索过程,你只需关心“查什么”,而不必关心“怎么查”(是用的余弦相似度还是其他算法)。
  • 代理 (Agents):这是实现复杂推理和工具调用的高级抽象。一个代理包含一个LLM、一系列Tools和一个决定如何调用这些工具的AgentExecutor。代理让LLM具备了“思考-行动-观察”循环的能力。
  • 记忆 (Memory):用于在对话或多次调用间持久化状态(如聊天历史)。它可以是简单的缓冲区,也可以是更复杂的、基于向量存储的长期记忆。

框架思维的核心就在于:当你拿到一个需求时,你的第一反应不再是“我要写一个函数,里面先做A,再做B,然后调用C...”,而是“这个需求可以由哪几个LangChain组件构成?它们之间的数据流是怎样的?”。你从编写指令式代码,转变为组装声明式管道。

3. 从零构建你的第一个LangChain.js应用

理论说得再多,不如亲手搭建一个。我们从最简单的开始,逐步增加复杂度,体会框架带来的便利。请确保你已安装Node.js环境(建议18.x或更高版本),并初始化了一个新的Node.js项目。

3.1 环境准备与基础链搭建

首先,安装核心依赖:

npm install langchain @langchain/openai

这里我们安装了langchain核心包和OpenAI的官方集成包@langchain/openai。LangChain社区现在更推荐使用这种按供应商划分的独立包,它们更新更及时,与官方SDK结合更紧密。

接下来,我们构建一个最简单的链,它接受一个主题,让AI生成一首关于该主题的俳句。

import { ChatOpenAI } from "@langchain/openai"; import { PromptTemplate } from "@langchain/core/prompts"; import { LLMChain } from "langchain/chains"; // 1. 初始化模型 // 记得将你的OpenAI API Key设置为环境变量 OPENAI_API_KEY const model = new ChatOpenAI({ modelName: "gpt-4o-mini", // 或 "gpt-3.5-turbo" temperature: 0.7, // 控制创造性,0更确定,1更多变 }); // 2. 创建提示词模板 const promptTemplate = PromptTemplate.fromTemplate( `你是一位诗人。请以{theme}为主题,创作一首俳句。俳句应遵循三行、五七五音节的格式。` ); // 3. 将模板和模型组合成链 const haikuChain = new LLMChain({ llm: model, prompt: promptTemplate, }); // 4. 运行链 async function generateHaiku() { const theme = "秋天的黄昏"; const result = await haikuChain.invoke({ theme: theme }); console.log(`主题:${theme}`); console.log(`生成的俳句:\n${result.text}`); } generateHaiku().catch(console.error);

实操心得:

  • API Key管理:永远不要将API Key硬编码在代码中。使用process.env.OPENAI_API_KEY从环境变量读取。可以创建.env文件并使用dotenv包来管理。
  • 模型选择ChatOpenAI默认使用gpt-3.5-turbo,性价比高。对于复杂任务,可以指定modelName: "gpt-4""gpt-4o"temperature参数很关键,生成创意内容时可调高(如0.8-0.9),需要稳定输出事实时调低(如0-0.2)。
  • LLMChain.invoke:这是运行链的标准方法。它接受一个输入对象(键值对),键名必须与提示词模板中的变量名(本例中的{theme})一致。

这个简单的例子已经体现了框架的优势:提示词和模型逻辑分离。如果你想修改提示词,只需改动promptTemplate,无需触及模型调用代码。

3.2 引入检索与记忆:构建对话式问答机器人

现在,我们构建一个更实用的应用:一个能基于自定义知识库进行多轮对话的问答机器人。这需要用到检索记忆组件。

假设我们有一些关于“MyCompany”产品的文本资料(company_docs.txt)。我们将:

  1. 将这些文档切片并转换为向量(嵌入),存入向量数据库。
  2. 根据用户问题,检索最相关的文档片段作为上下文。
  3. 将“上下文 + 历史对话 + 当前问题”组合成提示词,交给LLM生成答案。

这里我们使用内存型的向量数据库MemoryVectorStore和OpenAI的嵌入模型,方便演示。

npm install @langchain/openai langchain
import { ChatOpenAI, OpenAIEmbeddings } from "@langchain/openai"; import { MemoryVectorStore } from "langchain/vectorstores/memory"; import { RecursiveCharacterTextSplitter } from "langchain/text_splitter"; import { PromptTemplate } from "@langchain/core/prompts"; import { StringOutputParser } from "@langchain/core/output_parsers"; import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables"; import { BufferMemory } from "langchain/memory"; import { readFileSync } from "fs"; // 1. 准备数据并创建向量存储 async function createVectorStore() { const text = readFileSync("./company_docs.txt", "utf-8"); // 文本分割器:将长文档切成适合模型上下文的小块 const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 500, // 每个块的大小(字符数) chunkOverlap: 50, // 块之间的重叠,避免语义被切断 }); const docs = await textSplitter.createDocuments([text]); // 使用OpenAI嵌入模型将文本块转换为向量 const embeddings = new OpenAIEmbeddings(); // 将向量存入内存向量库 const vectorStore = await MemoryVectorStore.fromDocuments(docs, embeddings); return vectorStore; } // 2. 构建包含检索和记忆的链 async function buildConversationalChain(vectorStore) { const model = new ChatOpenAI({ modelName: "gpt-4o-mini", temperature: 0 }); const retriever = vectorStore.asRetriever(3); // 检索最相关的3个文档块 // 定义提示词模板 const promptTemplate = PromptTemplate.fromTemplate(` 你是我公司“MyCompany”的智能客服助手。请严格根据以下提供的上下文信息来回答问题。如果上下文中没有明确答案,请如实告知“根据现有资料,我无法回答这个问题”,不要编造信息。 上下文信息: {context} 历史对话: {chat_history} 用户问题:{question} 助手回答:`); // 定义记忆(存储对话历史) const memory = new BufferMemory({ memoryKey: "chat_history", // 这个键名会对应提示词模板中的 {chat_history} returnMessages: true, // 以消息对象格式返回,适用于聊天模型 }); // 使用新的 Runnable 接口构建链(更灵活、推荐) const chain = RunnableSequence.from([ { // 第一步:从输入中提取问题,并检索上下文 question: (input) => input.question, chat_history: async () => { // 从记忆中加载历史 const { chat_history } = await memory.loadMemoryVariables({}); return chat_history || ""; }, context: async (input) => { // 根据问题检索相关文档 const docs = await retriever.invoke(input.question); return docs.map(doc => doc.pageContent).join("\n---\n"); }, }, promptTemplate, // 第二步:填充提示词模板 model, // 第三步:调用模型 new StringOutputParser(), // 第四步:解析模型输出为字符串 ]); return { chain, memory }; } // 3. 运行对话 async function runChat() { const vectorStore = await createVectorStore(); const { chain, memory } = await buildConversationalChain(vectorStore); const questions = [ "我公司的旗舰产品是什么?", "它有哪些主要功能?", "如何购买?" // 这个问题可能不在上下文中 ]; let currentInput = { question: "" }; for (const question of questions) { console.log(`\n用户: ${question}`); currentInput.question = question; const response = await chain.invoke(currentInput); console.log(`助手: ${response}`); // 将本轮问答保存到记忆中 await memory.saveContext( { question: question }, { answer: response } ); } } runChat().catch(console.error);

核心环节解析:

  1. 文本分割与向量化RecursiveCharacterTextSplitter是处理长文档的关键。它尝试按字符、句子、段落等递归地分割文本,尽可能保持语义完整。chunkOverlap设置重叠可以防止一个完整的句子被切成两半。
  2. 检索器 (Retriever)vectorStore.asRetriever(3)创建了一个检索器,它会在向量空间中查找与问题嵌入最相似的3个文档块。这是实现“基于文档问答”的核心。
  3. 记忆 (Memory)BufferMemory将对话历史存储在内存中。saveContext方法将用户问题和助手回答保存为一组,loadMemoryVariables则将其加载到提示词变量中。这使得机器人具备了多轮对话能力。
  4. RunnableSequence:这是LangChain较新且推荐的构建链的方式。它清晰地定义了数据流:输入 -> 检索/加载记忆 -> 填充提示词 -> 调用模型 -> 解析输出。每一步都是一个独立的“可运行单元”,组合起来非常灵活。

注意MemoryVectorStore仅用于演示,程序重启后数据会丢失。生产环境应使用持久化的向量数据库,如ChromaPineconeWeaviatepgvector(PostgreSQL扩展)。

3.3 进阶:打造能使用工具的智能代理

链是预定义的工作流,而代理(Agent)则赋予了LLM自主决策和调用工具的能力,适合处理开放式任务。让我们创建一个能查询天气和进行简单计算的代理。

npm install @langchain/community

我们需要社区工具包来获取一些现成的工具。

import { ChatOpenAI } from "@langchain/openai"; import { initializeAgentExecutorWithOptions } from "langchain/agents"; import { Calculator } from "@langchain/community/tools/calculator"; import { SerpAPI } from "@langchain/community/tools/serpapi"; // 注意:SerpAPI需要注册并获取API Key,此处仅为示例。也可使用其他工具如 Tavily Search。 async function createAgent() { const model = new ChatOpenAI({ modelName: "gpt-4o-mini", temperature: 0, }); // 定义工具 const tools = [ new Calculator(), // 计算器工具 new SerpAPI(process.env.SERPAPI_API_KEY), // 搜索引擎工具(需配置API Key) ]; // 创建代理执行器 const executor = await initializeAgentExecutorWithOptions( tools, model, { agentType: "openai-functions", // 使用OpenAI函数调用格式的代理,效果更好 verbose: true, // 开启详细日志,可以看到代理的“思考过程” } ); return executor; } async function runAgent() { const agent = await createAgent(); const queries = [ "北京今天的天气怎么样?", "那个温度换算成华氏度是多少?", "123的平方再加上45等于多少?" ]; for (const query of queries) { console.log(`\n用户: ${query}`); const result = await agent.invoke({ input: query }); console.log(`助手: ${result.output}`); } } runAgent().catch(console.error);

运行这段代码(并配置好SERPAPI_API_KEY),你会看到控制台输出类似以下内容:

用户: 北京今天的天气怎么样? [Agent] 思考:用户想知道北京的天气,我需要一个能获取实时天气信息的工具。我有SerpAPI。 [Agent] 行动:调用 SerpAPI 工具,参数:{ query: "北京今天天气" } [Agent] 观察:SerpAPI返回了天气信息:北京,晴,15-25°C... [Agent] 思考:我得到了天气信息,可以回答用户了。 助手: 北京今天天气晴朗,气温在15到25摄氏度之间。

代理工作流解析:

  1. 工具定义:每个工具(Tool)都有一个namedescription_call方法。LLM通过阅读工具的description来决定在什么情况下使用它。
  2. 代理决策:当代理收到输入时,LLM会根据输入和可用工具的description,决定是直接回答,还是调用某个工具。如果调用工具,它会生成符合工具要求的参数。
  3. 执行与观察:代理执行器(AgentExecutor)调用工具,并将工具返回的结果(observation)再次交给LLM。
  4. 循环:LLM根据观察结果,决定下一步是继续调用工具还是给出最终答案。这个过程会一直循环,直到LLM认为可以给出最终输出。

verbose: true选项让我们能窥见代理的“思考链”,这对于调试和理解代理行为至关重要。

4. 实战避坑指南与性能优化

在实际项目中应用LangChain.js,你会遇到一些常见陷阱。以下是我从多个项目中总结出的经验。

4.1 提示词工程:从模糊到精确

糟糕的提示词是LLM应用失败的首要原因。框架帮你管理了提示词,但内容还得你自己设计。

常见坑点:

  • 指令模糊:例如“总结这篇文档”。LLM可能生成过于简略或过于详细的内容。
  • 缺少上下文或格式要求:没有明确输出格式(JSON、列表、特定字数),导致后续处理困难。
  • “幻觉”问题:当检索的上下文不足时,LLM容易编造答案。

优化策略:

  1. 结构化提示词:使用ChatPromptTemplate.fromMessages来清晰定义角色。
    const prompt = ChatPromptTemplate.fromMessages([ ["system", "你是一个严谨的技术文档助手。你必须只根据提供的上下文回答问题。如果不知道,就说不知道。"], ["human", "上下文:{context}"], ["human", "问题:{question}"], ]);
  2. 提供少量示例 (Few-Shot):在提示词中给出一两个输入输出的例子,能显著提升模型在特定任务上的表现。
  3. 明确输出格式:在指令中直接要求,如“请用JSON格式输出,包含summarykeywords两个字段”。
  4. 设置严格的停止条件:对于生成任务,可以设置stop序列,防止模型跑偏。

4.2 检索质量:找到真正相关的信息

“垃圾进,垃圾出。”如果检索器找不到相关文档,再好的LLM也无力回天。

常见坑点:

  • 块大小不合适chunkSize太大,可能包含无关信息;太小,可能割裂了关键语义。
  • 检索数量不当:检索太多块(k值太大)会引入噪声,增加token消耗和成本;检索太少可能遗漏关键信息。
  • 嵌入模型不匹配:用于生成向量存储的嵌入模型与任务不匹配(例如,用通用嵌入模型处理专业医学文献)。

优化策略:

  1. 分块策略实验:不要只用RecursiveCharacterTextSplitter。对于代码,可以尝试LanguageTextSplitter;对于Markdown,可以按标题分割。关键是评估,看哪种分块方式在问答测试集上效果最好。
  2. 元数据过滤:在存入向量库时,为每个块添加元数据(如来源文件、章节标题、类型)。检索时,可以结合语义搜索和元数据过滤,提高精度。
    await vectorStore.addDocuments(docsWithMetadata); const retriever = vectorStore.asRetriever({ k: 5, filter: { source: "user_manual.pdf" } // 只从特定文件检索 });
  3. 重排序 (Re-ranking):在初步检索出N个结果后,使用一个更小、更快的重排序模型(如BAAI/bge-reranker)对结果进行精排,只将Top K个最相关的片段送入LLM。这能有效提升答案质量并降低成本。
  4. 混合搜索:结合语义搜索(向量相似度)和关键词搜索(如BM25)。LangChain的HybridSearchRetriever可以做到这一点,能同时捕获语义相似性和关键词匹配。

4.3 性能与成本控制

LLM API调用是按token计费的,且可能有延迟。不加以控制,成本和响应时间都会失控。

常见坑点:

  • 无节制地输入长上下文:将所有检索到的文档不经处理直接塞进提示词。
  • 重复调用相同内容:在多轮对话中,每次都将完整历史记录发送给模型。
  • 未处理速率限制和超时:导致应用不稳定。

优化策略:

  1. 上下文压缩:在将检索到的文档送入LLM前,先进行压缩或总结。LangChain提供了ContextualCompressionRetriever,可以搭配LLMChainExtractor等压缩器使用,只提取与问题最相关的句子。
  2. 流式输出:对于生成时间较长的内容,使用模型的流式响应(stream)来提升用户体验。
    const stream = await model.stream(prompt); for await (const chunk of stream) { process.stdout.write(chunk.content); }
  3. 缓存:对频繁出现的、结果确定的查询(如“公司的成立时间”)进行缓存。可以使用InMemoryCache或集成Redis等外部缓存。
    import { InMemoryCache } from "langchain/cache"; const cache = new InMemoryCache(); const model = new ChatOpenAI({ cache });
  4. 设置超时和重试:在初始化模型时配置timeoutmaxRetries
    const model = new ChatOpenAI({ modelName: "gpt-4", timeout: 10000, // 10秒超时 maxRetries: 2, });
  5. 监控与评估:记录每次调用的token使用量、成本和延迟。定期评估检索的准确率和回答的相关性,持续迭代优化提示词和检索策略。

4.4 调试与监控

当链或代理行为不符合预期时,系统的复杂性会让调试变得困难。

调试技巧:

  1. 善用verbose模式:在初始化链或代理时设置verbose: true,这能打印出每一步的输入输出,是理解数据流最直接的方法。
  2. 中间结果检查:对于复杂的RunnableSequence,可以在中间插入自定义函数来打印或检查数据。
    const debuggingChain = RunnableSequence.from([ firstStep, (input) => { console.log("After firstStep:", input); return input; }, // 调试钩子 secondStep, ]);
  3. LangSmith:这是LangChain官方提供的追踪和监控平台。它能可视化整个链的执行过程,记录每个步骤的输入输出、耗时和token使用,是进行复杂应用调试和性能分析的强大工具。只需设置环境变量LANGCHAIN_TRACING_V2=trueLANGCHAIN_API_KEY,你的调用数据就会自动发送到LangSmith。

从手写代码到拥抱LangChain.js这样的框架,最大的转变不是语法,而是思维模式。你不再是一个事必躬亲的“工匠”,而是一个善于利用强大组件的“架构师”。框架帮你处理了底层的复杂性、差异性和重复性劳动,让你能更专注于核心的业务逻辑和创新。当然,框架本身也有学习成本,过度抽象有时也会带来新的复杂度。但毫无疑问,对于任何严肃的LLM应用开发项目,采用一个成熟的框架是通向可维护、可扩展、高性能系统的必经之路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询