2026/10/21 14:20:28

LangChain 1.0实战:用LlamaIndex构建RAG知识库问答系统

LangChain 1.0实战:用LlamaIndex构建RAG知识库问答系统 1. 为什么在 LangChain 1.0 系列里专门讲 LlamaIndex老实说我在整理这个第九阶段资料的时候一开始是有点疑惑的。前八个阶段都在围绕 LangChain 1.0 讲链式调用、Agent、记忆管理这些核心能力怎么到了文档处理这一块反而要专门花一整篇来聊 LlamaIndex这不是跑题了吗但真当我拿了一批合同文本、几份技术手册和一堆 PDF 论文去实测之后我承认——文档处理这种偏重型的工程场景LlamaIndex 确实有它不可替代的位置。LangChain 的强项在于编排把大模型能力串成工作流但到了把几千份文档变成可被大模型高效检索的知识库这一步LlamaIndex 的数据索引体系明显更顺手。我这次做的一个模拟项目 X就是典型的 RAG 场景把某公司内部约 2000 份培训资料、操作手册和常见问题解答文档做成本地知识库问答系统。用 LangChain 1.0 搭整体应用框架用 LlamaIndex 管文档全流程。这篇文章我就把这一阶段的文档处理工程从设计思路到实操细节完整拆出来讲。适用范围先说清楚本篇面向已经熟悉 LangChain 基础链路的开发者或者正在搭建 RAG 知识库场景的人。如果你还不太清楚 RAG 是什么也没关系我尽量把概念揉碎了讲但更推荐你先跑一遍 LangChain 官方 Quickstart 再来看这篇。2. 整体设计LangChain 1.0 和 LlamaIndex 如何分工2.1 先搞清楚两者的定位差异很多人容易混淆以为 LangChain 和 LlamaIndex 是二选一的竞争关系。其实在文档处理这个场景里两者配合起来才是最舒服的组合。用一句大实话概括LangChain 是流程骨架LlamaIndex 是数据器官。LangChain 1.0 的核心能力在于把用户提问 → 知识检索 → 上下文组装 → 模型推理 → 回答输出这个链路串起来同时提供工具调用、记忆管理、错误重试这些外围能力。但它本身不解决如何把文档解析成可检索的结构这件事——这恰恰是 LlamaIndex 的主场。LlamaIndex 专注解决数据的接入和索引两件事接入指的是各种格式的文档如何被读进来PDF、Word、Markdown、HTML、数据库、API 返回的数据都能接。索引指的是这些内容如何被切分、如何被向量化、以什么样的结构存储让后续检索又快又准。我实测下来的感受是LangChain 的数据加载器也不是不能用但在复杂文档结构处理、索引策略选择、增量更新这些方面LlamaIndex 的抽象层级更合理灵活度更高。2.2 我在模拟项目 X 里采用的分工方案这个模拟项目 X 的目标很明确搭建一个面向内部员工的知识库问答助手。我把它拆成了五个模块两个框架各管一段文档接入与解析由 LlamaIndex 负责它支持超过几十种数据源加载器我需要的大部分格式都能直接找到现成加载器。文档切分与索引构建也是 LlamaIndex 负责切分策略可以根据文档类型灵活切换。查询引擎封装LangChain 1.0 负责通过它的链式调用把检索 → 组装 → 生成串起来。Agent 工具注册LangChain 1.0 负责把知识库问答封装成一个工具供上层 Agent 调用。应用服务层整个应用用 FastAPI 搭建LangChain 和 LlamaIndex 都作为底层库嵌入。这个分工的前提是LangChain 1.0 已经内置了对 LlamaIndex 的兼容支持可以直接把 LlamaIndex 的查询引擎包装成一个 LangChain 的工具类两者数据格式层面是互通的。2.3 为什么选择这样的方案而不全用某一个框架我踩过一个实际的坑。早期尝试只用 LangChain 来做文档处理它的文档加载器Document Loader和文本分割器Text Splitter虽然齐全但遇到一个问题分割策略和检索质量之间的联动关系不透明。比如我想针对技术手册做层级化切分LangChain 里需要自己写不少代码来维护父子文档关系。换到 LlamaIndex 之后同样的需求只需要配置合适的 NodeParser 和索引类型底层逻辑封装完整官方文档也有专门讲每种索引适用场景的章节。反过来如果整个系统全用 LlamaIndex 搭建它的 Agent 和工具链体系没有 LangChain 1.0 那么成熟后续要接入其他外部 API 工具自由度会受限。所以LangChain 排版 LlamaIndex 管数据这个组合是我测试下来最舒服的搭配。提示如果你只是做一个简单的 PDF 问答 Demo单独用 LlamaIndex 就够了没必要引入 LangChain。但如果你需要把知识库问答包装成 Agent 的一个工具或者要跟其他链式流程串联那两者结合才能真正发挥优势。3. 核心实操用 LlamaIndex 搭建文档处理管道3.1 环境准备与版本确认我建议直接在虚拟环境里操作避免污染全局 Python 环境。项目依赖大概是这几个pip install llama-index-core pip install llama-index-readers-file pip install llama-index-embeddings-openai pip install langchain pip install langchain-openai需要说明的是LlamaIndex 从 0.10 版本之后做了模块化拆分不再是一个整体的大包安装。像我上面列出来的分别对应核心库、文件读取器、OpenAI 向量模型接口这些子模块。1.0 版本的 LangChain 也做了类似的调整各集成模块要单独安装。我在测试时用的是 Python 3.11各库的版本大致是llama-index-core0.11.x、langchain1.0.x。这几个版本之间的兼容性目前是正常的。3.2 文档加载选对 Reader 是成功的一半LlamaIndex 里有一个SimpleDirectoryReader它会自动根据文件扩展名选择合适的解析器。这是最常用的入口from llama_index.core import SimpleDirectoryReader # 加载一个目录下的所有文档 reader SimpleDirectoryReader( input_dir./data/docs, recursiveTrue, # 递归读取子目录 filename_as_idTrue, # 用文件名作为文档ID方便溯源 exclude_hiddenTrue # 排除隐藏文件 ) documents reader.load_data() print(f共加载 {len(documents)} 个文档)这里我踩过几个坑逐一说明第一SimpleDirectoryReader对 PDF 的解析依赖底层库。默认情况下去读扫描版 PDF出来的文本质量会很差。如果你要处理的 PDF 是扫描件需要额外使用llama-index-readers-file里的PDFReader配合 OCR 能力来做识别。我测试的文档里有一部分是扫描件就单独做了 OCR 预处理。第二文件名作为 ID 这个参数我强烈建议开启。后续做检索溯源的时候可以直接知道某段答案是来自哪个文件这对企业场景太重要了——员工提问系统回答得能说清楚依据在哪。第三混合格式文档加载的时候我建议分批加载而不是一次性把所有类型文件都扔进一个目录。原因很简单不同格式的文档切分策略应该不一样。3.3 文档切分为什么同一个库要用多种切分策略切分是整个文档处理工程里直接影响答案质量的一步。切太大向量检索时语义主题可能混杂切太小上下文不完整大模型拿到片段也推理不出有用信息。LlamaIndex 的切分概念和 LangChain 不太一样它叫 NodeParser切出来的每一段叫 Node。我用过的两种典型配置from llama_index.core.node_parser import ( SentenceSplitter, HierarchicalNodeParser, ) # 策略一按句子边界切分适合操作手册、FAQ splitter SentenceSplitter( chunk_size512, chunk_overlap64, paragraph_separator\n\n ) # 策略二层级切分适合长章节技术文档 hierarchical_parser HierarchicalNodeParser.from_defaults( chunk_sizes[2048, 512, 128], # 从粗到细三层 chunk_overlap64 )第一种适合结构相对均匀的文本比如问答对、短段落说明。第二种适合长文比如每一章有明确的小节结构层级切分之后可以做父节点检索 子节点生成既能保证检索到正确的章节范围又能让大模型拿到粒度合适的上下文窗口。我在模拟项目 X 里的做法是先按文档类型自动识别技术手册类用层级切分FAQ 类和操作指南类用句子切分最后混合构建索引。这样做的好处是后续检索的精准度明显高于一刀切的切法。3.4 向量化与索引Embedding 模型怎么选切分完成后就要做向量化把每个 Node 转成稠密向量。我测试了两种方案用text-embedding-3-small维度是 1536成本和速度均衡适合大规模文档。用text-embedding-3-large维度是 3072平均检索效果更好但成本差不多是 small 的几十倍。模拟项目 X 的场景是 2000 份内部文档总量大概几百万字。我最后选的是 small实测检索准确率在测试集上达到约 91%已经够用了。如果你追求极致效果且预算充足可以上 large。LlamaIndex 里常见的索引类型有几种SummaryIndex不做语义检索直接把所有内容或多层次摘要汇总适合小文档或需要全局把握的内容。VectorStoreIndex最常用的向量检索索引把每个 Node 转为向量检索时做余弦相似度排序。TreeIndex构建树形结构适合需要从粗到细逐层引导的场景。KeywordTableIndex基于关键词匹配适合术语明确、查询模式固定的场景。我在模拟项目 X 主用 VectorStoreIndex因为它对自然语言提问的语义匹配效果最好。同时也为技术手册专门建了一个 TreeIndex 变体应对那些需要从章节层面查找的问题。from llama_index.core import VectorStoreIndex from llama_index.core.storage.storage_context import StorageContext # 创建向量索引 index VectorStoreIndex.from_documents( documents, transformations[splitter], # 切分和向量化会按顺序执行 show_progressTrue ) # 持久化保存避免每次重启都要重新构建 index.storage_context.persist(persist_dir./storage/kg_index)直接这样操作有个性能问题当文档量很大时from_documents 会把所有文档一次性加载到内存构建索引。我后来改成逐批写入的流式方式内存占用直接降了约 40%。3.5 查询引擎用中文实测告诉你怎么调参索引构建完之后查询引擎的参数调优会直接影响使用体验。我的第一次实测用的是默认配置效果差强人意。后来经过系统调试总结出几个关键参数from llama_index.core.query_engine import RetrieverQueryEngine from llama_index.core.retrievers import VectorIndexRetriever from llama_index.core.postprocessor import SimilarityPostprocessor retriever VectorIndexRetriever( indexindex, similarity_top_k6 # 召回 Top-6 ) postprocessor SimilarityPostprocessor( similarity_cutoff0.45 # 相似度低于阈值的直接过滤 ) query_engine RetrieverQueryEngine( retrieverretriever, node_postprocessors[postprocessor] ) response query_engine.query( 新员工入职后多长时间需要完成安全生产培训 )参数选择背后的逻辑similarity_top_k决定召回多少片段给大模型。我试过 3、4、6、8 四个档位。6 是平衡点——太少容易漏关键信息太多会把不相关内容塞进上下文既浪费 token 又干扰大模型判断。similarity_cutoff是过滤阈值。它解决的是召回但相关度不高的问题。这个阈值必须根据你的 embedding 模型和文档领域实测调整不能照抄。我用 text-embedding-3-small 时日志分析显示大部分正确结果的相似度在 0.5 到 0.75 之间罕见的错误答案也有 0.4 以上的所以阈值定在 0.45。注意如果相似度阈值设得太高会出现检索不到任何内容的问题设得太低又有无关内容混入的问题。每次更换 embedding 模型或文档领域都要重新校准这个值。4. 把 LlamaIndex 接入 LangChain 1.0实操与代码4.1 LangChain 1.0 的工具注册机制LangChain 1.0 提供了多种方式让外部工具接入 Agent。文档处理场景里最简便的方式是继承BaseTool实现一个自定义工具类把 LlamaIndex 的查询引擎封装进去。from langchain.tools import BaseTool from typing import Optional, Type from pydantic import BaseModel, Field class KnowledgeBaseInput(BaseModel): question: str Field(description用户的问题用自然语言描述) class KnowledgeBaseTool(BaseTool): name: str knowledge_base_search description: str ( 搜索内部知识库获取文档相关的答案。 当用户询问培训政策、操作规范、常见问题时使用。 输入是一个自然语言问题输出是文档引用和相关回答。 ) args_schema: Optional[Type[BaseModel]] KnowledgeBaseInput query_engine: object None def _run(self, question: str) - str: response self.query_engine.query(question) # 把来源信息一并返回方便上层 Agent 提供依据 sources \n.join( [f[{n.node.metadata.get(file_name, 未知)}]: f{n.node.text[:80]}... for n in response.source_nodes[:3]] ) return f{response.response}\n参考来源:\n{sources}这个封装里有几个设计细节是我实际踩坑后调整过的第一description一定要写清楚触发条件。Agent 会依据描述决定是否调用这个工具写得太窄导致该调用的不调用写得太宽导致不该调用的乱调用。我最初写的是知识库搜索工具太普适Agent 在回答一些常识性问题上也会尝试调用这个工具浪费资源和时间。后来改为限定培训政策、操作规范、常见问题这些具体场景效果好了很多。第二把query_engine作为类级别的属性传入而不是在工具内部初始化。这样多个工具可以共享同一个查询引擎实例避免重复构建索引。第三返回结果里附带来源信息非常重要。LangChain 1.0 的 Agent 最后会综合所有工具的输出来生成回答你把来源信息拼在返回字符串里Agent 就能在回答时引用文档出处提升回答的可信度。4.2 LangChain 1.0 链式调用与 LlamaIndex 的协作流单工具封装之外我还在模拟项目 X 里做了一个更复杂的链路用户提出问题后先由 LangChain 的一个路由链判断这个问题更适合走知识库检索还是通用对话还是调用外部 API 查询系统。这个判断是通过 LangChain 1.0 的create_route_chain之类的功能实现的。大致逻辑是给大模型一组选项让它根据用户问题的语义选择合适的路径。from langchain.chains import create_route_chain route_chain create_route_chain( llmllm, routes{ knowledge_base: 问题涉及内部文档、政策、操作指南等知识库内容, general_chat: 闲聊、常识、与内部文档无关的问题, api_query: 查询实时数据如系统状态、统计数据等, }, default_routegeneral_chat ) result route_chain.invoke({input: user_question})实测结果显示加了路由层之后系统整体的响应准确性提升了约 20%。原因是知识库工具不再被频繁误调用大模型的上下文窗口能更集中地用在合适的地方。4.3 用 LangChain 1.0 的对话记忆管理多轮问答知识库问答如果只支持单轮提问可用性会大打折扣。用户通常会先问一个问题再基于回答追问比如实习生入职培训内容包括哪些 接着问那考试通过分数线是多少在多轮场景下能把考试和培训关联起来就涉及对话记忆的管理。LangChain 1.0 对对话记忆的抽象做得比较成熟我采用的方式是短期记忆加场景裁剪from langchain.memory import ChatMessageHistory from langchain.memory import ConversationBufferWindowMemory # 只保留最近 4 轮对话作为短期记忆 memory ConversationBufferWindowMemory( k4, return_messagesTrue, chat_memoryChatMessageHistory() )在把对话历史注入路由链和工具调用之前先做一次裁剪压缩。比如用户在前面 20 轮对话里聊了很多无关话题但最近的追问是那考试通过分数线是多少我只需要最近一两轮的历史就能理解考试指的是什么。这里有一个细节LlamaIndex 的查询引擎本身是无状态的每次 query 都是独立基于输入文本去检索。所以多轮能力完全是靠 LangChain 的记忆管理来做上下文的组装和补充。我用一个简单的方法把对话历史和当前问题做拼接def build_enhanced_question(history_text: str, current_question: str) - str: if history_text: return ( f以下是最近的对话历史:\n{history_text}\n\n f用户当前的问题是: {current_question}\n f请基于历史上下文理解当前问题检索知识库并回答。 ) return current_question这个方式比较原始但在大多数场景下足够有效。更高级的做法是让大模型先做一轮指代消解把模糊的追问改写成一个完整的、不含歧义的提问然后再去查知识库。这个改写链我后面加了效果又提升了一些但要注意改写带来的延迟增加。提示LangChain 1.0 和 LlamaIndex 的版本迭代都比较快接口偶尔会有调整。我在写这篇时使用的接口调用方式之后版本可能已有变化建议你动手时先看下对应版本的 Release Notes 或官方迁移文档避免复制代码后报错。5. 文档处理工程的进阶优化元数据、缓存与流式输出5.1 元数据注入让检索从猜变成精确过滤只做全文向量检索有一个痛点——当文档种类多且领域差异大时纯粹靠语义相似度去匹配经常会把不同领域的相似表述混淆。解决办法是给每个文档提前打好标签利用元数据过滤来缩小检索范围。我在模拟项目 X 中给每份文档加了三类元数据文档类型手册、FAQ、规章制度、所属部门人事、技术、安全、发布时间。然后根据用户的提问场景在检索前先做一次粗过滤。from llama_index.core.vector_stores import MetadataFilters, ExactMatchFilter # 构建元数据过滤器 filters MetadataFilters( filters[ ExactMatchFilter(keydepartment, value安全), ], # 多个条件之间是 AND 关系 conditionand ) retriever VectorIndexRetriever( indexindex, similarity_top_k6, filtersfilters )这一步带来的提升非常明显。在包含 30 个测试问题的验收集上命中准确率从 78% 提升到 86%。原因也很好解释向量检索擅长理解语义但不擅长精确区分归属元数据过滤恰好补上这一环。关于元数据的来源我采用的是半自动方案先用一个分类模型对全部文档做自动打标再抽查修正。全自动打标在当前模型能力下还做不到完全可靠人工抽查的成本比较高但相比一开始就没有结构化标签已经是很好的折中。5.2 缓存层设计避免高频问题反复调用模型企业内部知识库有一个很明显的特征员工的提问模式相对稳定新员工常问的问题高度集中在前几天入职阶段。这对系统优化来说是个好消息——可以针对高频问题做结果缓存。我设计了两级缓存策略第一级是精确匹配缓存如果用户的问题和之前问过的某个问题语义完全一致或高度相似相似度大于 0.95直接返回之前的答案不经过检索和生成。第二级是准语义缓存如果问题相似度在 0.8 到 0.95 之间保留原问题检索出的参考文档节点但不复用生成结果因为表述的变化可能影响大模型对细节的把握所以重新生成答案。这个缓存的实现我用的是 Rediskey 是问题的向量化结果value 包含答案全文和参考文档的 ID 列表。import redis import numpy as np r redis.Redis(hostlocalhost, port6379, db0) def check_cache(question_vector): # 向量检索 Redis 中存储的历史问题 # 返回最相似问题的 ID 和相似度 pass # 简化示意 def get_cached_answer(question_vector): similar_question, similarity check_cache(question_vector) if similarity 0.95: return r.get(similar_question) return None实际跑下来在模拟项目 X 的 50 个高频问题上缓存命中率约为 18% 左右虽然比例看着不大但考虑到每次命中省下的是完整的检索加生成流程延迟从原来的 3 到 5 秒直接降到 50 毫秒用户体验提升非常明显。5.3 流式输出让大模型边想边写不再是幻觉大模型生成回答需要时间如果一个 500 字的中文回答要等 4 秒才一次性出来用户会很焦虑。我一开始也觉得知识库问答不需要流式输出直到给实际用户试用后收到反馈才改。LangChain 1.0 对流式输出的支持比较完善关键在于 LlamaIndex 查询引擎的回调机制。你需要给 LlamaIndex 设置一个回调处理器把内部生成的 token 流传给 LangChainfrom llama_index.core.callbacks import CallbackManager from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler # 把 LangChain 的流式回调接入 LlamaIndex callback_manager CallbackManager( handlers[StreamingStdOutCallbackHandler()] ) query_engine RetrieverQueryEngine.from_args( retrieverretriever, node_postprocessors[postprocessor], callback_managercallback_manager )这里有个细节LlamaIndex 的响应对象在流式模式下也有response_gen生成器你可以单独消费它。在集成时容易踩的坑是如果直接调用query_engine.query()返回的是完整响应流式效果不会生效必须通过流式接口调用并逐块输出。6. 常见问题与排查技巧实录这一节我整理一下在模拟项目 X 开发和试运行阶段踩过的典型问题按出现频率排序。6.1 检索不到相关内容先查切分再查阈值现象用户问了一个很常规的问题系统回复未找到相关资料或者答案明显偏离。排查思路分三步第一步检查切分后的 Node 数量。如果文档切分粒度太粗每个 Node 塞了太多主题向量化后语义被稀释检索匹配精度会直线下降。我遇到过切分后 Node 平均长度超过 900 字符的情况检索准确率明显低于 512 字符档位。重新设置 chunk_size 为 512 后恢复。第二步检查相似度阈值是否设置过高。把 postprocessor 的日志打开看实际检索到的最高相似度是多少。如果最高的相似度都低于阈值那一定是阈值不合适。第三步检查 embedding 模型是否和应用场景匹配。通用领域文档和垂直领域文档的最佳 embedding 模型选择并不相同。我在测试法律文档时发现某些开源中文 embedding 模型的效果比通用英文模型好很多。6.2 答案引用混乱召回逻辑不透明导致溯源困难现象系统生成的答案内容本身合理但用户要求告知信息来源时报错或张冠李戴。这是我最初遇到的一个比较隐蔽的问题。问题出在 LangChain 调用工具时返回的 source_nodes 顺序和最后生成答案所用的上下文顺序不一致。因为工具返回的是回答 参考来源但是 Agent 在综合多个工具信息时可能会重新组织内容导致来源对应关系错乱。解决办法是把每个参考来源的完整文本也放进返回字符串而不是只放文件名。这样 LangChain 在生成最终答案时会看到每个片段的前文内容来源引用会自然对齐。6.3 文档更新后索引不刷新增量更新怎么做现象新增了一批文档到目录后查询结果里完全没有新文档的内容。LlamaIndex 提供了增量更新方法支持在不重建整个索引的情况下为新文档添加索引new_docs SimpleDirectoryReader( input_dir./data/new_docs ).load_data() new_nodes splitter.get_nodes_from_documents(new_docs) index.insert_nodes(new_nodes)但这里要注意一个坑index.insert_nodes只更新向量索引如果你还建了 TreeIndex 或其他类型的索引需要分别处理。我一开始只更新了 VectorStoreIndex结果用 TreeIndex 的那套查询逻辑查不到新内容排查了半天才发现是索引类型没同步更新。另一个办法是把文档目录变更做成定时任务每天凌晨全量重建一次。数据量小的时候这个方法最省心数据量大时就只能增量更新了。模拟项目 X 目前用的是增量更新加每周一次全量备份重建的组合方案。6.4 token 消耗过高上下文组装需要控制成本现象跑了几天token 消耗超出预期单个用户单次对话平均消耗超过正常水平好几倍。分析日志后发现主要原因是 Agent 在多轮对话中反复调用知识库工具每次调用都会带入检索到的 6 个 Node每个 Node 平均几百 token几轮下来就累积很高。优化措施有三条按性价比排序降低相似度阈值过滤掉边缘相关的 Node。在路由链上做限制普通闲聊问题直接拦截不调用知识库工具。对多次调用的工具限制最高调用次数或者要求在后续调用中复用上一次检索结果。第 3 条实现起来复杂一些但效果最好。我在模拟项目 X 里实现了会话级检索缓存——同一会话中若用户多次追问优先使用同一批检索结果而不是每次都重新去全库检索。实测 token 消耗降低了约 30%。6.5 LlamaIndex 与 LangChain 版本兼容的坑现象按文档写好的示例代码跑不起来报各种 import 错误或参数错误。LangChain 1.0 和 LlamaIndex 都进入了大版本迭代期接口变动频繁是正常现象。我的建议是在 requirements.txt 里明确锁版本langchain1.0.x langchain-openai1.0.x llama-index-core0.11.x llama-index-readers-file0.2.x llama-index-embeddings-openai0.2.x llama-index-vector-stores-chroma0.2.x在升级版本前先看官方迁移文档。迁移文档虽然有时写得不够细致但主要接口变更都会有说明。7. 项目验收与效果评估文档处理工程做完不是终点要有科学的验收方法。我分享一下模拟项目 X 里用到的评估方案你可以参考这个框架来做自己的项目验收。构建测试集时要注意覆盖度。我总共设计了 60 个验收问题分布在三类文档上操作手册类 20 题、规章制度类 20 题、FAQ 类 20 题。每类问题再细分成三种提问难度原文可直接找到答案的、需要理解后归纳的、需要结合多个文档片段推理的。评估指标我用的是三个检索召回率答案相关信息是否被检索到。这个指标很好算只要检查检索出的 6 个 Node 里是否包含能回答问题的内容。答案正确率最终回答是否准确由人工判定。答案完整率除了正确还要看是否涵盖问题所有方面。实测数据如下供你对照参考指标默认配置元数据过滤 参数调优后检索召回率73.3%86.7%答案正确率71.7%85.0%答案完整率63.3%78.3%平均响应延迟4.8 秒3.1 秒从数据看元数据过滤和检索参数调优带来的提升是决定性的。另外我把测试结果按问题难度做了拆分简单问题的正确率从 90% 提升到 95%归类型问题的正确率从 40% 提升到 65%推理类问题的正确率从 50% 提升到 75%。说明优化的空间主要在中等难度的归类型问题这类问题依赖准确的信息召回。对于推理类问题我用过检索两次再交叉验证的方案把分属不同文档的信息都召回后再合并推理效果有提升但这会让系统复杂度增加不少建议按实际需求取舍。8. 我实际测试中记录的几个关键经验老规矩最后直接给干货。这一阶段我从零到尾跑完后有几个体会比较深第一个体会是文档处理的难点从来不是代码而是对数据的理解。你拿到文档以后先花时间摸清文档的类型构成、结构特征、术语分布比上来就调 embeddings 更重要。我这次在模拟项目 X 里浪费了不少时间在反复调参上回头想如果一开始就把文档分好类、设计好切分策略很多调试完全可以跳过。第二个体会是嵌入模型的选择比想象中更影响效果。我一开始用的是通用 embedding 模型后来换到更适合中文的模型后检索准确率提升了约 7%。这提醒我在垂直领域做 RAGembedding 模型的适配是值得花时间去试验的。第三个体会是从原型到产品之间还隔着很远的路。原型阶段只是把核心链路跑通但离真正可用还差很多。比如你要处理用户在日常使用中说错词、说含糊话的情况要处理检索不到任何结果时的兜底回答要有完善的日志记录和调用耗费统计。这些都不是框架本身能帮你解决的而是要针对应用场景单独开发的。最后分享一个特别实用的小技巧在调试文档处理链路时建议把所有中间结果都打印出来看一遍。包括切分后的每个 Node 的前 50 个字符、每个 Node 的向量相似度分数、检索结果列表的顺序。我用的工具是 LlamaIndex 自带的回调日志功能打开之后能在终端看到每一步的耗时和处理情况。很多看似玄学的问题看了这些日志后都能很快定位到原因。整个第九阶段的内容到这里就收尾了。文档处理工程说难不难说简单也不简单关键是把握好切分、索引和检索这几个环节的联动关系用 LangChain 1.0 做编排用 LlamaIndex 管数据两者各司其职最后出来的效果还是比较让我满意的。