手记

基于LangChain和GPT的大篇幅文档交互与SQL查询助手

利用一下LangChain SQL Agent和GPT来进行文档分析与互动(它们是文档分析和交互的工具)

由Midjourney创建的图像,如一个悬浮在数据库里的数字文件。

最近我遇到了一个非常有趣的挑战,涉及AI对大量文档的数字化处理,并让用户能够提出复杂的、数据相关的问题,例如。

  • 数据检索类型的问题: 这类问题涉及从数据库中获取特定的数据点或数据集,例如“电子产品有多少个?”或“2021年第四季度的总销售额是多少呢?”
  • 聚合查询: 这类问题需要对数据进行总结,比如计算平均值、求和、计数等,例如“所有展示的产品的平均价格是多少?”或“每个地区的客户总数分别是多少?”
  • 数据关系探索: 这类问题探究不同数据实体之间的关系,例如“哪些客户购买了超过三种不同类型的产品?”或“列出上个月没有发货任何产品的所有供应商。”
  • 条件查询: 这类问题涉及条件或过滤器,例如“列出2022年单笔金额超过500美元的所有交易”或“列出所有缺货的产品。”

这些不是你通常能通过RAG解决的典型问题。相反地,我们将利用LangChain的SQL Agent工具从人类的文本中生成复杂的数据库查询。

这些文档应该包含各种规格要求,同时也要有更自然流畅的语言描述。

我们将按以下步骤操作,以便最终能够对大量文件提出复杂的问题。

  • 读取所有 PDF 文件
  • 用 GPT 分析文档内容,解析成 JSON 对象
  • 将这些对象写入 SQLite,并分到多个表里
  • 用 LangChain SQL Agents 自动生成 SQL 语句提问

免责声明: 本文涉及人工智能和数据操作的概念。为了获得最充分的理解,你应该具备Python编程、GPT模型、嵌入、向量搜索和SQL数据库等相关知识的基础。

向大家介绍:Osram 产品资料表

你可以看看Osram的产品资料表。他们的网站上有许多这样的文档,你可以在这里找到很多。

OSRAM专业照明

比如,“XBO 1000 W/HS OF”的产品数据表链接为:“XBO 1000 W/HS OF”

这里有各种各样的文本,比如产品名称、描述、应用领域和好处,应有尽有。

此外,还有各种各样的规范:

别忘了在多行表格中列出的规格:

总而言之,我们已经准备好了一个坚实的基础,来迎接我们的由AI驱动的任务!

使用Python、LangChain和GPT分析文档

我现在通常会做的就是启动Azure AI文档智能服务的实例,用少量PDF来训练模型,以检测内容为目的,让模型能够检测PDF内容。

不过这次我选择了另一条路。

我们将用Python和LangChain的来读取和分析PDF文件。我用的是Python 3.11。

首先,我们需要搭建环境,安装并引入所需的库:

在命令行中运行以下命令来安装所需的库:

    %pip install pypdf  
    %pip install langchain  
    %pip install langchain_openai  
    %pip install sqlite3

这些命令将分别安装 pypdflangchainlangchain_openaisqlite3 库,帮助你完成相关任务。

    从 pypdf 导入 PdfReader  

    从 langchain_core.messages 导入 HumanMessage, SystemMessage  
    从 langchain_openai 导入 ChatOpenAI

现在,让我们开始解析PDF文件。我们的目标是提取有价值的内容,忽略空行、页眉和页脚等不太重要的信息,使用_visitortext(文本访问者):

    document_content = None  # 初始化document_content为None  

    def visitor_body(text, cm, tm, fontDict, fontSize):  # 定义visitor_body函数来处理文本和位置信息
        y = tm[5]  # 获取y坐标
        if text and 35 < y < 770:  # 如果text不为空且y坐标在35到770之间
            page_contents.append(text)  # 将文本添加到page_contents列表中

    with open(f'./documents/ZMP_55852_XBO_1000_W_HS_OFR.pdf', 'rb') as file:  # 打开PDF文件
        pdf_reader = PdfReader(file)  # 初始化PdfReader对象

        page_contents = []  # 创建一个空列表来存储每页的内容

        for page in pdf_reader.pages:  # 遍历PDF中的每一页
            page.extract_text(visitor_text=visitor_body)  # 将每页的内容提取出来

        document_content = "\n".join(page_contents)  # 将所有页面的内容连接成一个字符串

    print(document_content)  # 打印文档内容

