基于知识图谱增强的RAG系统阅读笔记(六)使用大模型构建知识图谱

99 阅读28分钟

第六章 使用大模型构建知识图谱

本章的重点在于从原始文本中提取并结构化数据,将其转化为可以用于知识图谱构建的可用格式。

6.1 原理

网络上的数据或者企业内部数据,大多数以非结构化形式存在,如各类文档。仅仅使用文本嵌入明显无法满足相关的需求。以法律文本为例,当你需要具体检索来自某一章节的文档时,如果仅仅使用分块文本嵌入,会得到一些没什么检索返回的 top-k 文本块可能来自不同且无关的文档,从而影响回答的质量。因为系统仅基于语义相似度排序返回最高相关性块,而未始终判断这些块是否源自正确的章节,法律文本中经常会有很多相似的术语。随着分块数量的增加,误差只会越积累越多。同时,普通文本嵌入主要用于检索语义相近的内容,无法胜任过滤、排序或聚合等操作。此类任务必须依赖结构化数据,因为仅靠文本嵌入难以支持这些计算型操作。例如你想查询某领域有多少条法条。

对应的解决方法是预先设计结构化数据结构,基于大模型的自然与亚能理解能力,进行自动化抽取。万事万物都离不开具体问题具体分析

当前我们假设的任务是从给出的文本中提取出客户公司的相关信息。

6.2 实现

这是我们使用的txt文件

戴尔股份有限公司(英语:Dell Inc.,简称戴尔)是总部位于美国得克萨斯州朗德罗克的公司,由迈克尔·戴尔于1984年创立。创立时公司的名称是PC's Limited,1987年改为现在的名字。戴尔以生产、设计、销售家用以及办公室电脑而闻名,它同时也涉足高端电脑市场,生产与售卖服务器、数据储存设备和网络设备等。戴尔的其他产品还包括软件、打印机及电脑周边产品等。当公司逐渐发展到其它非电脑领域后,公司的股东们在2003年的股东大会中批准公司从“戴尔电脑”(Dell Computer)改名为“戴尔公司”。

受益于戴尔的直接商业模式(即去除中间人,直接向客人卖产品),公司能够以更低廉的价格为客人提供各种产品,并保证送货上门。此外也确保戴尔的产品还未生产出来就已经售出,即是先有订单,之后才按客户要求组装电脑。

该公司以创办人迈克尔·戴尔命名,是全球前几大的科技公司,目前全球员工已经超过九万六千名。于2010年戴尔入选美国《财富》杂志每年评选的“500强公司”排行榜的第38名。《财富》同时也将戴尔列入科技业中全球第五大最受尊崇的公司。

戴尔成长的方式包括了内部运营增长以及非运营增长,意即戴尔在成立之际,有多次令人瞩目的收购和合并行动,例如2006年的Alienware及2009年的Perot Systems。在2009年戴尔出售的产品包括个人电脑、服务器、资料存储设备、网络交换器、软件及电脑周边设备。戴尔同时也贩售打印机及由其他厂商所生产的电子产品。戴尔因为在供应链管理及电子商务的各项创新而受到赞誉。

2012年,《财富》杂志根据总营收将戴尔列为全美第44大公司以及在德州的第6大公司,同时也是德州第二大非石油公司(仅次于AT&T),也是在奥斯汀地区最大的公司。
=====
恒大集团(英语:Evergrande Group,港交所:3333),简称恒大,是总部位于深圳的中国大型综合性企业集团,1996年由许家印在广州创办。[3]集团核心业务为房地产开发,是中国大型地产发展商之一,项目遍及全国两百多个城市,此外亦发展新能源汽车、旅游、体育、金融、健康养老等多元化业务。其入股的广州恒大淘宝足球俱乐部是亚洲近年表现最出色的足球俱乐部之一。集团于2016年进入《财富》世界500强,[4]2020年排行第152位。[5]20219月时,恒大集团深陷债务危机,欠供应商、债权人和投资者总计1.9665万亿元人民币(合3000多亿美元),大致相当于2020年中国国内生产总值的2%[6]赵长龙于202178日调任为恒大物业集团有限公司的执行董事兼副董事长兼总经理。[7]2023年,恒大连续补发2021年报及2022年报,截至2022年末,负债总额为2.44兆元。[8][9]同年8月,恒大集团依据美国“破产法”第十五章向纽约曼哈顿法庭提出相关申请[10]928日,恒大集团于香港联交所发布公告,董事会主席许家印因涉嫌违法犯罪,已被依法采取强制措施。恒大集团及旗下公司股票全面暂停交易,当中包括恒大集团、恒大物业集团有限公司和恒大新能源汽车集团有限公司[11]2024年,香港高等法院下令中国恒大集团即时清盘,并在几个月后要求前高层披露资产。[12][13]
=====
高盛集团公司(英语:The Goldman Sachs Group, Inc.)是一家美国跨国投资银行与金融服务公司,其总部位于纽约市曼哈顿。高盛集团提供投资管理、证券、资产管理、主经纪商与证券承销等服务。高盛集团是全球最大的投资机构之一;[2]同时是美国国库证券的国债一级自营商,也是通常意义上的知名做市商。高盛集团还拥有一家直销银行——高盛美国银行(Goldman Sachs Bank USA)。高盛集团成立于1869年,其总部位于曼哈顿下城的韦斯特街200号;同时也在全球各大金融中心设有办事处。[3]

