让大模型自己写 SQL 又不越权——一个客服数据分析 Agent 的生存指南
用 Claude 的 Tool-Use 做了一个能自主查数据的客服分析 Agent。核心难点不是"让它会查",而是"让它安全地查、别烧钱、别乱编"。上线后最大的体感:给模型自由不难,难的是在给它自由的同时,用确定性系统把边界焊死。
一、为什么是 Agent,而不是更多报表接口
故事从一个真实的运营需求开始。运营想问的问题是开放式、组合式的:
"上周从小红书来的客户里,聊过但最后没成单的,流失原因 top3 是什么?"
这个问题拆开来有五个维度:时间(上周)、来源渠道(小红书)、行为条件(聊过)、结果筛选(没成单)、分析目标(流失原因排行)。走传统路线意味着:提需求 → 写 SQL → 配 BI 看板 → 运营拖拽筛选。一套流程下来,半天到一周过去了。而运营真正需要的,是从想法到答案的秒级闭环。
所以我把"自然语言 → 数据查询 → 结论"这一步交给模型编排,自己只负责提供可信的工具原语。最终暴露给模型的工具集只有 6 个:
|
工具 |
用途 |
设计意图 |
|---|---|---|
|
|
固定口径统计(总量、成单率) |
快、可控、结果确定 |
|
|
分类排行(top N 分析) |
预设聚合逻辑,避免模型手写复杂 GROUP BY |
|
|
会话级明细查询 |
限定范围,不走自由 SQL |
|
|
流失原因分析 |
调用预置分析逻辑 |
|
|
批量关键词检索 |
禁止在自由 SQL 中用 |
|
|
受限的自由 SQL 查询 |
仅用于真正开放的交叉分析 |
设计哲学很明确:固定口径走专用工具,开放式分析才放行自由 SQL。 前者是高速公路,后者是越野路段——不是不让走,而是只有必要时才放行。
二、难点一:让模型自由写 SQL,但绝不能越权
这是整个系统里最危险的地方。一旦模型写出 DROP TABLE 或 UPDATE users SET balance=999999,后果不堪设想。
很多人的第一反应是:"在 prompt 里写清楚不就行了?"
"你只能写 SELECT 语句,禁止任何增删改操作。"
这是最脆弱的方案。 Prompt 是最外层、最容易被绕过的一层。模型可能因为上下文污染、对抗性输入、甚至自身的"创造性"(它觉得需要临时建表来辅助分析)而违反约束。靠 prompt 守门,相当于在金库门口贴一张"请勿入内"的告示。
我的方案是纵深防御(Defense in Depth),每一层挡不同的东西:
┌──────────────────────────────────────────────────────────┐
│ 第 1 层:字符串白名单校验 │
│ · 必须以 SELECT 开头(startsWith,非正则匹配) │
│ · 黑名单关键字一律拒绝:INSERT/UPDATE/DELETE/DROP/ │
│ ALTER/TRUNCATE/CREATE/EXEC/LOAD/INTO OUTFILE/DUMPFILE │
│ 目的:快速失败,挡掉明显的写操作 │
├──────────────────────────────────────────────────────────┤
│ 第 2 层:表白名单 │
│ · 查询必须命中 chat_messages / session_analysis │
│ 目的:把可访问范围收缩到分析所需的最小集合 │
├──────────────────────────────────────────────────────────┤
│ 第 3 层:资源限制 │
│ · 强制 LIMIT 500(移除用户已有的 LIMIT 后统一追加) │
│ · 查询超时保护(数据库层 statement timeout) │
│ 目的:防止全表扫描或大结果集拖垮数据库 │
└──────────────────────────────────────────────────────────┘
对应到代码,核心就是几行朴素的校验:
if (!upper.startsWith("SELECT")) return error("仅支持 SELECT 查询");
if (upper.contains("INSERT") || upper.contains("DROP") || ...) {
return error("包含禁止的写操作关键字");
}
// 必须命中白名单表,再强制拼上 LIMIT 500
但要诚实:字符串校验不是终点
我很清楚这套方案的软肋——字符串/关键词检测总有被绕过的可能:大小写混淆、注释注入、编码绕过、用 UNION 拼接恶意子查询……只要在字符串层面博弈,攻防就没有尽头。而且"必须命中白名单表"是子串判断,它能保证查询引用了白名单表,却不能百分百保证它没同时碰别的表。
所以真正可靠的底座不该是"我检查它写了什么",而应该是"就算它写了坏的,数据库也没权限执行"——也就是用独立的只读账号 / 只读连接去跑这条 SQL,把 INSERT/UPDATE/DELETE/DROP 挡在数据库权限层。
这一层目前还没上,是我明确列出的下一步加固项。 当前线上兜底靠的是应用层这三道校验 + 只读的业务场景(这个 Agent 只做分析、不写库)。把它如实写出来,是因为:知道正确答案在哪、并把它排进计划,比假装已经做到了更重要。
这也是我想传递给读者的态度:工程成熟度的标志不是"没有漏洞",而是"清楚地知道自己还有哪些漏洞没补上,并且有计划去补"。
三、难点二:别让它烧钱空转
上线第一天我就发现一个现象:模型特别爱"反复交叉验证"。
它的行为模式是这样的:
-
查一遍数据,得到一个初步结论;
-
觉得不够确信,换个角度再查一遍验证;
-
又想到一个可能的遗漏,再查第三遍;
-
还想确认一下边界条件,再查第四遍……
每一轮都消耗 token、增加延迟,但结论的准确度并没有显著提升。它像一个完美主义者在反复检查自己的作业,而运营只想快点知道答案。
更严重的是,自由 SQL 的每次调用都可能触及较大的数据量。如果不加约束,单次对话的成本可能膨胀 5~10 倍。
解法:prompt 里的预算闸门
我在 system prompt 里加了三条硬性约束:
1. 关键词检索必须走 batch_search 工具,禁止在自由 SQL 中使用 LIKE '%关键词%'。
后者会触发全表扫描,既慢又贵。
2. 跨表分析只能用 query 工具调用一次。拿到结果后直接回答,
不要追加"验证性"查询。
3. 如果一次查询不足以回答全部问题,先给出基于现有结果的部分结论,
说明还需要哪些信息,而不是反复尝试不同角度的重查。
这不是限制模型的能力,而是在把它的自由度约束在"够用"的范围内。 就像一个优秀的顾问,不是让他无限深入地研究一个问题,而是在给定的时间和预算内给出"足够好的答案"。约束不是枷锁,是让系统变得可预测和可控制的必要条件。
四、难点三:500 行结果会撑爆上下文
自由 SQL 可能返回几百行结果。如果原样塞回模型的上下文,会出现两个问题:
-
Token 爆炸:500 行 × 每行几十个字符 = 上万 token,一次查询就吃掉大量预算;
-
截断风险:如果总上下文接近上限,模型可能在输出中途被截断,导致"有头无尾";
-
心算陷阱:模型拿到大量原始数据后,容易试图在上下文中手动做聚合、排序、统计——而这恰恰是它最容易出错的地方。
解法:摘要式回传
query 工具的返回格式是刻意设计过的摘要:
{
"ok": true,
"rows": 347,
"columns": ["date", "buyer", "platform", "lost_reason"],
"samples": [
{"date": "2026-05-14", "buyer": "u_82391", "platform": "小红书", "lost_reason": "价格偏高"},
{"date": "2026-05-15", "buyer": "u_47201", "platform": "小红书", "lost_reason": "对比竞品"}
],
"truncated": true
}
模型拿到的是:行数 + 列结构 + 最多 3 条样本(其中长文本字段还会截到 60 字)。它据此判断趋势和量级,如果需要细节,就主动收窄条件重查。
背后的原则是:模型应该做"理解和解释",而不是"存储和计算"。
五、难点四:流式输出与幻觉治理
流式输出:SSE 比 WebSocket 更轻
推理过程和最终结果用 Server-Sent Events (SSE) 流式推给前端。
为什么不用 WebSocket?因为这个场景是单向、服务端持续推的:
-
模型在思考 → 推"正在分析…"
-
调用工具 → 推"正在查询数据库…"
-
拿到结果 → 推"正在生成结论…"
-
最终输出 → 推正文
没有双向高频通信的需求,不需要 WebSocket 的复杂性。SSE 基于 HTTP,实现简单,重连机制天然,调试友好。对于 Agent 这种"服务端主导、客户端被动接收"的场景,SSE 是正解。
幻觉治理:从架构上而不是从道德上
关于幻觉,我的态度很明确:Agent 一定会算错数字。
指望模型"诚实地说我不知道"是不现实的——它的训练目标就是"生成看起来合理的下一个词"。与其祈祷它别错,不如从架构上让错误的影响最小化。
核心策略是计算下沉:
|
应该下沉到确定性系统 |
留给模型 |
|---|---|
|
计数( |
趋势判断 |
|
求和、平均、百分比 |
原因解读 |
|
排序( |
归纳总结 |
|
分组聚合( |
建议生成 |
|
时间范围过滤 |
自然语言转述 |
精确的数字计算,全部下沉到 SQL 里去做。 模型只负责"编排"(决定查什么)和"解释"(告诉用户这意味着什么)。
另一个小但重要的细节:max_tokens 截断时,区分"有没有 tool_use"分别处理。如果模型正在输出工具调用但被截断了,不要让它空转浪费一轮——直接终止并提示用户简化问题。
六、总结:给自由,同时焊死边界
做这个 Agent ,最大的体会可以用一句话概括:
给模型自由,同时用确定性系统兜住边界。
这四个维度的分工,是我反复踩坑后沉淀下来的:
|
维度 |
谁来保证 |
手段 |
|---|---|---|
|
会不会越权 |
应用层 + 数据库层 |
关键词/表白名单校验(当前)+ 只读连接(下一步兜底) |
|
会不会烧钱 |
编排层 |
prompt 预算闸门 + 工具调用次数限制 |
|
会不会撑爆上下文 |
工具层 |
结果摘要式回传 |
|
会不会算错 |
执行层 |
计算下沉到 SQL,模型只编排 |
七、一个更通用的模式:Agent 的"三明治架构"
把上面的经验抽象一下,你会得到一个适用于很多数据分析 Agent 的通用架构模式:
┌───────────────────────────────────────────────────────────┐
│ 上层:自然语言接口(用户输入) │
│ 自由度高,但不可信 │
├───────────────────────────────────────────────────────────┤
│ 中间层:Agent 编排(模型 + 工具定义) │
│ · 决定调什么工具、把用户意图翻译成工具参数 │
│ · 把工具结果翻译成自然语言 │
│ · 受预算闸门约束 │
├───────────────────────────────────────────────────────────┤
│ 下层:确定性系统(数据库 / 规则引擎) │
│ · SQL 执行 + 精确计算(聚合、排序、统计) │
│ · 权限与范围控制(表白名单、LIMIT、超时) │
│ 可信,但不灵活 │
└───────────────────────────────────────────────────────────┘
模型活在中间层,上面承接用户的任意表达,下面受制于确定性系统的硬约束。
它不是在"自由地探索数据",而是在一个精心设计的游乐场里"按规则玩耍"。围栏内的自由度已经足够解决 95% 的问题,而围栏本身确保了它不会伤到自己或别人。
八、最后一句话
让 AI 负责编排、理解、生成,把精确和真值交给确定性系统。
这句话说起来简单,真正做到需要你在每一个维度上问自己一个问题:"如果这个判断错了,兜底在哪里?"
有兜底的,放手让模型做;没有兜底的,老老实实写成代码。而对于那些你明确知道"应该有兜底但目前还没有"的地方(比如只读数据库连接),诚实地把它列出来、排进计划,比假装已经做到了更重要。
在 Agent 落地的世界里,克制不是能力的天花板,而是工程成熟的标志。如果你也在做数据分析 Agent,或者对"如何安全地让模型写 SQL"有更多想法,欢迎交流。
如果你觉得这篇文章对你有启发,也欢迎分享你在 Agent 安全设计上的经验和踩坑。
更多推荐

所有评论(0)