尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

LangChain.js框架入门:从零构建LLM应用的组件化开发实践

LangChain.js框架入门:从零构建LLM应用的组件化开发实践 1. 项目概述从“手搓”到“搭积木”的思维跃迁如果你和我一样是从零开始接触大语言模型应用开发的那你大概率也经历过一个阶段面对一个需求比如“让AI根据我的文档回答问题”第一反应就是打开代码编辑器开始“手搓”代码。从调用OpenAI的API到处理上下文长度再到设计提示词模板每一步都亲力亲为。这个过程很锻炼人但也很容易陷入细节的泥潭代码结构混乱可维护性差而且一旦需求稍微复杂一点比如需要联网搜索或者调用工具整个架构就得推倒重来。“LangChain.js 初探从手写代码到框架思维”这个标题精准地捕捉到了这个关键的转折点。它描述的不仅仅是学习一个名为LangChain.js的JavaScript/TypeScript框架更是一种开发范式的转变——从面向过程的、胶水式的代码编写转向面向组件的、声明式的应用构建。LangChain.js不是一个魔法黑盒它更像是一套精心设计的、标准化的“乐高积木”。它把LLM应用开发中那些重复、繁琐但又至关重要的环节如模型调用、提示词管理、记忆、工具使用、数据检索等抽象成了一个个可插拔的组件。我们的工作从“从零制造每一个零件”变成了“如何更优雅、更高效地组合这些现成的、经过验证的零件”。这次初探的核心价值就在于理解这套“积木”的搭建逻辑。我们将不再满足于仅仅跑通一个“Hello World”示例而是要深入其设计哲学弄明白为什么需要LLMChainAgent和Tool是如何协作的Retriever和Vector Store背后又隐藏着怎样的数据流。掌握了这种框架思维你就能在面对“构建一个能联网、能查数据库、能进行多轮对话的智能客服”这类复杂需求时不再感到无从下手而是能清晰地将其拆解为链、代理、记忆、工具等模块并快速组合实现。这对于任何希望将LLM能力产品化、工程化的开发者来说都是一项必备的核心技能。2. 框架思维解析LangChain.js 的核心设计哲学在开始写第一行LangChain.js代码之前我们必须先理解它试图解决的根本问题以及它为此提出的抽象模型。这决定了我们是用“框架的方式”还是用“旧脚本的方式”来使用它。2.1 为何需要框架手写代码的典型困境让我们用一个具体的场景来对比。假设我们需要构建一个简单的“公司产品问答机器人”它需要基于内部产品文档来回答用户问题。手写代码的典型路径硬编码提示词在代码里写死一个字符串比如“请根据以下文档回答问题{context}\n\n问题{question}”。手动处理上下文写一个函数将用户问题和检索到的文档片段拼接起来同时要小心翼翼地计算token数量确保不超过模型限制。如果超了还得自己写逻辑去截断或总结文档。直接调用API使用fetch或axios直接向OpenAI、Anthropic等服务的端点发送请求。解析响应手动解析返回的JSON提取出content字段。错误处理与重试自己实现网络错误、速率限制、模型过载等情况的处理逻辑。这段代码可能一开始只有几十行看起来很简单。但问题很快就会接踵而至需求变更老板说回答时语气要更友好一些。于是你不得不去修改那个硬编码的提示词字符串并确保所有用到的地方都同步更新。增加功能需要支持联网搜索。你不得不引入一个新的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。我们将将这些文档切片并转换为向量嵌入存入向量数据库。根据用户问题检索最相关的文档片段作为上下文。将“上下文 历史对话 当前问题”组合成提示词交给LLM生成答案。这里我们使用内存型的向量数据库MemoryVectorStore和OpenAI的嵌入模型方便演示。npm install langchain/openai langchainimport { 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);核心环节解析文本分割与向量化RecursiveCharacterTextSplitter是处理长文档的关键。它尝试按字符、句子、段落等递归地分割文本尽可能保持语义完整。chunkOverlap设置重叠可以防止一个完整的句子被切成两半。检索器 (Retriever)vectorStore.asRetriever(3)创建了一个检索器它会在向量空间中查找与问题嵌入最相似的3个文档块。这是实现“基于文档问答”的核心。记忆 (Memory)BufferMemory将对话历史存储在内存中。saveContext方法将用户问题和助手回答保存为一组loadMemoryVariables则将其加载到提示词变量中。这使得机器人具备了多轮对话能力。RunnableSequence这是LangChain较新且推荐的构建链的方式。它清晰地定义了数据流输入 - 检索/加载记忆 - 填充提示词 - 调用模型 - 解析输出。每一步都是一个独立的“可运行单元”组合起来非常灵活。注意MemoryVectorStore仅用于演示程序重启后数据会丢失。生产环境应使用持久化的向量数据库如Chroma、Pinecone、Weaviate或pgvectorPostgreSQL扩展。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摄氏度之间。代理工作流解析工具定义每个工具Tool都有一个name、description和_call方法。LLM通过阅读工具的description来决定在什么情况下使用它。代理决策当代理收到输入时LLM会根据输入和可用工具的description决定是直接回答还是调用某个工具。如果调用工具它会生成符合工具要求的参数。执行与观察代理执行器AgentExecutor调用工具并将工具返回的结果observation再次交给LLM。循环LLM根据观察结果决定下一步是继续调用工具还是给出最终答案。这个过程会一直循环直到LLM认为可以给出最终输出。verbose: true选项让我们能窥见代理的“思考链”这对于调试和理解代理行为至关重要。4. 实战避坑指南与性能优化在实际项目中应用LangChain.js你会遇到一些常见陷阱。以下是我从多个项目中总结出的经验。4.1 提示词工程从模糊到精确糟糕的提示词是LLM应用失败的首要原因。框架帮你管理了提示词但内容还得你自己设计。常见坑点指令模糊例如“总结这篇文档”。LLM可能生成过于简略或过于详细的内容。缺少上下文或格式要求没有明确输出格式JSON、列表、特定字数导致后续处理困难。“幻觉”问题当检索的上下文不足时LLM容易编造答案。优化策略结构化提示词使用ChatPromptTemplate.fromMessages来清晰定义角色。const prompt ChatPromptTemplate.fromMessages([ [system, 你是一个严谨的技术文档助手。你必须只根据提供的上下文回答问题。如果不知道就说不知道。], [human, 上下文{context}], [human, 问题{question}], ]);提供少量示例 (Few-Shot)在提示词中给出一两个输入输出的例子能显著提升模型在特定任务上的表现。明确输出格式在指令中直接要求如“请用JSON格式输出包含summary和keywords两个字段”。设置严格的停止条件对于生成任务可以设置stop序列防止模型跑偏。4.2 检索质量找到真正相关的信息“垃圾进垃圾出。”如果检索器找不到相关文档再好的LLM也无力回天。常见坑点块大小不合适chunkSize太大可能包含无关信息太小可能割裂了关键语义。检索数量不当检索太多块k值太大会引入噪声增加token消耗和成本检索太少可能遗漏关键信息。嵌入模型不匹配用于生成向量存储的嵌入模型与任务不匹配例如用通用嵌入模型处理专业医学文献。优化策略分块策略实验不要只用RecursiveCharacterTextSplitter。对于代码可以尝试LanguageTextSplitter对于Markdown可以按标题分割。关键是评估看哪种分块方式在问答测试集上效果最好。元数据过滤在存入向量库时为每个块添加元数据如来源文件、章节标题、类型。检索时可以结合语义搜索和元数据过滤提高精度。await vectorStore.addDocuments(docsWithMetadata); const retriever vectorStore.asRetriever({ k: 5, filter: { source: user_manual.pdf } // 只从特定文件检索 });重排序 (Re-ranking)在初步检索出N个结果后使用一个更小、更快的重排序模型如BAAI/bge-reranker对结果进行精排只将Top K个最相关的片段送入LLM。这能有效提升答案质量并降低成本。混合搜索结合语义搜索向量相似度和关键词搜索如BM25。LangChain的HybridSearchRetriever可以做到这一点能同时捕获语义相似性和关键词匹配。4.3 性能与成本控制LLM API调用是按token计费的且可能有延迟。不加以控制成本和响应时间都会失控。常见坑点无节制地输入长上下文将所有检索到的文档不经处理直接塞进提示词。重复调用相同内容在多轮对话中每次都将完整历史记录发送给模型。未处理速率限制和超时导致应用不稳定。优化策略上下文压缩在将检索到的文档送入LLM前先进行压缩或总结。LangChain提供了ContextualCompressionRetriever可以搭配LLMChainExtractor等压缩器使用只提取与问题最相关的句子。流式输出对于生成时间较长的内容使用模型的流式响应stream来提升用户体验。const stream await model.stream(prompt); for await (const chunk of stream) { process.stdout.write(chunk.content); }缓存对频繁出现的、结果确定的查询如“公司的成立时间”进行缓存。可以使用InMemoryCache或集成Redis等外部缓存。import { InMemoryCache } from langchain/cache; const cache new InMemoryCache(); const model new ChatOpenAI({ cache });设置超时和重试在初始化模型时配置timeout和maxRetries。const model new ChatOpenAI({ modelName: gpt-4, timeout: 10000, // 10秒超时 maxRetries: 2, });监控与评估记录每次调用的token使用量、成本和延迟。定期评估检索的准确率和回答的相关性持续迭代优化提示词和检索策略。4.4 调试与监控当链或代理行为不符合预期时系统的复杂性会让调试变得困难。调试技巧善用verbose模式在初始化链或代理时设置verbose: true这能打印出每一步的输入输出是理解数据流最直接的方法。中间结果检查对于复杂的RunnableSequence可以在中间插入自定义函数来打印或检查数据。const debuggingChain RunnableSequence.from([ firstStep, (input) { console.log(After firstStep:, input); return input; }, // 调试钩子 secondStep, ]);LangSmith这是LangChain官方提供的追踪和监控平台。它能可视化整个链的执行过程记录每个步骤的输入输出、耗时和token使用是进行复杂应用调试和性能分析的强大工具。只需设置环境变量LANGCHAIN_TRACING_V2true和LANGCHAIN_API_KEY你的调用数据就会自动发送到LangSmith。从手写代码到拥抱LangChain.js这样的框架最大的转变不是语法而是思维模式。你不再是一个事必躬亲的“工匠”而是一个善于利用强大组件的“架构师”。框架帮你处理了底层的复杂性、差异性和重复性劳动让你能更专注于核心的业务逻辑和创新。当然框架本身也有学习成本过度抽象有时也会带来新的复杂度。但毫无疑问对于任何严肃的LLM应用开发项目采用一个成熟的框架是通向可维护、可扩展、高性能系统的必经之路。
返回列表