我们如何为大型语言模型设计语义引擎?大型语言模型架构中语义层的主干。

趋势 AI Agent 的出现彻底改变了商业智能和数据管理的格局。在不久的将来,多个……

Howard Chi
Wren AI 联合创始人
更新于
2024 年 8 月 20 日
2024 年 11 月 25 日
12
分钟阅读
发布于
2024 年 5 月 24 日

趋势 AI Agent 的出现彻底改变了商业智能和数据管理的格局。在不久的将来,将部署多个 AI Agent 来利用和解释存储在数据库和数据仓库中的海量内部知识。为了促进这一点,语义引擎至关重要。该引擎将数据模式映射到相关的业务上下文,使 AI Agent 能够理解数据的底层语义。通过提供对业务上下文的结构化理解,语义引擎将使 AI Agent 能够生成针对特定业务需求的准确 SQL 查询,确保精确和上下文感知的数据检索。

大型语言模型在数据结构方面的问题?

使 AI Agent 能够直接与数据库对话。底层技术提供了一个将自然语言转换为 SQL 并查询数据库的接口。

然而,将模式与数据库中的上下文进行映射并非易事。仅仅存储模式和元数据是不够的。我们需要更深入地理解和处理数据。

缺乏语义上下文

当您在数据库之上直接启用大型语言模型时,您可以依赖数据库中已有的 DDL 信息来帮助大型语言模型学习您的数据库结构和类型。您还可以根据提供的 DDL 添加标题和描述,以帮助大型语言模型理解每个表和列的定义。

为了从大型语言模型获得最佳性能和准确性,仅仅拥有 DDL 和模式定义是不够的。大型语言模型需要理解各种实体之间的关系,并理解组织内部使用的计算公式。提供计算、指标和关系(连接路径)等附加信息对于帮助大型语言模型理解这些方面至关重要。

大型语言模型与语义之间缺乏接口定义

如前所述,拥有语义上下文至关重要,它允许大型语言模型理解计算、指标、关系等的复杂性。我们需要定义来概括我们面临的以下主题。

计算

预训练的大型语言模型对每个术语都有自己的定义,这与每家公司定义自己的 KPI 或公式的方式不同。计算是我们提供定义的地方,例如毛利润率等于(收入 — 销售成本)/ 收入。大型语言模型可能已经足够强大,可以理解常见的 KPI,例如毛利润率、净利润率、CLTV 等。

然而,在现实世界中,列通常很混乱,收入可能被设置为列名 rev ,并且我们可能会看到 rev1pre_rev_1rev2 等。如果没有语义上下文,大型语言模型无法理解它们的含义。

指标

“切片和切块”(Slice and dice)是一种数据分析技术,特别是在多维数据的上下文中,用于从不同角度分解和查看数据。这种方法有助于更详细地探索和分析数据。

例如销售指标

  • 总销售额:特定时期内产生的总收入。
  • 按区域划分的销售额:按地理区域细分的销售数据。
  • 按产品划分的销售额:按单个产品或产品类别细分的销售数据。
  • 按渠道划分的销售额:按不同销售渠道(例如,在线、零售、批发)细分的销售数据。

使用客户指标的另一个例子

  • 客户人口统计信息:按年龄、性别、位置等细分的客户数据。
  • 客户细分:根据行为、购买历史和偏好对客户进行分类。
  • 客户获取:特定时期内获得的新客户数量。
  • 客户流失率:停止与公司业务往来的客户百分比。

语义关系

语义关系与主键和外键不同,尽管它们在数据库和数据管理的上下文中是相关的概念。

语义关系是指不同数据片段之间的有意义的联系,通常基于它们在现实世界中的关系。这些关系描述了数据元素在概念上如何相互关联,不仅仅是主键和外键提供的结构链接;例如,客户表和订单表之间的语义关系可以描述为“一个客户可以下多个订单”。这捕捉了关系 beyond 只是技术连接在现实世界中的含义。

另一方面,主键和外键用于强制数据完整性并在数据库模式级别建立关系。语义关系用于在更广泛的上下文中描述和理解数据实体如何相关,您还可以定义在主键和外键设置中不可用的一对多、多对多、一对一关系。

