ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

DocSage:基于智能体与知识图谱的多文档多实体问答系统架构与实践

DocSage:基于智能体与知识图谱的多文档多实体问答系统架构与实践 1. 项目概述当AI需要“读”懂一堆文件时最近在折腾一个项目叫DocSage。这名字听起来有点玄乎直译过来是“文档智者”但它的核心任务其实很具体当一个AI智能体Agent需要回答一个涉及多个实体、且答案散落在多份文档里的复杂问题时它得先学会怎么“结构化”这些海量信息。这不像你问ChatGPT“今天天气如何”那么简单更像是你给一个律师助理扔过去一堆合同、邮件和会议纪要然后问它“客户A和供应商B在第三季度关于产品C的交付争议关键分歧点是什么各自引用了哪些条款”这就是典型的“多文档多实体问答”Multi-Doc Multi-Entity QA场景。实体Entity可以是人、公司、产品、时间、地点等任何有明确指代的对象。当问题同时牵扯好几个实体而描述这些实体关系的线索又像碎片一样埋在十几份甚至上百份PDF、Word或网页里时直接让大语言模型LLM去“通读”并给出精准答案效果往往不尽如人意。模型要么漏掉关键信息要么把不同实体的信息张冠李戴或者给出一个笼统、缺乏依据的总结。DocSage要解决的就是这个“信息过载”和“关系混乱”的问题。它本质上是一个信息结构化智能体。它的工作不是最终生成那一段答案文本而是在生成答案之前充当一个超级高效的“信息整理师”和“关系侦探”。它会把杂乱无章的原始文档转化成一个结构清晰、实体关系明确的“知识图谱”或“结构化摘要”为后续的精准问答提供一个坚实、可靠的事实底座。我之所以花大力气研究这个是因为在实际的企业知识库、法律分析、学术文献调研等场景下这种需求太普遍了而市面上现成的工具要么太重要么太“笨”。2. 核心设计思路分而治之与关系优先DocSage的设计哲学可以概括为“分而治之”和“关系优先”。它不是一个试图一口吞下所有文档的巨无霸模型而是一个由多个专门化模块组成的协作流水线。整个流程的核心目标是把非结构化的文本转化为机器和人都能更好理解的“结构化”表示。2.1 为什么是“智能体”Agent架构这里需要先厘清一个概念为什么用“Agent”而不仅仅是一个“模型”或“系统”在当前的AI语境下Agent强调的是一种自主感知、规划、执行并利用工具达成目标的能力。对于多文档问答这种复杂任务单一模型很难胜任。你需要感知Perception识别文档类型、编码处理扫描件OCR。规划Planning决定先处理哪份文档用什么策略抽取信息如何验证信息一致性。执行Execution调用实体识别模型、关系抽取模型、文本摘要模型或者去数据库里查询。工具使用Tool Use可能会用到计算器验证日期用搜索引擎补充背景知识用规则引擎校验合同条款。因此DocSage采用Agent架构意味着它内部有一个“调度中枢”或称为编排器这个中枢根据当前任务的状态动态地决定调用哪个子模块子智能体或者使用哪个外部工具。这使得系统更加灵活、健壮也更容易迭代——你可以单独优化实体识别模块而不影响关系抽取模块。2.2 核心四阶段流水线DocSage的工作流程通常分为四个核心阶段我把它画成了一个清晰的流水线原始文档集 - [文档解析与分块] - 文本块 - [实体识别与链接] - 实体列表 - [跨文档关系构建] - 关系图谱 - [查询聚焦与证据组织] - 结构化答案蓝图第一阶段文档解析与智能分块这是所有工作的基础。处理PDF、Docx、HTML、Markdown等不同格式的文档统一转化为纯文本。但直接整篇文档扔进去是不行的模型有上下文长度限制。因此需要“分块”Chunking。这里的关键不是简单地按固定字数切分而是要进行“智能分块”。怎么做优先依据自然段落、标题层级H1, H2、列表、表格进行分割。对于技术文档或合同一个完整的“条款”或“函数说明”应该尽量保持在一个块内。注意事项分块时要保留一定的重叠窗口例如前后各留50-100词防止一个实体或关键句子被恰好切在块边缘导致上下文丢失。同时必须为每个文本块记录元数据来源文档、页码、章节标题等这是后续追溯证据的生命线。第二阶段实体识别与跨文档链接在这个阶段系统需要像侦探一样从所有文本块中找出所有提到的实体。这不仅仅是命名实体识别NER——识别出“苹果公司”、“2023年Q4”、“iPhone 15”这些词属于什么类型组织、时间、产品。更重要的是“实体链接”Entity Linking或“共指消解”Coreference Resolution。实体链接确定不同表述是否指向同一实体。例如“Apple”、“苹果公司”、“Cupertino的科技巨头”在上下文中可能都指代同一个公司。系统需要将它们归并到同一个实体ID下。跨文档链接这是多文档场景的特有挑战。在文档A中提到的“项目代号‘北极星’”和在文档B中提到的“Polaris项目”需要被识别为同一个实体。这通常需要结合实体描述、上下文语境以及一些外部知识库如维基百科来实现。实操心得单纯依靠一个NER模型效果有限。我们通常采用“流水线投票”策略先用一个快速但覆盖面广的模型如Flair NER进行初筛再用一个精准但慢的模型如基于BERT微调的进行校验最后用一套规则如字符串模糊匹配、缩写全称对应来处理共指问题。实体识别的准确率直接决定了整个系统的天花板。第三阶段跨文档关系构建与图谱生成识别出实体后DocSage的核心工作才开始找出实体之间的关系。这是将信息“结构化”的关键一步。关系可以是显性的如“甲方A公司”与“乙方B公司”之间是“签订合同”关系也可以是隐性的需要推理如文档1说“A投资了B”文档2说“B是C的全资子公司”那么可以推导出“A间接投资了C”。关系抽取Relation Extraction, RE使用预训练的关系抽取模型分析包含两个实体的句子或段落判断它们之间是否存在预定义的关系类型如“位于”、“雇佣”、“生产”、“起诉”。构建属性图每个实体是图中的一个节点节点上带有属性类型、别名、出现位置。实体之间的关系是图中的边边上带有关系类型和最重要的——证据来源。这个证据来源必须精确到具体的文档、页码甚至句子。这样我们就得到了一个初步的“文档增强型知识图谱”。挑战多文档关系抽取的最大难点是“证据冲突”和“信息互补”。不同文档对同一事件的描述可能有细微差别甚至矛盾。DocSage需要记录所有来源的证据并在图谱中标记出置信度或冲突情况而不是武断地选择某一个。第四阶段查询聚焦与证据组织当用户提出一个具体问题时DocSage不会把整个庞大的图谱丢给答案生成模型。那样会引入大量噪声。相反它会启动“查询聚焦”模块。问题解析首先从用户问题中提取出关心的核心实体和关系。例如问题“A公司和B公司在C项目上的合作金额是多少”核心实体是【A公司 B公司 C项目】核心关系是【合作金额】。子图检索在全局知识图谱中以这些核心实体为起点进行一到两跳的遍历抽取出一个与问题高度相关的子图谱。这个子图谱包含了所有可能相关的实体和关系。证据排序与组织对于子图谱中的每一条关系边根据其证据来源的权威性如正式合同 vs. 会议纪要、时间新鲜度、以及与其他证据的一致性进行排序和打分。最后将排序后的、带有精确引用的证据链组织成一种结构化的格式例如JSON作为“答案蓝图”提供给最终的答案生成器。这个“答案蓝图”就是DocSage的核心产出。它不是最终的自然语言答案而是一个包含了所有相关事实、实体、关系及出处的结构化数据。后续的答案生成模块可以是一个LLM的任务就大大简化了它只需要根据这个清晰、可靠的蓝图用通顺的语言组织成答案即可极大地提高了答案的准确性和可信度。3. 关键技术选型与实现细节搭建DocSage这样的系统技术选型就像搭积木每个环节都有多种选择关键是要找到平衡性能、成本和复杂度的组合。以下是我在实现过程中对一些关键技术的思考和选择。3.1 文档解析层统一入口的建立文档解析是第一步也是最脏最累的活。目标是得到一个干净、结构化的文本流。我放弃了寻找一个“万能解析器”的想法而是建立了一个适配器工厂。PDF解析这是难点。对于文本型PDFPyPDF2或pdfplumber是基础但布局复杂的PDF如双栏论文、带表格的报告很容易解析错乱。我的选择是pymupdf又称fitz它不仅能提取文本还能较好地保留空间位置信息这对于后续判断文本块是否属于同一个表格或标题至关重要。对于扫描件必须集成OCR引擎Tesseract是开源首选但针对文档优化过的商业API如Azure Document Intelligence在准确率和版面分析上往往更胜一筹。Office文档对于.docxpython-docx库是标准选择它能完美提取段落、样式和表格。对于旧的.doc格式建议先通过LibreOffice的命令行工具批量转换为.docx或PDF再处理。HTML/网页使用BeautifulSoup或lxml进行解析但关键是要用readability或trafilatura这样的库进行“正文提取”自动过滤掉导航栏、广告等噪音内容。核心实现技巧为每个解析后的文档对象建立一个统一的内部表示Document类包含id、metadata文件名、类型、创建时间等和chunks列表。每个chunk对象包含text、source_doc_id、page_num、section_header等字段。这个统一的数据结构是后续所有流程的基础。3.2 信息抽取模型平衡精度与效率实体识别和关系抽取是DocSage的“大脑”。这里面临一个经典权衡使用庞大的、功能全面的通用模型如GPT-4做少样本抽取还是使用专门的、轻量化的精调模型。实体识别NER我采用了混合策略。对于通用实体人名、地名、组织名、时间使用像spaCy的en_core_web_trf基于Transformer这样的预训练模型它开箱即用效果不错。但对于领域特定实体如法律文书中的“条款编号”、医疗报告中的“药品剂量”预训练模型往往识别不准。这时就需要用领域数据对一个小型BERT模型如bert-base-uncased进行微调。为了兼顾速度可以部署一个轻量级模型如Flair的ner-english-fast做第一遍粗筛再用精调模型对候选实体进行复核。关系抽取RE这是更大的挑战。通用关系抽取模型如OpenNRE能识别一些常见关系但针对“合同金额”、“项目里程碑”这类特定关系效果很差。我们的做法是定义封闭的关系模式根据业务场景预先定义好需要抽取的关系类型如公司 投资 金额人 担任职务 公司。这限制了范围但提高了可控性。采用“管道式”或“联合抽取”模型管道式是先做NER再判断实体对间的关系简单但存在误差传播。联合抽取模型如SpERT、PURE能同时完成两项任务效果更好但更复杂。我们根据关系复杂程度混合使用。充分利用提示工程Prompt Engineering对于某些难以定义或样本极少的关系我们会将包含两个实体的上下文片段连同设计好的提示词如“请判断以下句子中‘[E1]’和‘[E2]’是什么关系选项A.合作 B.竞争 C.无关”发送给像GPT-3.5/4这样的LLM利用其强大的语义理解能力进行零样本或少样本抽取。这成为了处理长尾、复杂关系的有效补充手段。一个重要的注意事项所有模型抽取的结果都必须带有置信度分数。低置信度的结果不能直接进入知识图谱可以放入“待审核区”或者需要额外的证据进行佐证。这为系统增加了容错性。3.3 知识图谱存储与查询图数据库的优势当实体和关系被抽取出来后我们需要一个地方来存储和高效查询它们。传统的关系型数据库如MySQL在处理复杂的多跳关系查询时会非常笨重和低效。因此图数据库是几乎唯一的选择。为什么是图数据库因为它原生地用节点和边来存储数据查询语言如Cypher, Gremlin也是为遍历关系而设计的。例如要回答“找出所有与A公司有直接或间接投资关系的公司”在图数据库中就是一个简单的深度优先或广度优先遍历在关系型数据库中则需要多次复杂的表连接。选型考量Neo4j是最知名的图数据库生态完善但社区版有规模限制。Nebula Graph是国产开源分布式图数据库适合超大规模数据。JanusGraph基于Apache TinkerPop兼容多种存储后端如Cassandra灵活性高。对于DocSage初期或数据量在千万节点以下的情况Neo4j的易用性是巨大的优势。我们选择它可以快速实现“查询聚焦”中的子图检索功能。图谱建模在图谱中我们设计了两种主要节点类型Entity和Document。Entity节点有属性如name,type。Document节点有title,path等。Entity之间的关系用RELATED_TO边表示边上属性包括relation_type,confidence,source_text。最关键的是Entity和Document之间通过MENTIONED_IN边连接边上记录具体的chunk_id和offset。这样任何图谱中的关系都可以追溯到原文的精确位置。3.4 Agent框架与任务编排让流程“活”起来DocSage的各个模块解析、NER、RE、检索需要被有机地串联和调度这就是Agent框架的用武之地。我们并没有从头造轮子而是基于现有的Agent框架进行开发。框架选择LangChain和LlamaIndex是当前最流行的两个高级框架它们抽象了与LLM交互、工具调用、记忆等常见模式。LangChain更偏向于构建复杂的链和代理LlamaIndex则对文档索引和检索有更深度的优化。对于DocSage其核心挑战是确定性的信息处理流程与非确定性的LLM调用相结合。因此我们以LangChain作为主要编排框架因为它对自定义工具Tool和代理Agent的定义非常灵活。核心Agent设计我们设计了一个主协调AgentOrchestrator Agent它内部维护一个状态机。其决策逻辑基于规则和LLM的少量判断用户输入问题。Orchestrator调用一个“问题分析工具”该工具内部是一个小LLM用于提取问题中的实体和关系关键词。根据提取出的实体Orchestrator调用“图谱查询工具”在图数据库中检索相关子图。如果子图信息不足或置信度低Orchestrator可能会决定启动一个“文档深度分析工具”这个工具会针对特定文档调用更精细的NER/RE模型进行二次抽取。收集齐证据后Orchestrator调用“证据组织工具”将结构化的证据蓝图整理好。最后将蓝图交给一个专用的“答案生成Agent”该Agent被严格限定只能基于蓝图中的证据进行总结和回答不能自行编造信息。工具Tool的封装每一个底层能力如parse_pdfextract_entitiesquery_graph都被封装成一个标准的Tool有明确的输入输出描述。这样Orchestrator这个LLM才能理解在什么情况下该调用哪个工具。这是Agent架构能工作的关键。4. 实操部署与性能优化理论设计得再完美落地时总会遇到各种“坑”。下面分享我们在实际部署和优化DocSage原型系统时的一些关键步骤和教训。4.1 环境搭建与依赖管理项目涉及Python自然语言处理、机器学习、图数据库等多个领域依赖复杂。强烈建议使用conda或uv创建独立的虚拟环境并用requirements.txt或pyproject.toml严格管理依赖。核心依赖清单# 核心框架与工具 langchain0.1.0 llama-index0.10.0 pydantic2.0 # 用于数据验证 # 文档处理 pymupdf (fitz) python-docx beautifulsoup4 markdown tiktoken # 用于文本分块计数 # NLP模型与工具 transformers4.30.0 torch spacy flair sentence-transformers # 用于文本向量化可选 # 图数据库 neo4j5.0.0 py2neo # Neo4j的Python驱动 # 工具与工具 openai1.0.0 # 如需调用GPT API tqdm # 进度条避坑指南transformers、torch和spaCy的版本兼容性是个大坑。最好先确定你要用的核心模型如某个特定的NER模型然后去其官方页面查看推荐的torch和transformers版本再围绕这个版本来构建环境。盲目安装最新版很可能无法运行。4.2 分块策略的精细调优分块是影响后续所有步骤质量的基础。我们经过多次试验总结出一个混合分块策略基于语义的分割首先使用nltk的sent_tokenize进行分句。然后利用sentence-transformers计算句子间的语义相似度。当连续句子间的相似度低于某个阈值时就在那里进行切分。这能保证一个“语义完整”的段落或观点尽量在一个块里。基于长度的硬限制经过语义分割后每个块可能还是太长。我们设置一个最大token数例如针对GPT-4的上下文设为4000 tokens。如果块超长则优先在段落标记\n\n、句号、分号等处进行二次分割。重叠窗口每个块切分时与前一个块和后一个块保留约10%的重叠内容。这确保了边界信息不会丢失。特殊结构保留对于代码块、表格我们将其视为一个不可分割的整体单元即使它很长。为此我们在解析时就需要标记出这些特殊结构。4.3 图数据库的查询优化随着图谱规模增长查询效率成为瓶颈。以下优化措施效果显著索引是关键在Neo4j中务必为Entity节点的name属性和Document节点的doc_id属性创建索引。这能将以实体名为起点的查询速度提升几个数量级。CREATE INDEX entity_name_index IF NOT EXISTS FOR (e:Entity) ON (e.name); CREATE INDEX document_id_index IF NOT EXISTS FOR (d:Document) ON (d.doc_id);限制查询深度在“查询聚焦”时从问题实体出发的遍历必须设置最大深度例如3跳。因为现实中的关系网络可能非常庞大无限遍历会导致查询超时。通常答案所需的关系不会离问题实体太远。使用参数化查询永远不要用字符串拼接的方式来构造Cypher查询有注入风险且效率低。使用Py2neo或官方驱动的参数化查询功能。异步操作如果系统需要同时处理多个用户查询考虑使用异步IO如asyncio来并发执行图数据库查询和LLM API调用避免阻塞。4.4 与LLM的协同控制成本与质量DocSage大量使用LLM如GPT-4进行关系抽取、问题分析和最终答案生成。如何控制成本和保证质量是关键。分层使用模型不是所有任务都需要最强的GPT-4。我们建立了一个模型路由策略高精度任务最终答案生成、复杂关系推理使用GPT-4。中等难度任务问题解析、证据摘要使用GPT-3.5-Turbo。标准化任务文本清洗、格式转换使用成本更低的开源模型如Llama 3的API甚至规则。设计严格的提示词Prompt这是控制LLM输出质量的生命线。对于答案生成我们的提示词模板强制模型基于证据回答你是一个专业的文档分析助手。请严格根据以下提供的证据信息来回答问题。如果证据不足请明确说明“根据现有信息无法确定”。 问题{question} 证据结构化 {evidence_blueprint} 请生成答案并在答案中引用证据编号例如【证据1】。设置确定性护栏在Orchestrator Agent调用工具前我们对LLM的决策进行“护栏”校验。例如当Agent决定要调用“深度分析文档”工具时系统会先检查该文档是否已经被分析过或者该查询是否真的需要此步骤防止LLM做出不合理或重复的决策浪费API调用。5. 常见问题与实战排坑记录在开发和测试DocSage的过程中我们遇到了无数问题。这里记录下最具代表性的几个及其解决方案希望能帮你绕过这些坑。5.1 信息抽取的准确率瓶颈问题实体识别和关系抽取的准确率达不到实用要求特别是对于专业领域术语和复杂句式。排查与解决领域适配这是首要问题。通用NER模型在法律、医疗、金融等领域表现会大幅下降。解决方案是领域微调。收集哪怕几百条标注好的领域数据实体类型和关系对bert-base-uncased这类基础模型进行微调效果提升会非常明显。如果没有标注数据可以尝试用GPT-4生成合成数据再进行微调。上下文窗口不足很多关系存在于跨句甚至跨段落的语境中。例如“张三”在第一段被介绍为“A公司CEO”第五段说“他否决了该提案”。标准模型处理单句时可能无法将“他”与“张三”关联更无法建立“张三A公司CEO-否决-提案”的关系。解决方案是扩大模型上下文窗口或者采用“文档级”建模方法在分块时保留更大的窗口或者在推理时传入更多上下文。证据冲突处理不同文档对同一事实描述矛盾。DocSage不能掩盖矛盾。我们的策略是在图谱中允许同一对实体之间存在多条关系边每条边携带不同的证据来源和置信度。在生成答案时如果检测到冲突答案生成器会在答案中如实反映这种冲突例如“根据《合同A》第3条金额为100万元【证据1】但根据《补充备忘录B》金额修订为120万元【证据2】。请用户进一步核实最新有效文件。”5.2 图谱构建与查询的性能问题问题当文档数量达到万级实体和关系达到百万级时图谱构建速度慢查询延迟高。排查与解决批量操作 vs. 实时操作图谱构建写入通常是离线批量任务可以容忍较长时间。而查询读取是在线实时任务要求低延迟。因此要将两者解耦。我们设计了一个异步构建管道文档解析和信息抽取后先将结果写入一个中间消息队列如RabbitMQ或Kafka然后由消费者异步、批量地将数据写入图数据库。这样不会阻塞前端的查询请求。查询优化避免全图扫描确保查询都从带有索引的实体名开始。使用PROFILE在Neo4j浏览器中对慢查询使用PROFILE命令可以清晰看到查询计划发现全表扫描的瓶颈点从而优化Cypher语句或增加索引。缓存热点查询对于一些常见的实体或组合查询可以将查询结果缓存在Redis中设置合理的过期时间。5.3 Agent决策的不可控性问题作为调度核心的Orchestrator Agent基于LLM有时会做出匪夷所思的决策比如反复调用同一个工具或者调用一个完全不相关的工具。排查与解决强化工具描述LLM理解工具的能力完全依赖于你给它的工具描述description。描述必须极其清晰、无歧义并明确说明工具的适用场景和不适用场景。例如不仅说“这个工具用于查询知识图谱”还要说“当用户问题涉及具体实体如公司名、人名、产品名及其关系时使用此工具。如果问题非常笼统如‘介绍下这个行业’则不适用。”设置最大迭代次数在LangChain中可以为Agent设置max_iterations参数强制限制其思考-行动循环的次数防止陷入死循环。采用ReAct模式并输出完整链式思考要求Agent在调用工具前必须输出它的“思考”Thought过程。这样当出现错误时你可以查看它的思考链找出逻辑错误是在哪一步从而优化提示词或工具设计。降级方案当Agent多次尝试失败后系统应有一个降级策略。例如直接fallback到一个更简单但可靠的流程提取问题关键词在图谱中进行简单检索然后将top-k的相关证据直接送给答案生成器绕过复杂的Agent规划。5.4 答案的可解释性与可信度问题用户不信任AI给出的答案尤其是当答案涉及重大决策时。他们问“你这个结论是怎么得出来的”解决这是DocSage设计之初就考虑的核心价值。我们通过以下机制保障可解释性逐条引用最终生成的答案中每一个关键事实陈述后面都必须紧跟其证据来源的引用标记如【Doc:合同.pdf, P5】或【证据ID:123】。提供证据面板在返回答案的同时返回一个结构化的“证据列表”列出所有用于推导该答案的原始文本片段、出处以及置信度分数。可视化图谱子图对于高级用户或内部审核人员系统可以提供查询所涉及的那部分知识图谱的可视化展示让用户直观地看到实体和关系是如何连接起来的。 这种“白盒化”的设计虽然增加了系统复杂性但极大地提升了用户信任度和系统的实用性。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进