导读:Jev 最近很火,它擅长的语义判断如果直接放进数据库,能否让分类、筛选这类任务留在 SQL 里完成?本文以 Databend 的 Python UDF 集成为例,走通接入和查询过程,也看看远程模型调用的性能、成本与数据边界。
最近Jev 突然火起来了,稍微试了下做分类任务效果非常不错,成本也低廉。所以给 Databend 的 Python UDF 写了一个 Jev 集成,顺手把过程和想法整理成这篇文章。
先说 Jev 是个什么东西,再说为什么我觉得它放进数据库里很合适,最后是怎么在 Databend 里跑起来,以及我在实际用的过程中踩到的一些点。
先聊聊 Jev
Jev 是 TypeSafe 发布的第一个模型,他们管这一类模型叫 System One。
这个名字借的是卡尼曼《思考,快与慢》里的"系统一":快、直觉式的判断,而不是慢慢推演。名字取得挺贴切,因为 Jev 的设计目标确实不是写东西,而是做决定。
具体来说,你给它两样东西:一份材料(一段文本、一条 JSON、一封邮件、一张工单),和一个事先定好答案类型的问题。它给回来一个结构化的结果,常见的任务有 文章音视频标签分类,客户情绪值提取,穿衣订餐出行等决策,实时游戏决策等。
问题目前有三种:
-
Noul:这个陈述成立的概率是多少。回来一个 0 到 1 的数。
-
Choice:从你给的几个选项里挑一个。回来选了哪个,加上每个选项的概率。
-
Score:在一组有顺序的等级上打分。回来一个加权位置,加上各等级的概率。
比如你把一封邮件和三个类别丢给它,它不会回一段"我觉得这大概是一封账单相关的邮件,因为……",而是回这样一个东西:
{
"choice": "billing",
"probabilities": {
"billing": 0.91,
"technical": 0.07,
"sales": 0.02
},
"confidence": 0.82
}
拿到之后代码可以直接用,不用再从一段话里把答案抠出来。
我觉得它好在哪
我用过不少"让大模型做分类"的方案,通常的做法是写一段 prompt,要求模型只输出 JSON,然后解析、校验,格式不对就重试。能用,但总觉得别扭:明明只想要一个字段,却要等它一个 token 一个 token 地把 JSON 写完。
Jev 从根上绕开了这件事。答案空间是你事先定死的,它只在里面选,所以格式上不会出岔子,也不需要生成那一大段文本。这带来几个直接的好处。
第一是快。官方说大多数请求在一百毫秒上下。我实际从 Databend 这边走一圈,加上网络,也就几百毫秒,跟以前动辄几秒的体验完全不是一回事。
第二是便宜。因为它不产生输出 token,账单基本只看输入。批量给几百万行数据打标签这种事,以前得先算算钱,现在心里负担小多了。
第三是带概率。这点我觉得比前两点更重要。它不只告诉你选了什么,还告诉你有多确定。这意味着你可以在 SQL 或代码里设阈值:把握大的自动过,把握小的转人工。以前用生成模型,想拿到一个靠谱的置信度其实挺难的。
第四是它不抢流程的控制权。Jev 不是 Agent,不会自己决定下一步做什么。你的代码或 SQL 还是主干,它只是在需要判断的地方被调用一下,然后把结果交回来。对数据库场景来说,这个性格正合适。
也说说它不擅长的
写到这里得停一下,免得读者以为这是个万能的东西。
Jev 不会生成内容。让它写文章、写代码、做摘要、回邮件,都做不到。它也不会给出你没预先定义的答案,选项里没有的东西它不会凭空造出来。所以但凡需要开放式输出的场景,还是得用 GPT、Claude 这类模型。
它也不适合长链条推理。如果一个任务要先推出 A,根据 A 推 B,再综合出 C,这种一步接一步的活它做不好。独立的问题可以一起问,互相依赖的问题得你在代码里拆开串起来。
还有一点要说清楚:结构可靠不等于判断正确。它保证的是输出一定落在你给的选项里,不保证选的那个就是对的。官方说的"calibrated",指的是它说 80% 把握的那批预测,整体上大概真有 80% 是对的;这是统计意义上的准,不是每一条都准。所以高风险的动作,还是要配上规则校验和人工确认。
另外,选项设计对结果影响很大。类别之间意思重叠、问题写得含糊、输入里缺关键信息,都会让结果变差。用它不等于可以省掉设计和调试。
最后是现实层面的:它现在走托管 API,权重不公开,只处理文本(以及文本组成的 JSON 和数组),图片音频都不支持。数据要出数据库、要走网络、要付费,这些都得考虑。
总结一下我的理解:开放的事情交给会写字的模型,有界的判断交给 Jev,确定的逻辑和最终的控制权留在代码里。
为什么想把它放进 Databend
数据库里其实堆着大量需要"看一眼就能判断,但规则写不出来"的东西:客户反馈、工单、评论、日志、商品描述、销售线索。
以前处理这些,流程一般是:SQL 查出来,导到脚本里,调模型,再写回去。每一步都不难,但串起来就多了很多胶水代码和调度任务。
Databend 支持 Python UDF Server,而 Jev 的输入输出又天然是结构化的,两边接起来很顺。接好之后,语义判断就变成了一个普通的 SQL 函数:
SELECT *
FROM customer_feedback
WHERE jev(
OBJECT_CONSTRUCT('rating', rating, 'feedback', feedback),
'The customer reports a serious product failure that requires immediate support'
);
这还是一条 SQL,只是 WHERE 后面那个条件,是用一句话描述的。
扫描、过滤、连接、聚合、写回,这些 Databend 本来就擅长;判断"这条反馈到底在说什么",交给 Jev。分工挺清楚的。
图 1:Databend × Jev 集成架构。Databend 保留数据执行和流程控制权,Jev 负责有界的语义判断。
下面就是怎么把它跑起来。
这次做了五个函数
| 函数 | 返回值 | 用途 |
|---|---|---|
jev | BOOLEAN | 这条记录是否满足某个用自然语言描述的条件 |
jev_prob | DOUBLE | 条件成立的概率 |
jev_choice | VARCHAR | 从几个候选类别里选最合适的一个 |
jev_score | DOUBLE | 在一组有序等级上的位置 |
jev_eval | VARIANT | 完整结果,包括概率分布和 confidence |
前四个是为了在 SQL 里用起来顺手,返回的都是标量。最后一个 jev_eval 把 Jev 的原始回应整个返回来,适合需要看概率分布或者做人工复核的场景。代码在这里:python/example/jev.py
把服务跑起来
启动 UDF Server
先准备 TypeSafe 的 API Key,放到环境变量里:
export TYPESAFE_API_KEY='<your-api-key>'
别把 Key 写进代码、SQL 或者提交到仓库里。
然后克隆 databend-udf,进 Python 目录启动:
cd python
uv run python example/jev.py
默认监听:
UDF Server: 0.0.0.0:8815
Metrics: 0.0.0.0:8816
让 Databend 认识这个地址
Databend 的 Query 节点默认不允许连任意 UDF Server,需要在配置里打开并加白名单:
[query]
enable_udf_server = true
udf_server_allow_list = ["http://127.0.0.1:8815"]
udf_server_allow_insecure = true
改完重启 Query。
这是本机开发的配置。生产环境应该用内网域名加 TLS,白名单也要收紧。如果 Databend 跑在容器里,127.0.0.1 指的是容器自己,得换成 Query 节点真正能访问到的地址。
注册函数
CREATE OR REPLACE FUNCTION jev(VARIANT, VARCHAR)
RETURNS BOOLEAN
LANGUAGE python
HANDLER = 'jev'
ADDRESS = 'http://127.0.0.1:8815';
CREATE OR REPLACE FUNCTION jev_prob(VARIANT, VARCHAR)
RETURNS DOUBLE
LANGUAGE python
HANDLER = 'jev_prob'
ADDRESS = 'http://127.0.0.1:8815';
CREATE OR REPLACE FUNCTION jev_choice(
VARIANT,
VARCHAR,
ARRAY(VARCHAR NOT NULL)
)
RETURNS VARCHAR
LANGUAGE python
HANDLER = 'jev_choice'
ADDRESS = 'http://127.0.0.1:8815';
CREATE OR REPLACE FUNCTION jev_score(
VARIANT,
VARCHAR,
ARRAY(VARCHAR NOT NULL)
)
RETURNS DOUBLE
LANGUAGE python
HANDLER = 'jev_score'
ADDRESS = 'http://127.0.0.1:8815';
CREATE OR REPLACE FUNCTION jev_eval(
VARIANT,
VARCHAR,
VARCHAR,
ARRAY(VARCHAR NOT NULL)
)
RETURNS VARIANT
LANGUAGE python
HANDLER = 'jev_eval'
ADDRESS = 'http://127.0.0.1:8815';
跑一下 SHOW USER FUNCTIONS; 能看到这五个,就说明接上了。
图 2:一次 jev_* 调用的完整时序。确定性过滤留在 Databend,远程判断由 UDF Server 合批后提交给 Jev。
拿一张客户反馈表试试
建一张简单的表:
CREATE OR REPLACE TABLE customer_feedback (
id UINT64,
customer VARCHAR,
product VARCHAR,
rating UINT8,
feedback VARCHAR
);
INSERT INTO customer_feedback VALUES
(1, 'Alice', 'Cloud Warehouse', 5,
'The query engine is fast and setup was easy.'),
(2, 'Bob', 'Cloud Warehouse', 2,
'Queries are useful, but the dashboard often times out.'),
(3, 'Carol', 'Data Pipeline', 4,
'Reliable ingestion. I would like better monitoring.'),
(4, 'Dave', 'Data Pipeline', 1,
'The connector stopped working and we cannot load data.');
这几个函数的第一个参数都是 VARIANT。用 OBJECT_CONSTRUCT 把一行里想给模型看的字段拼成一个对象:
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
)
我建议不要只传一段文本。把产品名和评分一起带上,模型看到的是一条有上下文的记录,判断会准不少。
用 jev 做过滤
假设客服想找出"描述了严重故障、需要马上处理"的反馈。
以前大概会这么写:
WHERE feedback ILIKE '%error%'
OR feedback ILIKE '%failed%'
OR feedback ILIKE '%stopped%'
关键词列表永远列不全,而且 "not working" 和 "stopped working" 这种同义的表达一多,规则就越来越难维护。
换成 jev,直接把想找的东西说出来:
SELECT id, customer, product, rating, feedback
FROM customer_feedback
WHERE jev(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'The customer reports a serious product failure that requires immediate support'
);
在这张表上,Dave 那条会被筛出来,Bob 那条超时的抱怨则不会。
jev 返回 BOOLEAN,可以像普通条件一样放在 WHERE、CASE WHEN 或者聚合里。
默认是概率过 0.5 就算 TRUE。如果你的场景宁可漏掉一些也不想误判,可以在启动服务时把阈值调高:
export JEV_THRESHOLD='0.8'
uv run python example/jev.py
用 jev_prob 排序
布尔值适合过滤,但很多时候你想要的是一个能排序的数。
比如看流失风险:
SELECT
id,
customer,
rating,
feedback,
jev_prob(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'The customer is at high risk of churn'
) AS churn_probability
FROM customer_feedback
ORDER BY churn_probability DESC;
概率回到 SQL 里之后,阈值就由用的人自己定:
WITH scored AS (
SELECT
*,
jev_prob(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'The customer is at high risk of churn'
) AS churn_probability
FROM customer_feedback
)
SELECT *
FROM scored
WHERE churn_probability >= 0.8
ORDER BY churn_probability DESC;
运营团队要 0.8 以上的,销售团队可能只想看 Top 10,各自写各自的 SQL 就行,不用改模型那边的任何东西。
用 jev_choice 分类
反馈进了系统之后,第一件事往往是分给对的团队:
SELECT
id,
feedback,
jev_choice(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'Which category best describes this customer feedback?',
[
'Performance',
'Reliability',
'Usability',
'Feature request'
]
) AS category
FROM customer_feedback;
工单路由、评论主题、商品归类、线索分组,都是这个用法。
这里有一个经验:候选类别尽量互斥,边界清楚。如果两个选项意思差不多,模型的概率会在它们之间摇摆,confidence 也会跟着掉下来。
用 jev_score 打分
有些判断不是非黑即白,而是有程度的。情感就是典型:
SELECT
id,
feedback,
jev_score(
OBJECT_CONSTRUCT(
'rating', rating,
'feedback', feedback
),
'How positive is this customer feedback?',
[
'Very negative',
'Negative',
'Neutral',
'Positive',
'Very positive'
]
) AS sentiment_score
FROM customer_feedback
ORDER BY sentiment_score;
五个等级的位置是 0 到 4。返回的不是硬选一个整数,而是按各等级概率算出来的加权位置,所以会看到 2.5、3.7 这样的值。
因为是数值,可以直接聚合:
SELECT
product,
AVG(
jev_score(
OBJECT_CONSTRUCT('rating', rating, 'feedback', feedback),
'How positive is this customer feedback?',
['Very negative', 'Negative', 'Neutral', 'Positive', 'Very positive']
)
) AS avg_sentiment
FROM customer_feedback
GROUP BY product
ORDER BY avg_sentiment DESC;
判断是 Jev 做的,分组和平均是 Databend 做的。我觉得把模型能力塞进数据库,最实际的价值就在这种地方。
用 jev_eval 看完整结果
自动流程里有个类别就够了,但做审计、风控或者人工复核的时候,你会想知道模型到底有多确定。
SELECT jev_eval(
PARSE_JSON('{"text":"A SQL query engine written in Rust"}'),
'Which category best describes this text?',
'choice',
['Database', 'Cooking', 'Sports']
) AS result;
回来的是:
{
"choice": "Database",
"confidence": 1.0,
"probabilities": {
"Cooking": 0.0,
"Database": 1.0,
"Sports": 0.0
},
"type": "choice"
}
这是 VARIANT,在 Databend 里可以继续取字段:
WITH evaluated AS (
SELECT
id,
jev_eval(
OBJECT_CONSTRUCT('feedback', feedback),
'Which category best describes this feedback?',
'choice',
['Performance', 'Reliability', 'Usability', 'Feature request']
) AS result
FROM customer_feedback
)
SELECT
id,
result['choice']::VARCHAR AS category,
result['confidence']::DOUBLE AS confidence,
result['probabilities'] AS probabilities
FROM evaluated;
我自己比较常用的一个套路是:confidence 高的直接进下游系统,低的丢进人工复核队列,完整的概率分布存下来留着以后看。
jev_eval 的第三个参数是问题类型:
kind | 含义 | options |
|---|---|---|
noul | 是/否的概率判断 | 传 [] |
choice | 候选项分类 | 传候选类别 |
score | 有序等级打分 | 传有序等级 |
算过一次的结果,存下来
这几个函数背后是远程 API 调用,跟本地的 LOWER()、SUBSTR() 不是一回事。每次查询都重新算一遍,既慢又浪费钱。
如果一个结果会被报表、应用、下游任务反复读,算一次写回表里:
CREATE OR REPLACE TABLE feedback_enriched AS
SELECT
id,
product,
rating,
feedback,
jev_eval(
OBJECT_CONSTRUCT(
'product', product,
'rating', rating,
'feedback', feedback
),
'Which category best describes this customer feedback?',
'choice',
['Performance', 'Reliability', 'Usability', 'Feature request']
) AS category_result,
NOW() AS processed_at
FROM customer_feedback;
之后查询直接读这张表:
SELECT
category_result['choice']::VARCHAR AS category,
COUNT(*) AS feedback_count
FROM feedback_enriched
GROUP BY category
ORDER BY feedback_count DESC;
整个链路大概是这样:
图 3:客户反馈语义增强数据流。结果只计算一次并持久化,高 confidence 自动流转,低 confidence 进入人工复核。
真正用起来要注意的
先用 SQL 把范围缩小
能用日期、状态、分区、数值条件过滤掉的,先过滤,再调 Jev:
SELECT
id,
jev_prob(
OBJECT_CONSTRUCT('rating', rating, 'feedback', feedback),
'The customer needs immediate support'
) AS probability
FROM customer_feedback
WHERE rating <= 2
AND feedback IS NOT NULL;
别对着一张没筛过的大表直接调远程模型。
只传需要的字段
不要顺手把整行传过去。明确构造一个最小的对象,请求体小,也少往外发敏感数据。
同一批数据用同样的问题
UDF 实现里会把问题、类型和选项都相同的行合并成一个批次发出去。批处理的时候问题和选项固定,吞吐会好很多。
批次大小和并发可以调:
export JEV_BATCH_SIZE='20'
export JEV_CONCURRENCY='6'
服务里还带了超时、重试和一个进程内的 LRU 缓存,临时网络抖动和重复请求能扛一下。
上线前想清楚数据边界
参与判断的数据是要发到远程 API 的。上线之前几件事得有答案:
-
哪些字段可以出数据库;
-
里面有没有个人信息或其他敏感内容;
-
API Key 怎么注入、怎么轮换;
-
UDF Server 和 Databend 之间要不要走 TLS;
-
白名单和网络出口是否只放行可信地址;
-
结果要不要留存以备审计。
把 AI 能力接进 SQL,不等于数据治理可以放松。恰恰因为调用发生在数据链路里面,边界要画得更清楚。
数据库负责执行,Jev 负责判断
写完这个集成,我的感受是:Databend 负责存、筛、连、聚合、大规模执行,Jev 负责回答那些规则写不出来的语义问题。两边接上之后,用自然语言过滤、算概率、分类、打分、留 confidence 做人工复核,都能在 SQL 里完成。
对数据团队来说,不用改变工作方式。输入还是表,调用还是 SQL,输出还是可以进 WHERE、ORDER BY、GROUP BY、INSERT INTO 和数据管道。
Jev 现在还比较早期,评测是他们自己设计的,权重也没公开,该保留的怀疑我都保留着。但"数据库里那些小而模糊的判断,不必每次都请一个会写小说的模型来做"这件事,我觉得是成立的。
相关资源:
Databend UDF:github.com/databendlab…
Jev UDF 实现:python/example/jev.py
函数签名:
jev(row, condition) -- 自然语言条件,返回 BOOLEAN jev_prob(row, condition) -- 条件成立概率,返回 DOUBLE jev_choice(row, question, options) -- 候选项分类,返回 VARCHAR jev_score(row, question, levels) -- 有序等级打分,返回 DOUBLE jev_eval(row, question, kind, options) -- 完整结果,返回 VARIANT