将大型语言模型与异构数据源集成的挑战

SQL 生成性能不稳定

连接多个数据源并期望大型语言模型无缝处理不同的 SQL 方言带来了一个重大挑战:确保跨不同来源的性能一致性。随着数据源数量的增加,这一挑战变得更加突出。一致性是建立对 AI 系统信任的关键。确保稳定性能与您的 AI 解决方案的整体可用性和可靠性直接相关。

访问控制不一致

不同的数据源通常自带其访问控制机制。当这些源直接连接时,维护一致的数据策略变得困难,而这对于大规模数据团队协作至关重要。为了解决这个问题,建立一个中央治理层来管理所有大型语言模型用例的访问控制至关重要。该层确保数据策略得到统一执行,从而增强整个组织的安全性和合规性。

语义层的出现

直接连接到多个数据源在一致性和性能方面带来了重大挑战。更有效的方法是为大型语言模型用例实现一个语义层。

什么是语义层?

语义架构背后的核心概念是本体(ontology)。本体是对一个领域的正式表示,包含表示实体和属性的类以及它们与其他实体之间的关系。

通过为数据集领域提供本体,大型语言模型不仅能理解如何呈现数据,还能理解数据代表什么。这使得系统能够处理甚至推断出数据集中未明确声明的新信息。

本体示例

语义层的好处

语义层不仅仅帮助 AI Agent 理解不同域、实体和关系之间的语义。它还为 AI Agent 提供了框架,使其能够

  • 使用正确的公式进行计算
  • 为连接路径和指标提供上下文
  • 提供标准化的 SQL 层,确保跨不同数据源的一致性。
  • 在运行时应用封装的业务逻辑并管理实体之间的复杂关系。

因此,实现语义层通过弥合不同数据源与复杂业务上下文之间的差距,增强了 AI Agent 提供准确和一致见解的能力。

Wren Engine — 面向大型语言模型的语义引擎

这就是为什么我们设计了 Wren Engine,这款面向大型语言模型的语义引擎,旨在解决我们提出的挑战。

我们使用 Wren Engine 定义了一种“建模定义语言”(MDL),为大型语言模型提供上下文和适当的语义元数据,并且该引擎可以使用 MDL 根据不同的用户画像和语义数据建模方法重写 SQL。借助该引擎,可以在其之上构建解决方案,例如通常位于语义层中的访问控制和治理功能。

语义数据建模

本体的基本概念包括设计一个图结构的元数据和数据表示,通常称为知识图谱。使用 Wren Engine,您可以在这种基于图的架构中定义您的数据模型和指标。这使您可以指定不同模型中的列如何相关以及这些关系意味着什么。这种结构化定义不仅阐明了数据关系,还增强了准确高效地重写 SQL 查询的能力。

语义命名和描述

在 MDL 中,您可以轻松地在任何模型、列、视图以及关系中定义语义命名和描述。通过语义定义,您可以帮助大型语言模型理解数据结构的语义含义。

{
    "name": "customers",
    "columns": [
        {
            "name": "City",
            "type": "VARCHAR",
            "isCalculated": 0,
            "notNull": 0,
            "expression": "",
            // semantic properties, such as description, display name, and alias, could be added here.
            "properties": {
            "description": "The Customer City, where the customer company is located. Also called \"customer segment\".",
            "displayName": "City"
            }
        },
        {
            // semantic naming
            "name": "UserId",
            "type": "VARCHAR",
            "isCalculated": 0,
            "notNull": 0,
            "expression": "Id",
            "properties": {
            "description": "A unique identifier for each customer in the data model.",
            "displayName": "Id"
            }
        }
    ],
    "refSql": "select * from main.customers",
    "cached": 0,
    "refreshTime": null,
    // semantic properties, such as description, display name, and alias, could be added here.
    "properties": {
        "schema": "main",
        "catalog": "memory",
        "description": "A table of customers who have made purchases, including their city",
        "displayName": "customers"
    },
    "primaryKey": "Id"
},

