第2135篇:企业AI知识图谱构建——让RAG从"检索文本"升级到"理解关系"
大约 7 分钟
第2135篇:企业AI知识图谱构建——让RAG从"检索文本"升级到"理解关系"
适读人群:希望提升RAG推理能力的工程师 | 阅读时长:约20分钟 | 核心价值:理解知识图谱在RAG中的价值,掌握从非结构化文本自动构建知识图谱的工程实践
"为什么AI能找到'张三是CEO'这个事实,却回答不了'CEO是谁的上司?'这个推理问题?"
这是向量RAG的局限性。向量检索擅长找"语义相似的文本片段",但对多跳推理(需要把多个事实连接起来)和结构化关系查询效果很差。
知识图谱(Knowledge Graph)把信息存成"实体-关系-实体"的三元组,可以回答"张三的直接下属有哪些"这类需要沿关系遍历的问题。把知识图谱和向量RAG结合起来,是RAG系统升级的重要方向。
知识图谱 vs 向量RAG的互补性
/**
* 向量RAG的盲点和知识图谱的优势
*
* ===== 向量RAG擅长 =====
*
* ✓ "A的功能是什么" → 直接语义匹配
* ✓ "哪些文档提到了X" → 语义相似性搜索
* ✓ "X的详细说明" → 找最相关的文档片段
*
* ===== 向量RAG不擅长 =====
*
* ✗ "A是B的上级,B是C的上级,A和C是什么关系?"
* (需要关系推理)
*
* ✗ "和X直接相关的所有产品"
* (需要图遍历)
*
* ✗ "比较A、B、C三个方案的优缺点"
* (需要聚合多个实体的结构化信息)
*
* ===== 知识图谱擅长 =====
*
* ✓ 实体关系查询("X的所有子部门")
* ✓ 路径查询("从A到B有几跳")
* ✓ 聚合查询("B部门的所有产品")
* ✓ 规则推理("A管理B,B管理C,所以A间接管理C")
*
* ===== 结合策略 =====
*
* 用户问题
* ↓
* 意图分析
* ↓ 关系型问题 ↓ 事实/语义型问题
* 图谱查询 向量检索
* ↓ ↓
* 汇总结果 → LLM生成答案
*/知识三元组抽取
/**
* 从文本自动抽取知识三元组
*
* 三元组格式:(主体,关系,客体)
* 例:(张三,担任职位,CEO)
* (产品A,适用于,企业客户)
* (部门B,隶属于,技术中心)
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class KnowledgeTripleExtractor {
private final ChatLanguageModel llm;
/**
* 从文本抽取三元组
*/
public List<KnowledgeTriple> extract(String text, String context) {
String prompt = """
从以下文本中抽取知识三元组(实体-关系-实体)。
背景:%s
文本:%s
返回JSON数组:
[
{
"subject": "主体实体",
"subjectType": "实体类型(PERSON/PRODUCT/DEPARTMENT/PROCESS/CONCEPT)",
"predicate": "关系动词",
"object": "客体实体",
"objectType": "实体类型",
"confidence": 0.0-1.0
}
]
只提取明确的、有业务价值的关系,不要推断。
只返回JSON数组。
""".formatted(context, text.substring(0, Math.min(2000, text.length())));
try {
String response = llm.generate(prompt);
String json = extractJsonArray(response);
List<KnowledgeTriple> triples = new ObjectMapper()
.readValue(json, new TypeReference<List<KnowledgeTriple>>() {});
// 过滤低置信度的三元组
return triples.stream()
.filter(t -> t.confidence() > 0.7)
.toList();
} catch (Exception e) {
log.warn("三元组抽取失败: {}", e.getMessage());
return List.of();
}
}
/**
* 批量从文档库抽取三元组
*/
public List<KnowledgeTriple> extractFromDocuments(List<Document> documents) {
List<KnowledgeTriple> allTriples = new ArrayList<>();
for (Document doc : documents) {
List<KnowledgeTriple> docTriples = extract(
doc.getContent(),
"企业内部文档:" + doc.getTitle()
);
// 给每个三元组标注来源
docTriples.forEach(t -> {
allTriples.add(new KnowledgeTriple(
t.subject(), t.subjectType(),
t.predicate(),
t.object(), t.objectType(),
t.confidence(),
doc.getDocId() // 来源文档
));
});
}
log.info("批量抽取完成: docs={}, triples={}", documents.size(), allTriples.size());
return allTriples;
}
private String extractJsonArray(String text) {
int start = text.indexOf('[');
int end = text.lastIndexOf(']');
return (start >= 0 && end > start) ? text.substring(start, end + 1) : "[]";
}
/**
* 知识三元组
*/
public record KnowledgeTriple(
String subject,
String subjectType,
String predicate,
String object,
String objectType,
double confidence,
String sourceDocId
) {}
}知识图谱存储与查询
/**
* 知识图谱服务
*
* 使用Neo4j或简化的邻接表存储和查询
*
* 这里用内存邻接表演示核心概念
* 生产环境建议使用Neo4j
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class KnowledgeGraphService {
// 使用Neo4j的实际实现
private final Neo4jClient neo4jClient;
/**
* 存储三元组到图谱
*/
public void addTriple(KnowledgeTripleExtractor.KnowledgeTriple triple) {
// 使用Cypher语言写入Neo4j
String cypher = """
MERGE (s:%s {name: $subjectName})
MERGE (o:%s {name: $objectName})
MERGE (s)-[r:%s {confidence: $confidence, sourceDoc: $sourceDoc}]->(o)
""".formatted(
triple.subjectType(),
triple.objectType(),
formatRelation(triple.predicate())
);
neo4jClient.query(cypher)
.bind(triple.subject()).to("subjectName")
.bind(triple.object()).to("objectName")
.bind(triple.confidence()).to("confidence")
.bind(triple.sourceDocId()).to("sourceDoc")
.run();
}
/**
* 查询实体的直接关系
*
* 例:查询"产品A"的所有关系
*/
public List<GraphRelation> queryEntityRelations(String entityName, int depth) {
String cypher = """
MATCH (e {name: $name})-[r*1..%d]-(related)
RETURN e.name AS source, type(r[0]) AS relation, related.name AS target, labels(related)[0] AS targetType
LIMIT 50
""".formatted(depth);
return neo4jClient.query(cypher)
.bind(entityName).to("name")
.fetch()
.all()
.stream()
.map(row -> new GraphRelation(
row.get("source").toString(),
row.get("relation").toString(),
row.get("target").toString(),
row.get("targetType").toString()
))
.toList();
}
/**
* 基于自然语言查询图谱(GraphRAG)
*
* 把自然语言转换为图查询
*/
public String queryWithNaturalLanguage(String question) {
// 先用LLM把自然语言转为Cypher查询
String cypherPrompt = """
请把以下自然语言问题转换为Neo4j Cypher查询语句。
图谱中的实体类型:PERSON, PRODUCT, DEPARTMENT, PROCESS, CONCEPT
常见关系类型:MANAGES, BELONGS_TO, USES, DEPENDS_ON, CREATED_BY
问题:%s
只返回Cypher查询语句,不要解释。
""".formatted(question);
String cypher;
try {
cypher = llm.generate(cypherPrompt).trim();
} catch (Exception e) {
log.warn("Cypher生成失败: {}", e.getMessage());
return null;
}
// 执行查询
try {
List<Map<String, Object>> results = neo4jClient.query(cypher)
.fetch().all().stream()
.map(row -> new HashMap<String, Object>(row))
.toList();
if (results.isEmpty()) return null;
// 把查询结果格式化为文本,供LLM理解
return formatGraphResults(results, question);
} catch (Exception e) {
log.warn("图谱查询执行失败: cypher={}, error={}", cypher, e.getMessage());
return null;
}
}
private String formatGraphResults(List<Map<String, Object>> results, String question) {
StringBuilder sb = new StringBuilder("图谱查询结果:\n");
for (Map<String, Object> row : results) {
sb.append("- ");
row.forEach((k, v) -> sb.append(k).append(": ").append(v).append(", "));
sb.append("\n");
}
return sb.toString();
}
private String formatRelation(String predicate) {
// 把中文动词转为英文大写下划线格式(Neo4j关系类型要求)
return predicate.toUpperCase()
.replace(" ", "_")
.replace("担任", "HOLDS_POSITION")
.replace("隶属", "BELONGS_TO")
.replace("管理", "MANAGES")
.replace("使用", "USES")
.replace("适用于", "APPLICABLE_TO");
}
// Simplified Neo4j client interface
interface Neo4jClient {
QueryRunner query(String cypher);
interface QueryRunner {
QueryRunner bind(Object value);
QueryRunner to(String key);
QueryRunner bind(Object v).to(String k);
FetchBuilder fetch();
void run();
}
interface FetchBuilder { List<Map<String, Object>> all(); }
}
record GraphRelation(String source, String relation, String target, String targetType) {}
}GraphRAG:图谱与向量检索的融合
/**
* GraphRAG:融合图谱和向量检索
*
* 对不同类型的问题,自动选择或组合两种检索方式
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class GraphRagOrchestrator {
private final ChatLanguageModel llm;
private final KnowledgeGraphService graphService;
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
public String answer(String question) {
// 1. 分析问题类型
QuestionType questionType = classifyQuestion(question);
String graphContext = null;
String vectorContext = null;
// 2. 根据问题类型选择检索策略
if (questionType == QuestionType.RELATIONAL || questionType == QuestionType.HYBRID) {
graphContext = graphService.queryWithNaturalLanguage(question);
}
if (questionType == QuestionType.FACTUAL || questionType == QuestionType.HYBRID) {
float[] queryVector = embeddingModel.embed(question).content().vector();
List<VectorStore.SearchResult> hits = vectorStore.search(queryVector, 5, null);
vectorContext = hits.stream()
.map(VectorStore.SearchResult::getContent)
.collect(Collectors.joining("\n\n"));
}
// 3. 组合上下文生成答案
return generateAnswer(question, graphContext, vectorContext);
}
private QuestionType classifyQuestion(String question) {
// 简单的关键词启发式分类
if (question.contains("有哪些") || question.contains("所有") ||
question.contains("负责") || question.contains("上级") || question.contains("下级")) {
return QuestionType.RELATIONAL;
}
if (question.contains("是什么") || question.contains("怎么") || question.contains("如何")) {
return QuestionType.FACTUAL;
}
return QuestionType.HYBRID;
}
private String generateAnswer(String question, String graphContext, String vectorContext) {
StringBuilder contextBuilder = new StringBuilder();
if (graphContext != null && !graphContext.isEmpty()) {
contextBuilder.append("【关系图谱数据】\n").append(graphContext).append("\n\n");
}
if (vectorContext != null && !vectorContext.isEmpty()) {
contextBuilder.append("【文档知识库】\n").append(vectorContext);
}
if (contextBuilder.length() == 0) {
return "根据现有知识库无法回答此问题。";
}
String prompt = """
请根据以下知识回答用户的问题:
%s
问题:%s
请结合图谱关系数据和文档内容,给出准确的回答。
""".formatted(contextBuilder, question);
return llm.generate(prompt);
}
enum QuestionType { RELATIONAL, FACTUAL, HYBRID }
}实践建议
知识图谱的建设是长期工程,先从高价值场景入手
不要试图一开始就构建覆盖所有领域的全量知识图谱。挑一个高价值、关系清晰的场景先做:比如"组织架构和人员关系"(查某人的汇报关系、某部门的组成)、"产品和技术关系"(哪些产品用了哪些技术组件)。把这个小图谱做好,验证价值后再扩展。
自动抽取质量不够,需要人工审核机制
用LLM自动抽取的三元组,准确率约70-80%,会有错误和噪声。如果这些错误进入知识图谱,会导致AI给出错误的关系回答("A管理B"实际上是"A协作B")。建议:抽取出来的三元组先放到审核队列,由领域专家确认后才写入图谱。高置信度(>0.95)的可以自动入库,低置信度的必须人工审核。
GraphRAG的路径:增量引入,不是重建
如果你已经有了效果还不错的向量RAG,不需要推倒重来换成GraphRAG。正确的路径是:识别出当前向量RAG答不好的问题类型(通常是多跳推理和关系查询),针对这类问题引入图谱补充。两者并行工作,用路由层根据问题类型决定用哪个。这样迁移风险最小,也能快速验证图谱的实际价值。