由于在次贷危机期间参与了资产证券化,高盛集团在2007年至2008年全球金融危机中遭受损失。[4][5] 作为问题资产救助计划的一部分,高盛集团得到了美国财政部100亿美元的投资;该金融纾困计划源自《经济稳定紧急法案》。该投资于200811月开始,并于20096月开始偿还。[6][7]

想要实现这个功能, 首先要根据自己任务抽象出数据模型,并对其进行实现。然后通过编写提示词实现基于大模型的结构化数据提取功能。最后使用Pydantic库定义数据模式,将模式可传递给模型,引导其生成符合规范的输出。

import requests
import json
import re
from neo4j import GraphDatabase
from typing import List, Dict, Optional
from pydantic import BaseModel, Field


# 第一步:数据模型构造
# 构建用于信息提取的结构化数据模型

# 公司类型枚举定义
company_types = [
    "科技公司",
    "房地产",
    "咨询服务"
]


class Location(BaseModel):
    """
    位置对象:用于表示公司的物理地址信息
    """

    address: Optional[str] = Field(
        None,
        description="The street address of the location. 位置的街道地址"
    )
    city: Optional[str] = Field(
        None,
        description="The city where the location is situated. 位置所在的城市"
    )
    state_province: Optional[str] = Field(
        None,
        description="The state or province of the location. 位置所在的州或省份"
    )
    country: Optional[str] = Field(
        None,
        description="The country of the location. 位置所在的国家"
    )


class Company(BaseModel):
    """
    公司对象:用于表示公司的核心信息
    """

    # 必需字段 - 核心识别信息
    name: str = Field(
        ...,  # 必需字段
        description="The official name of the company. 公司的正式名称"
    )

    founded_year: int = Field(
        ...,  # 必需字段
        description="The year when the company was founded. 公司成立年份"
    )

    headquarters: Location = Field(
        ...,  # 必需字段 (Location对象本身是必需的)
        description="The headquarters location of the company. 公司总部位置信息"
    )

    is_public: bool = Field(
        ...,  # 必需字段
        description="Whether the company is publicly traded or privately held. 公司是否为上市公司"
    )

    company_type: str = Field(
        ...,  # 必需字段
        description="The type of company. 公司类型",
        enum=company_types,  # 使用枚举限制取值范围
    )

    # 可选字段 - 补充信息
    founded_date: Optional[str] = Field(
        None,  # 可选字段
        description="The date when the company was founded. Use yyyy-MM-dd format. 公司成立日期,使用yyyy-MM-dd格式"
    )

    website: Optional[str] = Field(
        None,  # 可选字段
        description="The official website URL of the company. 公司官方网站URL"
    )


# 第二步:信息提取
# 使用大语言模型从文本中提取结构化信息

# Neo4j 数据库连接配置
NEO4J_URI = "bolt://localhost:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "你的密码"

# Ollama 本地大语言模型配置
OLLAMA_BASE_URL = "http://localhost:11434"
LLM_MODEL = "qwen3:32b"

# 初始化 Neo4j 数据库连接
neo4j_driver = GraphDatabase.driver(NEO4J_URI, auth=(NEO4J_USER, NEO4J_PASSWORD))


def remove_think_tags(text: str) -> str:
    """
    从文本中移除 <think> 标签及其内容
    """
    pattern = r'<think>.*?</think>'
    cleaned_text = re.sub(pattern, '', text, flags=re.DOTALL)
    return cleaned_text.strip()