支持带有关系和计算的运行时 SQL 重写

使用 Wren Engine,您可以使用“建模定义语言”设计语义表示。我们还在我们的 AI 应用程序 Wren AI 中构建了一个用户界面,该应用程序也在此处开源。在 Wren AI 背后,不同实体之间的关系,以及在一对多、多对一、一对一中的声明都可以定义。

Wren UI

下面是一个如何定义关系的简单示例

{
    "name" : "CustomerOrders",
    "models" : [ "Customer", "Orders" ],
    "joinType" : "ONE_TO_MANY",  // it's a one-to-many architecture
    "condition" : "Customer.custkey = Orders.custkey"
}

关系由以下组成

  • name:关系名称。
  • models:与此关系关联的模型。Wren Engine 在一个关系中仅关联 2 个模型。
  • joinType:关系的类型。通常,2 个模型之间有 4 种关系:ONE_TO_ONE (1–1)、ONE_TO_MANY (1-M)、MANY_TO_ONE (M-1)、MANY_TO_MANY (M-M)。
  • condition:两个模型之间的连接条件。Wren Engine 在 SQL 生成过程中充当连接条件。

在模型中,您还可以将自定义计算添加到计算(表达式)中。

{
    "name": "Customer",
    "refSql": "select * from tpch.customer",
    "columns": [
        {
            "name": "custkey",
            "type": "integer",
            "expression": "c_orderkey"
        },
        {
            "name": "name",
            "type": "varchar",
            "expression": "c_name"
        },
        {
            "name": "orders",
            "type": "Orders",
            "relationship": "CustomerOrders"
        },
        {
            "name": "consumption",
            "type": "integer",
            "isCalculated": true,
            "expression": "sum(orders.totalprice)" // define expression
        }
    ],
    "primaryKey": "custkey"
},

支持可重用计算和函数式宏

关于计算

Wren Engine 提供计算字段来在模型中定义计算。计算可以使用同一模型中定义的列,或通过关系使用另一模型中的相关列。通常,一个通用指标与许多不同的表相关。通过计算字段,可以轻松定义一个在不同模型之间交互的通用指标。

例如,下面是一个名为 orders 的模型,包含 3 列。为了增强模型,我们可能希望添加一个名为 customer_last_month_orders_price 的列,以了解每个客户的增长情况。我们可以定义一个计算字段,如下所示

"columns": [
    {
        "name": "orderkey",
        "type": "INTEGER"
    },
    {
        "name": "custkey",
        "type": "INTEGER"
    }
    {
        "name": "price",
        "type": "INTEGER"
    },
    {
        "name": "purchasetimestamp",
        "type": "TIMESTAMP"
    },
    {
        "name": "customer_last_month_orders_price",
        "type": "INTEGER",
        "isCalculated": "true",
        // column
        "expression": "lag(price) over (partition by custkey order by date_trunc('YEAR', purchasetimestamp), 0, 0)"
    }
]

关于宏函数

宏是建模定义语言(MDL)的模板功能。它有助于简化您的 MDL 或集中一些关键概念。宏由 JinJava 实现,JinJava 是 JVM 中的一个模板引擎,遵循 Jinja 规范。使用宏,您可以定义一个模板来使用某些参数,并在任何表达式中使用它。

在以下场景中,twdToUsd 代表整个 MDL 中的一个通用概念。相反,revenue 和 totalpriceUsd 则体现了特定于各个模型的部分概念。

"macros": [
    {
        "name": "twdToUsd",
        "definition": "(twd: Expression) => twd / 30" // Macro definition
    }
],
"models": [
    {
        "name": "Orders",
        "columns": [
            {
                "name": "totalprice",
                "type": "double"
            }
            {
                "name": "totalpriceUsd",
                "expression": "{{ twdToUsd('totalprice') }}" // reuse Macro function
            }
        ]
    },
    {
        "name": "Customer",
        "columns": [
            {
                "name": "revenue",
                "isCalculated": true,
                "expression": "{{ twdToUsd('sum(orders.totalprice)') }}" // reuse Macro function
            },
            {
                "name": "orders",
                "Type": "Orders",
                "relationship": "OrdersCustomer",
            }
        ]
    }
]

