数据分析Agent:ChatBI自然语言查数据
第三个场景是数据分析平台。用户经常想查"上季度华东区退货率最高的三个品类",但他们不会写 SQL,也不想学。传统做法是提工单,数据团队排期,两天后收到一个 Excel。
ChatBI 的目标:用户输入自然语言,系统生成 SQL、执行、返回结果,再配一段人话解读。
架构:NL2SQL + 权限过滤 + 结果校验
代码块
JavaScript
用户问题 → ① 意图确认(是不是查数需求?要不要写?)
→ ② 表/字段映射(只允许白名单内的表和字段)
→ ③ NL2SQL(LLM 生成 SQL,带约束)
→ ④ 权限注入(强制拼 tenant_id 过滤 + 列级权限裁剪)
→ ⑤ 安全执行(只读账号 + 超时 + LIMIT 上限)
→ ⑥ 结果自检(LLM 复核 SQL 和结果是否匹配问题)
→ ⑦ 生成解读
每一步都在压缩"出错的空间"。我逐个说:
① 意图确认。 不是所有问题都该走 SQL。"为什么上个月销售额下降"可能根本不需要查库,或者需要先澄清口径。先让 LLM 判断这是不是查数需求、是否涉及写入操作。写操作直接拒绝,ChatBI 只读。聚合还是明细这类细节,等 SQL 生成时再处理,意图确认阶段不用管。
② 白名单。 LLM 不知道数据库全貌,只给它表清单和字段清单。表名、字段名全部来自系统元数据,LLM 无法发明表。
③④ 权限。 用户只能查自己有权限的表和行。实现上是两件事:工具层只暴露有权限的表;SQL 生成后强制注入 WHERE tenant_id = ?(多租户)和行级过滤条件。注入是服务端做的,LLM 生成的 SQL 会被改写。
⑤ 安全执行。 只读数据库账号、查询超时、LIMIT 上限。防止"生成了一条全表扫描"拖垮数据库。
⑥ 结果自检。 SQL 执行完,让 LLM 复核一遍:"这个 SQL 和用户的问题匹配吗?结果里有明显可疑的数据吗?"不匹配就重生成,最多重试两次。这一步挡住了不少低级错误,比如 LLM 把"最近30天"写成"最近30年",自检时一看时间范围就发现了。
核心代码
代码块
Python
# chatbi.py - 安全 NL2SQL:白名单 + 权限注入 + 结果自检
import re
class ChatBI:
def __init__(self, llm, schema, db, perm_checker):
self.llm = llm
self.schema = schema # 白名单表/字段元数据
self.db = db # 只读连接
self.perm_checker = perm_checker
def ask(self, question: str, user_ctx) -> dict:
# ① 意图确认:是不是查数需求
intent = self.llm.classify(
question,
labels=["data_query", "clarify", "write_request", "other"],
# 聚合/明细等细节不在意图层判断,交给下游 SQL 生成时处理
)
权限注入是字符串级的,不是 prompt 级的。 _inject_scope 在服务端直接改写 SQL,把租户条件拼进 WHERE。LLM 生成的 SQL 再花哨,最后执行的都是被改写过的版本。这是 ChatBI 场景里最重要的安全防线。
自检挡住的是"看起来对"的错误。 文本错误用户能发现,SQL 错误用户发现不了。自检这一层专门对付"时间范围错了""筛错维度了"这类低级但隐蔽的错误。不是万能的,但能把错误率压下一个量级。
踩坑:SQL 幻觉和"离线评估≠线上效果"
坑一:时间范围幻觉。
测试集上 SQL 正确率很高,上线后第一个真实用户问题就把我们干懵了:"查一下我们最近的表现。"
LLM 生成 SQL 时没写时间条件。"最近的表现"这种模糊说法,模型默认了整表数据。结果把过去三年的数据全拉出来了,还配了一段一本正经的解读:"贵司近三年表现稳定。"用户当场质疑数据。
修复了两层:第一,生成规则里强制"时间条件必须显式写出,禁止隐含默认值",没有明确时间范围就反问用户;第二,结果自检专门检查时间范围。
坑二:LLM 会发明不存在的逻辑。
有一次用户问"华东区各品类的退货率",LLM 生成 SQL 时把订单表和商品表 join 错了,用了 order.product_id = product.order_id 这种不存在的关联。结果没报错——两个表的 ID 都是自增序列,数值恰好能对上,查询"成功"了,还返回了一堆看起来正常的数字。AI 甚至配了一段解读:"华东区家居类退货率偏高,建议关注物流时效。"
我们对着结果愣了半天。数据没报错、数量级对、结论也合理,直到拿一条已知订单手工核对,才发现关联键完全是错的。这类错误比幻觉难抓得多:幻觉往往是"看起来不像话",它却是"看起来太合理"。还有一次,LLM 在 SQL 里写了个 HAVING count(*) > 0,完全多余,但因为结果没错,自检也没拦下来。
我们的应对是高频问题缓存:把常见问题("月度销售额""退货率 TOP10")的 SQL 固化下来,用户问相似问题时直接用固化 SQL,不经过 LLM 生成。缓存命中率越高,出错空间越小,成本也越低。
坑三:离线评估不等于线上效果。
我们在 80 条测试集上把 SQL 正确率刷到很高,上线后照样被真实问法打穿:"哪个月最惨""卖得最差的是啥""环比跌了没"。这些口语化、缺主语、缺时间的问法,测试集里一条都没有。
解法很土但有效:线上 bad case 全部进评估集,每周回归一次。 用户每问出一个让系统翻车的说法,就把它加进测试集,下次优化必须保证它通过。评估集从 80 条涨到 200 多条,系统被真实问法"训练"得越来越稳。