def extract_json_from_response(response_text: str) -> Optional[str]:
    """
    从LLM响应中提取JSON内容,忽略<think>标签
    """
    if not response_text:
        print(f"[JSON解析] 响应文本为空")
        return None

    # 首先尝试提取代码块中的JSON
    import re
    code_block_pattern = r'```(?:json)?\s*({[^`]*?})\s*```'
    match = re.search(code_block_pattern, response_text, re.DOTALL)
    if match:
        try:
            json_str = match.group(1)
            json.loads(json_str)  # 验证JSON有效性
            print(f"[JSON解析] 从代码块中提取JSON成功")
            return json_str
        except json.JSONDecodeError as e:
            print(f"[JSON解析] 代码块JSON验证失败: {e}")
            pass

    # 尝试直接查找JSON对象(处理没有代码块的情况)
    start_brace = response_text.find('{')
    end_brace = response_text.rfind('}')

    if start_brace != -1 and end_brace != -1 and end_brace > start_brace:
        json_str = response_text[start_brace:end_brace + 1]
        try:
            # 验证是否为有效的JSON
            json.loads(json_str)
            print(f"[JSON解析] 直接提取JSON成功")
            return json_str
        except json.JSONDecodeError:
            print(f"[JSON解析] 直接提取JSON验证失败")
            pass

    # 如果直接查找失败,尝试清理后查找
    cleaned_response = remove_think_tags(response_text)

    if not cleaned_response:
        print(f"[JSON解析] 清理后的响应为空")
        return None

    start_brace_index = cleaned_response.find('{')
    start_bracket_index = cleaned_response.find('[')

    start_index = -1
    if start_brace_index != -1 and (start_bracket_index == -1 or start_brace_index < start_bracket_index):
        start_index = start_brace_index
    elif start_bracket_index != -1:
        start_index = start_bracket_index

    if start_index != -1:
        potential_json = cleaned_response[start_index:].strip()
        if potential_json:
            try:
                parsed = json.loads(potential_json)
                if isinstance(parsed, (dict, list)):
                    # 找到匹配的结束括号
                    brace_count = 0
                    in_string = False
                    escape_next = False

                    for i, char in enumerate(potential_json):
                        if escape_next:
                            escape_next = False
                            continue
                        if char == '\\':
                            escape_next = True
                            continue
                        if char == '"' and not escape_next:
                            in_string = not in_string
                            continue
                        if not in_string:
                            if char == '{' or char == '[':
                                brace_count += 1
                            elif char == '}' or char == ']':
                                brace_count -= 1
                                if brace_count == 0:
                                    result = potential_json[:i + 1]
                                    print(f"[JSON解析] 从清理文本中提取JSON成功")
                                    return result
            except json.JSONDecodeError as e:
                print(f"[JSON解析] 清理文本JSON验证失败: {e}")

    print(f"[JSON解析] 无法提取有效的JSON内容")
    return None


def call_local_llm(prompt: str, model: str = LLM_MODEL) -> str:
    """
    调用本地 Ollama 大语言模型
    """
    payload = {
        "model": model,
        "prompt": prompt,
        "stream": False
    }

    try:
        response = requests.post(f"{OLLAMA_BASE_URL}/api/generate", json=payload)
        response.raise_for_status()
        return response.json()["response"].strip()
    except Exception as e:
        print(f"调用 LLM 失败: {e}")
        return ""


