利用人工智能生成数据库查询很酷,但是访问控制呢?
大型语言模型向世界展示了其惊人的多功能性。它最显著的能力之一是将模糊的人类语言转化为可执行代码。这不仅有助于工程师提高工作效率,还能让非技术人员完成过去需要开发人员协助才能完成的工作。
生成分析数据查询是基于LLM的代码生成最常见的应用场景之一。对于业务分析师来说,还有什么比用简单的语言提出问题,几秒钟内就能获得可视化结果更酷的呢?传统上,只有像Power BI和Google Sheets这样的大型软件才能提供类似的功能,但现在LLM已经普及了人工智能功能,降低了门槛。在本文中,我将演示一个简单的实现,并探讨一个重要但经常被忽视的主题:访问控制。
要求
假设我们有一个销售电子产品、家具和户外装备的电商网站。该网站的数据模型(已高度简化)如下:
我们设想的场景是,业务分析师想用通俗易懂的语言询问有关商店数据的分析性问题,并希望得到图表形式的反馈。例如,问题可能是:“请按类别显示产品的总销售额”。
为了增加一层保障,我们还希望确保分析师只能访问他们有权查看的数据。每个分析师用户都有一个分配的“区域”,他们只能查看该区域内的数据。
堆
以下是构建此演示所使用的框架和库:
- Remix.run作为全栈框架
- 用于与 OpenAI API 交互的Langchain
- Prisma ORM用于数据建模数据库访问
- ZenStack访问控制
- Charts.js用于创建图表
执行
您可以在本文末尾找到最终的项目代码。
一般工作流程
该实施方案的一般工作流程如下:
此演示与其他基于人工智能的数据查询项目的一个关键区别在于,我们选择生成 Prisma 查询而不是原始 SQL。这种选择有利有弊:
- ⬆️ Prisma 查询更具可移植性。我们无需处理 SQL 方言。
- ⬆️ 这样更安全,因为我们可以轻松排除写入操作,而无需复杂的 SQL 解析和验证。
- ⬆️ Prisma 查询的灵活性远不如 SQL,这往往会降低推理的复杂性。
- ⬇️ 显然,LLM 拥有比 Prisma 查询代码多得多的 SQL 训练数据。这可能在很大程度上抵消了之前的优势。
- ⬇️ Prisma 查询在分析任务方面的表达能力远不如 SQL。
我们这样做还有一个原因,您将在下一节中看到,我们将讨论访问控制。
数据建模
我们演示中使用的 Prisma 架构非常简单:
// Analyst user
model User {
id String @id @default(cuid())
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// the assigned region, this controls what data the user can access
region String
}
// Product
model Product {
id String @id @default(cuid())
name String
category String
price Float
orderItems OrderItem[]
}
// Order
model Order {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
items OrderItem[]
region String
}
// Order item
model OrderItem {
id String @id @default(cuid())
quantity Int
productId String
orderId String
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
}
// Helper view for joining orders, order items, and products
view OrderItemDetail {
id String @id
createdAt DateTime
updatedAt DateTime
region String
product String
category String
unitPrice Float
quantity Int
subtotal Float
}
快捷工程
与所有涉及语言学习模型(LLM)的应用一样,最具挑战性的部分在于设计合适的提示。在我们的演示中,我们需要设计两个提示,一个用于将人类语言转换为 Prisma 查询,另一个用于将查询结果数据集转换为 Charts.js 配置。我们选择的模型是 OpenAI 的“gpt-4”。
一些提示明确强调要确保 GPT-4 输出正确的 JSON 数据。OpenAI 今天发布了 GPT-4-turbo,它新增了JSON 模式。希望这能从根本上解决问题!
1. 提示生成 Prisma 查询
与 LLM 的第一次交互是将自然语言问题转换为 Prisma 查询。
系统消息:
You are a senior developer who knows Prisma ORM very well.
用户留言:
Given the following Prisma schema:
{schema}
The "OrderItemDetails" view contains order items with more fields joined from "Order" and "Product". You can use it for aggregations.
When you do aggregation, use "_sum", "_avg", "_min", "_max" to aggregate numeric fields, use "_count" to count the number of rows.
An aggregation can be written like:
{
by: ['field'],
_sum: {
metricField: true,
},
}
Using Prisma APIs including "findMany", "aggregate", and "groupBy", compute a Prisma query for the following question:
{query}
However, don't return the Prisma function call, instead, transform it into a JSON object like the following:
{
"model": "Order",
"api": "findMany",
"arguments": { ... }
}
The "model" field contains the name of the model to query, like "Order", "OrderItemDetails".
The "api" field contains the Prisma API to use, like "groupBy", "aggregate".
The "arguments" field contains the arguments to pass to the Prisma API, like "{ by: ['category'] }".
Return only the JSON object. Don't provide any other text.
LLM 的输出结果如下:
{
"model": "OrderItemDetail",
"api": "groupBy",
"arguments": {
"by": ["category"],
"_sum": {
"subtotal": true
}
}
}
然后我们可以将其转换为 Prisma 查询语句,例如:
prisma.orderItemDetail.groupBy({
by: ['category'],
_sum: {
subtotal: true,
},
});
并返回类似这样的结果数据集:
[
{
"_sum": {
"subtotal": 1099
},
"category": "Electronics"
},
{
"_sum": {
"subtotal": 2199
},
"category": "Furniture"
},
{
"_sum": {
"subtotal": 307
},
"category": "Outdoor"
}
]
为什么不直接生成整个 Prisma 调用呢?
您可能已经注意到,我们强制 LLM 为 Prisma 查询生成结构化的 JSON,而不是直接调用查询函数。原因在于,您永远不应该信任并执行来自外部来源的代码,人工智能也不例外。通过生成 JSON 而不是代码,我们可以进一步检查、过滤(例如,只允许“读取”方法调用),并限制执行的代码。
2. 提示生成 Charts.js 配置
与 LLM 的第二次交互的目标是将查询结果数据集转换为 Charts.js 配置对象。
系统消息:
You are a senior developer who knows Charts.js very well.
用户留言:
Generate a bar chart using Charts.js syntax for the following JSON data:
{data}
Use the chart configuration that you feel is most appropriate for the data.
Return only the Charts.js input object converted to JSON format.
Make sure keys and string values are double quoted.
Don't call Charts.js constructor. Don't output anything else.
LLM 的输出如下所示,我们可以将其直接传递给 Charts.js:
{
"type": "bar",
"data": {
"labels": ["Electronics", "Furniture", "Outdoor"],
"datasets": [
{
"label": "Subtotal",
"data": [1099, 2199, 307],
"backgroundColor": ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)","rgba(255, 206, 86, 0.2)"],
"borderColor": ["rgba(255, 99, 132, 1)","rgba(54, 162, 235, 1)","rgba(255, 206, 86, 1)"],
"borderWidth": 1
}
]
},
"options": {
"scales": {
"yAxes": [{
"ticks": {
"beginAtZero": true
}
}]
}
}
}
不妨一试
所有部件连接完毕后,我们现在可以进行测试了。以下是一个简单的演示:
由于 OpenAI API 的延迟(录制过程是快进的),它的速度相当慢,而且仍然无法可靠地处理许多情况。但它仍然很酷,不是吗?
门禁控制方面呢?
回顾我们的需求,我们会发现一个重要环节仍然缺失:访问控制。分析师应该只能看到其分配区域的数据。我们如何确保生成的 Prisma 查询只返回来自正确区域的数据?我们目前的实现方式是将所有数据都包含在查询结果中。
一般来说,如果要对基于LLM的查询生成实施访问控制,有几种可能的解决方案:
- 如果你使用 PostgreSQL,你可以设置行级安全性并将其与你的用户系统连接起来,但这并不简单。
- 如果使用 SQL 生成,可以使用 SQL 解析器对生成的查询进行后处理,并注入额外的筛选条件。
- 如果您像我们这里一样生成 Prisma 查询,您可以将额外的过滤条件注入到生成的查询对象中。更棒的是:ZenStack可以自动为您完成这项工作。
ZenStack 是一个基于 Prisma 构建的工具包。它为 Prisma 提供了许多强大的扩展功能,与我们讨论相关的扩展功能包括:
- 允许您定义访问控制策略的架构扩展
- 自动强制执行访问控制的运行时扩展
为了充分利用 ZenStack,我们将使用名为 ZModel 的建模语言(而不是 Prisma schema)来集中定义数据模型和访问策略。它的样子如下:
model Product {
...
// 🔐 login is required to read products
@@allow('read', auth() != null)
}
model Order {
...
// 🔐 analysts can only read orders of their assigned region
@@allow('read', auth().region == region)
}
model OrderItem {
...
// 🔐 analysts can only read order items of their assigned region
@@allow('read', auth().region == order.region)
}
view OrderItemDetail {
...
// 🔐 analysts can only read order details of their assigned region
@@allow('read', auth().region == region)
}
几点简要说明:
- 数据建模与 Prisma Schema Language 完全相同。
- 该
@@allow属性用于定义访问控制策略 - 除非明确允许,否则默认情况下所有访问均被拒绝。
- 该
auth()函数返回会话中的当前用户
在运行时,当我们需要使用 Prisma 查询数据时,我们可以用 ZenStack 提供的增强版 PrismaClient 替换 PrismaClient,该版本会自动将访问控制条件注入到查询对象中。
// get user id from session
const userId = await requireUserId(request);
// fetch the user
const user = await prisma.user.findUniqueOrThrow({
where: { id: userId },
select: { id: true, region: true },
});
// create an enhanced PrismaClient
const db = enhance(prisma, { user });
// use the enhanced PrismaClient to query data, e.g.:
// db.orderItemDetail.groupBy({ ... })
做出这些更改后,我们可以看到,不同的分析师对同一个问题会得出不同的结果:
项目代码
您可以在https://github.com/ymc9/llm-data-query找到已完成的项目代码。
挑战
通过结合合适的工具和LLM的强大功能,我们以出乎意料的轻松方式构建了一个简洁高效的商业智能系统。这在生成式人工智能出现之前是不可想象的。然而,要使其成为一个可用于生产环境的系统,仍然有很多挑战需要克服:
- GPT-4 的生成速度太慢,而 GPT-3.5 的生成质量又不够好。可能需要进行微调才能同时提高速度和质量。
- 幻觉是一个主要问题。LLM 可以创建 Prisma 不支持的查询语法。同样,通过微调(或更及时的工程改进)可以改善这种情况。
- Prisma 的查询语法过于局限,无法胜任分析任务。GitHub 上已经有很多相关的 issue,但目前尚不清楚何时能够实现。
- 最后,LLM固有的非确定性使得重复提出同一问题时很难获得一致的结果,这可能会让用户感到困惑。
使用 LLM 构建现实世界的产品仍然是一个充满挑战的过程,需要我们发明以前不存在的模式和技巧。
ZenStack是我们开源的 TypeScript 工具包,旨在帮助您更快、更智能、更高效地构建高质量、可扩展的应用程序。它将数据模型、访问策略和验证规则集中到一个基于 Prisma 的声明式模式中,非常适合 AI 增强型开发。立即开始将ZenStack集成到您现有的技术栈中吧!
文章来源:https://dev.to/zenstack/using-ai-to-generate-database-query-is-cool-but-what-about-access-control-cc



