尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多模态知识图谱+RAG:企业级智能问答系统架构实战

多模态知识图谱+RAG:企业级智能问答系统架构实战 1. 项目背景与整体架构1.1 为什么是企业级知识库的下一站我得先说句实话很多团队做知识库问答卡在同一个地方——数据形态太多、太杂没法统一喂给大模型。你有PDF、Word、Excel有图片截图有产品说明书里的架构图还有一批散落在Wiki和Confluence里的非结构化文本。传统的RAG方案只能处理纯文本遇到图片就抓瞎遇到表格就转换失真更别提那些隐藏在“二跳页面”里的关联关系了。RAG-Anything这个项目本质上是把多模态底座 知识图谱 检索增强生成三条线拧成一条。名字虽然带Anything但它不是把所有数据都变成向量那么粗暴。它的核心思路是对非结构化文本走向量召回对图片和表格走视觉解析对实体和关系走图谱查询最后在生成阶段把所有召回结果融合在一起交给大模型产出答案。这样一套组合拳下来企业里那些“看着有用但始终用不上”的存量数据才算真正被盘活了。适合谁来参考我觉得至少这么几类人可以从这篇文档里拿到东西正在做企业级知识库问答方案选型的技术负责人被“PPT里全是图、文档里全是表”搞到头大的RAG开发工程师以及准备把Neo4j引入现有系统但不知道怎么和多模态检索耦合的算法同学。1.2 这套系统到底要解决什么问题如果不把问题定义清楚后面所有设计都会跑偏。我团队落地这个项目时最先梳理的是企业场景里最痛的三个问题第一图片和图表里的知识是“沉默知识”。一张系统架构图、一张业务流程图可能比一万字说明文档信息密度都高但传统RAG管线看都不看就直接丢掉了。第二多源数据实体指代混乱。“财务部”和“Finance Dept”在文档里是同一个实体在向量检索语境下却很难建立等价关系。第三问答系统缺乏可解释的推理路径。纯向量的结果是概率匹配用户问“A部门为什么会影响B业务”系统答不上来或者答出来也没法追溯到一张图、一段代码、一条业务规则。知识图谱在这套系统里的作用不是替代向量检索而是给向量检索加一副“骨架”。关系是显式的推理路径是可追溯的跨模态的实体对齐也有了落脚点。多模态大模型负责把非结构化内容转化为结构化的图谱节点和关系图谱反过来约束大模型生成的边界。这就是RAG-Anything和普通“PDF问答机器人”最本质的区别。1.3 整体架构分层整个系统从下往上分四层我习惯用一条流水线来类比源数据层PDF、Word、图片、HTML、数据库导出文件以及外部API抓取的内容。解析与抽取层文本走OCR和版面分析图片走视觉模型理解表格走结构化识别最后统一抽取出实体、关系和属性。存储与索引层双引擎并行。Neo4j存知识图谱负责精确查询和路径推理向量数据库我们用的是Milvus换成Qdrant或pgvector也可以存Embedding负责语义召回。问答服务层接收问题之后做意图分类、图谱检索、向量检索、重排融合最后交给大模型生成带引用的回答。这个架构不是一上来就拍脑袋定的而是踩过“只用向量库”和“只用图谱”两个极端方案的坑之后才摸索出来的折中方案。如果你问我能不能只用一个组件搞定我的答案是短期可以长期会被数据形态和业务关系的复杂度反噬。2. 核心组件拆解与选型考量2.1 多模态解析不是所有模型都值得上生产多模态解析是整个项目的入口也是坑最密集的地方。选型时不是只看模型在公开benchmark上的分数关键要看三点推理成本、并发能力、对中文文档的处理效果。我们最终采用“小模型先过滤大模型再理解”的策略。初始解析用版面分析模型把文档切成块判断哪些是纯文本、哪些是表格、哪些是图片纯文本直接走OCR和Embedding图片再交给视觉语言模型做描述生成和实体抽取。这样做最大的好处是省钱——不是每页PDF都需要视觉大模型跑一遍很多页面的文字信息用普通OCR就够用了。视觉模型输出的图片描述不要直接塞进向量库一定要先经过一轮清洗和结构化。我见过很多团队直接把视觉模型生成的英文描述丢进召回管道结果中文用户用中文提问语义匹配效果一塌糊涂。我们的做法是让视觉模型输出固定的JSON结构包含“图中实体”“实体关系”“核心结论”三个字段再分别进图谱和向量库。2.2 Neo4j知识图谱建模实体关系才是问答的灵魂知识图谱建模是一个容易用力过猛的地方。刚开始我们想把文档里所有名词都抽成实体、所有动词都抽成关系结果图谱密度高到查询性能急剧下降而且大量关系是噪音。后来总结经验实体类型控制在15到20种关系类型控制在25种以内只保留对业务问答有区分度的关系。举个实际例子。我们在做企业IT运维知识库时最核心的实体是“系统”“服务”“团队”“文档”“故障事件”五类。围绕“故障事件”只需要保留“影响系统”“处理团队”“恢复时间”“关联文档”这四种关系。用户问“上次支付系统故障影响了哪些下游业务”Cypher查询可以顺着关系一路遍历比向量检索精准得多。Neo4j的建模还有一个细节属性不要全塞在关系里高频过滤字段放节点属性低频描述信息放JSON属性。这样查询时能用索引快速缩小范围而不是在每个关系上做属性过滤。2.3 向量与图谱双检索引擎的职责边界双检索引擎不是把所有问题都同时发给两个引擎然后合并结果那样既慢又容易冲突。我们的判断规则很简单实体型问题涉及具体的人、系统、部门、产品优先走图谱。比如“订单中心的owner是谁”。语义型问题描述模糊、没有明确实体优先走向量。比如“怎么排查接口超时的问题”。混合型问题既包含实体又需要语义扩展先图谱后向量两路结果在重排阶段融合。这套规则在工程上落地为一个轻量的意图分类器用大模型做few-shot分类但限定输出必须是“graph”“vector”“hybrid”三个值之一。分类错误的问题会自动降级为hybrid宁可多查也不能漏查。3. 关键实现步骤与核心代码实践3.1 数据管道从原始文档到结构化知识数据管道是整个系统里最“脏活累活”的部分没有捷径。我们的管道是一个Python异步任务队列分五个阶段文档解析PDF转图片走OCRWord和HTML走结构化解析。多模态理解图片和表格交给视觉模型输出结构化JSON。实体对齐把抽取出的实体映射到Neo4j中已有的实体节点。关系抽取基于规则和LLM混合策略抽取实体间的关系。索引写入图谱写入Neo4j文本和向量写入Milvus。每一阶段都要求可观测、可重跑。我们给每个文档生成了唯一的doc_id所有中间产物都标记doc_id和page_number出问题时能精准定位到具体页面的具体抽取结果。# 实体对齐的核心逻辑先精确匹配再模糊匹配最后交给LLM判断 def align_entities(extracted_entities, existing_entity_index): aligned [] for entity in extracted_entities: exact existing_entity_index.get(entity.name) if exact: aligned.append({matched_id: exact.id, confidence: 1.0}) continue fuzzy fuzzy_match(entity.name, existing_entity_index.keys(), threshold85) if fuzzy: aligned.append({matched_id: fuzzy.id, confidence: 0.9}) continue # 低频实体、容易混淆实体交给LLM做最终对齐 llm_result llm_judge(entity, existing_entity_index.sample(50)) aligned.append({matched_id: llm_result.id, confidence: llm_result.confidence}) return aligned这里有个容易忽略的点实体对齐不只是“名字对上”还要维护一个同义词表。我们一开始把同义词表放在配置文件里后来数量一多就改存到Neo4j里作为一个独立的“Synonym”节点类型。这样业务侧可以通过界面维护同义词不用发版。3.2 图谱写入Cypher语句与批量性能优化Neo4j写入图谱最怕逐条执行Cypher性能会差到让人怀疑人生。正确的做法是用UNWIND批量写入一次提交几百条关系效率能提升一两个数量级。// 批量写入实体节点 UNWIND $batch AS row MERGE (e:Entity {name: row.name}) SET e.type row.type, e.source_doc row.doc_id ON CREATE SET e.created_at timestamp()// 批量写入关系 UNWIND $batch AS row MATCH (a:Entity {name: row.source}) MATCH (b:Entity {name: row.target}) MERGE (a)-[r:RELATES_TO {type: row.relation}]-(b) SET r.evidence row.page注意上面的MERGE在有并发写入时会产生竞争我们线上使用的是Neo4j的apoc.periodic.iterate分批提交配合唯一性约束来保证幂等。如果后续想支持增量更新建议给每个实体加一个updated_at字段并在关系上保留evidence字段用来追溯知识来源。这个设计对RAG系统特别关键最终回答里要能引用知识出处而图谱恰好是保存出处信息最好的地方。3.3 多模态检索文本、图片与图谱三路召回检索阶段我们用一个统一的入口函数接收问题内部并行执行三路召回。async def retrieve(question: str, top_k: int 10): results [] tasks [ vector_search(question, top_k), graph_search(question), visual_search(question, top_k), ] for coro in asyncio.as_completed(tasks): results.extend(await coro) return rerank(results)vector_search走的是Embedding模型我们用的是一套中英双语的向量模型这里提醒一句别忽略中文场景下的Embedding选型直接用通用英文模型对中文长尾词很不友好。graph_search则把问题先解析成实体再通过Cypher在Neo4j里做子图扩展召回直接或间接关联的实体。visual_search比较特殊——它检索的不是原始图片而是图片经过视觉模型生成的结构化描述和实体标签。三路召回的结果会在重排阶段打分打分的依据有三个维度语义相关性、图谱路径深度、来源可信度。图谱路径越短说明实体关系越直接得分越高来源可信度则是根据文档类型设定权重内部规范文档的权重高于外部抓取内容。3.4 生成阶段用知识约束大模型而不是放任自由发挥最后一步是把召回结果交给大模型生成答案。这里最容易犯的错误是把所有召回结果一股脑塞进Prompt既不排序也不裁剪。我们的做法是先写一个精简的“知识摘要”模块对召回内容做去重、压缩和格式统一然后再拼进Prompt。Prompt模板大概是这个结构你是企业知识库问答助手。请基于以下知识片段回答问题。 如果知识片段不足以支撑答案请明确说“知识库中暂无相关信息”不要编造。 【知识片段】 - [来源: 系统架构文档.pdf 第12页] 支付系统依赖订单系统提供的订单状态接口 - [来源: 图谱路径: 支付系统-订单系统-订单详情服务] 订单服务故障会导致支付系统无法确认订单状态 【问题】 支付系统故障会影响哪些下游业务在生成阶段还有一个容易被忽视的细节引用格式必须结构化。我们要大模型输出引用编号例如“根据知识片段1和3”而不是“根据文档”这种模糊说法。最终前端展示时每个回答都带可点击的引用来源这对企业用户的信任建立非常关键。实操下来结构化引用能把“幻觉投诉率”降低一半以上。4. 常见问题与排查技巧实录4.1 多模态解析的“脏数据”问题我们跑了一周之后就发现视觉模型会把PDF里同一张图重复抽取好几次还会把页眉页脚里的公司Logo识别成实体。后来在管道入口加了一个“去重与噪声过滤”组件先对图片做感知哈希去重再用正则和词表过滤掉Logo、版权信息、页码等噪声。代码实现不复杂但效果显著def filter_noise(entities): noise_keywords {版权所有, 页码, confidential, logo, header} return [e for e in entities if not any(k in e.name.lower() for k in noise_keywords)]注意噪声词表一定要结合企业自身文档的特点来维护。我们一开始用通用词表发现把“Copyright”相关的合法实体也误删了。后来改成“词表上下文判断”的组合策略上下文里如果出现“©”或“All rights reserved”才过滤误删率就降下来了。4.2 图谱查询性能告急如何从秒级降到毫秒级系统上线两周后图谱查询开始变慢最慢的Cypher要跑超过15秒。排查看傻子都猜得到是深链查询——用户问“A系统最终影响哪些终端用户”会遍历很多层关系。这里优化空间最大的不是索引而是查询写法。核心优化三板斧给高频查询字段建索引特别是Entity.name和关系类型字段。限制查询深度设置可变长度关系的maxLevel为3避免全图扫描。把高频子查询结果缓存到Redis里设置5分钟过期热点问题直接命中缓存。这里特别想强调第2点因为很多人写Cypher时不自觉地用了*导致查询深度无限。业务上没人需要知道“朋友的朋友的朋友的朋友”是谁限制到3层在绝大多数企业场景下都够了。如果确实有超过3层的场景那说明你的图谱模型可能需要重新设计而不是靠查询硬撑。4.3 向量召回效果差的排查思路向量召回效果差不要第一时间怀疑Embedding模型不行。先去看几类问题是否对文档做了合理的chunk切分太长则语义杂质太多太短则上下文不足。是否去掉停用词和噪声企业文档里“根据”“如下”“请参见”这类词对Embedding没有帮助。是否有领域术语需要加入扩展词典比如“支付”“清算”“对账”这些词在通用向量模型里距离可能很远。我们有一次把“支付网关超时”这个问题召回结果排第一的竟然是“退款流程说明”原因就是chunk里包含了大量退款和支付共现的文本。后来加了基于业务规则的权重调整把“支付”“网关”“超时”等高权重词通过BM25和向量分数做混合排序问题才解决。4.4 多模态问答的“引用溯源”难题用户最反感的是“答非所问还说得振振有词”尤其是企业场景。我们的解决方案是建立一个“证据链”模块把最终答案里的每一条结论都映射回具体的召回来源。如果来源是多模态图片那么引用信息里应该包含图片缩略图或OCR识别文本如果来源是图谱关系那么引用信息里应该展示路径上的实体节点。技术层面这要求我们在召回阶段就不丢弃来源元数据。很多开箱即用的RAG框架把这一步省略了导致最后只能回答“根据相关资料显示”这种没有说服力的话。要做到真正的企业级问答证据链必须完整保留这个问题绕不过去。5. 实测效果与经验沉淀5.1 一组来自生产环境的实测数据项目在内部知识库上跑了一个月覆盖了3万份文档、约5000张架构图和流程图Neo4j里累计了近20万个实体节点、40万条关系。拿三个典型问题做对比测试测试问题传统文本RAGRAG-Anything“订单系统宕机影响哪些下游业务”答出部分文本描述无推理路径图谱链路清晰列出受影响系统及文档引用“支付流程图里用了哪几种中间件”无法回答图片内容缺失图片解析OCR输出较完整答案“去年双十一大促的节点有哪些”只能匹配零散文本无时间线多模态摘要图谱关系拼出完整时间线从数据看图片相关问题的正确率从0提升到约75%实体关系类问题的回答速度因为图谱索引的存在反而比纯向量快很多。唯一没有明显提升的是纯文本语义类问题——这类问题本来就是传统RAG的强项双引擎融合并没有拖后腿但也没必要神话这套架构。5.2 工程化阶段容易被忽视的四个细节第一文档更新机制。企业知识库文档几乎每周都在变我们的做法是给每个doc_id记录内容哈希源文档变化时触发管道重跑重跑后旧版本图谱节点打上deprecated标记向量库中旧向量删除重写。不要试图做“增量更新”来节省计算知识抽取类任务很难做到可靠的增量全量重跑配合并行调度反而是最稳妥的。第二权限控制。企业级系统里不是所有人都应该看到所有知识。我们的方案是在图谱节点和文档片段上挂载权限标签检索阶段根据用户角色过滤结果生成阶段再把不可见来源从Prompt中剔除。这一步要在检索前做如果靠大模型在生成时判断权限根本靠不住。第三成本控制。多模态大模型调用贵尤其是图片解析和LLM抽取。我们的经验是最小化LLM调用能用规则和词典解决的实体抽取绝不用LLM能用小模型解决的问题绝不用大模型。整体算下来LLM调用占总流程的比例只有不到三成。第四灰度发布。任何Prompt的修改、模型的升级、图谱建模规则的调整都先在独立环境跑一批历史问题做回归对比再逐步切流量。问答系统的行为变化很难提前预料灰度是最便宜的安全保险。5.3 踩过的坑也是一笔财富我自己在这个项目里踩的最深的坑是对“多模态”三个字的理解偏差。一开始我以为“能解析图片、能处理表格”就叫多模态其实真正的多模态是把不同模态的信息在语义空间中统一表示。图片里的实体、文字里的实体、表格里的数值应该在知识图谱里有唯一的对应关系而不是各存各的、各查各的。另一个坑是工程上过度设计。我们第一版做了复杂的实体解析、关系加权、多路召回、自研重排模型结果上线后发现性能瓶颈全在数据管道上重排模型根本没有足够的高质量训练数据。后来砍掉自研重排直接用一个简单的规则排序器效果不但没降还更容易解释。先进架构解决的是复杂问题但不要用先进架构解决数据质量问题。先把数据管道做扎实比什么都重要。最后再分享一个小技巧在Prompt里让大模型回答“不确定”比回答“确定但错误”更有价值。我们的系统在知识片段覆盖不足时会强制输出“知识库中暂无相关信息”并附带相关但未命中的文档列表用户对这个反馈的满意度反而更高因为方向给对了后面可以继续追问。这套系统后续还能扩展的方向很多比如把实时日志接入图谱做动态故障分析或者利用图谱路径生成多跳推理的思维链。但如果你的团队正准备从零开始构建RAG系统我建议先跑通“文档解析 - 图谱存储 - 混合检索 - 证据链生成”这条最小闭环把每次解析失败的案例可视化出来再逐步增加复杂度。多模态知识图谱问答的最大价值不是技术炫技而是让企业过去那些“沉睡的数据”真正能被问、能被查、能被追溯。
返回列表