从LlamaIndex.TS到LangChain.js:构建RAG应用的现代TypeScript实践
1. 项目概述一个已停更的LLM应用数据框架如果你在2024年或更晚的时间点搜索如何用TypeScript或JavaScript构建基于大语言模型LLM的智能应用比如一个能和你PDF文档对话的聊天机器人或者一个能自动分析代码库的智能助手你很可能会遇到一个叫LlamaIndex.TS的项目。它的GitHub仓库run-llama/LlamaIndexTS赫然标注着“已弃用”Deprecated。这就像一个你慕名前往的网红餐厅到了却发现门口贴着“永久停业”的告示。但别急着走了解它为什么存在、它曾试图解决什么问题、以及它停更后我们该怎么办对于任何一个想踏入AI应用开发领域的工程师来说都是一笔宝贵的财富。简单来说LlamaIndex.TS 是一个旨在为你的LLM应用提供数据框架的TypeScript/JavaScript库。它的核心使命是解决LLM应用开发中的一个经典难题如何让你自己的私有数据公司文档、个人笔记、数据库记录等能够被像GPT-4、Claude、Llama这类“通才”模型理解和运用。LLM本身是在海量公开数据上训练的它并不知道你公司内部的季度报告内容。LlamaIndex.TS 提供了一套工具帮你把这些外部数据“喂”给LLM让LLM能基于这些数据回答问题、进行分析或生成内容。它本质上是Python生态中大名鼎鼎的LlamaIndex框架在Node.js/TS运行时环境下的官方移植版本目标是让前端和全栈开发者也能轻松构建AI应用。然而这个项目已经停止了维护。这引出了几个关键问题它当初的设计思路是什么我们还能从中学到什么在它停更后我们该如何继续前行这篇文章我将以一个经历过从探索、使用到寻找替代方案的开发者视角为你拆解LlamaIndex.TS的遗产并分享在当前技术环境下构建同类应用的实战思路。2. 核心架构与设计思路解析要理解LlamaIndex.TS的价值首先得明白LLM应用处理私有数据的典型流程这通常被称为“检索增强生成”Retrieval-Augmented Generation, RAG。想象一下你是一个拥有庞大图书馆你的私有数据的管理员而LLM是一个博闻强识但没读过你馆藏书籍的学者。每当学者需要回答一个专业问题时他不可能现场读完所有书。这时你需要一个高效的“图书检索员”和“知识摘要员”。2.1 RAG流程与LlamaIndex.TS的定位一个标准的RAG流程包含几个核心步骤加载Loading从各种来源本地文件、网络、数据库获取原始数据。分割Splitting将长文档切成语义连贯的小片段节点因为LLM有上下文长度限制。嵌入Embedding将这些文本片段通过嵌入模型Embedding Model转换成高维向量。这个过程就像给每段文字拍一张“数学指纹”。索引Indexing将这些向量“指纹”存储到专门的向量数据库如Pinecone, Weaviate, Chroma中以便快速进行相似性搜索。检索Retrieval当用户提问时将问题也转换成向量然后在向量数据库中快速找出与之“指纹”最相似的几个文本片段。生成Generation将检索到的相关片段作为上下文和用户问题一起提交给LLM让LLM生成一个基于上下文的精准回答。LlamaIndex.TS 的定位就是提供一套开箱即用的工具链来封装和简化上述步骤中的第1、2、3、5、6步特别是数据加载、处理与查询接口的抽象。它让你不必从零开始写文件解析器、文本分割算法或是手动拼接提示词Prompt。它通过“索引”Index、“检索器”Retriever、“查询引擎”QueryEngine等高级抽象让开发者可以像搭积木一样构建RAG系统。2.2 核心抽象Index, Retriever, QueryEngine这是LlamaIndex.TS及其Python版本最精髓的设计理解了它们就理解了框架的思维方式。索引Index这是框架的核心数据结构。它不是简单的倒排索引而是一个对已处理数据文档片段、向量等的封装和管理器。你通过它来存储和组织你的数据。创建索引的过程就隐含了加载、分割、嵌入和存储到内存或向量数据库的完整流程。检索器Retriever附着在索引之上的组件负责执行“检索”步骤。它定义了如何从索引中获取与查询相关的信息。最常见的是“向量检索器”基于嵌入向量的相似性进行搜索。框架也支持关键词检索或混合检索。查询引擎QueryEngine在检索器之上的更高层抽象。它接收一个自然语言查询利用检索器获取相关上下文然后自动构造一个包含上下文和问题的提示词发送给LLM并返回LLM生成的答案。它把“检索”和“生成”两步无缝衔接了起来。这种分层设计的好处是解耦和可替换性。你可以轻松更换底层的LLM从OpenAI换成Anthropic、嵌入模型、或者检索策略而上层的业务逻辑代码几乎不用改动。2.3 多运行时支持的设计考量LlamaIndex.TS 的一个显著特点是其广泛的环境支持Node.js, Deno, Bun, 甚至边缘运行时如Vercel Edge和Cloudflare Workers。这反映了其面向现代JavaScript全栈开发的野心。为了实现这一点它在设计上必须考虑纯ES模块ESM优先确保在现代运行时中的兼容性。最小化原生依赖避免使用仅在Node.js中可用的核心模块或提供替代方案。异步上下文AsyncLocalStorage的挑战项目文档提到浏览器支持受限是因为缺乏类似AsyncLocalStorage的API。这揭示了在复杂异步工作流中如跟踪一个请求链路上的所有LLM调用保持上下文一致性是一个底层难题。框架需要这种机制来管理对话状态、日志追踪等而在浏览器中实现类似功能较为复杂。注意虽然项目已停更但这种“一次编写多处运行”的设计思路对于今天开发需要部署在无服务器Serverless或边缘环境的AI应用仍然具有重要的参考价值。在选择新工具时运行环境兼容性是一个关键评估点。3. 从概念到实践构建一个简易文档问答机器人尽管LlamaIndex.TS已不再维护但通过复现其核心思想我们能更深刻地理解RAG应用的构建。下面我将使用当前2024年末更活跃、维护更好的现代TypeScript AI工具链来演示如何构建一个功能类似的简易文档问答机器人。我们会用到 LangChain.js 和一些其他库因为它们在生态和社区支持上目前更具优势。3.1 现代工具链选型与环境搭建首先我们明确替代方案的核心组件应用框架LangChain.js。它是目前JavaScript/TypeScript生态中功能最全面、社区最活跃的LLM应用开发框架提供了与LlamaIndex.TS类似甚至更丰富的抽象。LLM提供商OpenAI GPT-4o。因其API稳定、能力强大且文档丰富作为示例首选。当然你也可以轻松替换为Anthropic Claude、GroqLlama模型等。嵌入模型OpenAI的text-embedding-3-small。性价比高效果足够好。向量数据库内存向量存储MemoryVectorStore。为了演示简便我们使用LangChain自带的内存存储。生产环境通常会选用Pinecone、Weaviate或Chroma也有TS版本。文本分割器RecursiveCharacterTextSplitter。这是最常用、效果也相对稳定的分割方法。文档加载器PDFLoader来自langchain/community。用于加载PDF文档。初始化项目与安装依赖# 创建一个新的TypeScript项目 mkdir ts-ai-rag-bot cd ts-ai-rag-bot npm init -y npm install typescript ts-node types/node --save-dev npx tsc --init # 安装核心依赖 npm install langchain langchain/community npm install pdf-parse # PDFLoader的底层依赖 # 安装OpenAI相关包如果你使用其他模型需安装对应的集成包 npm install langchain/openai # 创建必要的文件 touch index.ts .env配置环境变量.env文件OPENAI_API_KEY你的OpenAI_API密钥3.2 核心代码实现与逐步解析接下来我们将在index.ts中实现一个完整的、可以处理PDF文档的问答流程。我会逐段解释这比直接看最终代码更有助于理解。第一步环境配置与基础导入import { config } from dotenv; config(); // 加载.env文件中的环境变量 import { PDFLoader } from langchain/community/document_loaders/fs/pdf; import { RecursiveCharacterTextSplitter } from langchain/text_splitter; import { MemoryVectorStore } from langchain/vectorstores/memory; import { OpenAIEmbeddings, ChatOpenAI } from langchain/openai; import { StringOutputParser } from langchain/core/output_parsers; import { PromptTemplate } from langchain/core/prompts; import { RunnableSequence } from langchain/core/runnables;这里导入了所有需要的模块。dotenv用于管理密钥PDFLoader负责加载PDFRecursiveCharacterTextSplitter用于文本分割MemoryVectorStore是我们的简易向量数据库OpenAIEmbeddings和ChatOpenAI分别对应嵌入模型和聊天LLMStringOutputParser等是LangChain用于构建链Chain的核心工具。第二步文档加载与处理async function loadAndProcessDocument(filePath: string) { console.log(正在加载文档: ${filePath}); // 1. 加载文档 const loader new PDFLoader(filePath); const rawDocs await loader.load(); console.log(原始文档页数: ${rawDocs.length}); // 2. 分割文档 const textSplitter new RecursiveCharacterTextSplitter({ chunkSize: 1000, // 每个片段约1000字符 chunkOverlap: 200, // 片段间重叠200字符避免语义被割裂 separators: [\n\n, \n, 。, , , , , , ], // 中文友好的分隔符 }); const splitDocs await textSplitter.splitDocuments(rawDocs); console.log(分割后文档片段数: ${splitDocs.length}); return splitDocs; }chunkSize和chunkOverlap是关键参数。chunkSize太大检索精度可能下降且可能超出LLM上下文窗口太小则可能丢失完整语义。chunkOverlap能确保关键信息如一段话的结尾和开头同时出现在相邻片段中提高检索连贯性。对于中文调整separators很重要默认设置可能以英文标点和空格为主效果不佳。实操心得chunkSize没有绝对最优值需要根据你的文档类型技术文档、小说、财报和使用的LLM上下文窗口如GPT-4o的128K进行实验调整。可以从500-1500这个范围开始测试。第三步创建向量存储索引async function createVectorStore(docs: Document[]) { console.log(正在创建向量存储嵌入并索引...); // 使用OpenAI的嵌入模型将文本转换为向量 const embeddings new OpenAIEmbeddings({ model: text-embedding-3-small, // 指定嵌入模型 dimensions: 512, // 可选降低向量维度以节省成本/空间但可能轻微影响精度 }); // 将文档片段和它们的向量嵌入存储到内存中 const vectorStore await MemoryVectorStore.fromDocuments(docs, embeddings); console.log(向量存储创建完成); return vectorStore; }这里OpenAIEmbeddings会自动为每一段文本调用OpenAI的嵌入API生成向量。MemoryVectorStore.fromDocuments方法封装了“嵌入”和“索引”两步。注意成本嵌入API调用是按token收费的。处理大量文档前最好估算一下token数量。text-embedding-3-small是目前性价比很高的选择。生产环境考量MemoryVectorStore数据在进程重启后会丢失。生产环境应使用持久化的向量数据库其客户端如PineconeStore的使用方式与MemoryVectorStore类似LangChain提供了统一的接口。第四步构建检索与生成链查询引擎function createRAGChain(vectorStore: MemoryVectorStore) { // 1. 创建检索器从向量库中获取最相关的4个片段 const retriever vectorStore.asRetriever({ k: 4 }); // 2. 定义提示词模板 const promptTemplate PromptTemplate.fromTemplate( 你是一个专业的文档助手请严格根据以下提供的上下文信息来回答问题。 如果上下文信息中不包含答案请直接说“根据提供的资料我无法回答这个问题”不要编造信息。 上下文 {context} 问题{question} 请给出详细、准确的回答 ); // 3. 初始化LLM const llm new ChatOpenAI({ model: gpt-4o-mini, // 使用成本更低的gpt-4o-mini能力足够 temperature: 0.1, // 低温度值使输出更确定、更忠于上下文 }); // 4. 组装运行链检索 - 格式化上下文 - 生成提示词 - 调用LLM - 解析输出 const ragChain RunnableSequence.from([ { // 第一步接收原始问题通过检索器获取相关文档 context: async (input: { question: string }) { const docs await retriever.invoke(input.question); // 将检索到的文档内容合并成一个字符串作为上下文 return docs.map(doc doc.pageContent).join(\n---\n); }, question: (input: { question: string }) input.question, }, promptTemplate, // 第二步将上下文和问题填入提示词模板 llm, // 第三步将填充好的提示词发送给LLM new StringOutputParser(), // 第四步将LLM的响应解析为纯字符串 ]); return ragChain; }这是整个应用的核心。RunnableSequence允许你将多个步骤串联成一个可执行的“链”。提示词工程模板中的指令至关重要。明确要求模型“根据上下文”回答并指示其在无法回答时如实告知可以显著减少模型“幻觉”胡编乱造的情况。temperature参数设置为较低值如0.1可以使模型输出更加稳定和可预测这对于基于事实的问答任务非常有利。检索数量k检索4个片段是一个常见的起始值。太少可能信息不足太多则可能引入无关噪声并增加token消耗。需要根据文档分割的大小和问题的复杂度进行调整。第五步主函数与交互async function main() { try { const filePath ./你的文档.pdf; // 请替换为你的PDF文件路径 // 1. 处理文档并创建索引向量存储 const processedDocs await loadAndProcessDocument(filePath); const vectorStore await createVectorStore(processedDocs); // 2. 创建RAG链 const ragChain createRAGChain(vectorStore); console.log(\n文档助手已就绪输入你的问题输入 exit 退出:\n); // 3. 简单的命令行交互循环 const readline require(readline).createInterface({ input: process.stdin, output: process.stdout }); const askQuestion () { readline.question( , async (question: string) { if (question.toLowerCase() exit) { console.log(再见); readline.close(); return; } console.log(思考中...); const startTime Date.now(); try { // 执行RAG链 const answer await ragChain.invoke({ question }); const endTime Date.now(); console.log(\n回答耗时${endTime - startTime}ms:\n${answer}\n); } catch (error) { console.error(出错, error); } askQuestion(); // 继续下一个问题 }); }; askQuestion(); } catch (error) { console.error(程序运行失败, error); } } // 启动程序 main();这个主函数串联了所有步骤并提供了一个简单的命令行交互界面。在实际项目中你可能会将其改造成REST API使用Express、Hono等或集成到前端界面如Next.js中。3.3 运行与效果评估将你的PDF文件放到项目根目录并更新filePath变量。然后运行npx ts-node index.ts程序会先加载、分割、索引你的文档这可能需要一些时间取决于文档大小和网络速度然后进入问答模式。如何评估效果事实准确性问一些你知道答案的、文档中明确提及的问题检查回答是否准确。拒绝能力问一个文档中绝对没有涉及的问题看它是否会承认无法回答而不是胡编乱造。综合理解问一些需要联系文档中多个部分才能回答的问题测试其检索和综合能力。响应速度感受从提问到获得答案的延迟这取决于检索速度、LLM API调用速度。4. 深入优化与高级技巧一个基础的RAG系统搭建起来后你会发现它可能在一些复杂场景下表现不佳。以下是一些常见的优化方向这些也正是像LlamaIndex这类框架试图提供高级功能来解决的问题。4.1 提升检索质量超越简单的向量搜索简单的向量相似性搜索有时会失灵比如当用户问题中的关键词和文档中的表述差异很大时语义相似度低但主题相关。以下是一些进阶策略混合检索Hybrid Search结合向量搜索语义相似和关键词搜索如BM25词汇匹配。即使语义不完全匹配关键词匹配也能捞回相关文档。LangChain支持与一些向量库如Weaviate, Qdrant的混合检索。重排序Re-ranking先通过向量/关键词检索出较多的候选文档例如20个然后使用一个更小、更快的“重排序模型”对这些候选文档进行精排选出最相关的3-5个。这能显著提升最终上下文的质量。Cohere和BGE都有专门的重排序模型。元数据过滤在索引时为每个文档片段附加元数据如来源文件名、章节标题、创建日期。检索时可以先根据元数据过滤例如“只在Q3财报中搜索”再进行向量搜索能大幅缩小搜索范围提升精度和速度。示例为文档添加元数据并过滤// 在分割文档后可以手动为每个片段添加元数据 const docsWithMetadata splitDocs.map((doc, index) { return { ...doc, metadata: { ...doc.metadata, source: filePath, chunkIndex: index, // 你可以在这里添加任何自定义字段 }, }; }); // 假设使用支持过滤的向量库如Pinecone // 在检索时可以添加过滤器 const retriever vectorStore.asRetriever({ k: 4, filter: { source: filePath }, // 只从特定来源检索 });4.2 优化提示词与对话管理基础的提示词模板可能不够用。对于多轮对话需要管理历史消息。系统提示词System Prompt在对话开始时给LLM一个更稳固的角色设定和行为指令这比在用户消息中重复指令更有效。ChatOpenAI等模型支持system角色。对话历史将之前的问答对作为上下文传递给LLM使其能进行连贯的多轮对话。需要注意管理上下文长度避免无限增长。思维链Chain-of-Thought对于复杂问题可以提示模型“逐步思考”这有时能提升推理能力。示例使用带历史记录的对话链import { AIMessage, HumanMessage, SystemMessage } from langchain/core/messages; import { ChatPromptTemplate, MessagesPlaceholder } from langchain/core/prompts; const chatPrompt ChatPromptTemplate.fromMessages([ new SystemMessage(你是一个严谨的文档分析助手。), new MessagesPlaceholder(history), // 预留位置存放历史消息 [human, {question}], ]); // 在调用链时需要传入历史消息数组 const chain RunnableSequence.from([ { context: retriever, question: (input) input.question, history: (input) input.history, // 传入历史 }, // ... 后续组装 ]);4.3 处理复杂文档与多模态现实中的文档不仅仅是文本文档。复杂格式PPT、Word、Excel、HTML网页。需要对应的加载器langchain/community提供了很多。扫描版PDF/图片包含文字信息但无法直接复制。需要OCR光学字符识别技术。可以使用Tesseract.js或调用云服务如Azure Form Recognizer的API先提取文字。结构化数据数据库、API。需要专门的连接器来加载和索引。长文档处理对于书籍或超长报告简单的递归分割可能不够。可以考虑“层次化索引”先对章节标题建立高层索引再对章节内容建立详细索引实现“先粗筛后精查”。5. 常见问题、排查与替代方案探讨5.1 实战中遇到的典型问题与解决方案问题现象可能原因排查与解决思路LLM回答“根据资料无法回答”但资料中明明有。1. 检索失败没找到相关片段。2. 检索到的片段质量差不完整、噪声大。3. 提示词指令不够强LLM“偷懒”。1.检查检索结果在代码中打印出retriever获取到的原始文本看是否包含答案。2.调整分割参数减小chunkSize或调整separators确保关键信息完整在一个片段内。3.优化提示词在系统提示中强调“必须使用上下文”或采用更严格的指令。LLM的回答包含事实错误幻觉。1. 检索到了无关或错误信息。2. LLM过于自信忽略了上下文。3. 上下文信息本身模糊或矛盾。1.提升检索精度尝试混合检索、重排序、元数据过滤。2.降低temperature设为0或接近0的值。3.在提示词中要求引用来源例如“请引用上下文中的句子来支持你的答案”。处理速度很慢。1. 文档分割太细片段数量多嵌入和索引耗时。2. 向量数据库查询慢。3. LLM API调用延迟高。1.增大chunkSize减少总片段数。2.对于生产环境使用专业的向量数据库如Pinecone其索引和查询经过高度优化。3.考虑使用更快的LLM如GPT-3.5-Turbo如果任务不复杂或本地模型。内存占用过高Node.js进程崩溃。1. 使用MemoryVectorStore存储了大量向量。2. 文档过大全部加载到内存中处理。1.切换到外部向量数据库这是根本解决方案。2.流式处理文档分批加载、处理和索引文档而不是一次性处理所有。回答不连贯像多个片段的拼凑。检索到的多个片段之间缺乏连贯性LLM难以整合。1.增加chunkOverlap让相邻片段有更多交集。2.尝试不同的分割方法如按语义分割需要更复杂的库。3.在提示词中要求“综合以下信息给出一个连贯的回答”。5.2 LlamaIndex.TS停更后的技术选型既然原项目已停更对于新的TypeScript/JavaScript AI应用项目我们应该选择什么LangChain.js首选推荐优势生态极其繁荣集成提供了数百种组件LLM、工具、检索器、记忆等社区活跃文档和示例丰富抽象层次高能快速构建复杂Agent和工作流由LangChain公司主导开发维护有保障。适用场景绝大多数需要快速构建原型或生产级RAG、Agent应用的场景。Vercel AI SDK优势由Vercel官方维护与Next.js等框架集成度极高API设计非常简洁、现代化专注于流式UI响应前端开发体验好。适用场景主要面向前端和全栈开发者构建需要优秀用户体验的聊天界面或文本生成应用。其底层可能仍会使用LangChain或其他库。直接使用模型提供商SDK 自定义逻辑优势依赖最轻量没有抽象开销性能和控制力最强。劣势所有RAG流程加载、分割、嵌入、检索、提示都需要自己实现或组合其他库开发成本高。适用场景需求极其简单明确或对性能和包体积有极致要求且团队有能力维护底层实现。其他新兴框架如Microsoft Autogen专注于多智能体协作、CrewAI基于LangChain更面向工作流编排等它们有特定的适用领域。我的个人建议是对于大多数从零开始的团队LangChain.js是目前最稳妥、功能最全面的选择。它可能有一定学习曲线但其提供的抽象和工具集能极大提升开发效率。你可以从本文示例中的简单链开始逐步探索其更强大的功能如智能体Agent、工具Tool调用等。5.3 关于LlamaCloud/LlamaParse的说明在LlamaIndex.TS的弃用通知中它指向了LlamaCloud。这是LlamaIndex团队推出的云服务提供了更强大的文档解析LlamaParse和托管RAG管道等功能。如果你需要处理极其复杂或格式混乱的文档如扫描表格、手写体并且不想自己搭建和维护OCR或解析管道LlamaParse这样的托管服务是一个值得考虑的选项。不过这通常意味着额外的服务成本和潜在的供应商锁定。回过头看LlamaIndex.TS的诞生和沉寂正是AI工程领域快速迭代的缩影。作为一个开发者重要的不是死守某个特定的框架而是理解其背后解决的核心问题模式如RAG并掌握一套能够适应技术潮流变化的、基于核心概念和现代工具链的构建能力。希望这篇从“遗产”剖析到“现代重建”的长文能为你打开一扇门让你在构建自己的AI应用时思路更清晰脚步更稳健。