def extract_company_info(text: str) -> dict:
    """
    使用大语言模型从文本中提取公司信息
    """
    print(f"[信息提取] 开始提取公司信息")

    # 构建提示词,指导LLM按照指定格式提取信息
    prompt = f"""
    你是一个专业的信息提取专家。请从以下文本中提取公司信息,并严格按照指定的JSON格式返回。

    IMPORTANT: 你必须严格按照以下规则:
    1. 只返回JSON数据,不要包含任何解释、思考过程或额外文本
    2. 不要使用<think>标签或其他标记
    3. 不要在JSON前后添加任何说明文字
    4. 确保返回的JSON格式完全正确
    5. 如果某些信息在文本中找不到,请使用合理的默认值或留空
    6. 地址信息如果找不到具体街道地址,可以使用城市名作为地址

    公司信息模型定义:
    - name (String): 公司的正式名称
    - founded_year (Integer): 公司成立年份
    - headquarters (Object): 总部位置信息,包含address, city, state_province, country字段
    - is_public (Boolean): 是否为上市公司
    - company_type (Enum): 公司类型,必须从以下选项中选择: {company_types}
    - founded_date (String, 可选): 公司成立日期,格式为 yyyy-MM-dd
    - website (String, 可选): 公司官方网站

    可用的公司类型枚举值: {company_types}

    文本内容:
    {text}

    请严格按照以下JSON格式返回结果,不要包含任何其他内容:
    {{
        "name": "公司名称",
        "founded_year": 1990,
        "headquarters": {{
            "address": "详细地址",
            "city": "城市",
            "state_province": "州/省",
            "country": "国家"
        }},
        "is_public": true,
        "company_type": "科技公司",
        "founded_date": "1990-01-01",
        "website": "http://www.example.com"
    }}

    只返回JSON,不要其他内容,使用代码块格式:
    ```json
    {{
        "name": "公司名称",
        "founded_year": 1990,
        "headquarters": {{
            "address": "详细地址",
            "city": "城市",
            "state_province": "州/省",
            "country": "国家"
        }},
        "is_public": true,
        "company_type": "科技公司"
    }}
"""

    # 调用LLM进行信息提取
    response = call_local_llm(prompt)
    print(f"[LLM原始响应] {response}")

    # 从LLM响应中提取JSON格式的数据
    json_str = extract_json_from_response(response)
    if json_str:
        try:
            company_data = json.loads(json_str)
            print(f"[信息提取] 成功提取公司信息: {company_data['name']}")
            return company_data
        except json.JSONDecodeError as e:
            print(f"[信息提取] JSON解析失败: {e}")
            return {}
    else:
        print(f"[信息提取] 无法提取有效信息")
        return {}


# 第三步:写入数据库
# 将提取的结构化信息写入Neo4j图数据库
def create_constraints():
    """
    创建Neo4j唯一性约束,确保数据一致性
    """
    with neo4j_driver.session() as session:
        # 为Company节点的name属性创建唯一性约束
        session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (c:Company) REQUIRE c.name IS UNIQUE")
        # 为Location节点的country属性创建唯一性约束
        session.run("CREATE CONSTRAINT IF NOT EXISTS FOR (l:Location) REQUIRE l.country IS UNIQUE")
    print("唯一性约束已创建")


def insert_company_to_neo4j(company_data: dict):
    """
    将公司信息插入到Neo4j数据库
    建立公司节点、位置节点以及它们之间的关系
    """
    print(f"[数据库插入] 开始插入公司信息: {company_data.get('name', 'Unknown')}")

    try:
        with neo4j_driver.session() as session:
            # 提取总部位置信息
            headquarters = company_data.get('headquarters', {})

            # 插入或合并总部位置节点 (Location),提供默认值防止None错误
            session.run("""
                MERGE (l:Location {
                    country: $country,
                    city: $city,
                    state_province: $state_province,
                    address: $address
                })
                """,
                        country=headquarters.get('country', '') or '',
                        city=headquarters.get('city', '') or '',
                        state_province=headquarters.get('state_province', '') or '',
                        address=headquarters.get('address', '') or ''
                        )

            # 插入或合并公司节点 (Company),并设置属性,提供默认值
            session.run("""
                MERGE (c:Company {name: $name})
                SET c.founded_year = $founded_year,
                    c.is_public = $is_public,
                    c.company_type = $company_type,
                    c.founded_date = $founded_date,
                    c.website = $website
                """,
                        name=company_data.get('name', '') or '',
                        founded_year=company_data.get('founded_year', 0) or 0,
                        is_public=company_data.get('is_public', False) or False,
                        company_type=company_data.get('company_type', '') or '',
                        founded_date=company_data.get('founded_date', None),
                        website=company_data.get('website', None)
                        )

            # 建立公司与总部位置之间的关系 (HEADQUARTERED_IN)
            session.run("""
                MATCH (c:Company {name: $name})
                MATCH (l:Location {country: $country})
                MERGE (c)-[:HEADQUARTERED_IN]->(l)
                """,
                        name=company_data.get('name', '') or '',
                        country=headquarters.get('country', '') or ''
                        )

        print(f"[数据库插入] 公司信息插入成功")

    except Exception as e:
        print(f"[数据库插入] 插入失败: {e}")
        import traceback
        traceback.print_exc()