支持标准 SQL 语法

Wren Engine 内置了 SQL 处理器和转译器,通过 Wren Engine,我们将解析查询到 Wren Engine 的 SQL,然后从符合标准 ANSI SQL 的 WrenSQL 语法中解包并翻译成不同的方言,例如 BigQuery、PostgreSQL、Snowflake 等。

Wren Engine 架构

下面是一个简单的示例,在这里您定义了数据集的 MDL,当您提交 SQL 时,所有的关系、计算和指标都会被转译为目标方言特定的 SQL。

这是一个 MDL 文件示例(请在 Gist 查看)

如果您提交如下查询

SELECT * FROM orders

Wren Engine 将根据 MDL 定义将 Wren SQL 转换为如下方言特定的 SQL。

WITH
    "order_items" AS (
    SELECT
        "order_items"."FreightValue" "FreightValue"
    , "order_items"."ItemNumber" "ItemNumber"
    , "order_items"."OrderId" "OrderId"
    , "order_items"."Price" "Price"
    , "order_items"."ProductId" "ProductId"
    , "order_items"."ShippingLimitDate" "ShippingLimitDate"
    FROM
        (
        SELECT
        "order_items"."FreightValue" "FreightValue"
        , "order_items"."ItemNumber" "ItemNumber"
        , "order_items"."OrderId" "OrderId"
        , "order_items"."Price" "Price"
        , "order_items"."ProductId" "ProductId"
        , "order_items"."ShippingLimitDate" "ShippingLimitDate"
        FROM
        (
            SELECT
            "FreightValue" "FreightValue"
            , "ItemNumber" "ItemNumber"
            , "OrderId" "OrderId"
            , "Price" "Price"
            , "ProductId" "ProductId"
            , "ShippingLimitDate" "ShippingLimitDate"
            FROM
            (
            SELECT *
            FROM
                main.order_items
            )  "order_items"
        )  "order_items"
    )  "order_items"
) 
, "payments" AS (
    SELECT
        "payments"."Installments" "Installments"
    , "payments"."OrderId" "OrderId"
    , "payments"."Sequential" "Sequential"
    , "payments"."Type" "Type"
    , "payments"."Value" "Value"
    FROM
        (
        SELECT
        "payments"."Installments" "Installments"
        , "payments"."OrderId" "OrderId"
        , "payments"."Sequential" "Sequential"
        , "payments"."Type" "Type"
        , "payments"."Value" "Value"
        FROM
        (
            SELECT
            "Installments" "Installments"
            , "OrderId" "OrderId"
            , "Sequential" "Sequential"
            , "Type" "Type"
            , "Value" "Value"
            FROM
            (
            SELECT *
            FROM
                main.payments
            )  "payments"
        )  "payments"
    )  "payments"
) 
, "orders" AS (
    SELECT
        "orders"."ApprovedTimestamp" "ApprovedTimestamp"
    , "orders"."CustomerId" "CustomerId"
    , "orders"."DeliveredCarrierDate" "DeliveredCarrierDate"
    , "orders"."DeliveredCustomerDate" "DeliveredCustomerDate"
    , "orders"."EstimatedDeliveryDate" "EstimatedDeliveryDate"
    , "orders"."OrderId" "OrderId"
    , "orders"."PurchaseTimestamp" "PurchaseTimestamp"
    , "orders"."Status" "Status"
    , "RevenueA"."RevenueA" "RevenueA"
    , "Sales"."Sales" "Sales"
    FROM
        (((
        SELECT
        "orders"."ApprovedTimestamp" "ApprovedTimestamp"
        , "orders"."CustomerId" "CustomerId"
        , "orders"."DeliveredCarrierDate" "DeliveredCarrierDate"
        , "orders"."DeliveredCustomerDate" "DeliveredCustomerDate"
        , "orders"."EstimatedDeliveryDate" "EstimatedDeliveryDate"
        , "orders"."OrderId" "OrderId"
        , "orders"."PurchaseTimestamp" "PurchaseTimestamp"
        , "orders"."Status" "Status"
        FROM
        (
            SELECT
            "ApprovedTimestamp" "ApprovedTimestamp"
            , "CustomerId" "CustomerId"
            , "DeliveredCarrierDate" "DeliveredCarrierDate"
            , "DeliveredCustomerDate" "DeliveredCustomerDate"
            , "EstimatedDeliveryDate" "EstimatedDeliveryDate"
            , "OrderId" "OrderId"
            , "PurchaseTimestamp" "PurchaseTimestamp"
            , "Status" "Status"
            FROM
            (
            SELECT *
            FROM
                main.orders
            )  "orders"
        )  "orders"
    )  "orders"
    LEFT JOIN (
        SELECT
        "orders"."OrderId"
        , sum("order_items"."Price") "RevenueA"
        FROM
        ((
            SELECT
            "ApprovedTimestamp" "ApprovedTimestamp"
            , "CustomerId" "CustomerId"
            , "DeliveredCarrierDate" "DeliveredCarrierDate"
            , "DeliveredCustomerDate" "DeliveredCustomerDate"
            , "EstimatedDeliveryDate" "EstimatedDeliveryDate"
            , "OrderId" "OrderId"
            , "PurchaseTimestamp" "PurchaseTimestamp"
            , "Status" "Status"
            FROM
            (
            SELECT *
            FROM
                main.orders
            )  "orders"
        )  "orders"
        LEFT JOIN "order_items" ON ("orders"."OrderId" = "order_items"."OrderId"))
        GROUP BY 1
    )  "RevenueA" ON ("orders"."OrderId" = "RevenueA"."OrderId"))
    LEFT JOIN (
        SELECT
        "orders"."OrderId"
        , sum("payments"."Value") "Sales"
        FROM
        ((
            SELECT
            "ApprovedTimestamp" "ApprovedTimestamp"
            , "CustomerId" "CustomerId"
            , "DeliveredCarrierDate" "DeliveredCarrierDate"
            , "DeliveredCustomerDate" "DeliveredCustomerDate"
            , "EstimatedDeliveryDate" "EstimatedDeliveryDate"
            , "OrderId" "OrderId"
            , "PurchaseTimestamp" "PurchaseTimestamp"
            , "Status" "Status"
            FROM
            (
            SELECT *
            FROM
                main.orders
            )  "orders"
        )  "orders"
        LEFT JOIN "payments" ON ("payments"."OrderId" = "orders"."OrderId"))
        GROUP BY 1
    )  "Sales" ON ("orders"."OrderId" = "Sales"."OrderId"))
) 
SELECT *
FROM
    orders

