20 KiB
MySQL 智能问数设计
日期:2026-05-15 状态:Draft v2 Owner:与 lixin 共识 范围:MySQL 只读数据源配置、Agent Tool、跨表 JOIN、问数 Prompt Skill
0. v2 架构修订
v2 根据系统架构 review 收紧了安全边界:
mysql_queryMVP 只允许SELECT,不再允许自由SHOW、DESCRIBE、EXPLAIN。- schema、索引、外键和少量样例通过受控工具读取,不通过自由 SQL 读取。
- 增加表级白名单、表级黑名单和列级脱敏配置。
- 增加 JOIN 成本保护:查询超时、最大 JOIN 表数、禁止无条件 JOIN、结果行数硬上限。
- 增加连接池生命周期设计。
- 增加查询审计表。
- 明确新增公共
SecretCryptoService,不复用当前 SkillSecretService 的明文写入行为。 - 区分后台安全投影和 Agent 工具投影,
mysql_list_sources不向 Agent 暴露 host、username、passwordEncrypted。 - 对显式
LIMIT做硬上限校验,禁止用超大 LIMIT 绕过结果限制。 - MVP 对
maskedColumns采取保守策略:自由 SQL 直接引用脱敏列时拒绝执行,避免 alias 绕过。
1. 背景与目标
Neta 当前已经有完整的 Agent Tool 治理、Skill Runtime 和 Agent 配置体系。智能问数能力需要同时满足两类需求:
- 平台能力:配置 MySQL 连接、加密保存密码、控制哪些 Agent 可用、执行只读 SQL、限制超时和返回行数。
- 问数流程:让 Agent 先理解 schema,必要时澄清业务口径,再生成 SQL,最后解释结果、口径和限制。
因此本功能采用“Tool 做执行能力,Skill 做问数方法论”的组合方案。MySQL 连接和 SQL 执行进入 NetaClaw Tool 系统;问数策略进入一个 prompt skill,供需要问数的 Agent 绑定。
2. 非目标
第一版不实现以下能力:
- 不支持 PostgreSQL、SQLite、SQL Server、Oracle 等其他数据库。
- 不执行 INSERT、UPDATE、DELETE、CREATE、ALTER、DROP、TRUNCATE、CALL、事务语句或存储过程。
- 不做可视化 BI 图表、不新增复杂 dashboard。
- 不做自然语言到 SQL 专用模型微调。
- 不自动优化复杂 SQL,只提供 schema、样例和执行反馈,让 Agent 在只读约束内生成 SQL。
- 不跨多个数据源做 JOIN;跨表 JOIN 仅限同一个 MySQL 数据源和当前连接可访问的库。
- 第一版不允许用户通过
mysql_query自由执行SHOW、DESCRIBE、EXPLAIN;这些元信息由mysql_schema等受控工具提供。
3. 用户体验
管理员在后台新增 MySQL 数据源,填写名称、连接地址、端口、数据库名、账号和密码。密码只保存加密值,不在接口回显明文。
管理员在 Agent 配置中启用 mysql toolset,并按需绑定 data-analyst-mysql skill。用户在对话中问“上个月各门店销售额是多少”“按客户和订单表统计复购率”这类问题时,Agent 会先读取 schema,必要时读取表样例,再执行只读 SQL。回答中需要包含:
- 结论。
- 使用的 SQL。
- 关联口径,尤其是跨表 JOIN 的关联字段。
- 结果限制,例如采样、LIMIT、字段缺失或关联字段为推断关系。
4. 架构
整体架构沿用 NetaClaw 当前工具链路:
netaclaw_data_source + netaclaw_data_source_query_audit
-> SecretCryptoService + DataSourceService 授权过滤
-> MysqlPoolManager + MysqlQueryService / MysqlIntrospectionService
-> mysql_list_sources / mysql_schema / mysql_table_sample / mysql_query
-> Tool Catalog + Tool Governance + Tool Resolver
-> Agent Runtime
-> data-analyst-mysql Skill 约束问数流程
新增能力分为四层:
- 数据源配置层:保存 MySQL 连接和 Agent 授权。
- MySQL 执行层:封装连接池、schema 查询、样例读取和只读 SQL 执行。
- Tool 层:向 Agent 暴露稳定工具接口,并接入 catalog、resolver、runtime policy。
- Skill 层:定义问数流程、JOIN 推理规则和回答格式。
5. 数据模型
新增实体 NetaClawDataSourceEntity,表名 netaclaw_data_source。
字段:
| 字段 | 类型 | 说明 |
|---|---|---|
name |
varchar(100), unique | 数据源唯一名,工具调用使用 |
label |
varchar(200) | 展示名 |
type |
varchar(20) | 固定为 mysql |
host |
varchar(255) | MySQL host |
port |
int | 默认 3306 |
database |
varchar(128) | 默认库名 |
username |
varchar(255) | 连接用户名 |
passwordEncrypted |
text | AES-256-GCM 加密密码 |
readonly |
int | 1 表示只读,MVP 固定为 1 |
status |
int | 0 禁用,1 启用 |
allowedAgentIds |
json | 为空表示不授权任何 Agent;数组内 Agent 可用 |
extra |
json | SSL、connectTimeout、queryTimeout、maxRows、allowedTables、blockedTables、maskedColumns、maxJoinTables 等扩展 |
extra 权限字段:
| 字段 | 说明 |
|---|---|
allowedTables |
可访问表白名单,空数组表示默认不开放任何业务表 |
blockedTables |
表黑名单,优先级高于白名单 |
maskedColumns |
需要脱敏的列,格式为 `{ "table.column": "hash |
schemaVisibility |
allowed-only 或 all-names-only,MVP 默认 allowed-only |
queryTimeoutMs |
单次 SQL 超时,默认 10000,硬上限 30000 |
maxRows |
默认 200,硬上限 1000 |
maxJoinTables |
默认 4,硬上限 6 |
poolConnectionLimit |
默认 3,硬上限 10 |
密码必须用新增公共 SecretCryptoService 加密保存。当前 SkillSecretService.encrypt() 新写入路径是 JSON 明文,不能作为数据源密码的加密实现直接复用。
服务端必须区分两种投影:
- 管理端安全投影:可返回
host、port、database、username、hasPassword、授权与权限配置,但不返回passwordEncrypted或明文密码。 - Agent 工具投影:
mysql_list_sources只返回name、label、database、status,不返回host、username、passwordEncrypted、权限细节或连接串。
新增实体 NetaClawDataSourceQueryAuditEntity,表名 netaclaw_data_source_query_audit。
字段:
| 字段 | 类型 | 说明 |
|---|---|---|
dataSourceId |
int | 数据源 ID |
agentId |
int | 调用 Agent |
userId |
int | 当前用户,可为空 |
toolCallId |
varchar(100) | 工具调用 ID |
sqlHash |
varchar(64) | SQL SHA-256 |
sqlPreview |
text | 截断后的 SQL 摘要,不含结果集 |
status |
varchar(20) | success / rejected / failed |
rejectReason |
varchar(100) | 被 guard 拒绝原因 |
elapsedMs |
int | 耗时 |
rowCount |
int | 返回行数 |
errorCode |
varchar(50) | MySQL errno / SQLSTATE,可为空 |
6. 后端服务
新增 SecretCryptoService:
encryptJson(value: Record<string, string>): stringdecryptJson(ciphertext: string): Record<string, string>encryptText(value: string): stringdecryptText(ciphertext: string): string- 使用 AES-256-GCM,密钥来自
NETA_SECRET_KEY,开发环境可回退到APP_SECRET。 - 密文必须带版本化 envelope,例如 base64(JSON.stringify({ v: 1, alg: 'aes-256-gcm', iv, tag, ct })),便于后续密钥轮换、格式识别和 Skill secret 迁移。
- 新数据源密码只允许写入加密格式;Skill secret 的现有兼容逻辑可后续迁移到该公共服务。
新增 DataSourceService:
listForAgent(agentId: number): Promise<AgentDataSourceSummary[]>listAdminSafe(): Promise<AdminSafeDataSource[]>getAuthorizedSource(name: string, agentId: number): Promise<NetaClawDataSourceEntity>saveConfig(input): Promise<AdminSafeDataSource>testConnection(input): Promise<{ ok: boolean; error?: string }>- 应用
allowedAgentIds、allowedTables、blockedTables、maskedColumns和schemaVisibility。 - 编辑已有数据源并测试连接时,如果没有传新密码,必须从数据库读取旧
passwordEncrypted,不能依赖接口回显密文。
新增 MysqlPoolManager:
getPool(source): Promise<Pool>- 按
dataSource.id缓存连接池。 - 默认
connectionLimit=3,由extra.poolConnectionLimit覆盖但不得超过 10。 - 保存、禁用或删除数据源时关闭对应 pool。
- 连接失败时不泄漏 host、username、password,只返回脱敏错误。
新增 MysqlIntrospectionService:
listSchema(source, options): Promise<MysqlSchemaResult>- 读取
information_schema.columns、statistics、key_column_usage。 - 只返回授权表。字段输出要应用
maskedColumns标记,让 Agent 知道该列敏感;MVP 中查询或样例请求直接引用该列时由后端拒绝,后续具备可靠列血缘解析后再支持脱敏展示。
新增 MysqlQueryService:
executeReadOnly(source, sql, options): Promise<MysqlQueryResult>- 使用
mysql2/promise。 - 每次查询设置超时、最大行数和结果截断。
SELECT查询若没有LIMIT,外层包裹为SELECT * FROM (<sql>) AS neta_limited_query LIMIT ?。- 显式
LIMIT必须小于等于maxRows;超过上限直接拒绝或重写为安全上限,不能先拉取大结果再在 Node 中slice。 - 执行前调用 SQL guard、授权表检查和 JOIN 成本检查。
- 自由 SQL 引用
maskedColumns时,MVP 直接拒绝执行,避免SELECT email AS e绕过脱敏;后续只有在引入 SQL parser 并能追踪 source column 到 output alias 后,才允许查询脱敏列并在返回前脱敏。 - 执行错误必须通过
sanitizeMysqlError(err)脱敏,只返回code、sqlState、errno和固定短文案,不返回原始err.message。 - 执行后写 query audit,不记录结果集。
7. Tool 设计
新增 toolset:mysql。
mysql_list_sources
输入:
{}
输出:
{
"sources": [
{
"name": "sales_prod",
"label": "销售生产库",
"database": "sales",
"status": 1
}
]
}
只返回当前 Agent 授权的数据源摘要,不返回 host、username、passwordEncrypted、明文密码、连接串或表/列权限细节。
mysql_schema
输入:
{
"source": "sales_prod",
"tables": ["orders", "customers"]
}
tables 可选。为空时返回表摘要和字段摘要;指定时返回详细字段、索引和外键。
输出包含:
tables[].nametables[].commenttables[].columns[]tables[].primaryKey[]tables[].indexes[]tables[].foreignKeys[]
mysql_table_sample
输入:
{
"source": "sales_prod",
"table": "orders",
"columns": ["id", "customer_id", "created_at"],
"limit": 5
}
用于辅助 Agent 判断字段含义和 JOIN key。limit 最大 20。表名和列名只能来自 introspection 结果,后端需要做 identifier 校验和反引号转义。
mysql_table_sample 不能通过拼接自由 SQL 绕过权限。后端必须先校验:
table在allowedTables内且不在blockedTables内。columns均来自mysql_schema可见字段。- 如果请求列命中
maskedColumns,MVP 返回拒绝错误;后续可加入专门脱敏样例能力。
mysql_query
输入:
{
"source": "sales_prod",
"sql": "SELECT c.name, COUNT(*) AS order_count FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.name ORDER BY order_count DESC LIMIT 20"
}
允许:
SELECT- 单数据源内的跨表 JOIN
禁止:
- 多语句。
- SQL 注释,包括
--、#、/* ... */和 MySQL 版本注释。 - DML、DDL、DCL、事务和存储过程。
SELECT ... INTO OUTFILE、LOAD_FILE、SLEEP、BENCHMARK。SHOW、DESCRIBE、DESC、EXPLAIN自由 SQL。UNION、CTE、用户变量和临时表。- 无限制返回大结果集。
输出:
{
"columns": ["name", "order_count"],
"rows": [
{ "name": "A 客户", "order_count": 12 }
],
"rowCount": 1,
"truncated": false,
"elapsedMs": 38,
"sql": "SELECT ..."
}
8. SQL 安全策略
安全校验采用保守白名单规则。不要把 Skill prompt 当作安全边界,所有强制规则必须在 Tool/Service 层实现。
- 第一版优先采用 SQL parser 或词法 token guard;如果没有稳定 parser,就采用更严格的拒绝策略。
- 只允许单条
SELECT。 - 拒绝 SQL 注释,避免版本注释和注释夹带绕过。
- 拒绝包含分号的多语句。
- 拒绝非白名单开头语句。
- 拒绝
UNION、CTE、INTO、用户变量、临时表、危险函数和文件访问能力。 - 查询涉及的表必须全部落在
allowedTables内,并且不在blockedTables内。 - SQL guard 必须逐个检查
JOIN片段:每个显式JOIN在下一个JOIN、WHERE、GROUP BY、ORDER BY、LIMIT或语句结束前都必须出现ON或USING。 - 自由 SQL 引用
maskedColumns时直接拒绝,避免 alias 绕过。 SELECT必须限制行数;无 LIMIT 时后端包裹追加限制。- 显式 LIMIT 必须不超过
maxRows。 - 查询超时默认 10 秒。
- 最大返回行数默认 200,硬上限 1000。
- 默认最大 JOIN 表数 4,硬上限 6。
- 拒绝无
ON/USING的显式 JOIN,避免笛卡尔积。 - 错误信息只返回 SQLSTATE、errno/code 和固定短文案,不返回原始连接错误、连接密码、host、username 或完整连接串。
- 每次查询无论成功、失败或被拒绝,都写入审计记录。
这层只保证工具层安全,不负责判断业务 SQL 是否“正确”。业务口径由 Skill 和 Agent 交互处理。
9. 跨表 JOIN 策略
跨表 JOIN 是 MVP 能力。Agent 可生成多表 JOIN、LEFT JOIN 和聚合查询,但必须遵循 Skill 约束:
- 先调用
mysql_schema获取相关表字段、索引和外键。 - 有外键时优先使用外键关系。
- 无外键时,可根据字段名、字段类型、索引和样例值推断关联字段。
- 推断关联必须在回答中标明,例如“orders.customer_id 与 customers.id 为字段名和样例推断关联”。
- 对不确定的关联关系或业务口径,必须先向用户澄清。
- JOIN 查询仍受
mysql_query的只读、超时、LIMIT 和最大行数限制。 - JOIN 查询不得超过
maxJoinTables。 JOIN必须带ON或USING,不允许隐式逗号连接。- 每个
JOIN都必须有自己的ON或USING条件,不能只检查整条 SQL 是否出现过一次ON。 - 如果查询因超时或成本限制失败,Agent 应缩小时间范围、减少表数量、添加过滤条件或向用户澄清。
10. Skill 设计
新增 prompt skill:packages/backend/skills/data-analyst-mysql/SKILL.md。
Frontmatter:
---
name: data-analyst-mysql
description: 使用 MySQL 工具进行只读智能问数,支持 schema 分析、跨表 JOIN、口径澄清和 SQL 结果解释。
version: 1.0.0
metadata:
skillType: llm
tags: [mysql, data-analysis, question-answering]
conditions:
requires_tools: ["mysql_list_sources", "mysql_schema", "mysql_query"]
---
Skill 指令要求:
- 先确认可用数据源。
- 先读取相关 schema,再写 SQL。
- 需要 JOIN 时先找外键、索引和样例值。
- 跨表 JOIN 不确定时先解释拟采用的关联字段,必要时澄清。
- 不确定业务口径时使用
clarify。 - 只执行只读 SQL。
- 回答必须包含结论、SQL、口径说明和限制。
- 不暴露连接地址、账号、密码。
- 不承诺“全量统计”除非 SQL 口径和过滤范围明确。
11. Tool Resolver 和 Runtime Policy
tools/catalog.ts 新增 TOOLSET_MYSQL = 'mysql',并集中 import MySQL tool 文件。
MySQL 工具注册:
visibility: 'tool'capability: 'text'isCore: falsecanDisable: true
主 Agent 可直接执行 MySQL 工具。子 Agent 默认不在 worker 本地执行 MySQL 工具,runtime policy 应将这类工具路由为 main-process-proxy 或 disabled。推荐第一版走 main-process-proxy,避免子进程持有数据库密钥。
12. 管理接口
新增 admin controller:controller/admin/data_source.ts。
接口:
GET /admin/netaclaw/data-source/listPOST /admin/netaclaw/data-source/savePOST /admin/netaclaw/data-source/testPOST /admin/netaclaw/data-source/delete
保存接口接受明文密码,但只用于加密保存。若编辑时密码为空,则保留原密码。
Agent 编辑页后续可接入授权配置,但 MVP 后端先支持 allowedAgentIds 字段和接口。
13. 测试策略
测试采用 TDD。
优先新增单元测试:
mysql_sql_guard.test.ts:验证只读 SQL 白名单、危险关键字、多语句、SELECT INTO OUTFILE、SLEEP拒绝。mysql_sql_guard.test.ts还要验证SHOW、DESCRIBE、EXPLAIN、注释、UNION、CTE、用户变量、无条件 JOIN、部分 JOIN 缺少 ON/USING、显式 LIMIT 超过 maxRows、查询脱敏列、超过最大 JOIN 表数被拒绝。mysql_schema_mapper.test.ts:验证 information_schema 行映射为表、字段、索引和外键结构。mysql_query_service.test.ts:用 mock mysql pool 验证 LIMIT 包裹、超时参数、结果截断和错误脱敏。mysql_query_service.test.ts还要验证表级授权、脱敏列拒绝、审计写入、pool 复用和mysql_table_sampleidentifier 白名单。secret_crypto.test.ts:验证数据源密码使用 AES-GCM 加密,密文不包含明文密码。data_source_projection.test.ts:验证管理端投影不返回密码,Agent 投影不返回 host/username/passwordEncrypted。tool_resolver.test.ts:验证mysqltoolset 可以被 Agent 启用,并在子 Agent runtime profile 中不走 worker-local。entity_exports.test.ts:验证新增 entity 导出。
不在单元测试里连接真实 MySQL。真实连接测试通过 admin test 接口和后续手工环境验证完成。
14. 分阶段交付
Phase 1:后端最小闭环
- 新增数据源 entity、公共 secret crypto service、data source service。
- 新增 query audit entity。
- 新增公共
SecretCryptoService。 - 新增
MysqlPoolManager。 - 新增 MySQL SQL guard、schema mapper、query service。
- 新增四个工具:
mysql_list_sources、mysql_schema、mysql_table_sample、mysql_query。 - 接入 catalog、resolver、runtime policy。
- 新增
data-analyst-mysqlprompt skill。 - 补充单元测试。
Phase 2:管理端配置
- 新增数据源管理接口。
- 前端工具管理或 Agent 编辑页接入数据源授权。
- 支持测试连接和编辑密码。
Phase 3:问数体验增强
- 根据真实问数记录优化 Skill。
- 增加 query result 前端结构化渲染。
- 增加审计日志查询页和慢查询提示。
15. 验收标准
- 管理员可以配置一个 MySQL 数据源并授权给指定 Agent。
- 数据源密码以 AES-GCM 密文保存,接口不回显密文或明文。
- 启用
mysqltoolset 的 Agent 能列出授权数据源。 - Agent 只能读取授权表的 schema、字段、索引和外键。
- Agent 能读取授权表的少量样例。
- Agent 能执行只读 SELECT 查询。
- Agent 能执行同一数据源、授权表范围内的跨表 JOIN 查询。
SHOW、DESCRIBE、EXPLAIN、多语句、注释、写入语句、无条件 JOIN 和超出 JOIN 表数限制的 SQL 被拒绝。- 查询或样例请求命中
maskedColumns时,MVP 后端拒绝执行,避免 alias 绕过;后续如引入 SQL parser 再实现安全脱敏返回。 - 每次查询写入审计记录。
- 危险 SQL 被拒绝,错误原因可读且不泄漏连接秘密。
- Agent 绑定
data-analyst-mysqlskill 后,回答包含结论、SQL、口径和限制。 - 单元测试覆盖 SQL guard、schema mapper、query service、secret crypto、pool manager、tool resolver 和 entity 导出。