
在LlamaIndex里做过几个RAG项目之后我对“LLM到底在管什么”的认知被彻底刷新。很多人以为它只是最后回答问题的那张嘴实际上从文档进入索引那一步开始LLM就已经深度参与数据摄取、查询理解、结构化操作、Agent编排、质量评估每个环节它都能扛不同的活。这篇文章就围绕我在实际工程里反复用到的五类场景展开把搭建思路、代码片段和踩过的坑一并写上。适合正在用LlamaIndex做企业知识库、复杂问答、多文档分析或者准备把RAG从“Demo能跑”推向“线上能扛”的人。1. 先摸清全貌LLM在LlamaIndex的流水线里干了哪些活1.1 一条典型数据流里LLM卡位的五大环节LlamaIndex不是一个简单的“文档塞进去、问题丢出来”的黑盒。把它拆开看它的流水线大致是数据接入与解析入库、索引构建、查询规划、检索器召回、响应合成再外面还有Agent循环和评估层。在这条流水线里LLM可以出现在几乎每一个“需要理解语义”的位置。我按工程里最常见的分工把它的核心应用归纳成五个方面环节LLM承担的核心任务典型落地对象数据摄取提取元数据、生成摘要、抽取实体关系MetadataExtractor、KnowledgeGraphIndex查询理解改写、扩展、压缩查询判断去哪个数据源QueryTransform、RouterQueryEngine结构化交互把自然语言转成SQL、查询图谱三元组NLSQLTableQueryEngine、GraphIndexAgent编排选择工具、拆解任务、多步执行、结果整合FunctionCallingAgentWorker质量评估判断答案是否忠实、是否相关生成评测集FaithfulnessEvaluator、RelevancyEvaluator这五类不是并列的小工具而是层层递进的关系。数据摄取决定了系统“知道什么”查询理解决定了“怎么问”结构化交互决定了“能不能问出深度”Agent编排决定了“复杂任务能不能自己拆”评估决定了“你敢不敢把它放上线”。很多项目只把LLM用在最后一个问答环节前面全靠字符串匹配和向量相似度效果当然能跑但一遇到长尾表达就露馅。1.2 配置前必须想清楚的边界LLM和传统程序各管哪一段聊应用之前我觉得先把边界厘清更重要。LlamaIndex里不全靠LLM底层很多模块是传统确定性算法比如文档切分的规则、embedding模型的向量化、向量索引的最近邻检索、SQL执行器本身的运算。这些环节你用LLM去替代往往是画蛇添足。我现在的划分方式是一句话凡是“有标准答案、需要精确计算、需要可复现”的事尽量交给传统代码凡是“语义开放、表达多样、需要理解意图”的事才值得把LLM叫出来。比如让LLM算一个订单总金额它可能会因为读错数字而翻车但让它把问题“上个月哪个品类的退货率最高”改写成“SELECT对应字段”的意图它做得很好。另一个容易被忽略的问题是同一个LLM不一定适合所有环节。我在某个项目里曾用一个开源小模型同时做元数据提取和SQL生成结果关键词提取勉强能用SQL生成一塌糊涂。后来把策略改成“重任务用强模型轻任务用弱模型”成本反而降了因为强模型的调用次数少了。这件事放到后面每个环节里都成立不要在全局只配置一个LLM就万事大吉。2. 第一类文档摄取阶段的LLM指令——入库前先加工一遍2.1 元数据提取让检索从“全文比较”变成“属性过滤”很多初版RAG系统都是直接把文档切块后全部向量化查询时做相似度匹配。这种做法不是不行而是效率低、噪音大一个十几页的PDF真正和问题相关的可能就一小段但你仍然要把所有片段都过一遍相似度还可能被相似但无关的段落干扰。LLM在入库阶段能做的最实际的一件事就是提取结构化元数据。LlamaIndex里的MetadataExtractor可以让你给每个节点打上标题、关键词、主题等标签。我常用的是这样一套配置from llama_index.core.extractors import ( MetadataExtractor, TitleExtractor, KeywordExtractor, ) from llama_index.core.node_parser import TokenTextSplitter extractor MetadataExtractor( extractors[ TitleExtractor(nodes5), KeywordExtractor(keywords15), ], ) node_parser TokenTextSplitter( chunk_size1024, chunk_overlap64, )这段代码做的事情很直白把长文档切块后每块额外生成一个标题和一批关键词。有了这些字段后我可以在查询阶段先按关键词过滤再去向量检索召回精度提升非常明显。举个例子一个合同库里有采购合同、服务合同、保密协议如果节点上面有“合同类型”这个元数据用户问“保密协议里仲裁条款怎么写的”就能先过滤掉八成无关内容。这里有个容易理解偏差的点标题和关键词这类信息常规规则其实也能提取一部分比如正则抓日期、按文件路径猜分类。但开放域的文档没有那么好的规律LLM的价值在于它不需要预先定义每个文档类型就能给出语义上合理的标签。这也是为什么很多知识库项目把这一步当作标配。2.2 摘要、剥离与节点组织的实际做法入库阶段的另一个高价值操作是生成节点摘要。摘要不是简单地把内容缩短而是给检索和问答提供一个“概览层”。我在做多文档问答时经常遇到的情况是用户问的是跨文档的对比问题比如“三份报价单里有哪些不同”。这时候如果把每份文档的原始切片都交给LLM去比较上下文窗口很快被撑爆。更聪明的做法是让LLM先为每份文档生成一个摘要节点检索层先命中摘要然后再深入对应的原始切片。我用过一段比较顺手的配置给摘要节点打了一个独立的类型标签from llama_index.core.schema import MetadataMode # 示意在已有节点基础上生成摘要并给节点打上 type 字段 for node in nodes: summary await llm.acomplete( f请用3-5句话总结这个文档片段的核心内容\n{node.text} ) node.metadata[node_type] summary node.metadata[summary_text] str(summary)这种做法等于给知识库建立了一个“先看目录、再翻正文”的阅读习惯。检索时如果问题的概括性很强就先命中摘要节点如果问题问到具体数字再落到原始切片。实际测下来这种分层检索对长文档问答的帮助比单纯调大chunk_size更明显。还要注意切片数量越多需要LLM生成的摘要数量越大这一步通常离线执行别等用户提问时才现算。我的习惯是文档一进来就异步跑一遍加工管道加工结果持久化到索引里线上查询完全不碰提取任务。2.3 我在跑批处理时踩过的token和稳定性问题这里一定要说几个坑几乎每个项目都会遇到。第一是token消耗被低估。假设你有一万份文档每个节点用LLM生成一次摘要和关键词平均每次消耗几百token一轮下来就是几百万token。所以我后来强制给自己定了一条规则摘要任务用强模型关键词提取用轻量模型二者分开配置。很多框架支持按不同组件指定不同LLM别嫌麻烦这个收益直接体现在账单上。第二是失败的幂等处理。大模型调用偶尔会超时、返回空内容、或者返回一段JSON解析失败。批量跑的时候如果进程中途挂了最好支持断点续跑。我一般会把“已生成的摘要文本字段是否为空”当作处理状态标记跑完之后扫描一遍缺失字段再补跑而不是每次重新全量执行。第三是截断问题。LLM对长文档做摘要时如果直接传入整个节点文本后台可能会悄悄截断导致摘要只覆盖前半段。规避办法是控制输入长度超过上限的分两次摘要再合并。这也是为什么我习惯先切块再做摘要而不是把整份文档直接扔给模型。3. 第二类查询面的理解与路由调整——让检索入口变得更聪明3.1 查询转换多查询、折叠、HyDE类改写数据加工完之后检索不是直接从问题跳到向量中间还需要一个查询理解层。原因很简单用户的问题通常很口语化而文档里的表述往往更书面。用户问“最近一波行情对制造业的冲击”文档里可能只有“供应链中断”“订单波动”之类的话直接拿原问题去做向量匹配召回效果会打折扣。LlamaIndex里处理这个问题的主要思路是查询转换它的目标是把“用户的原始表达”变成“更可能在文档中命中语义的表达”。我常用的几种方式包括多查询让LLM根据原问题生成多个同义改写分别去检索最后合并结果去重。适合文档里同一件事有不同叫法的场景。子问题拆分把“A和B的区别是什么”拆成“A有什么特征”“B有什么特征”“二者有哪些共同点”等子问题分别检索再拼答案。假设性文档Embedding类似HyDE的做法让LLM先根据问题写一段“如果文档里有答案它大概会怎么写”的假想回答再用这段假想回答去做向量检索能吃下不少表述差异。用官方组件来实现多查询大致的框架长这样from llama_index.core.query_engine import RetrieverQueryEngine from llama_index.core.retrievers import QueryFusionRetriever retriever QueryFusionRetriever( [vector_index.as_retriever(similarity_top_k4)], num_queries4, modereciprocal_rerank, use_asyncTrue, ) query_engine RetrieverQueryEngine.from_args(retriever)我建议从单路改写开始做别一开始就铺多路召回。多查询意味着多个LLM调用和多次检索查询延迟会成倍上涨。对于响应时间敏感的场景先评估原始查询是否足够清晰再决定要不要做扩展。我在一个客服场景里把多查询的开关做成动态的当第一轮检索分数低于阈值时才触发多查询效果和全量开启接近但成本低了很多。3.2 LLM做路由数据源选择与索引选择不少人忽略的一个事实是企业里很少只有一个知识库。可能是SQL数据库加上文档库再加上一个操作手册Wiki用户一句话可能需要去不同数据源找答案。这时候LLM的用途是当“路由分发器”决定该去哪个索引或工具里查。LlamaIndex里的RouterQueryEngine就是干这个的。它的核心想法是每个查询引擎被描述成一个带说明的工具LLM根据用户问题判断哪个工具最合适。我一般这样组织from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.tools import QueryEngineTool sql_tool QueryEngineTool.from_defaults( query_enginesql_query_engine, description用于查询订单、价格、库存等结构化数据。, ) doc_tool QueryEngineTool.from_defaults( query_enginedoc_query_engine, description用于查询产品手册、制度文件等非结构化文档。, ) router RouterQueryEngine.from_defaults( query_engine_tools[sql_tool, doc_tool], selectorLLMSingleSelector(llmllm), )这个思路很简单但实际工程里有三个坑。一是工具描述不能写得太模糊。如果你只写“查询订单数据”LLM可能把“上个月的销售额是多少”这种统计问题也丢给文档检索而不会去SQL引擎。我后来在描述里明确加上“支持聚合、筛选、按时间范围统计”等关键词路由准确率立刻上来了。二是路由失败的兜底非常关键。我见过不少系统路由判断错了就直接答错。稳妥的做法是加一个默认兜底引擎当LLM对路由结果不确定时路由到“同时检索多个数据源再合并”。代价是慢一点但至少不会因为路由错误而给出完全无依据的回答。三是日志里一定要能看到路由结果。每次用户问题最终走了哪个工具这个信息对调试极有价值。你可能会发现大量问题被路由到了同一个工具而另一个工具几乎无人问津那要么是描述有问题要么是业务上那个数据源本来就不该接入。3.3 一个带降级策略的路由配置示例我在生产环境里常用的不是单一Router而是叠加了降级策略。大体逻辑是先用轻量分类器判断问题是否包含“统计、金额、数量”这类结构化信号命中就强制走SQL工具否则走LLM路由如果LLM路由给出的置信度很低默认落到向量检索。伪代码如下def route_query(question: str): if contains_struct_signal(question): return sql result llm_complete( f判断问题应使用哪个工具\n{question}\n f可选sql / docs / both / none ) if result in (sql, docs, both): return result return docs # 默认兜底这不是什么高深技术但能很大程度避免花哨的LLM判断带来低级的错误。很多团队一上来就用RouterQueryEngine却忘了它的输出本质上是一个概率判断不是绝对真理。再加上一层业务规则和兜底系统才会稳。另一个经验是路由查询和普通检索最好分开配置模型。路由任务对LLM的推理要求不高但对速度和成本敏感用一个中等偏小的模型就够了。真正需要把大模型资源留给后端答案生成的阶段。4. 第三类自然语言转结构化操作——SQL与知识图谱场景4.1 Text-to-SQL引擎的搭建思路文档向量检索擅长处理非结构化内容但企业问答里大量问题是结构化的订单金额、库存数量、每个月的变化趋势。这些问题需要精确计算向量模型给不了可靠的数字答案这时候就得让LLM去生成SQL。LlamaIndex里有NLSQLTableQueryEngine这类组件我通常会用类似下面的方式建立SQL问答能力from llama_index.core.query_engine import NLSQLTableQueryEngine from sqlalchemy import create_engine engine create_engine(sqlite:///sales.db) sql_query_engine NLSQLTableQueryEngine( sql_databaseengine, tables[orders, customers, products], llmllm, )它内部的逻辑是先把数据库表结构注册到LLM上下文里用户提问后LLM生成一个SQL框架再执行SQL并把结果返回。这套流程有两个明显的好处第一用户不需要懂SQL第二只要SQL正确答案数值就是精确的不存在编造。但Text-to-SQL远没有看上去那么省心。我踩过最典型的问题就是“表结构太多导致生成错误”。当数据库有几十张表时如果全部塞给LLM它会经常选错表或写错关联关系。解决办法有两个方向一是只给问题可能涉及的表减少干扰二是在表名和字段名后面附上一句话注释比如“order_status表示订单状态1为已支付2为已取消”。LLM去理解带注释的表结构比裸看字段名可靠得多。另外强烈建议给SQL执行加一道安全校验。给LLM一个只读权限的数据库账号禁止DROP、DELETE这类危险操作。这个不是LlamaIndex框架层的配置而是你连接数据库时就应该做好的事情。我见过有人直接用生产库连接字符串去跑NL2SQL吓得我当时就让他改了。4.2 知识图谱索引中的实体与关系抽取结构化交互的另一大场景是知识图谱问答。它解决的问题是“多跳关系推理”比如“某项目由哪个团队负责该团队之前是否做过同类项目”。这类问题如果靠向量检索每一跳都可能有语义偏差答案往往东拼西凑而图谱能把实体和关系连起来答案来源更清楚。构建知识图谱Index时LLM负责从文本里抽取实体和关系三元组。类似下图这样from llama_index.core.indices.knowledge_graph import KnowledgeGraphIndex kg_index KnowledgeGraphIndex.from_documents( documents, llmllm, include_summaryTrue, max_triplets_per_chunk10, )抽取三元组的效果很大程度上取决于模型对实体边界的理解。同一个“苹果”在一篇讲水果的文档里是食物在一篇讲科技公司的文档里是公司。所以我一般在给LLM的提示词里明确加上“结合上下文判断实体类型不要所有专有名词都当成公司名”这样的约束。这个提示词看起来不起眼但在实际评测里可以把三元组准确率提升一截。知识图谱不能替代向量索引它们更像是互补关系。向量索引适合“找相关段落”图谱适合“找关联路径”。在实际系统里我通常会把文档同时建两种索引让路由层决定什么时候用哪一种。如果问题明显需要关系链推理走图谱如果只是按主题找资料走向量。4.3 什么情况下不该硬上这类结构化能力聊到这儿必须泼一盆冷水不是所有问题都值得上SQL或图谱方案。我有一次遇到一个项目产品经理希望所有合同条款都能做精确对比。听起来很适合图谱实际上合同文本这种强规则文本用结构化抽取成本极高而且条款之间的差异常常是自然语言层面的细节三元组表达不了那么细。最后我们还是用了带条款编号的元数据过滤加摘要对比效果反而好。我的判断标准有三个问题是否频繁要求精确数值是考虑SQL否向量检索足够。实体之间的关系是否稳定且可枚举是图谱有优势关系高度开放图谱维护成本会失控。数据质量是否支持结构化如果源数据本身就是噪声很大的OCR文档先清洗数据别急着建图谱。结构化能力的建设成本远高于普通的向量索引它需要更仔细的数据建模、更干净的元数据、更长的调优周期。如果用户实际问答里这类需求占比不高理性选择是先用文档检索顶住把结构化能力放到第二阶段迭代。5. 第四类Agent编排与多步规划——LLM开始当调度员5.1 工具选择与函数调用的运行机制当你把“查询文档”“查数据库”“发请求”“写文件”这些能力都封装成工具后单轮的检索问答就不够用了。用户的问题往往需要组合多个工具比如“查一下最近三个月的销售额再对比去年同期把差距最大的品类列出来”。这中间涉及两步查询甚至可能需要调用一次计算工具。这时LLM的角色从“信息生成者”变成了“任务调度员”。LlamaIndex里的Agent机制核心是函数调用框架把工具列表暴露给LLMLLM在对话循环里自己决定调用哪个工具、传什么参数、然后再根据结果决定下一步动作。我在项目里通常这样组织from llama_index.core.agent import FunctionCallingAgentWorker from llama_index.core.tools import QueryEngineTool tools [ QueryEngineTool.from_defaults( query_enginesql_engine, description查询销售、客户、产品结构化数据支持聚合统计。, ), QueryEngineTool.from_defaults( query_enginedoc_engine, description查询产品手册、技术文档、Wiki等非结构化内容。, ), ] agent_worker FunctionCallingAgentWorker.from_tools( tools, llmllm, max_iterations8, verboseTrue, ) agent agent_worker.as_agent()它的运行机制可以通俗地理解为LLM拿到用户任务后先想“要解决这个问题需要哪些信息”然后调用第一个工具拿到结果再判断信息够不够不够就继续调下一个工具直到拼出完整答案。传统程序靠if-else写分支Agent靠LLM动态决策分支。5.2 一个多文档摸查Agent的构建片段我做过一个比较典型的案例输入是若干份技术评估报告用户不是问某一篇报告的内容而是要横向比较“哪份方案的成本最低、风险最大、交付周期最短”。这属于跨文档归纳任务单靠检索很难一次完成用Agent就顺很多。我的做法是给Agent注册两个工具一个是“按文档名检索特定报告内容”另一个是“跨文档汇总比较”。Agent会先分别调第一类工具拿到每份报告的关键段落再调用第二类工具做汇总生成。过程中它的轨迹类似于识别问题需要比较三份报告。调用工具A检索第一份报告的成本和风险段。调用工具A检索第二份报告的成本和风险段。调用工具A检索第三份报告的成本和风险段。调用工具B把三段信息综合成对比结论。这里面最关键的是每个工具的描述要足够清楚地告诉LLM“这个工具能干什么、不能干什么”。比如“工具A只能单篇检索不能跨文档对比。跨文档对比请用工具B”。限制条件写得越明确Agent跑偏的概率越低。5.3 生产环境里我反复遇到的三个Agent坑Agent看起来很美好一旦上线坑也不少。我根据自己的实践列三个最常遇到的。第一死循环。LLM可能反复调用同一个工具返回结果还是一样然后它继续调用。这通常是因为工具返回的信息不含“任务已经解决”的信号。我的解决方式是设硬性的迭代上限比如8次同时在每轮工具结果里加一个简短的“已找到关键信息”或“仍缺少信息”的状态说明帮助LLM判断何时停止。第二上下文膨胀。Agent把多轮工具结果都塞进了对话历史每调用一次工具token占用就涨一大截。跑到第6、7轮时上下文窗口可能就被塞满了后面的决策质量直线下降。我的经验是工具返回长文档时先做裁剪只保留与当前任务最相关的段落更狠一点的做法是超过三轮后要求LLM输出阶段性摘要把前面内容压缩掉。第三工具权限过大。Agent本身没有善恶判断它只负责完成目标。如果你把删除文件、写外部API等危险能力也包装成工具一旦用户问题被提示词注入后果很难收拾。我的原则是Agent能看到的工具必须是最小权限集合查询类Agent只给只读工具。永远不要让Agent在无人工确认的情况下执行有副作用的操作。6. 第五类拿LLM评估LLM——让输出质量可度量6.1 检索评估里“LLM裁判”的评判逻辑前面四个应用都是让LLM去“做事”第五个应用恰好反过来让LLM去“挑错”。一个RAG系统上线之前你不光要知道答案能不能生成还要知道答案是不是“忠实于证据”的。坏的情况是检索到的文档本身就不相关LLM硬生生编了一个听上去合理的回答。这种情况人工排查效率低用LLM当裁判来做自动评估就成了刚需。LlamaIndex里有两个我常用的评估器FaithfulnessEvaluator和RelevancyEvaluator。前者检查“回答是否忠实地建立在检索上下文上而不是凭空捏造”后者检查“回答是否真正回应了用户的问题”。我一般把它们串起来用from llama_index.core.evaluation import ( FaithfulnessEvaluator, RelevancyEvaluator, ) faithfulness FaithfulnessEvaluator(llmllm) relevancy RelevancyEvaluator(llmllm) eval_result await faithfulness.evaluate_response( queryquery, responseresponse, )这两个评估器的逻辑都是让LLM以标准Prompt判断输出是/否和理由。它们的价值在于可以批量跑评测集不用人工一条条看。但要注意它们本质上是另一个LLM的主观判断同样有可能出错。所以我从来不把单一的LLM裁判分数当作绝对标准而是当作筛选工具分数低的直接看理由分数高的抽检确认。6.2 自动合成评估数据与提升指标没有测试集就谈不上系统优化。手工标一条Q-A对成本高、速度慢所以很多团队会采用合成数据的方式生成评测集。具体做法是拿一批真实的领域文档让一个强LLM在阅读文档后生成“问题-标准答案-参考来源”的组合。这个组合自动覆盖了不同难度的查询之后再拿你的RAG系统去跑看答案能不能覆盖到参考来源以及答案的相关性和忠实度如何。我自己常用的做法是问题分成三类简单抽取型、归纳总结型、多跳推理型。分别统计通过率就能知道你的检索链路短板在哪。比如简单抽取型通过率高但多跳推理型通过率很低说明当前检索召回的多是单一来源片段缺少跨文档组合能力。这时候就该考虑上Agent编排或者知识图谱来补充多跳检索。合成数据也有一个明显问题LLM生成的测试问题经常过于规整不像真实用户的表达。比如用户会问“之前聊的那个事情进展怎么样”而合成问题往往是完整的书面语。所以合成评估集要和少量真实用户问题混用比例大概可以控制在“真实问题三成、合成问题七成”。完全没有真实问题的评测集指标再好看上线也可能翻车。6.3 评估器也要被校验如何防止LLM裁判失真LLM裁判同样会偏好某种表达风格比如它可能偏爱更长的答案、偏爱列点式回答、甚至偏爱使用特定连接词的句子。这些偏好会导致评估分数和人工判断不一致。我在一个项目中就遇到过忠实度评估器几乎给所有结构化列点回答打了满分仔细看理由才发现它把“回答格式规范”当成了“忠实”。解决这类问题我的经验是分三步。第一小范围人工标注一批问答对比如100条把人工分数和LLM裁判分数对比找出系统性的偏差第二如果发现裁判对长度敏感就在Prompt里明确约束“忽略答案长度只看是否基于上下文”第三最终指标建议综合多个评估器而不是只信一个分数比如同时看Faithfulness、Relevancy和正确答案语义相似度三者的加权结果。还有一点是 Prompt 中的评价标准要尽可能具体。让LLM判断“是否相关”时给一个例子比给十句抽象定义都有效。“比如查询中问到退款政策而回答只介绍了商品规格则判定为不相关”这种示例能明显降低裁判打分的波动。所有评估器上线前都值得先拿几十条人工标注样本把标准对齐一遍。到现在我自己的项目里已经习惯把这五个应用编进同一条流水线数据入库时让LLM抽取元数据查询入口让LLM路由和改写遇到结构化需求就让LLM生成SQL或图谱查询复杂任务交给Agent编排最后再开一层自动评估来盯着整体质量。每一步都单独调优过但真正让系统稳定的是它们在同一套配置里彼此协作。如果你正在打算把LlamaIndex落地到真实业务我建议别急着一次性全铺开先选当前最痛的环节切入检索不准就先做查询理解和元数据提取答案老出错就先上评估层。跑起来再逐步加后面的能力这样每一步的收益都看得见也更容易说服团队继续投入。