手扒Github项目文档级知识图谱构建框架RAKG(保姆级)Day5

在完成实体抽取和实体消歧之后,就需要开始构建完整的知识图谱了,构建知识图谱的方法仍然在kgAgent.py文件中的NER_Agent类下,嵌套结构如下:

python 复制代码
def get_target_kg_all
    def get_target_kg_single
        def get_retriever_context
        def get_sentences_for_entity

现在我们对这些方法一一深挖讲解: 1.匹配原句函数

python 复制代码
def get_sentences_for_entity(self, entity_dic, entity_id, id_to_sentence):
    """
    这个函数是一个"考古定位器",它的任务是:
    给定一个实体(比如"苹果公司"),找出所有提到它的原始文献。
    
    为什么需要这个?
    - 在实体消歧阶段,多个文本块可能提到同一个实体
    - 这些文本块的ID都被记录在实体的 chunkid 字段中(用;;;分隔)
    - 现在我们需要把这些ID转换回实际的文本内容
    """
    # 第1步:安全检查 - 确保要查找的实体确实存在
    if entity_id not in entity_dic:
        raise ValueError(f"Entity '{entity_id}' not found in entity_dic.")
        # 为什么用 raise 而不是 return None?
        # 因为这是一个编程错误,不是正常的业务逻辑,应该立即暴露问题

    # 第2步:获取实体的来源标记
    chunkids = entity_dic[entity_id].get('chunkid', '')
    # .get('chunkid', '') 的妙处:
    # - 如果实体有 chunkid 字段,返回其值
    # - 如果没有,返回空字符串,避免 KeyError
    # 这是防御性编程的典范

    if not chunkids:
        return []  # 没有来源信息,返回空列表
        # 为什么返回 [] 而不是 None?
        # 因为调用者期望的是列表,空列表可以直接迭代,None 会导致错误

    # 第3步:解析多个来源ID
    chunkid_list = chunkids.split(';;;')
    # 为什么用 ';;;' 作为分隔符?
    # - 三个分号几乎不会在正常文本中出现
    # - 避免了与常见分隔符(逗号、分号)的冲突

    chunkid_list = [cid.strip() for cid in chunkid_list if cid.strip()]
    # 这行代码做了两件事:
    # 1. strip() 去除每个ID前后的空格
    # 2. if cid.strip() 过滤掉空字符串
    # 防止 "chunk1;;; ;;;chunk2" 这种情况产生空ID

    # 第4步:将ID转换为实际文本
    sentences = []
    for chunkid in chunkid_list:
        if chunkid in id_to_sentence:
            sentences.append(id_to_sentence[chunkid])
        else:
            print(f"Warning: Chunk ID '{chunkid}' not found in id_to_sentence.")
            # 为什么只是警告而不是报错?
            # 因为数据可能不完整,但我们希望尽可能多地恢复信息
            # 宁可得到部分结果,也不要完全失败

return sentences

实际能解决的问题可以通过下面的例子来理解,这个方法相当于匹配到实体出现在原文中的句子,并把这些句子以list的形式返回出来。

makefile 复制代码
# 输入数据
entity_dic = {
    "entity1": {
        "name": "苹果公司",
        "chunkid": "doc1_chunk1;;;doc1_chunk3;;;doc2_chunk1"
    }
}
id_to_sentence = {
    "doc1_chunk1": "苹果公司成立于1976年。",
    "doc1_chunk3": "苹果公司推出了iPhone。",
    "doc2_chunk1": "苹果公司市值超过万亿。"
}

# 输出
result = ["苹果公司成立于1976年。", "苹果公司推出了iPhone。", "苹果公司市值超过万亿。"]

2.查询语义相似的原文内容

