Crispin's Blog

基于百炼平台构建贷款业务 Text-to-SQL 智能问答 Agent

背景

在贷款业务中,运营团队每天需要高频查询各类数据指标:放款量、逾期率、客群分布、渠道转化……传统模式下,每一次数据需求都要经历”提需求 → BI → 手写 SQL → 导出结果”的漫长流程,单次耗时 5~10 分钟,遇到复杂多表关联甚至更久, 效率特别低下。

因为本身我有后端背景,而且自己也感兴趣,并且最后需要在 xxx Editor 上构建一个”智能问答”组件,所以这个任务就被我接下来了。


整体架构

1
2
3
4
5
6
7
8
9
10
11
用户(钉钉 / xxx 页面)


百炼 Agent API

┌─────┼──────┐
意图识别 SQL生成 图表渲染
│ │ │
澄清追问 安全校验 卡片/组件推送

业务数据库

核心链路:意图识别 → SQL 生成 → 安全校验 → 查询执行 → 可视化反馈

入口不止一处——既可以通过钉钉 Robot 对话触发,也可以在 xxx Editor 搭建的运营页面中直接嵌入”智能问答”组件,拖拽即用,零门槛接入,特别好使,哈哈。


一、Agent 工作流设计

利用百炼平台的 Workflow 能力,将整个问答过程拆解为以下节点:

节点 职责
意图识别 判断用户是在查放款数据、逾期数据还是客群分析
参数补全 对模糊时间范围(”上个月”、”最近一周”)自动澄清
SQL 生成 调用 LLM 结合 Schema 上下文生成 SQL
安全校验 语法规则 + 字段白名单双重拦截
查询执行 沙箱环境执行,返回结构化结果集
图表渲染 动态生成图表,输出至钉钉卡片或 Quark 组件

多轮对话机制让模糊意图变得可处理,而不是直接报错:

1
2
3
4
5
6
用户:查一下最近的放款情况
Agent:请问您想查哪个时间段?(今天 / 本周 / 本月)
用户:本月
Agent:需要按渠道拆分吗?
用户:是的,按渠道
Agent:[生成图表] 本月各渠道放款金额如下 ↓

二、Text-to-SQL 工程优化

Schema 上下文构建

贷款业务涉及 30+ 张核心表,字段命名风格不一、指标口径各异,直接交给模型效果很差。我们为每张表构建了结构化的 Schema 描述,明确字段含义与业务口径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SCHEMA_CONTEXT = """
表名: table_A(贷款订单表)
字段说明:
- loan_id: 贷款订单ID
- user_id: 借款人ID
- apply_amount: 申请金额(元)
- approve_amount: 审批金额(元)
- loan_date: 放款日期
- channel: 获客渠道(APP/H5/线下)
- product_code: 产品编码
- status: 订单状态(1-放款中 2-还款中 3-已结清 4-逾期)

指标口径:
- 放款量 = COUNT(loan_id) WHERE status IN (2,3,4)
- 放款金额 = SUM(approve_amount) WHERE status IN (2,3,4)
- 逾期率 = COUNT(status=4) / COUNT(status IN (2,3,4))
"""

Few-Shot 示例库

针对贷款业务高频查询场景,沉淀典型 NL→SQL 样本,引导模型对齐业务语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FEW_SHOT_EXAMPLES = [
{
"question": "查本月各渠道放款金额",
"sql": """
SELECT channel, SUM(approve_amount) AS loan_amount
FROM table_A
WHERE DATE_FORMAT(loan_date, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m')
AND status IN (2, 3, 4)
GROUP BY channel
ORDER BY loan_amount DESC
"""
},
{
"question": "最近7天逾期率趋势",
"sql": """
SELECT loan_date,
ROUND(SUM(status=4) / COUNT(*) * 100, 2) AS overdue_rate
FROM table_A
WHERE loan_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY loan_date
ORDER BY loan_date
"""
}
]

经过 Schema 上下文 + Few-Shot 双重优化,复杂查询的 SQL 生成准确率从 **60% 提升至 90%**左右。


三、安全校验机制

贷款数据高度敏感,SQL 执行前设置两道防线:字段黑名单拦截身份证、手机号等 PII 字段,关键字过滤阻断一切写操作。

1
2
3
4
5
6
7
8
9
10
11
12
FIELD_BLACKLIST = {"id_card", "mobile", "bank_card", "password"}
DANGEROUS_KEYWORDS = {"DROP", "DELETE", "UPDATE", "INSERT", "TRUNCATE", "ALTER"}

