NL2SQL 正确率怎么提升:ChatBI 的 <error-msg> 错误反馈闭环
标签:
NL2SQLText2SQLLLMPrompt EngineeringChatBI
背景
Text-to-SQL 工程里,LLM 生成的 SQL 常见三类错误:
| 类型 | 示例 |
|---|---|
| 语法错误 | 关键字拼错、引号/方言不匹配 |
| 语义错误 | 字段不存在、JOIN 条件错误 |
| 权限错误 | 访问了不该碰的表 |
ChatBI 在首轮生成前,已通过 M-Schema 表结构、术语库、SQL 样例训练降低错误率。但对复杂查询,仍需要第二层保障:执行 → 失败 → 捕获原生报错 → 注入 Prompt → 再生成。
整体架构
flowchart TD
A[用户提问] --> B[LLM 生成 SQL]
B --> C[exec_sql 执行]
C --> D{结果}
D -->|成功| E[返回数据]
D -->|失败| F[save_error exec-sql-err]
F --> G[用户再次提问]
G --> H[get_last_execute_sql_error]
H --> I[注入 error-msg]
I --> B
核心实现
1. 执行失败:捕获并结构化存储
backend/apps/chat/task/llm.py:
python
elif isinstance(e, ChatBIDBError):
error_msg = orjson.dumps({
'message': 'Execute SQL Failed',
'traceback': str(e),
'type': 'exec-sql-err'
}).decode()
self.save_error(message=error_msg)
traceback 是数据库原生报错,不是 LLM 生成的解释。
2. 下轮读取:只取 exec-sql-err
backend/apps/chat/curd/chat.py:
python
def get_last_execute_sql_error(session, chart_id):
obj = orjson.loads(res)
if obj.get('type') == 'exec-sql-err':
return obj.get('traceback')
return None
连接错误(db-connection-err)不会进入 SQL 修正闭环。
3. Prompt 注入
llm.py 初始化时:
python
if last_execute_sql_error:
self.chat_question.error_msg = f'''<error-msg>
{last_execute_sql_error}
</error-msg>'''
backend/template.yaml 用户 Prompt:
yaml
user: |
<background-infos>
<current-time>{current_time}</current-time>
</background-infos>
{error_msg}
<user-question>{question}</user-question>
System Prompt 已说明:<error-msg> 提供上次执行 SQL 的错误信息。
4. 多轮上下文控制
- 历史消息通过
generate_sql_logs保留 base_message_count_limit = 6限制上下文,控制 token- 当前为用户触发的多轮修正,非服务端静默自动重试
时序图
sequenceDiagram
participant U as 用户
participant FE as 前端
participant LLM as LLMService
participant DB as 数据库
participant Store as ChatRecord
U->>FE: 提问
FE->>LLM: 发起问数请求
LLM->>LLM: 读取上次 exec-sql-err
LLM->>LLM: 组装 Prompt(含 error-msg)
LLM->>LLM: 生成 SQL
LLM->>DB: exec_sql
DB-->>LLM: 执行失败 + 原生报错
LLM->>Store: save_error(traceback)
LLM-->>FE: 返回错误
FE-->>U: 展示错误详情
U->>FE: 再次提问
Note over LLM,Store: 下轮自动带上 error-msg
错误分类策略
flowchart LR
A[异常] --> B{类型}
B -->|连接| C[db-connection-err → 提示检查连接]
B -->|执行| D[exec-sql-err → 注入 error-msg]
B -->|解析| E[ParseSQLResultError → 单独处理]
B -->|业务| F[SingleMessageError → 直接返回]
| 错误 | 处理 |
|---|---|
syntax error at or near |
修正关键字/引号,1--2 轮通常可解决 |
column does not exist |
回查 M-Schema,可能需换字段或补 JOIN |
permission denied |
不重试,直接降级提示 |
前端 ErrorInfo.vue 区分展示:db-connection-err 显示数据源无效,exec-sql-err 显示执行失败并可展开 traceback。
重试上限与降级(演进方向)
现状:用户驱动 + 6 条历史上限,无自动重试计数器。
建议演进:
python
MAX_AUTO_RETRY = 3
RETRYABLE = ['syntax error', 'does not exist', 'unknown column']
NON_RETRYABLE = ['permission denied', 'access denied']
def should_retry(error: str, count: int) -> bool:
if count >= MAX_AUTO_RETRY:
return False
if any(k in error.lower() for k in NON_RETRYABLE):
return False
return any(k in error.lower() for k in RETRYABLE)
超限降级:友好提示 + 展示最后 SQL + 推荐简化问题。
设计 Checklist
- 捕获原生报错 --- 不用 LLM 翻译错误
- 结构化存储 --- JSON 区分
exec-sql-err/db-connection-err - 专用标签注入 ---
<error-msg>与用户问题分离 - 按类型分流 --- 权限/连接错误不进修正闭环
- 重试上限 + 降级 --- 避免无限烧 token
相关代码路径
| 模块 | 路径 |
|---|---|
| 错误注入 | backend/apps/chat/task/llm.py |
| 错误读取 | backend/apps/chat/curd/chat.py |
| Prompt 模板 | backend/template.yaml |
| 异常定义 | backend/common/error.py |
| 前端错误展示 | frontend/src/views/chat/ErrorInfo.vue |
总结
NL2SQL 工程化的核心,是把正确SQL从训练数据搬到运行时:数据库说错了什么,就把什么喂回模型。
ChatBI 的 <error-msg> 闭环不复杂,但抓住了「错了能改、改时有据」这个关键点。