ini 复制代码
def get_retriever_context(self, query, sentences, sentence_to_id, vectors, top_k=5):
    """
    这是一个"智能雷达",它能找到与查询语义最相似的文本。
    
    核心思想:
    - 实体可能以不同形式出现在文本中
    - 通过向量相似度,我们能找到语义相关但措辞不同的文本
    - 这些额外的文本能提供更丰富的上下文信息
    
    """
    # 第1步:将查询文本转换为向量
    query_vector = self.embeddings.embed_query(query)
    # embed_query vs embed_documents 的区别:
    # - embed_query: 针对短查询优化,通常会加入特殊的查询标记
    # - embed_documents: 针对文档优化,适合较长文本
    # 这里用 embed_query 因为实体名称通常很短

    # 第2步:计算余弦相似度
    sentence_vectors = np.array(vectors)
    # 为什么转换为 numpy array?
    # - numpy 的向量运算比 Python 列表快100倍以上
    # - cosine_similarity 函数需要 numpy 格式

    similarities = cosine_similarity([query_vector], sentence_vectors)[0]
    # 为什么 [query_vector] 要加方括号?
    # - cosine_similarity 期望第一个参数是二维数组(多个查询)
    # - [0] 取出第一行,因为我们只有一个查询

    # 第3步:选择最相似的 top_k 个句子
    top_indices = np.argsort(similarities)[::-1][:top_k]
    # 这行代码的三个技巧:
    # 1. np.argsort(): 返回排序后的索引,而不是值
    # 2. [::-1]: Python 的切片技巧,反转数组(从大到小)
    # 3. [:top_k]: 只取前 k 个

    retriever_context = []
    for idx in top_indices:
        sentence = sentences[idx]
        similarity = similarities[idx]
        sentence_id = sentence_to_id[sentence]
        retriever_context.append((sentence, similarity, sentence_id))
        # 为什么返回元组而不是字典?
        # - 元组更轻量,内存占用小
        # - 三元组的顺序固定,便于解包:sentence, sim, id = context[0]

return retriever_context

实际能解决的问题可以通过下面的例子来理解,这个方法相当于以query中的值为核心(该值是一个实体),通过计算语义相似度来获取句子list中最相似的前几个句子,把它们进行返回。

ini 复制代码
# 假设我们有这样的场景:

# 1. 文档中的所有句子
sentences = [
    "苹果公司是全球知名的科技公司。",           # 句子0
    "乔布斯于1976年创立了苹果公司。",          # 句子1
    "iPhone是苹果公司的主要产品之一。",        # 句子2
    "微软是苹果的主要竞争对手。",              # 句子3
    "苹果公司总部位于加州库比蒂诺。",          # 句子4
    "今天天气很好,适合吃苹果。"              # 句子5(无关句子)
]

# 2. 句子到ID的映射
sentence_to_id = {
    "苹果公司是全球知名的科技公司。": "chunk_001",
    "乔布斯于1976年创立了苹果公司。": "chunk_002",
    "iPhone是苹果公司的主要产品之一。": "chunk_003",
    "微软是苹果的主要竞争对手。": "chunk_004",
    "苹果公司总部位于加州库比蒂诺。": "chunk_005",
    "今天天气很好,适合吃苹果。": "chunk_006"
}

# 3. 查询
query = "苹果公司"  # 这通常是某个实体的名称

# 4. 调用函数(top_k=3,返回最相似的3个句子)
result = get_retriever_context(
    query="苹果公司",
    sentences=sentences,
    sentence_to_id=sentence_to_id,
    vectors=vectors,  # 预先计算好的句子向量
    top_k=3
)
# get_retriever_context 的返回值:
result = [
    (
        "苹果公司是全球知名的科技公司。",     # 第1相似的句子
        0.95,                                # 相似度分数(0-1之间)
        "chunk_001"                          # 对应的chunk ID
    ),
    (
        "乔布斯于1976年创立了苹果公司。",     # 第2相似的句子
        0.89,                                # 相似度分数
        "chunk_002"                          # 对应的chunk ID
    ),
    (
        "苹果公司总部位于加州库比蒂诺。",     # 第3相似的句子
        0.87,                                # 相似度分数
        "chunk_005"                          # 对应的chunk ID
    )
]

3.获取单个实体的知识图谱,包括了前面两个方法,即找到原始文本和扩展语义。再调用llm生成图谱,返回结果。