def process_company_file(file_path: str):
    """
    处理公司信息文件,批量提取和导入公司数据
    """
    print(f"[文件处理] 开始处理文件: {file_path}")

    try:
        # 读取文件内容
        with open(file_path, 'r', encoding='utf-8') as file:
            content = file.read()

        # 按 ===== 分割不同公司的文本
        company_texts = content.split('=====')

        print(f"[文件处理] 找到 {len(company_texts)} 个公司文本块")

        # 处理每个公司的文本
        for i, text in enumerate(company_texts):
            text = text.strip()
            if not text:
                continue

            print(f"\n[处理进度] 处理第 {i + 1} 个公司")
            print(f"文本长度: {len(text)} 字符")

            # 使用大语言模型提取公司信息
            company_data = extract_company_info(text)

            if company_data and 'name' in company_data:
                # 将提取的信息写入数据库
                insert_company_to_neo4j(company_data)
            else:
                print(f"[处理进度] 第 {i + 1} 个公司信息提取失败")

    except Exception as e:
        print(f"[文件处理] 处理文件时发生错误: {e}")


def query_companies():
    """
    查询数据库中的公司信息,用于验证导入结果
    """
    print(f"[数据库查询] 查询所有公司信息")

    try:
        with neo4j_driver.session() as session:
            # 查询公司及其总部位置信息
            result = session.run("""
                MATCH (c:Company)-[:HEADQUARTERED_IN]->(l:Location)
                RETURN c.name AS name, 
                       c.founded_year AS founded_year,
                       c.company_type AS company_type,
                       c.is_public AS is_public,
                       l.country AS country,
                       l.city AS city
                """)

            companies = []
            for record in result:
                company_info = {
                    'name': record['name'],
                    'founded_year': record['founded_year'],
                    'company_type': record['company_type'],
                    'is_public': record['is_public'],
                    'country': record['country'],
                    'city': record['city']
                }
                companies.append(company_info)
                print(f"[查询结果] {company_info}")

            return companies

    except Exception as e:
        print(f"[数据库查询] 查询失败: {e}")
        return []


def main():
    """
    主函数:协调整个流程 - 数据模型构造 → 信息提取 → 写入数据库
    """
    print("开始处理公司信息提取和导入任务")

    # 第一步:数据模型构造(通过Pydantic模型定义完成)
    print("第一步:数据模型构造 - 已通过Company和Location类定义完成")

    # 第二步:信息提取
    print("\n第二步:信息提取")
    # 创建数据库约束
    create_constraints()

    # 处理公司文件
    file_path = "/data/KGRAG/company.txt"
    process_company_file(file_path)

    # 第三步:写入数据库
    print("\n第三步:写入数据库")
    # 查询并显示结果
    print("查询数据库中的公司信息:")
    companies = query_companies()

    print(f"\n总共处理了 {len(companies)} 个公司")

    # 关闭数据库连接
    neo4j_driver.close()
    print("\n任务完成,数据库连接已关闭")


if __name__ == "__main__":
    main()
 开始处理公司信息提取和导入任务
==================================================
第一步:数据模型构造 - 已通过Company和Location类定义完成

第二步:信息提取
唯一性约束已创建
[文件处理] 开始处理文件: /data/KGRAG/company.txt
[文件处理] 找到 3 个公司文本块

[处理进度] 处理第 1 个公司
文本长度: 741 字符
[信息提取] 开始提取公司信息
[LLM原始响应] <think>
好的,我需要从提供的文本中提取戴尔公司的信息,并按照指定的JSON格式返回。首先,我要仔细阅读文本,找出所有相关的信息点。

首先,公司名称。文本中提到“戴尔股份有限公司(英语:Dell Inc.,简称戴尔)”,所以正式名称应该是“戴尔股份有限公司”。

成立年份,文本中说“由迈克尔·戴尔于1984年创立”,所以founded_year是1984。成立日期的话,文本里没有提到具体的月份和日期,所以可能无法填写founded_date,但规则允许留空,所以这里可以不填。

总部位置,文本提到“总部位于美国得克萨斯州朗德罗克”。这里城市是朗德罗克(Round Rock),州是得克萨斯州,国家是美国。但文本里没有具体街道地址,所以address字段可以用城市名代替,即“朗德罗克”。

是否为上市公司,文本中没有明确说明,但提到“入选美国《财富》杂志每年评选的‘500强公司’排行榜”,通常这样的公司都是上市公司,所以可能推断is_public为true。不过不确定的话,但根据规则可以合理默认,所以填true。

