
1. 项目概述当文本分类遇上云原生向量数据库最近在做一个内容审核相关的项目需要快速对海量用户生成的短文本比如评论、帖子标题进行多标签分类。传统的方案要么是写一堆复杂的规则引擎维护起来头大要么是调用外部AI服务延迟和成本都让人有点心疼。正好团队在用的Hologres最近上线了AI Function功能号称能直接用SQL调用内置的模型做推理这让我来了兴趣。毕竟对于数据开发和分析师来说SQL是看家本领如果能用几句SELECT就把文本分类给做了那开发和迭代效率岂不是直接起飞Hologres本身是阿里云推出的一款实时交互式分析引擎和PostgreSQL高度兼容最近其向量计算能力特别火。而这个AI Function简单理解就是它把一些常见的AI模型比如文本向量化、文本生成、分类等做成了内置的SQL函数。你不需要关心模型怎么部署、服务怎么拉起只需要像调用SUM()或COUNT()一样在查询语句里写上这个函数它就能返回推理结果。这次实战的核心就是利用hg_ai_text_embedding和hg_ai_complete这类函数完成一个端到端的文本分类流水线并且会深入到提示词Prompt工程和KV-Cache调优这些直接影响效果和成本的细节。听起来很美好但真用起来会发现从“能用”到“好用”之间有道鸿沟。比如怎么设计Prompt才能让大语言模型LLM准确理解你的分类体系直接让模型输出“科技”、“体育”这类标签它会不会胡言乱语更实际的问题是当你要对成千上万条文本进行批量分类时推理速度慢、成本高怎么办这就是KV-Cache调优要解决的问题。我会把这次从零开始摸索包括踩过的坑、总结的技巧通过具体的SQL示例完整地走一遍。目标很明确让你看完之后能直接在你的Hologres环境里复现一个高效的文本分类方案。2. 核心思路为什么选择SQLAI Function方案在深入代码之前我们得先盘清楚为什么这个方案值得一试。面对文本分类任务我们通常有几种选择1使用传统的机器学习库如scikit-learn自训练模型2调用云厂商提供的现成NLP API3自行部署开源模型如BERT提供HTTP服务4使用Hologres AI Function。第一种方案灵活但门槛高需要特征工程、训练、评估、部署一整套流程周期长。第二种方案最简单但按调用次数计费对于海量数据成本是线性增长的且数据需要出到公网有安全和延迟顾虑。第三种方案折中但涉及资源管理、服务运维、负载均衡等对很多数据团队来说是额外的负担。而Hologres AI Function方案的核心优势在于“原位计算”和“开箱即用”。所谓原位计算就是数据和计算都在Hologres内部完成。你的文本数据本来就存在Hologres表里现在直接在同一引擎内进行模型推理避免了数据在不同系统间迁移带来的网络开销、序列化反序列化成本这是性能上的先天优势。开箱即用意味着你无需成为MLOps专家省去了模型部署、服务监控、扩缩容等一系列繁琐工作Hologres已经帮你把模型以函数的形式准备好了。具体到文本分类我们主要会用到两个核心函数hg_ai_text_embedding: 将文本转换为高维向量。这是很多AI任务的基础也可以用于基于向量相似度的分类例如计算文本向量与各类别代表向量之间的余弦相似度。hg_ai_complete: 直接与大语言模型如通义千问、ChatGLM等对话通过设计Prompt让模型根据你的指令输出分类结果。这是本次实战的重点。选择hg_ai_complete进行文本分类本质上是利用了LLM强大的指令理解和零样本/少样本学习能力。你不需要提供训练数据只需要用自然语言清晰地告诉模型分类规则它就能给出判断。这对于分类规则复杂、类别经常变动的场景如舆情情感细分、投诉工单归类尤其友好。那么整个流程的蓝图就清晰了我们将数据存储在Hologres的普通表里通过SQL调用AI Function将模型推理结果直接写入另一张结果表或者与原始表关联查询。全程无需切换工具一条SQL链路贯穿数据存储、处理和产出极大地简化了架构。3. 环境准备与数据基础理论说得再多不如动手跑一条SQL。首先你需要一个开通了AI Function功能的Hologres实例。目前这个功能可能在特定地域或版本中提供建议先在阿里云控制台查看或咨询一下。确保你的账号有相应的权限。接下来我们准备一下测试数据。假设我们有一个user_comments表存储了用户的评论信息现在需要对这些评论进行情感倾向正面/负面/中性和主题产品功能、售后服务、价格、其他的双重分类。-- 创建测试表 CREATE TABLE IF NOT EXISTS user_comments ( comment_id BIGINT PRIMARY KEY, user_id BIGINT, comment_text TEXT, created_time TIMESTAMP ); -- 插入一些示例数据 INSERT INTO user_comments VALUES (1, 1001, 这款手机的拍照功能太强了夜景模式简直无敌就是电池有点不够用。, 2023-10-01 10:00:00), (2, 1002, 客服响应太慢了等了半天都没人理体验很差。, 2023-10-01 11:00:00), (3, 1003, 价格有点高不过材质和做工确实对得起这个价位可以考虑。, 2023-10-01 12:00:00), (4, 1004, 系统更新后比以前流畅多了赞一个, 2023-10-01 13:00:00), (5, 1005, 快递包装破损里面的商品也有轻微划痕心情不好。, 2023-10-01 14:00:00);数据准备好了只是一个简单的文本字段。接下来最关键的一步来了如何设计给AI的“指令”也就是Prompt。4. 提示词设计让AI准确理解你的分类意图直接对模型说“给这个文本分类”它大概率会懵。Prompt设计是决定分类准确率的“方向盘”。我们的目标是将模糊的分类需求转化为LLM能精确执行的清晰指令。设计Prompt时我总结了一个“CRISP”原则C (Clear): 清晰。明确任务是什么。R (Role): 角色。给模型赋予一个角色比如“你是一个专业的文本分析助手”。I (Instruction): 指令。具体要做什么步骤是什么。S (Structure): 结构化输出。规定好输出的格式最好是JSON、XML或固定的关键词便于后续程序化解析。P (Positive/Negative Examples): 正负例。提供少量例子进行少样本学习Few-Shot Learning效果提升显著。让我们基于这个原则为我们的双分类任务设计Prompt。假设我们要求模型以JSON格式输出情感sentiment和主题topic。初版Prompt效果一般请分析以下用户评论的情感倾向和主要讨论主题。 评论{comment_text}这个Prompt太模糊了。“情感倾向”具体指什么三分类还是五分类“主题”的范围是什么模型可能会自由发挥输出“情感是积极的主题是手机”这种不规范的答案无法解析。优化版Prompt遵循CRISP原则你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能} 现在请分析以下评论 评论{comment_text}这个Prompt的改进点在于角色明确“电商平台的产品评论分析专家”让模型进入专业场景。指令分步用“1. 2.”列出了具体步骤逻辑清晰。选项封闭情感和主题都给出了明确的封闭选项限制了模型的输出空间避免了胡言乱语。输出结构化强制要求JSON格式并给出了示例极大方便了后续用SQL的JSON函数如json_extract_path_text直接提取字段。少样本示例虽然这里没有在Prompt里写例子但我们可以通过后续的hg_ai_complete函数参数以“系统消息”或“上下文消息”的方式提供少量示例效果更好。在Hologres中我们将这个优化后的Prompt模板作为字符串在SQL中动态拼接评论内容。注意实际使用中如果分类类别很多可以把类别列表单独定义为一个变量或从配置表中读取避免Prompt过长。同时对于“其他”类可以追加指令“如果无法归入上述任何主题则选择其他并在JSON中增加一个reason字段简要说明原因。”这样能收集到模型不确定的案例用于后续优化类别体系。5. 核心实战用一条SQL完成文本分类有了精心设计的Prompt我们就可以祭出hg_ai_complete函数了。这个函数的基本调用形式如下SELECT hg_ai_complete( qwen-max, -- 模型名称例如 qwen-max, qwen-plus, chatglm3-6b等 CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能}\n\n, 现在请分析以下评论\n评论, c.comment_text), {}::jsonb -- 可选参数可用于设置temperature, max_tokens等 ) as ai_response, c.* FROM user_comments c;执行这段SQLai_response字段就会得到模型返回的完整文本例如{sentiment: 正面, topic: 产品功能}。但我们的目标是把结果结构化存储方便后续分析。这就需要用到PostgreSQL/Hologres强大的JSON处理能力-- 创建结果表 CREATE TABLE IF NOT EXISTS comment_analysis ( comment_id BIGINT PRIMARY KEY, original_text TEXT, sentiment VARCHAR(10), topic VARCHAR(20), ai_raw_response TEXT, analysis_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 执行分类并将结果结构化插入 INSERT INTO comment_analysis (comment_id, original_text, sentiment, topic, ai_raw_response) SELECT c.comment_id, c.comment_text, json_extract_path_text( hg_ai_complete( qwen-max, CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能}\n\n, 现在请分析以下评论\n评论, c.comment_text), {temperature: 0.1, max_tokens: 500}::jsonb -- 控制生成参数 ), sentiment ) as sentiment, json_extract_path_text( hg_ai_complete( qwen-max, CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能}\n\n, 现在请分析以下评论\n评论, c.comment_text), {temperature: 0.1, max_tokens: 500}::jsonb ), topic ) as topic, hg_ai_complete( qwen-max, CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能}\n\n, 现在请分析以下评论\n评论, c.comment_text), {temperature: 0.1, max_tokens: 500}::jsonb ) as ai_raw_response FROM user_comments c; -- 查询结果 SELECT * FROM comment_analysis;这里有个严重的性能问题细看上面的SQL我们对同一条评论c.comment_text调用了三次hg_ai_complete函数这意味着同一段文本会被发送到AI模型推理三次产生了三倍的成本和耗时。这在生产环境是不可接受的。正确的做法是只调用一次模型然后解析其输出。但json_extract_path_text需要在一个已经包含了完整JSON的列上操作。我们可以通过子查询或者CTE公共表表达式来优化-- 方法一使用子查询 INSERT INTO comment_analysis (comment_id, original_text, sentiment, topic, ai_raw_response) SELECT sub.comment_id, sub.original_text, json_extract_path_text(sub.ai_raw_response, sentiment) as sentiment, json_extract_path_text(sub.ai_raw_response, topic) as topic, sub.ai_raw_response FROM ( SELECT c.comment_id, c.comment_text as original_text, hg_ai_complete( qwen-max, CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能}\n\n, 现在请分析以下评论\n评论, c.comment_text), {temperature: 0.1, max_tokens: 500}::jsonb ) as ai_raw_response FROM user_comments c ) sub; -- 方法二使用CTE (WITH子句)更清晰 WITH classified_comments AS ( SELECT c.comment_id, c.comment_text as original_text, hg_ai_complete( qwen-max, CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象包含两个键sentiment和topic。例如{sentiment: 正面, topic: 产品功能}\n\n, 现在请分析以下评论\n评论, c.comment_text), {temperature: 0.1, max_tokens: 500}::jsonb ) as ai_raw_response FROM user_comments c ) INSERT INTO comment_analysis (comment_id, original_text, sentiment, topic, ai_raw_response) SELECT comment_id, original_text, json_extract_path_text(ai_raw_response, sentiment), json_extract_path_text(ai_raw_response, topic), ai_raw_response FROM classified_comments;这样每条评论只调用一次AI函数获取完整的JSON响应然后在应用层这里就是SQL本身进行字段提取效率最高。这就是“全程SQL搞定”的精髓用SQL处理数据也用SQL处理AI返回的复杂结构。6. 性能瓶颈与KV-Cache调优实战当处理几条、几十条数据时上面的方法很完美。但设想一下如果你要对一张百万行评论表进行全量分类直接SELECT ... FROM huge_table调用AI函数会遇到两个核心问题速度慢和成本高。速度慢是因为LLM的推理是计算密集型操作串行处理百万数据不可行。成本高则是因为大多数AI服务按Token消耗量计费。这时就需要引入两个重要的优化策略批量处理和KV-Cache调优。1. 批量处理 (Batching)Hologres的AI Function通常支持在单次调用中传入一个文本数组模型会并行处理这些输入。这比逐条调用效率高得多因为减少了网络往返开销和模型本身的上下文准备开销。-- 假设我们每次处理100条评论 WITH batch_data AS ( SELECT array_agg(comment_id) as id_array, array_agg(comment_text) as text_array FROM user_comments -- 可以加WHERE条件进行分批例如 WHERE comment_id BETWEEN 1 AND 100 GROUP BY (comment_id - 1) / 100 -- 每100条一个批次 ), batch_result AS ( SELECT b.id_array, b.text_array, hg_ai_complete( qwen-max, CONCAT(你是一个电商平台的产品评论分析专家。请严格遵循以下步骤对用户评论进行分析\n, 1. 判断评论的情感倾向仅限[正面, 负面, 中性]三个选项。\n, 2. 判断评论讨论的核心主题仅限[产品功能, 售后服务, 价格, 其他]四个选项。\n\n, 输出要求必须且只能输出一个JSON对象数组每个对象包含sentiment和topic键。\n\n, 现在请分析以下评论列表\n, (SELECT string_agg(评论 || (idx1) || : || text_array[idx1], \n) FROM generate_subscripts(b.text_array, 1) as idx) ), {temperature: 0.1, max_tokens: 2000}::jsonb -- 增加max_tokens以适应更长输出 ) as batch_raw_response FROM batch_data b ) -- 后续需要复杂的JSON解析来将batch结果拆分成单条记录这里略去...注意批量处理时Prompt需要调整要求模型返回一个JSON数组并且输入部分要清晰地列出所有评论。同时max_tokens参数需要调大以容纳更长的输出。解析批量结果会比单条复杂需要用到json_array_elements等函数将数组拆分为行。2. KV-Cache调优 (关键中的关键)这是本次实战的进阶重点。LLM在生成每个Token时都需要基于之前所有Token的Key和Value向量进行计算和缓存这个缓存就是KV-Cache。对于分类任务我们通常只需要模型输出一个简短的标签如“正面”但模型仍然会为整个输入Prompt可能很长和生成过程维护KV-Cache这造成了巨大的内存和计算浪费。hg_ai_complete函数允许通过参数控制KV-Cache的行为核心参数是max_input_tokens和max_output_tokens。但更关键的是理解**“输入”和“输出”在计费和缓存中的角色**。max_input_tokens: 限制模型“看”到的输入文本的最大长度。超过部分会被截断。合理设置此值可以显著降低计算和存储成本。对于分类任务我们的Prompt是固定的加上变长的用户评论。你需要估算一个绝大多数评论长度都不会超过的值。例如如果99%的评论在200字以内加上Prompt模板约150字总输入Token约350个中文字符大约1.5个字符对应1个Token。那么设置max_input_tokens: 400就是一个安全且节约的选择。max_output_tokens: 限制模型生成文本的最大长度。对于分类任务我们期望的输出非常短一个JSON字符串所以这个值可以设得很小比如50或100。设置过大会浪费资源。-- 优化后的单条调用显式控制Token长度 SELECT hg_ai_complete( qwen-max, CONCAT([精简后的Prompt], c.comment_text), { temperature: 0.1, max_tokens: 50, -- 输出很短50足够 max_input_tokens: 400 -- 根据Prompt评论平均长度设定 }::jsonb ) as ai_response FROM user_comments c;实操心得KV-Cache调优的本质是做“资源预算”。你需要像管理数据库连接池一样管理模型的输入输出窗口。通过监控AI函数调用的实际Token消耗部分模型或平台会返回usage字段不断调整max_input_tokens和max_output_tokens找到在保证效果前提下的最小必要值。对于分类任务将max_output_tokens设小是效果最明显的优化手段之一。7. 错误处理与稳定性保障在生产环境中运行我们不能假设每次AI调用都成功。网络波动、模型服务暂时不可用、输入格式意外、输出不符合预期等情况都可能发生。SQL虽然强大但错误处理能力相对较弱。我们需要在架构上考虑稳定性。1. 重试机制与幂等性可以在应用层调用SQL的脚本或任务实现重试逻辑。更“SQL思维”的做法是利用Hologres的存储过程或定时任务在失败后重新运行。关键是保证操作的幂等性即同一批数据重复处理不会导致重复或错误的结果。我们的INSERT INTO ... SELECT语句如果基于主键comment_id并且结果表有主键约束重复插入就会失败这需要我们在逻辑中处理例如使用ON CONFLICT (comment_id) DO UPDATE ...。2. 输出格式校验与兜底策略即使Prompt要求输出JSON模型偶尔也可能输出非JSON内容如附加解释。我们需要在SQL中增加校验。WITH raw_analysis AS ( SELECT comment_id, comment_text, hg_ai_complete(...) as raw_response FROM user_comments ), parsed_analysis AS ( SELECT comment_id, comment_text, raw_response, -- 尝试解析JSON如果失败则返回NULL CASE WHEN raw_response ~ ^\{.*\}$ THEN json_parse(raw_response) ELSE NULL END as response_json FROM raw_analysis ) SELECT comment_id, comment_text, COALESCE(json_extract_path_text(response_json, sentiment), ERROR) as sentiment, COALESCE(json_extract_path_text(response_json, topic), ERROR) as topic, raw_response FROM parsed_analysis;这里使用了正则表达式^\{.*\}$简单判断响应是否被大括号包围即JSON对象然后尝试解析。解析失败或字段缺失时使用COALESCE函数提供默认值‘ERROR’便于后续筛选和人工复核。3. 限流与分批对于海量数据不要试图用一个巨型SQL语句处理所有数据。务必采用分批策略如之前示例的GROUP BY (comment_id - 1) / 100。这不仅能避免单次查询超时或内存溢出也便于控制对AI服务的请求速率避免触发限流。可以在存储过程中使用循环或者用调度工具如Airflow编排多个分批次SQL任务。8. 效果评估与方案对比方案上线后如何评估其效果我们至少需要关注三个维度准确率、性能和成本。1. 准确率评估由于是零样本/少样本分类没有标注好的测试集我们可以采用抽样评估。人工抽检定期从结果表中随机抽取一批数据人工判断分类是否正确计算准确率。可以在结果表中增加一个review_status字段标记是否已复核。一致性检查如果历史上有基于规则或简单模型的分类结果可以将AI分类结果与之对比分析差异点。AI往往能发现规则覆盖不到的复杂案例。A/B测试对于新产生的数据可以同时用旧方案和新方案AI进行分类对比一段时间内的结果差异观察AI是否带来了正向提升如更细的粒度、更少的“其他”类。2. 性能与成本监控性能记录每条SQL或每个批次的处理耗时。关注hg_ai_complete函数的执行时间。如果使用分批计算总体吞吐量条/秒。成本关注AI服务的Token消耗。虽然Hologres AI Function的具体计费方式需参考官方文档但通过控制max_input_tokens和max_output_tokens我们已经是在进行成本管控。可以尝试估算总成本 ≈ (平均输入Token数 平均输出Token数) * 数据条数 * 单价。优化Prompt、精简输入输出能直接降低成本。3. 与替代方案对比vs. 自建模型服务省去了运维、部署、资源预留的成本和精力起步快适合快速验证和迭代。但在极端定制化、对延迟有极致要求、或需要完全控制模型版本的情况下自建服务更灵活。vs. 外部API数据不出内网延迟更低尤其是Hologres与AI服务同地域时批量处理效率可能更高取决于API的并发限制。成本结构可能不同需要根据实际调用量详细测算。vs. 传统规则引擎AI方案的准确率和覆盖率上限更高能处理复杂、模糊的语义。规则引擎维护成本高但确定性100%对于简单、明确的分类规则可能更快更便宜。我个人在实际项目中的体会是对于中等数据量日增数万至百万级、分类逻辑复杂且可能变化的场景Hologres AI Function提供的SQL原生AI能力是一个“甜点”方案。它极大地降低了算法能力的数据应用门槛让数据分析师也能快速构建智能分类管道。最大的挑战和乐趣其实在于如何通过精巧的Prompt设计和资源调优用最低的成本获得最稳定的效果。这个过程本身就是对数据和AI理解的一次深度实践。