python 复制代码
def get_target_kg_sigle(self, entity_dic, entity_id, id_to_sentence, 
                        sentences, sentence_to_id, vectors, output_file):
    """
    这是整个系统的"心脏",它orchestrates(编排)所有组件:
    1. 收集原始上下文(准确性)
    2. 扩展语义上下文(完整性)
    3. 调用 LLM 提取知识(智能性)
    4. 持久化结果(可追溯性)
    """
    # 第1步:获取实体的原始出处文本
    chunk_text_list = self.get_sentences_for_entity(entity_dic, entity_id, id_to_sentence)
    # 这些是"铁证"------实体确实在这些文本中出现过,返回的是实体对应的原文原句,实现准确性

    # 第2步:获取实体名称作为检索查询
    query = entity_dic[entity_id].get('name', '')
    # 用实体名称作为查询,寻找更多相关信息

    # 第3步:通过向量检索获取扩展上下文
    context = self.get_retriever_context(query, sentences, sentence_to_id, vectors, top_k=5)
    # top_k=5 是经验值:
    # - 太少:可能错过重要信息
    # - 太多:引入噪音,增加 LLM 负担
    # 返回的是一系列语义相似的上下文,实现完整性
    
    # 第4步:提取检索到的句子文本
    sentences = [item[0] for item in context]
    # context 中每个元素是 (句子, 相似度, ID)
    # 我们只需要句子文本,其他要素我们不需要

    # 第5步:合并并去重
    unique_sentences = list(set(chunk_text_list + sentences))
    # 为什么要去重?
    # - 原始文本和检索文本可能有重叠
    # - 重复信息会让 LLM 困惑或产生偏见
    # set() 自动去重,list() 转回列表

    # 第6步:构建最终输入文本
    chunk_text = ", ".join(unique_sentences)
    # 为什么用逗号+空格连接?
    # - 保持文本的可读性
    # - LLM 能够理解这是多个独立的信息片段

    # 第7步:准备并执行 LLM 调用
    prompt = ChatPromptTemplate.from_template(extract_entiry_centric_kg_en_v2)
    chain = prompt | self.model
    # 管道操作符 | 创建了一个处理链
    # 此处的extract_entiry_centric_kg_en_v2是prompt.py里的构建知识图谱的提示词

    result = chain.invoke({
        "text": chunk_text,                                    # 所有相关文本
        "target_entity": entity_dic[entity_id].get('name'),   # 焦点实体
        "related_kg": 'none'                                  # 可以传入已有知识
    })

    # 第8步:解析 LLM 响应
    if hasattr(result, 'content'):
        result_json = json.loads(result.content)
    else:
        result_json = json.loads(result)
    # 兼容不同的 LLM 响应格式

    # 第9步:持久化完整记录
    with open(output_file, 'a') as f:  # 'a' 模式:追加而不覆盖
        combined_data = {
            "chunk_text": chunk_text,      # 输入文本(可审计)
            "entity": entity_dic[entity_id], # 实体信息(可追溯)
            "kg": result_json              # 提取结果(核心输出)
        }
        f.write(json.dumps(combined_data) + '\n')
        # JSONL 格式:每行一个 JSON,便于流式处理

return result_json

4.这一步又包装了前面的单个实体生成知识图谱的方法,遍历处理每一个实体,为其生成知识图谱。

python 复制代码
def get_target_kg_all(self, entity_dic, id_to_sentence, sentences, 
                      sentence_to_id, vectors, output_file):
    """
    这是"自动化生产线",将单个实体的处理扩展到批量。
    
    """
    
    results = {}
    for entity_id in entity_dic:
        # 这个 if 检查看似多余(entity_id 肯定在 entity_dic 中)
        # 但它是防御性编程:防止在循环过程中字典被意外修改
        if entity_id in entity_dic:
            result = self.get_target_kg_sigle(
                entity_dic, entity_id, id_to_sentence,
                sentences, sentence_to_id, vectors, output_file
            )
            results[entity_id] = result
        else:
            print(f"Entity {entity_id} not found in entity_dic.")
    
    return results  # 返回所有实体的知识图谱
ini 复制代码
def convert_knowledge_graph(self, input_data):
    output = {
        "entities": [],
        "relations": []
    }

    entity_registry = {}
    
    # First pass: Process original entities
    # 目标:遍历所有"个人档案",把档案的主人公登记到市民名册里,并记录他们的属性。
    for entity_key in input_data:
        central_entity = input_data[entity_key]["central_entity"]
        entity_name = central_entity["name"]

        # 这是去重的核心:如果市民名册里还没有这个名字,就给他登记!
        if entity_name not in entity_registry:
            # 创建一个标准格式的实体记录
            entity = {
                "name": entity_name,
                "type": central_entity["type"],
                "description": central_entity.get("description", ""),
                "attributes": {} # 准备一个空字典来存放属性
            }

            # 将LLM返回的属性列表,转换为更易用的字典格式
            if "attributes" in central_entity:
                for attr in central_entity["attributes"]:
                    # 之前: [{"key": "founded_year", "value": "1976"}]
                    # 之后: {"founded_year": "1976"}
                    entity["attributes"][attr["key"]] = attr["value"]

            # 将整理好的实体记录,以其名字为键,存入市民名册
            entity_registry[entity_name] = entity

    # Second pass: Process relationships
    # 目标:再次遍历所有实体"个人档案",这次专门看档案里的"社会关系"部分。
    for entity_key in input_data:
        central_entity = input_data[entity_key]["central_entity"]

        if "relationships" in central_entity:
            for rel in central_entity["relationships"]:
                # 这行代码非常灵活,因为LLM可能返回单个目标,也可能返回一个列表
                # e.g., "founder": "Steve Jobs" 或 "co-founders": ["Steve Jobs", "Steve Wozniak"]
                target_names = rel["target_name"] if isinstance(rel["target_name"], list) else [rel["target_name"]]
                target_type = rel["target_type"]

                # 遍历关系中的每一个目标实体
                for target_name in target_names:
                    # 关键一步:检查关系中提到的人,是否已经在市民名册里了
                    # 比如,如果"比尔·盖茨"只在关系中被提及,从未作为中心实体出现过
                    if target_name not in entity_registry:
                        # 那么,就地为他创建一个基本的登记记录
                        entity_registry[target_name] = {
                            "name": target_name,
                            "type": target_type,
                            "description": rel.get("target_description", ""),
                            "attributes": {}
                        }

                    # 构建一个标准的"关系四元组"并添加到总的关系网络中
                    relation_description = rel.get("relation_description", "")
                    output["relations"].append([
                        central_entity["name"],      # 主语 (Subject)
                        rel["relation"],             # 谓语 (Predicate)
                        target_name,                 # 宾语 (Object)
                        relation_description         # 关系描述 (可选)
                    ])

    # 将市民名册(字典)中的所有值(实体记录)取出来,转换成一个列表
    output["entities"] = list(entity_registry.values())

    # 返回组装完成的、标准的知识图谱
    return output