公司类型,戴尔主要生产电脑、服务器等科技产品,属于科技公司,所以company_type选“科技公司”。

官方网站,文本中没有提到,所以website留空。

现在检查所有字段是否符合要求,确保JSON格式正确,没有多余内容。特别是注意required字段是否都填写了。最后,按照示例格式输出,不包含额外内容。
</think>

```json
{
    "name": "戴尔股份有限公司",
    "founded_year": 1984,
    "headquarters": {
        "address": "朗德罗克",
        "city": "朗德罗克",
        "state_province": "得克萨斯州",
        "country": "美国"
    },
    "is_public": true,
    "company_type": "科技公司"
}
```
[JSON解析] 从代码块中提取JSON成功
[信息提取] 成功提取公司信息: 戴尔股份有限公司
[数据库插入] 开始插入公司信息: 戴尔股份有限公司
[数据库插入] 公司信息插入成功

[处理进度] 处理第 2 个公司
文本长度: 596 字符
[信息提取] 开始提取公司信息
[LLM原始响应] <think>
好的,我需要从用户提供的文本中提取恒大集团的信息,并按照指定的JSON格式返回。首先,我需要仔细阅读文本,找出所有相关的信息点。

首先,公司名称是明确的,文本开头提到了“恒大集团”,所以name应该是“恒大集团”。成立年份方面,文本中提到“1996年由许家印在广州创办”,所以founded_year是1996。不过要注意,用户可能希望用整数类型,这里没问题。

总部位置,文本中说总部位于深圳,所以city是深圳。地址部分没有具体街道,所以address字段可以留空或者用城市名。根据规则,如果找不到具体地址,可以用城市名作为地址。因此address填“深圳市”,state_province应该是广东省,国家是中国。所以headquarters里的address是“深圳市”,city是“深圳”,state_province是“广东省”,country是“中国”。

是否为上市公司,文本中提到港交所代码3333,说明是上市公司,所以is_public设为true。公司类型,核心业务是房地产开发,所以company_type应该是“房地产”。

成立日期,文本中只提到1996年,但没有具体月份和日期,所以founded_date可能无法确定。根据规则,如果找不到可以留空或者用默认值,但用户示例中有这个字段,所以可能需要留空或者不包含。不过用户给的示例中要求可选,所以这里可能不填或者留空字符串。但用户提供的示例JSON里有这个字段,但说明中说可以不包含,所以可能需要留空。不过用户给的JSON Schema里required不包含这个字段,所以可以省略。不过用户给的例子中包含了,所以可能需要处理。文本中提到“1996年由许家印在广州创办”,但没有具体日期,所以founded_date可能无法填写,所以留空或者不填。根据规则,如果找不到用默认值或留空,所以这里设为空字符串。

网站方面,文本中没有提到官方网站,所以website留空或者不填。根据用户示例,可能需要包含,但规则允许可选,所以可以留空。

最后,检查所有字段是否符合要求,特别是枚举类型是否正确。公司类型是房地产,符合选项。JSON结构是否正确,确保没有多余字段,所有必填字段都存在。例如,required字段包括name、founded_year、headquarters、is_public、company_type。这些都需要正确填写。
</think>

```json
{
    "name": "恒大集团",
    "founded_year": 1996,
    "headquarters": {
        "address": "深圳市",
        "city": "深圳",
        "state_province": "广东省",
        "country": "中国"
    },
    "is_public": true,
    "company_type": "房地产"
}
```
[JSON解析] 从代码块中提取JSON成功
[信息提取] 成功提取公司信息: 恒大集团
[数据库插入] 开始插入公司信息: 恒大集团
[数据库插入] 公司信息插入成功

[处理进度] 处理第 3 个公司
文本长度: 402 字符
[信息提取] 开始提取公司信息
[LLM原始响应] <think>
好的,我需要从用户提供的文本中提取高盛集团公司的信息,并按照指定的JSON格式返回。首先,我要仔细阅读用户的要求,确保不遗漏任何细节。

首先,公司名称是“高盛集团公司”,对应的英文是The Goldman Sachs Group, Inc.,但用户要求的是中文名称,所以直接使用“高盛集团公司”。

接下来是成立年份,文本中提到“高盛集团成立于1869年”,所以founded_year应该是1869。这里要注意是否是整数,用户示例中的格式是正确的,所以没问题。