我们来看看解析出来的文档吧:

产品系列优势  
_  
短弧高亮度,提供更明亮的屏幕照明  
_  
整灯寿命期间色温恒定为6,000 K  
_  
易于维护  
_  
电弧稳定性高  
_  
热重启功能使屏幕瞬间点亮  
_  
宽调光范围  
产品系列特性  
_  
色温:约6,000 K(日光)  
_  
功率:450…10,000 W  
_  
色温显色指数优良:Ra > 90  
产品数据表  

XBO 1000 W/HS OFR  
适用于电影放映的XBO短弧灯,功率范围:450…10,000 W  

[...]   

包装单位  

(件/个)  
尺寸(长 x 宽 x 高)  
体积(立方分米)  
毛重(克)  
4008321082114  
XBO 1000 W/HS OFR  
运输纸箱  

1  
410 mm x 184 mm x  

180 mm  
13.58 dm³  
819.00 g  

[...]  

在审查解析内容之后,可以看出,内容缺乏一定的结构——表格之间缺乏联系,相关实体分散在各处。

这里我们请GPT来帮忙建立秩序:

  • 我们将告诉GPT将解析后的数据格式化成结构化的JSON对象。
  • 通过提供一个解析数据的示例,并加上以<<<为前缀的战略提示,我们可以引导GPT理解和构建文档结构。
  • 使用OpenAI的聊天API,我们将让GPT从一组新的解析产品数据生成一个JSON对象。

让我们构建一个有洞察力的系统消息来启动这个过程。我们首先给GPT一个清晰的指令,接着提供解析的数据作为上下文,并穿插有针对性的提示来优化输出。

