AI 知识图谱踩坑记录

客服知识库升级:用户问「怎么退款」,系统把带「退款」的文档全列出来,还得自己点进去找答案。关键词搜不理解上下文,「退款流程」和「退款失败」混为一谈;同一知识点散在多篇文档里,也没法自然追问。

预算和时间都卡着,团队熟 Python 和 Neo4j,最后选了图数据库方案而不是从零搭 RDF/OWL 那套。

需求到底是什么

项目背景是个客服系统的知识库升级。原有的系统就是个简单的关键词搜索,用户问"怎么退款",系统就把所有带"退款"两个字的文档都列出来,用户还得自己点进去看哪篇才是真正需要的。

这次要解决的问题有几个:

  1. 召回太宽泛。关键词搜索根本不理解上下文,“退款流程"和"退款失败"都被认为是相关结果。
  2. 答案不直接。用户想要的是答案,不是文档链接。
  3. 知识碎片化。同一个知识点散落在多个文档里,用户得自己拼凑。
  4. 不支持追问。用户问完一个问题后,没法自然地接着问下去。

需求说完了,但还有一个现实限制:预算有限,时间也很紧。不能搞个几百人的团队,也不能搞个大型分布式系统。

方案怎么选

摆在面前的方案有几个:

  • 传统知识图谱: 用三元组 (实体, 关系, 实体) 保存知识,用 RDF/OWL 这种标准格式。优点是结构清晰、标准化程度高,缺点是构建成本高,非技术人员根本没法维护。

  • 图数据库直接干: 用 Neo4j 之类的图数据库存关系,用 Cypher 查询。优点是查询灵活、可视化方便,缺点是要自己搞定数据抽取和映射。

  • 向量 + 知识图谱结合: 用向量做语义召回,用知识图谱做关系推理。这是比较流行的路线,但复杂度也上去了。

考虑到项目的时间限制和团队技术栈,最后选了图数据库方案,具体是 Neo4j。主要原因是:

  • 工具生态比较成熟,APOC 插件能处理不少复杂场景
  • 社区资料多,遇到问题容易找答案
  • 可视化比较友好,老板能看懂

构建流程怎么走

把知识图谱建起来,大概要经历这么几个步骤:

graph LR A[原始文档] --> B[数据清洗] B --> C[实体抽取] C --> D[关系抽取] D --> E[图谱构建] E --> F[质量评估] F --> G[应用对接]

数据清洗:先把垃圾清理干净

原始文档质量参差不齐,有些是 Word 转 PDF 的乱码版,有些是不同部门写的重复内容。这一步不做干净,后面的实体抽取全是白费。

清洗重点有几个:

  1. 去重。用 MD5 去重只能处理完全相同的文件,还得用文本相似度检测找出那些"几乎一样"的。
  2. 格式统一。把不同格式转成统一的 Markdown,方便后续处理。
  3. 分段处理。长文档要拆成段落级别的 chunk,太大不好抽取,太小又丢失上下文。

这一步花了不少时间,但很值。之前跳过直接干抽取,结果后面修数据的时间比从头清洗还多。

实体抽取:把有用的东西挖出来

实体抽取其实就是从文本里找出"东西”,比如人名、地名、产品名、流程名等等。

可以自己训练模型,但考虑到时间成本,我们用了现成的 NER 工具。具体用了什么不重要,关键是效果要够用。

但模型不是万能的,有几个问题:

  • 专业名词识别不准。客服系统里有很多内部术语,模型没见过。
  • 同义词问题。“退款单"和"退款申请"其实是一个东西,但模型会当成两个。
  • 边界模糊。有些实体前后缀很多,模型要么截太短要么截太长。

解决办法是搞了个白名单,把业务上明确的实体都列出来,模型抽取出来后再白名单过滤一遍。虽然有点土办法,但稳。

关系抽取:把东西连起来

有了实体还不行,得知道它们之间什么关系。比如"订单"和"退款"是"包含"关系,“用户"和"订单"是"发起"关系。

关系抽取比实体抽取难多了。模型容易把"相关关系"当成"特定关系”,比如看到两个词在同一个句子里就认为有关系。