总部地址部分,文本里说“总部位于纽约市曼哈顿”,更具体的地址是“曼哈顿下城的韦斯特街200号”。所以地址字段应该是“曼哈顿下城的韦斯特街200号”,城市是“纽约市”,州/省这里可能需要填“纽约州”,国家是“美国”。不过用户提到如果找不到具体街道地址,可以用城市名作为地址,但这里文本中提供了详细地址,所以应该填详细地址。

是否为上市公司,高盛集团是知名的投行,属于上市公司,所以is_public设为true。

公司类型需要从给定的枚举中选择:科技公司、房地产、咨询服务。高盛是投资银行和金融服务公司,这三个选项都不符合,但用户可能希望选择最接近的。不过根据用户提供的选项,这三个之外可能需要选其中一个,但可能没有合适的。这里可能需要留空或者选默认值,但用户示例中是“科技公司”,但高盛显然不属于科技公司。不过用户可能希望根据提供的选项来选择,但原文中没有提到属于这三个类型中的任何一个,所以可能需要留空或者选默认。但用户示例中公司类型是必填的,所以可能需要选最接近的,但这里可能没有合适的,可能需要留空。不过用户提供的示例中公司类型是“科技公司”,但高盛属于金融行业,所以这里可能存在问题。但根据用户给出的选项,可能需要选其中一个,但实际可能无法正确分类。不过用户可能希望严格按照文本内容,但文本中没有提到这三个类型中的任何一个,所以可能需要留空或者选默认。但用户提供的示例中公司类型是必填的,所以可能需要检查是否有其他信息。文本中提到的是投资银行和金融服务公司,而用户提供的选项中没有金融类,所以可能需要选“咨询服务”作为最接近的,但不确定。或者可能用户希望留空,但根据规则,如果找不到,可以留空。不过用户示例中的公司类型是必填的,所以可能需要选一个默认值。但根据用户提供的模型定义,company_type是必须的,所以必须从三个选项中选一个。这里可能需要选“咨询服务”作为最接近的,但不确定是否正确。可能用户希望根据公司类型严格匹配,但高盛属于金融,所以可能无法匹配。但用户可能希望在这种情况下留空,但根据模型定义,company_type是必须的,所以必须填。这可能是一个问题,但根据用户提供的信息,可能需要选“咨询服务”作为最接近的,因为金融服务可能属于咨询服务的一种。或者可能用户希望这里不填,但根据模型定义,company_type是必须的,所以必须选一个。因此,可能需要选“咨询服务”。

成立日期,文本中只提到了成立年份1869年,没有具体日期,所以founded_date可能留空或者设为1869-01-01,但用户示例中使用了默认值,所以可以设为“1869-01-01”。

网站部分,文本中没有提到官方网站,所以留空或者不包含该字段,但用户示例中包含,所以可能需要留空,但根据规则,如果找不到可以留空,所以可以不填或者设为空字符串。但用户示例中的website是可选的,所以可以省略。但根据用户提供的JSON结构示例,可能需要包含website字段,但用户说明中提到如果找不到可以留空,所以可能设为空字符串或者不包含。但用户给出的示例中包含website字段,所以可能需要包含,但文本中没有提供,所以留空。

总结:

name: 高盛集团公司
founded_year: 1869
headquarters.address: 韦斯特街200号
headquarters.city: 纽约市
headquarters.state_province: 纽约州(假设)
headquarters.country: 美国
is_public: true
company_type: 这里可能无法正确分类,但必须选一个,可能选“咨询服务”
founded_date: 1869-01-01(假设)
website: 留空或不包含

但根据用户提供的示例,可能不需要填写website,因为是可选的。所以最终JSON可能如下:

{
    "name": "高盛集团公司",
    "founded_year": 1869,
    "headquarters": {
        "address": "曼哈顿下城的韦斯特街200号",
        "city": "纽约市",
        "state_province": "纽约州",
        "country": "美国"
    },
    "is_public": true,
    "company_type": "咨询服务"
}
</think>