def validate_sql(sql: str) -> tuple[bool, str]:
sql_upper = sql.upper()
for kw in DANGEROUS_KEYWORDS:
if kw in sql_upper:
return False, f"包含禁止操作: {kw}"
for field in FIELD_BLACKLIST:
if field in sql.lower():
return False, f"包含敏感字段: {field}"
return True, "ok"

四、工具执行与图表渲染

Python Custom Tool

在独立沙箱中执行 SQL,并将结果集动态渲染为图表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd
import matplotlib.pyplot as plt
from sqlalchemy import create_engine, text

def execute_query_and_render(sql: str, chart_type: str = "bar") -> dict:
df = pd.read_sql(text(sql), create_engine(DB_URL))

fig, ax = plt.subplots(figsize=(8, 4))
df.plot(kind=chart_type, ax=ax, marker="o" if chart_type == "line" else None)

img_path = f"/tmp/chart_{uuid4().hex[:8]}.png"
plt.tight_layout()
plt.savefig(img_path)
plt.close()

return {"data": df.to_dict(orient="records"), "chart": img_path}

钉钉交互卡片

将图表和数据表格封装为钉钉 ActionCard,运营同学在手机端即可直接查看,并支持一键导出:

1
2
3
4
5
6
7
8
9
10
11
{
"msgtype": "action_card",
"action_card": {
"title": "📊 放款数据报表",
"text": "![chart](${chart_url})\n\n${data_table}",
"btns": [
{ "title": "导出 Excel", "actionURL": "${export_url}" },
{ "title": "继续追问", "actionURL": "${bot_url}" }
]
}
}

xxx Editor 组件化接入

除钉钉 Robot 外,我们将 Text-to-SQL Agent 进一步封装为 xxx Editor 低代码组件。运营人员在搭建数据看板或活动页面时,直接从组件库拖拽”智能问答”模块到任意位置,无需任何开发介入,即可在页面内嵌入完整的自然语言查数能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// xxx 组件注册
export default defineComponent({
name: "TextToSQLWidget",
props: {
placeholder: { type: String, default: "请输入您想查询的数据,例如:本月各渠道放款金额" },
defaultChartType: { type: String, default: "bar" },
},
setup(props) {
const query = ref("");
const result = ref(null);

async function handleQuery() {
result.value = await callBailianAgent({ query: query.value });
}

return () => (
<div class="text-to-sql-widget">
<Input v-model={query.value} placeholder={props.placeholder} onPressEnter={handleQuery} />
{result.value && <ChartRenderer data={result.value} chartType={props.defaultChartType} />}
</div>
);
},
});

组件对外暴露 placeholderdefaultChartType 两个配置项,编辑器内可视化调整,真正做到拖拽即用、配置即得


五、系统集成与稳定性

完整调用链路: 钉钉 Robot / 业务页面 → 百炼 API → 业务数据库

关键保障措施:

  • SSO 鉴权:通过钉钉 userId 映射企业内部权限,不同角色只能查看授权范围内的数据
  • 异常熔断:SQL 执行超时(>10s)自动降级,返回友好提示并触发告警
  • 查询限流:单用户每分钟最多 10 次,防止数据库压力过大
1
2
3
4
5
6
7
8
@circuit_breaker(timeout=10, fallback="查询超时,请稍后重试或联系数据团队")
@rate_limit(max_calls=10, period=60)
def safe_execute(sql: str, user_id: str) -> dict:
check_permission(user_id, sql)
valid, msg = validate_sql(sql)
if not valid:
raise ValueError(msg)
return execute_query_and_render(sql)

效果对比

指标 上线前 上线后
单次查询耗时 5~10 分钟 平均 30 秒
SQL生成准确率 60% 90%
运营查数占比 ~10% ~85%
BI介入次数 每日 20+ 次 每日 2~3 次

总结

这套方案的核心价值在于打通了从自然语言到数据洞察的完整链路,并通过多端接入降低了使用门槛。
虽然本身难度不高,因为很多组件和能力百炼平台都已经提供了,但在实际落地过程中,还是给了我不少启发:

  • Schema 上下文 + Few-Shot:解决业务语义理解难题,是准确率跃升的关键
  • 多轮对话澄清:让模糊意图变得可处理,交互体验更接近真人分析师
  • 安全校验前置:在贷款敏感数据场景下守住安全底线
  • 组件化:将 AI 能力下沉为低代码积木,运营团队拖拽即用,彻底消除技术门槛

后续计划引入查询语义缓存(相同意图直接命中缓存)和定时报表订阅(关键指标自动推送),不过这都是后话了。