这段代码有点长,我们通过举例一些变量来理解实现的功能:

输入数据 (input_data) 示例

这是 get_target_kg_all 函数的输出,是一个以实体ID为键的字典:1. "苹果公司" 和 "史蒂夫·乔布斯" 被定义了两次。关系也是从两个角度描述的,存在冗余。

json 复制代码
{
  "entity1": {
    "central_entity": {
      "name": "苹果公司",
      "type": "Company",
      "attributes": [{"key": "founded_year", "value": "1976"}],
      "relationships": [{
        "relation": "founder",
        "target_name": "史蒂夫·乔布斯",
        "target_type": "Person"
      }]
    }
  },
  "entity2": {
    "central_entity": {
      "name": "史蒂夫·乔布斯",
      "type": "Person",
      "attributes": [],
      "relationships": [{
        "relation": "founded",
        "target_name": "苹果公司",
        "target_type": "Company"
      }]
    }
  }
}

函数通过两次遍历 input_data 来完成这个复杂的转换,非常高效。

entity_registry:实体注册表(核心工具)

在开始之前,函数创建了一个空字典 entity_registry = {}。 这就像是书记官手上的空白市民名册。它的作用是:

  1. 快速查找:用实体名称作为键,可以瞬间判断一个实体是否已被登记。
  2. 自动去重:因为字典的键是唯一的,所以天然就防止了重复登记。

执行第一个for循环进行扫描后,可以得到一系列实体

makefile 复制代码
entity_registry = {
    "苹果公司": {
        "name": "苹果公司",
        "type": "Company",
        "description": "",
        "attributes": {"founded_year": "1976"}
    },
    "史蒂夫·乔布斯": {
        "name": "史蒂夫·乔布斯",
        "type": "Person",
        "description": "",
        "attributes": {}
    }
}

执行第二个for循环进行扫描后,可以得到记录的所有关系

css 复制代码
output["relations"] = [    ["苹果公司", "founder", "史蒂夫·乔布斯", ""],
    ["史蒂夫·乔布斯", "founded", "苹果公司", ""]
]

最终,通过这个转换函数,就把多个以自身为中心的实体+关系分解成了所有实体+所有关系的输出,从而完成整个文本知识图谱的构建。

相关推荐
weixin_5824701724 分钟前
GS-IR:3D 高斯喷溅用于逆向渲染
人工智能·算法
GetcharZp1 小时前
玩转AI绘画,你只差一个节点式“魔法”工具——ComfyUI 保姆级入门指南
人工智能·stable diffusion
一休哥助手2 小时前
Naive RAG:简单而高效的检索增强生成架构解析与实践指南
运维·人工智能·架构
机器之心2 小时前
究竟会花落谁家?DeepSeek最新大模型瞄准了下一代国产AI芯片
人工智能·openai
赵英英俊2 小时前
Python day51
人工智能·pytorch·python
双向332 小时前
金融风控AI引擎:实时反欺诈系统的架构设计与实现
人工智能
星期天要睡觉2 小时前
计算机视觉(opencv)实战六——图像形态学(腐蚀、膨胀、开运算、闭运算、梯度、顶帽、黑帽)
人工智能·opencv·计算机视觉
悟纤3 小时前
AI翻唱实战:用[灵龙AI API]玩转AI翻唱 – 第6篇
人工智能·ai翻唱·ai cover
神码小Z3 小时前
NewsNow搭建喂饭级教程
人工智能·业界资讯