跨来源的一致访问控制

由于访问控制机制不同,管理各种数据源的访问控制可能具有挑战性。Wren Engine 还旨在解决以下问题,例如

  1. 定义数据策略:确保所有数据源遵守相同的安全和访问协议。
  2. 统一身份验证和授权:通过将不同的数据源集成到单个引擎下,身份验证和授权过程变得简化。这种统一性降低了未经授权访问的风险,并确保用户在所有数据源中拥有一致的访问权限。
  3. 基于角色的访问控制 (RBAC):实现 RBAC,其中访问权限是基于角色而不是单个用户分配的。

我们将在项目实施时分享更多细节!

开放且独立的架构

Wren Engine 是开源的,它被设计为一个独立的语义引擎,您可以轻松地与任何 AI Agent 集成,您可以将其用作语义层的一般语义引擎。

Wren Engine 工作流程

最后总结

Wren Engine 的使命是作为大型语言模型的语义引擎,为语义层提供主干,并将业务上下文传递给 BI 和大型语言模型。我们相信构建一个开放社区,以确保引擎与任何应用程序和数据源的兼容性。我们还致力于提供一种架构,允许开发人员在其之上自由构建 AI Agent。

如果您对 Wren AI 和 Wren Engine 感兴趣,请查看我们的 GitHub。它们都是开源的

立即使用 AI 赋能您的数据?!

谢谢您!您的提交已收到!
糟糕!提交表单时出了点问题。