```json
{
    "name": "高盛集团公司",
    "founded_year": 1869,
    "headquarters": {
        "address": "曼哈顿下城的韦斯特街200号",
        "city": "纽约市",
        "state_province": "纽约州",
        "country": "美国"
    },
    "is_public": true,
    "company_type": "咨询服务"
}
```
[JSON解析] 从代码块中提取JSON成功
[信息提取] 成功提取公司信息: 高盛集团公司
[数据库插入] 开始插入公司信息: 高盛集团公司
[数据库插入] 插入失败: {code: Neo.ClientError.Schema.ConstraintValidationFailed} {message: Node(47) already exists with label `Location` and property `country` = '美国'}
neo4j.exceptions.GqlError: {gql_status: 22N80} {gql_status_description: error: data exception - index entry conflict. Index entry conflict: Node(47) already exists with label `Label[10]` and property `PropertyKey[29]` = '美国'.} {message: 22N80: Index entry conflict: Node(47) already exists with label `Label[10]` and property `PropertyKey[29]` = '美国'.} {diagnostic_record: {'_classification': 'CLIENT_ERROR', 'OPERATION': '', 'OPERATION_CODE': '0', 'CURRENT_SCHEMA': '/'}} {raw_classification: CLIENT_ERROR}

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/data/KGRAG/main.py", line 337, in insert_company_to_neo4j
    session.run("""
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/work/session.py", line 330, in run
    self._auto_result._run(
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/work/result.py", line 236, in _run
    self._attach()
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/work/result.py", line 430, in _attach
    self._connection.fetch_message()
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/io/_common.py", line 193, in inner
    func(*args, **kwargs)
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/io/_bolt.py", line 863, in fetch_message
    res = self._process_message(tag, fields)
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/io/_bolt5.py", line 1208, in _process_message
    response.on_failure(summary_metadata or {})
  File "/root/anaconda3/envs/KGRAG/lib/python3.10/site-packages/neo4j/_sync/io/_common.py", line 263, in on_failure
    raise self._hydrate_error(metadata)
neo4j.exceptions.ConstraintError: {code: Neo.ClientError.Schema.ConstraintValidationFailed} {message: Node(47) already exists with label `Location` and property `country` = '美国'}

第三步:写入数据库

==================================================
查询数据库中的公司信息:
[数据库查询] 查询所有公司信息
[查询结果] {'name': '恒大集团', 'founded_year': 1996, 'company_type': '房地产', 'is_public': True, 'country': '中国', 'city': '深圳'}
[查询结果] {'name': '戴尔股份有限公司', 'founded_year': 1984, 'company_type': '科技公司', 'is_public': True, 'country': '美国', 'city': '朗德罗克'}

总共处理了 2 个公司

任务完成,数据库连接已关闭

进程已结束,退出代码为 0
很明显处理过程中出现了问题,这个故事告诉我们使用大模型的时候一定要做好各种异常处理。至于当前的问题怎么处理,很简单,希望有人可以在评论区输出。
flowchart LR
    A[原始文本文件] --> B{信息提取层}
    B --> C[结构化JSON数据]
    C --> D{数据库持久化层}
    D --> E[(Neo4j图数据库)]
    
    subgraph 数据处理流程
        B -->|"LLM + Prompt"| C
        C -->|"Cypher Query"| D
    end
    
    style A fill:#fff3e0
    style C fill:#e8f5e8
    style E fill:#e3f2fd
MATCH (c:Company)-[r:HEADQUARTERED_IN]->(l:Location)
RETURN c, r, l

在这里插入图片描述

6.3 实体消歧

因为拼写差异、命名惯例不同、格式轻微不一致,同一实体可能出现多种表示形式(美国和美利坚合众国)。实体消歧(Entity Resolution):识别并合并数据集中或知识图谱中同一现实实体的不同表示,将分散引用统一为图谱中的单一、连贯节点。可以提升数据完整性,增强图谱进行精确推理与关系推断的能力。我们通常使用字符串匹配聚类算法基于上下文的机器学习方法来进行实现。针对不同的领域,要设计不同的标准,这又是后话了。

总结

  • 仅对文档进行分块以用于检索,可能导致结果不准确或信息混杂,尤其在法律文档等对文档边界敏感的领域。
  • 过滤、排序与聚合等检索任务需依赖结构化数据,因文本嵌入无法胜任此类操作。
  • LLM 在从非结构化文本中提取结构化数据方面表现优异,可将其转化为表格或键值对等可用格式。
  • LLM 的结构化输出功能允许开发者定义数据模式,确保响应格式合规,减少后期处理需求。
  • 明确定义包含合同类型、参与方与日期等属性的数据模型,对引导 LLM 准确提取信息至关重要。
  • 知识图谱中的实体消歧对于合并同一实体的不同表示、提升数据一致性与准确性极为重要。
  • 在知识图谱中融合结构化与非结构化数据,既保留原始材料的丰富性,又支持更精准的查询与分析。