请大家仔细观察我们如何结合各种线索来形成所需的精准JSON输出:

    你分析产品描述,将其导出为JSON格式。我会提供一个产品数据表,并描述各个JSON对象和属性,用`<<<`标记。然后你根据另一个产品数据表创建一个JSON对象。

    >>> 示例产品:

    产品优势 <<< benefits (string[])  
    _  
    短弧高亮度,屏幕照明更亮 <<< benefits.[*]  
    _  
    整个灯泡寿命内色温恒定为6,000 K <<< benefits.[*]  

    _  
    广泛的调光范围 <<< benefits.[*]  
    产品特性 <<< product_family (object)  
    _  
    色温:约6,000 K(日光) <<< product_family.temperature = 6000  
    _  
    功率范围:450…10,000 W <<< product_family.watts_min = 450, product_family.watts_max = 10000  
    _  
    高显色指数:Ra >90  
    产品数据表  

    XBO 1000 W/HS OFR <<< name  
    适用于电影放映的XBO | 氙气短弧灯450…10,000 W <<< description  

    [..]  

    技术数据  
    电气特性 <<< technical_data (object)  
    额定电流  
    50 A <<< technical_data.nominal_current = 50.00  
    电流控制范围  
    30…55 A <<< technical_data.control_range = 30, technical_data.control_range = 55  
    额定功率  
    1000.00 W <<< technical_data.nominal_wattage = 1000.00  
    额定电压  
    19.0伏特 <<< technical_data.nominal_voltage = 19.0  
    尺寸及重量 <<< dimensions (object)  

    [..]  

    安全使用说明  
    识别候选名单物质 <<< environmental_information.safe_use (开始字符串)  

    足够保证产品的安全使用。 <<< environmental_information.safe_use (结束字符串)  
    SCIP数据库声明编号  
    22b5c075-11fc-41b0-ad60-dec034d8f30c <<< environmental_information.scip_declaration_number (单个字符串!)  
    国家特定信息  

    [..]  

    包装尺寸  

    1  
    410 mm x 184 mm x <<< packaging_unity.length = 410, packaging_unit.width = 184  

    180 mm <<< packaging_unit.height = 180  

    [..]  
    """

我的建议是一堆不同的方法:

  • < << benefits (字符串[]) — 这里开始是一个字符串列表。
  • *< << benefits.[]** — 这是字符串列表中的一行。
  • < << product_family (对象) — 这里开始定义一个对象。
  • < << product_family.temperature = 6000 — 这行是对象的一个整数属性,表示温度为6000。
  • < << product_family.watts_min = 450, product_family.watts_max = 1000 — 这行定义了两个整数属性,分别是最小功率450瓦和最大功率1000瓦。
  • 以此类推。

在这里你可以尽情发挥创意,尝试任何你觉得合适的事情。还有其他一些例子,如:

  • 翻译成另一种语言。
  • 提供摘要或完整的句子(如项目符号点所示)。
  • 拆分数据,比如名字和姓氏。
  • 等等。
测试提示语

好了,轮到GPT上场了,看看它能不能把我们乱七八糟的PDF文本转换成整齐的JSON对象。

我用GPT-3.5-Turbo 0125而不是更强的GPT-4,因为Azure的Document Intelligence太贵了,我得找一个既省钱又能完成任务的替代方案。

撰写本文之际,GPT-3.5-Turbo的价格已经相当诱人了。

  • 每1K输入的令牌费用为0.0005美元
  • 每1K输出的令牌费用为0.0015美元

更重要的是,GPT-3.5-Turbo 版本 0125 在回复 JSON 等格式时准确率更高,这对于我们的情况来说非常完美!更棒的是它的上下文窗口达到了 16,385 个令牌数。

我们已经准备好了系统消息内容,并将它与文档内容信息配对作为输入。

    chat = ChatOpenAI(model_name='gpt-3.5-turbo-0125', temperature=0)  

    # 将文档内容转换为JSON格式
    def convert_to_json(document_content):  
        messages = [  
            SystemMessage(  
                content=system_message  
            ),  
            HumanMessage(  
                content=document_content  
            )  
        ]  

        answer = chat.invoke(messages)  
        return answer.content  

    json = convert_to_json(document_content)  

    print(json)

好的,来看看我们得到的JSON输出吧,快速检查一下。

{
    "name": "XBO 1000 W/HS OFR",
    "description": "XBO 用于电影放映 | 氙气短弧灯 450…10,000 W",
    "applications": [
        "经典35毫米胶片放映",
        "数字电影和视频投影",
        "建筑和效果照明(‘光束效果’)",
        "模拟自然阳光"
    ],
    "technical_data": {
        "nominal_current": 50.00,
        "control_range_min": 30,
        "control_range_max": 55,
        "nominal_wattage": 1000.00,
        "nominal_voltage": 19.0
    },
    "dimensions": {
        "diameter": 40.0,
        "length": 235.0,
        "length_base": 205.00,
        "light_center_length": 95.0,
        "electrode_gap": 3.6,
        "weight": 255.00
    },
    "operating_conditions": {
        "max_temp": 230,
        "lifespan": 2000,
        "service_lifetime": 3000
    },
    "additional_data": {
        "base_anode": "SFa27-11",
        "base_cathode": "SFcX27-8",
        "product_remark": "OFR = 无臭氧版本 / H = 适合水平点燃位置 / S = 短型"
    },
    "capabilities": {
        "cooling": "强制风冷",
        "burning_position": "S20/P20"
    },
    "environmental_information": {
        "declaration_date": "10-03-2023",
        "primary_product_number": "4008321082114 | 4050300933566",
        "candidate_list_substance": "铅",
        "cas_number": "7439-92-1",
        "safe_use": "候选清单物质的标识已足够确保产品的安全使用。",
        "scip_declaration_number": "SCIP 声明编号"
    },
    "logistical_data": {
        "product_code": "4008321082114",
        "product_name": "XBO 1000 W/HS OFR",
        "packaging_unit": {
            "product_code": "4008321082114",
            "product_name": "XBO 1000 W/HS OFR",
            "length": 410,
            "width": 184,
            "height": 180,
            "volume": 13.58,
            "weight": 819.00
        }
    }
}

不得不说,这真的很不错,它找到的对象和属性都非常准确。

然而,有一个明显的遗漏:GPT却忽略了几个关键要素,例如_benefits_和_productfamily\,这两个要素却完全不见踪影。

那么,我们的方案是什么?是转向GPT-4版本,它提供了增强的能力,但成本更高且无疑更慢,还是调整策略以结合函数调用来优化资源配置同时保持效率?

当然,我们选第二个!

通过提供 JSON 格式来优化提示内容

函数调用是我使用GPT时最喜欢的功能之一。它不仅让你能够指定GPT可以执行的函数,还能指定你需要的JSON格式。

看看下面这个例子:

    "function": {  
        "name": "get_current_weather",  
        "description": "获取某个位置的当前天气",  
        "parameters": {  
            "type": "object",  
            "properties": {  
                "location": {  
                    "type": "string",  
                    "description": "城市及州,例如:San Francisco, CA",  
                },  
                "unit": {"type": "string", "enum": ["℃", "℉"]},  
            },  
            "required": ["location"],  
        },  
    },

最新推出的模型 gpt-3.5-turbogpt-4-turbo-preview ,被训练得在适当时候启动函数调用,并能更准确地输出符合指定签名(function signature)的JSON。

为了利用这一点,我们将提示进行优化,使其包含我们期望得到的JSON格式。

你需要分析产品描述,并将其转换成JSON格式。我会用 <<< 和 >>> 来描述各个JSON对象和属性。我会提供一份产品数据表给你,然后你根据另一份产品数据表创建一个JSON对象。

>>> 示例产品如下:

产品系列优势 <<< 优势(字符串数组)

[...]

-----
请按照以下模式提供你的JSON内容:

{
  "type": "object",
  "properties": {
    "name": {
      "type": "string"
    },
    "description": {
      "type": "string"
    },
    "applications": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "benefits": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "product_family": {
      "type": "object",
      "properties": {
        "temperature": {
          "type": "number"
        },
        "watts_min": {
          "type": "number"
        },
        "watts_max": {
          "type": "number"
        }
      }
    },
    "technical_data": {
      "type": "object",
      "properties": {
        "nominal_current": {
          "type": "number"
        },
        "control_range_min": {
          "type": "number"
        },
        "control_range_max": {
          "type": "number"
        },
        "nominal_wattage": {
          "type": "number"
        },
        "nominal_voltage": {
          "type": "number"
        }
      }
    },
    "dimensions": {
      "type": "object",
      "properties": {
        "diameter": {
          "type": "number"
        },
        "length": {
          "type": "number"
        },
        "length_base": {
          "type": "number"
        },
        "light_center_length": {
          "type": "number"
        },
        "electrode_gap": {
          "type": "number"
        },
        "weight": {
          "type": "number"
        }
      }
    },
    "operating_conditions": {
      "type": "object",
      "properties": {
        "max_temp": {
          "type": "string"
        },
        "lifespan": {
          "type": "number"
        },
        "service_lifetime": {
          "type": "number"
        }
      }
    },
    "logistical_data": {
      "type": "object",
      "properties": {
        "product_code": {
          "type": "string"
        },
        "product_name": {
          "type": "string"
        },
        "packaging_unit": {
          "type": "object",
          "properties": {
            "product_code": {
              "type": "string"
            },
            "product_name": {
              "type": "string"
            },
            "length": {
              "type": "number"
            },
            "width": {
              "type": "number"
            },
            "height": {
              "type": "number"
            },
            "volume": {
              "type": "number"
            },
            "weight": {
              "type": "number"
            }
          }
        }
      }
    }
  }
}

调整了我们的方法之后,我们来看看新的输出。

{  
  "name": "XBO 1000 W/HS OFR",  
  "description": "XBO 适用于电影放映 | Xenon 短弧灯 450…10,000 瓦",  
  "applications": [  
    "经典35毫米电影胶片放映",  
    "数字电影和视频放映",  
    "建筑和效果照明('光笔')",  
    "模拟自然阳光"  
  ],  
  "benefits": [  
    "短弧光源,亮度极高,屏幕更加明亮",  
    "在整个灯泡寿命期间,色温保持恒定(6000 K)",  
    "易于维护",  
    "高电弧稳定性",  
    "热重启功能实现瞬间屏幕点亮",  
    "广泛的调光范围"  
  ],  
  "product_family": {  
    "temperature": 6000,  
    "watts_min": 450,  
    "watts_max": 10000  
  },  
  "technical_data": {  
    "nominal_current": 50,  
    "control_range_min": 30,  
    "control_range_max": 55,  
    "nominal_wattage": 1000.00,  
    "nominal_voltage": 19.0  
  },  
  "dimensions": {  
    "diameter": 40.0,  
    "length": 235.0,  
    "length_base": 205.00,  
    "light_center_length": 95.0,  
    "electrode_gap": 3.6,  
    "weight": 255.00  
  },  
  "operating_conditions": {  
    "max_temp": "230 °C",  
    "lifespan": 2000,  
    "service_lifetime": 3000  
  },  
  "logistical_data": {  
    "product_code": "4008321082114",  
    "product_name": "XBO 1000 W/HS OFR",  
    "packaging_unit": {  
      "product_code": "4008321082114",  
      "product_name": "XBO 1000 W/HS OFR",  
      "length": 410,  
      "width": 184,  
      "height": 180,  
      "volume": 13.58,  
      "weight": 819.00  
    }  
  }  
}

那不是美得很嘛!

输出详细地列出了名称和描述,将好处和用途分类成字符串清单,并确定了最小值和最大值的范围——这完全符合我们对JSON模式的期待!

从成本上来说,我们得到了一个很好的交易:大约3000个输入的token和500个输出的token加起来大约是0.22美分。这个价格意味着我们只需花一美分就能分析几乎五个完整的PDF文档——真是划算极了,特别是考虑到这些模型随着时间推移不断降价(和效率提升)这一点。

我没有计算在开发过程中解析了多少份文档,但大致是几十甚至上百份,而我的总费用不到1美元。

探索LangChain问答的潜力

尽管本文的主要焦点更侧重于PDF分析和SQL代理,但如果你想先试一试,可以试试LangChain Q&A with RAG,因为它与我们的总体目标非常吻合。

我直接给出一些代码及其输出,不做过多解释,这样我们就可以尽快进入重点内容了,随后。

我们先收集一些更多的产品数据。从Osram下载一些额外的PDF文件并把它们存放在指定的_documents_文件夹中。

然后,我们需要加入一些额外的类:

导入 os

# 从以下模块导入所需功能
from langchain.chains.question_answering 导入 load_qa_chain
from langchain_openai 导入 OpenAIEmbeddings
from langchain.vectorstores 导入 FAISS

下一步是读取并处理转换所有下载的PDF文件,然后将它们的JSON输出结果聚合到一个数组中。

    # 获取当前目录下所有的PDF文件
    pdf_files = [f for f in os.listdir('./documents') if f.endswith('.pdf')]   

    # 创建一个空列表用于存储转换后的JSON文档
    json_documents = []   

    # 遍历所有PDF文件
    for pdf_file in pdf_files:  
        # 以二进制读取模式打开文件
        with open(f'./documents/{pdf_file}', 'rb') as file:  
            pdf_reader = PdfReader(file)  

            # 存储每页内容的列表
            page_contents = []  

            # 遍历PDF中的每一页
            for page in pdf_reader.pages:  
                # 提取页面文本
                page.extract_text(visitor_text=visitor_body)  

            # 将提取的文本转换为JSON格式
            json = convert_to_json("\n".join(page_contents))  

            # 将转换后的JSON文档添加到列表中
            json_documents.append(json)

现在,我们开始使用OpenAI的文本嵌入模型(又称text-embedding-3-large),告别旧的_ada-002_版本。这个模型每1000个token只需$0.00002。所以无论导入哪个文档都无需担心费用。

此外,我们将使用FAISS Similarity Search库,使其与我们的文档内容和嵌入模型相适应,以便更好地将内容转换为向量。

FAISS 是 Meta 在 2017 年开发的(链接:https://engineering.fb.com/2017/03/29/data-infrastructure/faiss-a-library-for-efficient-similarity-search/),它是 Azure AI 搜索在 Azure 机器学习中的开源替代品,在比较嵌入向量方面,它的表现相当不错。真是个好选择!

    embeddings = OpenAIEmbeddings(model="text-embedding-3-large")  # 嵌入模型,用于生成文本嵌入向量

    docsearch = FAISS.from_texts(文本, embeddings)

打好基础后,我们开始互动环节,就是提问了。

    chain = load_qa_chain(chat, chain_type="stuff", verbose=True)  

    query = "这个XBO 1000 W/HS OFR能放进一个长350毫米、宽200毫米的盒子里吗?"  

    docs = docsearch.similarity_search(query)  
    chain.run(input_documents=docs, question=query)

看看这个基于最相关文档内容的回复:

‘实际上,XBO 1000 W/HS OFR 的尺寸是长 410 毫米,宽 184 毫米,比这个盒子的尺寸(长 350 毫米,宽 200 毫米)更大。’

嗯,差不多对。这是PDF文件。

它做得不错。现在让我们来看看更高级的应用。

SQLight 和 LangChain SQL 工具

RAG 是一种建立的技术,让用户可以和他们自己的数据对话。它在需要非结构化内部信息的场景中特别强大。

虽然RAG在处理非结构化信息方面表现很棒,当我们需要更具体的数据时,应该怎样提问呢?比方说,给我所有功率至少4000瓦特的产品。再比如,我们怎样知道装所有产品所需的货箱尺寸呢?

说到以数据为中心的查询,SQL的精确度和结构就变得很重要。

我们可以在SQLite数据库中管理我们的数据

为了管理我们的数据,需要将数据系统化地存储在一个数据库中。直观的步骤是将数据组织成SQLite中的表格,这样我们就能执行更复杂的查询以支持这些查询。

虽然这并不是强制性的,但我建议额外增加一个预备步骤:定义与我们JSON输出结构一致的类。

通过在数据插入数据库之前整合这些类,我们不仅简化了数据验证的过程,还确保数据符合预期格式。如果解析出来的 JSON 字典缺少未标记为可选的必要属性,则可能出问题了。

    import json  

    # 导入json模块,从typing模块导入Any、List、Optional类型,从dataclasses模块导入dataclass和field
    from typing import Any, List, Optional  
    from dataclasses import dataclass, field
@dataclass  
class ProductFamily:  
    watts_min: int  
    watts_max: int  
    temperature: Optional[int] = field(default=0)  

    @staticmethod  
    def from_dict(obj: Any) -> 'ProductFamily':  
        _watts_min = int(obj.get("watts_min"))  
        _watts_max = int(obj.get("watts_max"))  
        _temperature = obj.get("temperature")  
        return ProductFamily(_watts_min, _watts_max, _temperature)  # Any类型对象转换为ProductFamily  

# 省略部分  

@dataclass  
class Product:  
    name: str  
    description: str  
    benefits: List[str]  
    product_family: ProductFamily  

    @staticmethod  
    def from_dict(obj: Any) -> 'Product':  
        _name = str(obj.get("name"))  
        _description = str(obj.get("description"))  
        _benefits = obj.get("benefits")  
        _product_family = ProductFamily.from_dict(obj.get("product_family"))  # 基于产品家族对象创建ProductFamily实例  
        return Product(_name, _description, _benefits, _product_family)

建立这些类之后,我们再次查看我们的PDF文档。这次我们把它们转成JSON格式,并创建一个个_Product_对象。此外,我会把处理完的文档都挪到已处理的文件夹里。

    import traceback

    pdf_files = [f for f in os.listdir('./documents') if f.endswith('.pdf')]  
    products = []  

    for pdf_file in pdf_files:  
        json_content = None  
        try:  
            with open(f'./documents/{pdf_file}', 'rb') as file:  
                pdf_reader = PdfReader(file)  

                page_contents = []  

                for page in pdf_reader.pages:  
                    page.extract_text(visitor_text=visitor_body)  

                文档内容 = "\n".join(page_contents)  

                json_content = convert_to_json(文档内容)  
                json_data = json.loads(json_content)  

                产品 = Product.from_dict(json_data)  
                products.append(产品)  
        except Exception as e:  
            print("{filename} 出现问题: {e}".format(filename=pdf_file, e=e))  
            print(traceback.format_exc())  
            print(json_content)  
        else:  
            os.rename(f'./documents/{pdf_file}', f'./processed/{pdf_file}')

太好了,我们现在准备好了强大的产品实例列表,准备插入到数据库表中。

对于我们这个演示来说,一个 SQLite 数据库就足够了,我们将用它来创建三个表来存储我们数据集中的信息:

  • 产品信息 — 产品的基本规格(名称、描述信息、长度)。
  • 特定产品的应用列表 — 特定产品的应用列表。
  • 特定产品的优势列表 — 特定产品的益处列表。

我们将按照以下步骤来操作。

  • 初始化数据库并建立表。
  • 为每个表创建包含相关产品信息的记录。
  • 执行数据插入操作。

让我们开始创建数据库和表吧。

    import sqlite3
    if not os.path.exists('./db'):  
        os.makedirs('./db')  

    db_file = './db/products.db'  

    db_connection = sqlite3.connect(db_file)  
    db_cursor = db_connection.cursor()  

    db_cursor.execute('''CREATE TABLE IF NOT EXISTS Product  
        (name TEXT PRIMARY KEY,  
        description TEXT,  
        temperature INTEGER,  
        watts_min INTEGER,  
        watts_max INTEGER,  
        dimension_diameter REAL,  
        dimension_length REAL,  
        dimension_weight REAL,  
        packaging_length INTEGER,  
        packaging_width INTEGER,  
        packaging_height INTEGER,  
        packaging_weight REAL) WITHOUT ROWID  
    ''')  

    db_cursor.execute('''  
    CREATE TABLE IF NOT EXISTS ProductApplication (  
        id INTEGER PRIMARY KEY AUTOINCREMENT,  
        product TEXT,  
        text TEXT NOT NULL,  
        FOREIGN KEY (product) REFERENCES Product(name)  
    )  
    ''')  

    db_cursor.execute('''  
    CREATE TABLE IF NOT EXISTS ProductBenefit (  
        id INTEGER PRIMARY KEY AUTOINCREMENT,  
        product TEXT,  
        text TEXT NOT NULL,  
        FOREIGN KEY (product) REFERENCES Product(name)  
    )  
    ''')  

    db_connection.commit()

接下来创建数据对:

    products_sql_tuples = [
        (
            p.name,
            p.description,
            p.product_family.温度,
            p.product_family.最小功率,
            p.product_family.最大功率,
            p.dimensions.直径,
            p.dimensions.长度,
            p.dimensions.重量,
            p.logistical_data.packaging_unit.长度,
            p.logistical_data.packaging_unit.宽度,
            p.logistical_data.packaging_unit.高度,
            p.logistical_data.packaging_unit.重量,
        ) for p in products
    ]

    applications_sql_tuples = []
    for product in products:
        applications_sql_tuples.extend([
            (product.name, application) for application in product.applications
        ])

    benefits_sql_tuples = []
    for product in products:
        benefits_sql_tuples.extend([
            (product.name, benefit) for benefit in product.benefits
        ])

最后输入数据:

    db_cursor.executemany('''  
        REPLACE INTO Product (名称(名称), 描述(描述), 温度, 最小功率, 最大功率, 产品直径, 产品长度, 产品重量, 产品包装长度, 产品包装宽度, 产品包装高度, 产品包装重量)  
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)  
    ''', products_sql_tuple_list)  

    db_cursor.executemany('''  
        REPLACE INTO ProductApplication (产品, 文本)  
        VALUES (?, ?)  
    ''', applications_sql_tuple_list)  

    db_cursor.executemany('''  
        REPLACE INTO ProductBenefit (产品, 文本)  
        VALUES (?, ?)  
    ''', benefits_sql_tuple_list)  

    db_connection.commit()

别忘了关闭光标和连接。

db_cursor.close()  # 关闭数据库游标
db_connection.close()  # 关闭数据库连接

我们看看数据库里的表。一些字段是NULL,只是因为在产品数据表中没有这些信息。所以没问题。

但是,所有关键的信息都成功解析并转换了,所以我对这个结果真的很满意!

使用LangChain SQL代理: 查询相关信息

我们现在来到了一个实至名归的部分:向这个LangChain SQL Agent提问问题。

LangChain 的 SQL Agent 提供了一种与 SQL 数据库进行动态交互的方式。它擅长解析表结构,并根据用户提示生成 SQL 查询,这真是令人印象深刻。

使用SQL代理的主要优点有:

  • 它可以基于数据库的结构和内容(比如描述特定的表)来回答问题。
  • 它可以通过运行生成的查询、捕获错误并重新生成正确的查询来从错误中恢复。
  • 它可以根据需要多次查询数据库来回答用户的问题。
  • 它会通过仅从相关表格中获取结构信息来节省令牌。

正如你对LangChain所期望的那样,它的代码非常简洁明了,即使在后台执行复杂的操作也是如此。

为了使用SQL Agent进行查询,这次我想利用GPT-4来帮忙。与处理大量文档所需的复杂操作相比,数据量相对较小,可以轻松处理。这就是为什么在这里我可以稍微多用一点资源。

    从 langchain_community.utilities 导入 SQLDatabase as SQLDatabase  
    从 langchain.chains 导入 create_sql_query_chain as create_sql_query_chain  
    从 langchain_openai 导入 ChatOpenAI as ChatOpenAI
from langchain_community.agent_toolkits import create_sql_agent  # 从langchain_community导入create_sql_agent函数

db = SQLDatabase.from_uri("sqlite:///db/products.db")  # 初始化一个SQL数据库链接

# 创建一个ChatOpenAI对象,设置模型为"gpt-4-0125-preview",温度设置为0,这里的ChatOpenAI是用于对话的AI模型
llm = ChatOpenAI(model="gpt-4-0125-preview", temperature=0)  

# 初始化一个SQL执行器,使用create_sql_agent函数,设置agent类型为"openai-tools",verbose设置为True,以提供详细日志信息
agent_executor = create_sql_agent(llm, db=db, agent_type="openai-tools", verbose=True)

一切都准备好了,是时候问一些问题了。我们先从一个简单的问题开始,比如我们现在有多少种产品

    prompt = "你有多少个产品?"  

    result = agent_executor.invoke({"input": prompt})  # 技术函数名,无需翻译

它给出了一个完全正确的回答,包括这条SQL语句,

调用 sql_db_query,参数为 SELECT COUNT(*) AS NumberOfProducts FROM Product,返回值为 [(20,)]

数据库中有20个产品

虽然看起来很简单,但SQL Agent不仅能生成SQL查询,还能识别已存在的表,这真的非常酷。

现在,我们来增加点难度。我想找到一个适合所有产品的包装尺寸。

    prompt = "我需要找到一个适合所有产品的包装尺寸。这样的包装尺寸会是多少?"

    result = agent_executor.invoke({"input": prompt})

结果正好:

为了适应所有产品的包装需求,包装尺寸需要如下规格:

长度:605毫米 / 宽度:420毫米 / 高度:900毫米

重约7177克

这个尺寸适合数据库中所有产品的包装需求。

这是SQL Agent设计的查询语句,用来获取所需信息:

使用 sql_db_query 如下:SELECT MAX(packaging_length) AS max_length, MAX(packaging_width) AS max_width, MAX(packaging_height) AS max_height, MAX(packaging_weight) AS max_weight FROM Product.

好的,我们要更多内容!让我们要求温度最高的可能的产品信息及其应用情况——顺便说一下,这些信息存储在另一个表中。

    prompt = "提供最高温度的产品及其应用。"  

    result = agent_executor.invoke({"input": "提供最高温度的产品及其应用。"})

而且答案正确啊。

SharXS 1500W Brilliant 是一款先进的金属卤素灯,双端结构,旨在提供卓越的照明性能。

其最大温度可达6200°C,非常适合需要高强度和可靠照明的场合。

这款灯特别适合音乐会和特效照明,其中鲜艳且动态的照明至关重要。

绝对,“SharXS 1500W 亮”肯定是这个温度最高的产品:

而且,SQL Agent 更是能够找到 ProductApplication 表中的引用信息。

结论部分

像往常一样,经过调整并利用过去几年在大型语言模型方面的经验,我对这些经过调整后的技术之间的无缝配合印象深刻。

此外,GPT版本0125也运行得很好,能提供JSON格式的数据也毫无问题。很明显,OpenAI致力于提高功能调用的能力,并确保其符合创建计算机可读格式的标准场景。

我会把SQL Agent交给一个企业级生产数据库吗?只有在它确实是为了这种特定情况设计的情况下,并且仅限内部员工使用!允许一个语言模型自主执行查询在我看来这仍然显得过于冒险。

不过,对于公开场合,我愿意探索更安全的替代方案,例如发送GraphQL请求。

嗯,也许下次吧……

我希望这次的经历对你来说同样引人入胜,就像它对我一样启发人心。

玩得开心,下次再聊。

0人推荐
随时随地看视频
慕课网APP