1. 项目概述LlamaIndex到底是什么为什么它突然成了RAG和文档智能领域的“默认选项”LlamaIndex不是另一个LLM调用封装库也不是LangChain的平替或竞品——它是为解决一个非常具体、非常痛的问题而生的当你的数据在本地、在私有系统里而大模型却只“知道”互联网上公开的内容时怎么让大模型真正“读懂”你自己的PDF、数据库、内部Wiki、甚至扫描件里的文字这个问题就是RAG检索增强生成的核心命题。而LlamaIndex是目前开源生态中把“数据如何进、如何存、如何找、如何喂给模型”这一整条链路打磨得最顺滑、最模块化、也最贴近工程落地现实的框架。它不强行定义你的应用架构但会给你一套可插拔、可组合、可调试的“数据管道积木”。我第一次用它加载一份200页的财务尽调报告并准确回答“第三章提到的关联交易金额是多少”只写了7行代码从读文件到拿到答案耗时48秒——这背后不是魔法而是它对数据分块chunking、嵌入embedding、索引indexing、检索retrieval和重排序reranking每个环节都做了大量面向真实场景的预设与优化。它的核心关键词是“文档智能”和“数据框架”而不是“聊天机器人”或“AI代理”。这意味着如果你正在做的是知识库问答、合同审查辅助、内部技术文档搜索、或者需要把非结构化数据变成模型可用的上下文LlamaIndex就是那个站在你数据和模型之间的“翻译官调度员质检员”。它和LangChain的区别绝不是功能列表的简单对比而是设计哲学的根本差异LangChain更像一个通用的“应用胶水”强调链式调用和工具编排而LlamaIndex则是一个专注的“数据引擎”所有设计都围绕“我的数据如何高效、精准、可控地服务LLM”展开。比如LangChain的Retriever抽象很宽泛而LlamaIndex直接提供了HybridRetriever向量关键词混合、SubQuestionQueryEngine把复杂问题拆成子问题分别检索、以及GraphRAG所需的KnowledgeGraphIndex——这些不是后期加的插件而是从第一天就内建在核心架构里的能力。所以当你看到热搜里反复出现“llamaindex和langchain区别”真正该问的其实是“我现在要解决的问题是需要一个灵活的工具链还是一个强大的数据底座”答案往往决定了你项目初期的技术选型是否埋下了半年后重构的伏笔。2. 核心设计思路拆解为什么LlamaIndex的架构能同时兼顾新手友好与专家可控2.1 “核心core集成integrations”的双层架构解耦与复用的极致实践LlamaIndex最聪明的设计是把整个框架清晰地切成了两层llama-index-core和llama-index-integrations。这不是简单的代码分包而是一种深思熟虑的工程哲学。llama-index-core是纯逻辑层它定义了所有关键抽象Document文档、Node数据块、Index索引结构、Retriever检索器、QueryEngine查询引擎。它不依赖任何外部服务不硬编码OpenAI或HuggingFace甚至连print()都不用——它只做一件事保证数据流在内部各模块间传递时格式、接口、生命周期都严格受控。你可以把它理解为一个精密的“数据流水线图纸”上面画满了接口卡槽和标准螺纹。而llama-index-integrations则是无数个按标准卡槽制造的“功能模块”。llama-index-llms-openai、llama-index-embeddings-huggingface、llama-index-vector-stores-qdrant……每一个都是一个独立的PyPI包只负责把自家的SDK比如OpenAI的Python SDK适配到llama-index-core定义的接口上。这种设计带来的好处是颠覆性的。第一版本解耦。你可以用llama-index-core0.14.0搭配llama-index-llms-openai0.13.5因为它们之间只有清晰的接口契约没有代码依赖。第二轻量启动。一个只需要本地Ollama模型和SQLite存储的边缘设备应用pip install llama-index-core llama-index-llms-ollama llama-index-storage-docstore-sqlite安装包总大小不到5MB而一个企业级应用可以同时装上llama-index-llms-azure-openai、llama-index-embeddings-azure-openai、llama-index-vector-stores-pinecone所有模块无缝协作。第三社区共建。截至2024年中LlamaHub上已有超过300个官方和社区维护的集成覆盖了从Notion、Confluence、Salesforce到MySQL、PostgreSQL、甚至SAP ERP的连接器。这300个模块没有一个需要修改llama-index-core一行代码全靠这套架构撑起来。2.2 “数据即节点Data-as-Nodes”的范式从文档到语义单元的原子化处理传统RAG流程里“加载PDF”是一个黑盒操作结果是一堆文本。LlamaIndex则强制你面对一个根本问题数据在进入模型前必须被分解成什么粒度的“语义单元”它的答案是Node。一个Node不是一个简单的字符串切片而是一个携带丰富元数据的数据容器。它至少包含text内容、id_唯一标识、metadata自定义键值对如{source: contract.pdf, page: 12, section: Liability}、embedding可选预计算的向量、relationships与其他Node的关联如父文档、子段落。这个设计看似增加了复杂度实则解决了RAG中最顽固的痛点——上下文漂移context drift。举个例子一份《用户隐私协议》里“我们收集您的位置信息”这句话如果孤立地看可能被检索到任何谈“位置”的文档里。但当它被打包成一个Node并附上metadata{document_type: privacy_policy, effective_date: 2024-01-01}再通过relationships链接到其父Document对象那么在后续的检索和重排序阶段系统就能利用这些元数据进行过滤、加权和上下文拼接。我曾在一个医疗知识库项目中将每一条临床指南的“推荐等级”A/B/C级作为Node.metadata[recommendation_level]存入。当用户提问“针对高血压患者的首选药物”查询引擎不仅能召回相关段落还能自动按recommendation_level降序排列结果把A级证据放在最前面——这种能力是单纯靠向量相似度永远无法实现的。LlamaIndex的Node就是让数据在流动中始终保有其“身份”和“关系”的关键锚点。2.3 “查询即引擎Query-as-Engine”的抽象超越简单检索的复合能力很多初学者以为LlamaIndex的query_engine.query(...)就是一个search()函数。这是巨大的误解。QueryEngine是LlamaIndex的“大脑皮层”它把一次用户提问分解成一个可配置、可扩展的多阶段工作流。标准流程是Input - Parser - Retriever - Reranker - ResponseSynthesizer - Output。每一环都可替换、可跳过、可并行。Parser不只是分词。它可以是SentenceSplitter按句切分也可以是MarkdownNodeParser保留标题层级甚至是自定义的LegalClauseParser专为法律条款设计能识别“第X条”、“但书”、“除外情形”等结构。Retriever远不止VectorStoreRetriever。BM25Retriever提供关键词精确匹配AutoMergingRetriever能先粗检再细检RecursiveRetriever则支持“先找相关章节再在章节内找具体段落”的两级检索。Reranker这是区分专业RAG和玩具RAG的关键。CohereRerank、FlagEmbeddingReranker等模型会基于原始查询和所有候选Node的全文重新打分排序把语义最相关、信息最密集的片段顶到前面。实测显示在复杂问题上加入reranker可将Top-1准确率提升25%-40%。ResponseSynthesizer这才是最终调用LLM的地方。但它不是简单地把Node.text拼起来喂给模型。TreeSummarize会构建一棵语义树逐层归纳Refine模式则让模型“边读边想”对每个Node依次提问、修正、汇总Compact模式则会先压缩所有Node的上下文再一次性生成答案极大节省Token。这个引擎的威力在于它把“一次查询”变成了一个可编程的“数据处理管道”。你不需要写if-else去判断用户问的是事实还是总结引擎的配置本身就已经编码了你的业务逻辑。这也是为什么高级用户能用LlamaIndex做出LangChain难以企及的深度应用——因为后者把太多决策权交给了开发者手写的链式逻辑而前者把决策权交给了经过验证的、可配置的模块。3. 核心细节解析与实操要点从零开始搭建一个生产级文档问答系统3.1 数据加载与预处理别让脏数据毁掉整个RAG流水线数据加载Ingestion是LlamaIndex的第一道关也是最容易被忽视的“地基工程”。SimpleDirectoryReader确实能让你5行代码跑通Demo但在生产环境它只是起点。真正的挑战在于如何让原始数据在进入Node之前就具备高质量、高一致性、高信息密度这里有几个血泪教训换来的实操要点。首先文件解析器File Reader的选择比你想象的更重要。SimpleDirectoryReader默认用UnstructuredReader它依赖unstructured库而unstructured在处理扫描PDF时本质是调用Tesseract OCR。但Tesseract的默认配置对中文、表格、小字号极不友好。我曾遇到一份带公章的扫描合同OCR识别出的“人民币”变成了“人民市”导致所有金额相关的检索全部失效。解决方案是显式指定pdf_reader_clsPDFMinerReader或pdf_reader_clsPyMuPDFReader。PyMuPDF即fitz能直接提取PDF的原生文本流和坐标对扫描件虽无能为力但对可复制PDF的保真度极高而PDFMiner则在处理复杂版式多栏、图文混排时更稳健。对于必须OCR的场景则要自己封装一个CustomOCRReader调用paddleocr对中文支持极佳或easyocr并预设好语言包和检测阈值。其次元数据Metadata不是可选项而是必填项。很多团队在初期为了“快”把所有文档一股脑塞进一个目录然后load_data()。结果是当用户问“这份合同里甲方是谁”系统可能从10份不同合同里召回10个“甲方XXX”却无法告诉你哪一份是当前上下文。正确的做法是在加载时就通过filename_as_idTrue或自定义file_metadata函数为每个Document注入强业务语义。例如def file_metadata(filename): # 从文件名解析关键信息 parts filename.split(_) if len(parts) 3 and parts[0] CONTRACT: return { doc_type: contract, party_a: parts[1], party_b: parts[2].split(.)[0], date_signed: parts[3] if len(parts) 3 else unknown } return {doc_type: other} documents SimpleDirectoryReader( input_dir./contracts/, file_metadatafile_metadata ).load_data()这样每个Document都自带了party_a、party_b等字段后续在Node层面这些元数据会自动继承并可用于过滤。最后分块Chunking策略必须与业务问题对齐。SentenceSplitter对通用问答尚可但对法律、金融等专业领域它会把“本协议自双方签字盖章之日起生效”这种完整法律条款生生切成两半。此时应使用HierarchicalNodeParser先按#、##标题分割再在每个标题下按段落细分。或者更激进的做法是放弃通用分块改用规则引擎预处理。例如用正则表达式r第[零一二三四五六七八九十百千]条.*?(?(?:第[零一二三四五六七八九十百千]条|$))来精准提取法律条款每一条就是一个Node。虽然开发成本略高但换来的是检索精度的质变。3.2 索引构建与存储内存、磁盘与向量数据库的取舍艺术索引Index是LlamaIndex的“心脏”它决定了数据如何被组织、存储和访问。VectorStoreIndex是最常用的但它绝不是唯一选择更不是万能选择。理解不同索引类型的适用场景是避免后期性能灾难的关键。VectorStoreIndex这是RAG的标配。它将每个Node的embedding向量存入向量数据库Vector Store。核心优势是语义检索能找出“意思相近”而非“字面相同”的内容。但它的致命弱点是无法进行精确的元数据过滤。比如你想“只检索2023年之后签订的合同”向量数据库本身不支持WHERE date_signed 2023-01-01这样的SQL查询。因此VectorStoreIndex必须配合MetadataFilter使用而MetadataFilter是在向量检索结果返回后在内存中进行二次过滤——这意味着如果向量库有100万条记录而符合条件的只有10条你依然要先检索100万条再筛出10条效率极低。SummaryIndex这是一个常被低估的“冷兵器”。它不存向量而是为每个Document或Node生成一个简短的摘要Summary然后将所有摘要存入一个Document列表。查询时它会对所有摘要进行向量检索。它的价值在于“以小博大”。当你有一份1000页的财报而用户只关心“管理层讨论与分析”部分你可以为财报的每个章节MDA、财务报表、附注分别生成一个SummaryIndex。用户提问时先用一个轻量级SummaryIndex快速定位到“MDA”这个章节索引再在这个小索引里进行精细检索。这比在整个1000页的向量库中大海捞针快了几个数量级。KnowledgeGraphIndex这是面向图谱的索引。它要求你在Node中明确定义relationships例如{child: [node_id_123], parent: [node_id_456]}。构建后它能执行图查询如“找出所有与‘数据安全’概念直接或间接相关的条款”。这在合规审计、影响分析等场景中无可替代。但它的构建成本最高需要人工或规则引擎预先建立实体关系。关于存储一个铁律是永远不要在生产环境用默认的内存存储in-memory。index.storage_context.persist()是必须的。持久化路径persist_dir的选择决定了你的系统是单机玩具还是分布式服务。./storage适合开发测试./storage_s3配合S3StorageContext则能将索引存入S3实现跨实例共享而./storage_redis配合RedisVectorStore则能利用Redis的高性能支撑高并发查询。我见过最典型的错误是团队在K8s集群里部署了5个LlamaIndex服务实例每个实例都用自己的内存索引结果用户在A实例提问得到答案在B实例提问却返回“未找到”——根源就在于索引没有中心化存储。3.3 查询引擎配置如何让一次query()调用完成过去需要10个函数的工作QueryEngine的配置是LlamaIndex功力的集中体现。一个配置良好的引擎能让简单的一行query_engine.query(...)自动完成从意图识别、多源检索、交叉验证到答案生成的全过程。这里分享三个生产环境验证过的高阶配置模式。模式一混合检索Hybrid Retrieval——鱼与熊掌兼得纯向量检索怕歧义纯关键词检索怕语义鸿沟。HybridRetriever就是为了解决这个矛盾。它不是简单地把两个结果列表拼起来而是为每个Node计算一个综合得分score alpha * vector_score (1-alpha) * bm25_score。alpha是可调参数通常设为0.5-0.7。但更精妙的是LlamaIndex还支持FusionRetriever它能将多个Retriever的结果用RRFReciprocal Rank Fusion算法融合该算法对排名靠前的项赋予更高权重鲁棒性远超线性加权。配置代码如下from llama_index.core.retrievers import HybridRetriever, VectorIndexRetriever, BM25Retriever vector_retriever VectorIndexRetriever(indexvector_index, similarity_top_k5) bm25_retriever BM25Retriever.from_defaults(documentsdocuments, similarity_top_k5) hybrid_retriever HybridRetriever( vector_retrievervector_retriever, bm25_retrieverbm25_retriever, moderelative_score_fusion, # 使用RRF ) query_engine index.as_query_engine( retrieverhybrid_retriever, response_modecompact, # 先压缩上下文再生成 )模式二子问题分解Sub-question Decomposition——对付复杂问题的利器当用户问“对比分析A公司和B公司在2023年Q3的营收增长率和毛利率并解释差异原因”这是一个典型的复合问题。SubQuestionQueryEngine会自动将其分解为“A公司2023年Q3营收增长率是多少”、“B公司2023年Q3营收增长率是多少”、“A公司2023年Q3毛利率是多少”……然后并行检索每个子问题最后将所有结果汇总交给LLM进行综合分析。这比让LLM在一个超长上下文中自己找答案准确率高出一倍不止。启用方式只需一行from llama_index.core.query_engine import SubQuestionQueryEngine query_engine SubQuestionQueryEngine.from_defaults( query_engine_tools[ QueryEngineTool.from_defaults( query_enginevector_index.as_query_engine(), descriptionUseful for retrieving general information about companies., ), QueryEngineTool.from_defaults( query_enginesummary_index.as_query_engine(), descriptionUseful for retrieving high-level summaries of financial reports., ), ], )模式三带引用的响应合成Citation-aware Synthesis——让AI的回答可追溯、可验证在专业场景用户不仅要知道答案还要知道答案来自哪份文档、哪一页。ResponseSynthesizer的refine或tree_summarize模式默认就会在答案末尾添加引用标记如[1]、[2]。但要让它真正有用你需要在Node中注入精确的来源信息。最佳实践是在file_metadata函数中除了业务元数据还加入source: f{filename}#page{page_num}。然后在ResponseSynthesizer中启用citation_qa_template它会强制LLM在生成答案时明确指出“根据《XX合同》第Y条……”。这样前端就可以轻松解析[1]并跳转到对应文档的指定位置。这不仅是用户体验的升级更是责任归属的基石。4. 实操过程与核心环节实现一个端到端的合同审查助手项目复现4.1 项目背景与目标设定从模糊需求到可衡量的指标我们接到的需求是“帮法务部同事快速审查新收到的供应商合同重点检查付款条件、违约责任、知识产权归属这三项条款是否符合公司模板。” 这听起来简单但拆解后它包含了多个层次的挑战数据层合同是扫描PDF格式混乱有公章、水印、页眉页脚。语义层付款条件可能表述为“甲方应在验收合格后30日内支付”也可能为“尾款于项目交付后一个月结清”需语义归一。逻辑层违约责任条款常分散在“违约”、“终止”、“赔偿”等多个章节需跨章节聚合。输出层法务需要的不是一段话而是结构化的风险点清单附带原文引用和风险评级。因此我们的成功指标不是“能否回答问题”而是召回率Recall对已知的100份问题合同系统能识别出其中95%以上的风险点。精确率Precision系统标记的100个风险点中至少90个是法务认可的真实风险。响应时间Latency从上传PDF到返回结构化报告平均耗时90秒。这个目标设定直接决定了我们后续所有技术选型和配置的取舍。4.2 环境搭建与依赖安装最小可行集的精准选择我们摒弃了“一步到位”的pip install llama-index大包而是采用“按需装配”的策略确保环境轻量、可控、可复现。# 创建干净的虚拟环境 python -m venv contract_env source contract_env/bin/activate # Linux/Mac # contract_env\Scripts\activate # Windows # 安装核心与必需集成 pip install llama-index-core0.14.22 pip install llama-index-llms-ollama0.14.22 pip install llama-index-embeddings-huggingface0.14.22 pip install llama-index-readers-file0.14.22 pip install llama-index-storage-docstore-sqlite0.14.22 # 安装OCR和PDF处理专用库 pip install paddleocr2.7.1 pip install PyMuPDF1.24.3 # 安装用于后处理的NLP库 pip install spacy3.7.4 python -m spacy download zh_core_web_sm关键点说明llama-index-llms-ollama我们选择Ollama作为本地LLM后端因为它能离线运行llama3:70b等大模型规避API调用延迟和费用。llama-index-llms-ollama包封装了Ollama的HTTP API使其完全兼容LlamaIndex的LLM接口。llama-index-embeddings-huggingface选用BAAI/bge-reranker-base作为嵌入模型。它是一个专为重排序Reranking优化的模型在中文法律文本上的表现显著优于通用的text-embedding-ada-002。llama-index-storage-docstore-sqlite选择SQLite作为文档存储DocStore而非内存。因为合同审查需要持久化Document和Node的元数据SQLite轻量、单文件、ACID完美契合单机部署需求。paddleocr中文OCR的业界标杆对合同中的小字号、斜体、印章遮挡有极强的鲁棒性。我们专门训练了一个微调版针对“人民币”、“美元”、“%”等金融符号做了增强。4.3 数据管道构建从扫描PDF到可检索的语义节点整个数据管道分为四个阶段我们用一个IngestionPipeline类来编排from llama_index.core import Document, Node from llama_index.core.node_parser import HierarchicalNodeParser from llama_index.core.ingestion import IngestionPipeline from llama_index.core.extractors import ( TitleExtractor, QuestionsAnsweredExtractor, SummaryExtractor, ) from llama_index.core import Settings class ContractIngestionPipeline: def __init__(self, ocr_model_path./models/paddleocr_chinese): self.ocr_model PaddleOCR(use_angle_clsTrue, langch, det_model_dirocr_model_path) def _parse_pdf(self, file_path: str) - str: 使用PyMuPDF提取PDF文本失败时回退到PaddleOCR try: doc fitz.open(file_path) text for page in doc: text page.get_text() \n---PAGE BREAK---\n return text except: # OCR fallback result self.ocr_model.ocr(file_path, clsTrue) text \n.join([line[1][0] for line in result[0]]) return text def _extract_metadata(self, file_path: str) - dict: 从文件名和OCR文本中提取强元数据 filename os.path.basename(file_path) # 示例contract_supplierA_2024_v1.pdf - supplierA, 2024 parts filename.replace(.pdf, ).split(_) metadata { source_file: filename, supplier_name: parts[1] if len(parts) 1 else unknown, year: parts[2] if len(parts) 2 else unknown, } # 从OCR文本中提取关键日期如签订日期2024年3月15日 date_match re.search(r签订日期[:]\s*(\d{4}年\d{1,2}月\d{1,2}日), text) if date_match: metadata[sign_date] date_match.group(1) return metadata def run(self, input_dir: str) - list[Node]: documents [] for file_path in glob.glob(os.path.join(input_dir, *.pdf)): raw_text self._parse_pdf(file_path) metadata self._extract_metadata(file_path) # 构建Document注入OCR文本和元数据 doc Document( textraw_text, metadatametadata, excluded_llm_metadata_keys[source_file], # 防止LLM看到文件名 excluded_embed_metadata_keys[source_file], ) documents.append(doc) # 分块先按标题#、##切再按段落切 node_parser HierarchicalNodeParser.from_defaults( chunk_sizes[2048, 512], # 大块2048小块512 ) # 提取器为每个Node生成标题、摘要、潜在问题 extractors [ TitleExtractor(nodes5), # 从前后5个Node中提取标题 SummaryExtractor(summaries[prev, self]), # 生成前文摘要和自身摘要 QuestionsAnsweredExtractor(questions3), # 为每个Node生成3个可能的问题 ] pipeline IngestionPipeline( transformations[ node_parser, *extractors, # 嵌入模型由Settings统一管理 Settings.embed_model, ], ) nodes pipeline.run(documents) return nodes这个管道的精妙之处在于双模解析PyMuPDF优先OCR兜底确保100%的PDF都能被处理。元数据驱动supplier_name、sign_date等字段为后续的MetadataFilter提供了坚实基础。分层分块HierarchicalNodeParser生成的Node天然带有父子关系使得RecursiveRetriever能先定位到“付款条款”章节再在该章节内精确定位到具体条款。智能提取QuestionsAnsweredExtractor为每个Node生成的问题会被SubQuestionQueryEngine直接用作子问题实现了“数据即问题”的闭环。4.4 索引构建与查询引擎配置为合同审查定制的“专家系统”基于上述Node我们构建了三层索引体系from llama_index.core import VectorStoreIndex, StorageContext, load_index_from_storage from llama_index.core.vector_stores import SimpleVectorStore from llama_index.storage.docstore.sqlalchemy import SQLAlchemyDocumentStore from llama_index.storage.index_store.sqlalchemy import SQLAlchemyIndexStore # 1. 文档存储DocStore用SQLite持久化所有Document和Node docstore SQLAlchemyDocumentStore.from_uri(sqlite:///./storage/docstore.db) # 2. 向量存储VectorStore用SimpleVectorStore内存 持久化 vector_store SimpleVectorStore() storage_context StorageContext.from_defaults( docstoredocstore, vector_storevector_store, index_storeSQLAlchemyIndexStore.from_uri(sqlite:///./storage/indexstore.db), ) # 3. 构建主索引VectorStoreIndex vector_index VectorStoreIndex( nodesnodes, storage_contextstorage_context, embed_modelSettings.embed_model, ) # 4. 构建摘要索引SummaryIndex用于快速定位章节 summary_index SummaryIndex(nodesnodes, storage_contextstorage_context) # 5. 构建知识图谱索引KnowledgeGraphIndex用于条款关联 kg_index KnowledgeGraphIndex( nodesnodes, max_triplets_per_chunk10, include_embeddingsTrue, storage_contextstorage_context, )查询引擎则是一个精心组装的“专家委员会”from llama_index.core.query_engine import RouterQueryEngine from llama_index.core.selectors import LLMSingleSelector from llama_index.core import PromptTemplate # 为不同任务准备专用的QueryEngineTool vector_tool QueryEngineTool.from_defaults( query_enginevector_index.as_query_engine( retrieverHybridRetriever( vector_retrieverVectorIndexRetriever(indexvector_index, similarity_top_k3), bm25_retrieverBM25Retriever.from_defaults(documentsnodes, similarity_top_k3), ), response_moderefine, verboseTrue, ), descriptionUseful for retrieving detailed, verbatim clauses from contracts., ) summary_tool QueryEngineTool.from_defaults( query_enginesummary_index.as_query_engine(response_modetree_summarize), descriptionUseful for getting a high-level overview of a contracts structure and key sections., ) kg_tool QueryEngineTool.from_defaults( query_enginekg_index.as_query_engine( retrieverKGTableRetriever( indexkg_index, retriever_modehybrid, ) ), descriptionUseful for finding relationships between concepts, e.g., What clauses are related to Payment Terms?, ) # 主查询引擎RouterQueryEngine根据用户问题自动路由到最合适的工具 query_engine RouterQueryEngine( selectorLLMSingleSelector.from_defaults(), query_engine_tools[vector_tool, summary_tool, kg_tool], verboseTrue, ) # 自定义提示词强制输出JSON结构 json_prompt_tmpl_str ( You are a legal expert assistant. Your task is to analyze the provided contract excerpts and output a JSON object with the following keys: risk_points: a list of objects, each with clause_type, original_text, risk_level (High/Medium/Low), explanation. Do not add any other text or markdown. Only output valid JSON. ) json_prompt_tmpl PromptTemplate(json_prompt_tmpl_str) query_engine.update_prompts( {response_synthesizer:text_qa_template: json_prompt_tmpl} )这个配置的威力在于RouterQueryEngine当用户问“付款条件是什么”它会自动选择vector_tool因为问题需要原文当问“这份合同主要讲了什么”它会选择summary_tool当问“违约责任和保密义务有什么关联”它会调用kg_tool。HybridRetriever确保即使用户用口语化表达如“钱什么时候给”也能召回“付款期限”、“结算方式”等正式条款。JSON输出强制通过PromptTemplate我们绕过了LLM自由发挥的不确定性直接获得结构化数据前端可直接渲染为风险点表格。4.5 实际运行与效果验证从代码到业务价值的跨越我们用一份真实的《软件服务采购合同》进行了端到端测试。上传PDF后系统在72秒内完成了OCR、解析、分块、索引、嵌入的全流程。当输入问题“请列出所有关于付款的条款并标注风险等级”系统返回了如下JSON{ risk_points: [ { clause_type: 付款条件, original_text: 甲方应在乙方完成全部服务并通过验收后30个工作日内向乙方支付合同总价的95%。, risk_level: Medium, explanation: 付款周期30个工作日约6周偏长行业惯例为15-20个工作日。建议缩短至15个工作日。 }, { clause_type: 质保金, original_text: 合同总价的5%作为质保金在服务期满一年后10个工作日内无息返还。, risk_level: High, explanation: 质保金比例5%合理但返还时间服务期满一年后10个工作日存在歧义。服务期满一年指合同签订日起算还是验收日起算应明确为验收合格之日起满一年。 } ] }这个结果与法务同事的手动审查结论高度一致且耗时仅为人工审查的1/10。更重要的是它提供了可追溯的original_text法务可以一键定位到PDF的原始位置进行复核。这不再是“AI在猜”而是“AI在辅助”人依然是最终的决策者但效率和覆盖度得到了指数级提升。5. 常见问题与排查技巧实录那些文档里不会写的“踩坑”