我们用了一个相对简单的策略:

  1. 先定义一套关系模式。比如"谁 发起 了 什么订单”、“哪个订单 包含 哪个退款”。
  2. 用规则匹配 + 模型辅助的方式提取。规则保证覆盖核心场景,模型补漏。
  3. 人工抽检校正。这个没法省,实体关系错了,后面的应用全是错的。

图谱构建:存进去

前面几步都是数据处理,这一步才是真正把数据塞进 Neo4j。

建图的时候遇到一个问题:怎么处理多对多关系?比如一个订单可能有多个产品,一个产品也可能在多个订单里。如果直接用关系连,图会变得很复杂,查询性能也不好。

我们的方案是在中间加个"订单项"节点:

graph LR 订单 --> 订单项 订单项 --> 产品 订单项 -.包含数量.-> 数量

这样虽然多了节点,但查询逻辑清晰很多,性能也没受太大影响。

踩坑记录

这一节列几个印象比较深的坑。

坑一:节点爆炸

刚开始为了图省事,把每个文档里的每个词都当成候选节点。结果图里有几十万个节点,大部分都是没用的。

教训是:宁可少漏,不要多塞。白名单机制很关键,只保留业务上明确有用的实体。

坑二:关系类型爆炸

为了"精确描述",一开始定义了几十种关系类型。结果是:维护成本太高,新增一个场景就得想半天应该用哪个关系。

后来合并成了七八种核心关系,虽然精度下降了一点,但维护成本大幅降低,整体反而更实用。

坑三:查询性能陷阱

图数据库的查询很灵活,但也容易写出"看起来很对"但实际上很慢的查询。

比如有个查询要找"某个用户发起的所有退款订单",一开始写成了:

MATCH (u:User {id: '123'})-[:发起]->(o:Order)-[:包含]->(r:Refund)
RETURN r

这个查询看起来没问题,但在图很大的情况下会遍历大量无关节点。优化后改成:

MATCH (u:User {id: '123'})-[:发起]->(o:Order)
WHERE exists((o)-[:包含]->(:Refund))
MATCH (o)-[:包含]->(r:Refund)
RETURN r

加了 WHERE 过滤,先缩小范围再做连接,性能提升很明显。

坑四:数据一致性

图是动态更新的,但新增数据的时候很容易出现"孤节点"——只有关系没有实体,或者反过来。

解决办法是建图的时候强制检查:每个节点至少要有一个关系,每个关系两端的节点都要存在。虽然会漏掉一些边界情况,但避免了大部分问题。

最终效果

折腾了几个月,系统上线了。效果怎么样呢?

  1. 召回准确率提升了 30% 左右。用户问"退款要几天到账",系统直接给出"3-5 个工作日",而不是扔一堆包含"退款"和"到账"的文档。

  2. 支持追问。用户问完"退款要几天到账",可以接着问"周末算不算",系统能理解"周末"指的是"退款到账时间"的周末,而不是随便一个周末。

  3. 可视化有帮助。Neo4j 的可视化界面能展示实体之间的关系,产品经理用它梳理流程,客服用它快速查关联问题。

但也不是没有问题:

  • 数据维护成本。新业务上线的时候,得有人维护实体和关系。不能指望模型自动搞定。
  • 冷启动问题。新领域的文档一开始没有图谱,效果不如老领域。
  • 边界情况处理。有些问题本身就很模糊,图谱也帮不上。

结语

把一个知识图谱从零搭起来,比想象中要花时间。理论上"只需三步"的事情,实际上要考虑数据质量、实体边界、关系类型、查询性能、维护成本等等问题。

但搭起来之后,确实比之前的搜索系统好用不少。用户不用自己拼凑答案,系统能给出相对直接的回复,也能处理一些追问场景。

最关键的是:图谱不是一次性工程,需要持续维护。业务在变,实体在变,关系在变,如果不管它,慢慢就变成一个没人用的"遗产系统"了。

所以,如果有人跟你说"搞个知识图谱吧,包治百病",你可以回一句:“这东西确实有用,但别指望它自己跑,得有人养。”

版权声明: 本文首发于 指尖魔法屋-AI 知识图谱踩坑记录https://blog.thinkmoon.cn/post/263-ai-knowledge-graph-construction-application-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!