基于源码版本:1.0.0-SNAPSHOT,核心代码位置
workflow/node/SqlGenerateNode.java(211行)+workflow/node/SemanticConsistencyNode.java(123行)+workflow/node/SqlExecuteNode.java(307行)+resources/prompts/new-sql-generate.txt+resources/prompts/semantic-consistency.txt+resources/prompts/sql-error-fixer.txt贯穿示例:用户输入「统计上月各部门销售额」
一、模块定位与架构
1.1 在执行链路中的位置
SQL执行步骤是 PlanExecutor 路由的三种步骤类型之一,当 Planner 生成的计划中某个步骤的 toolToUse = "SQL_GENERATE_NODE" 时,PlanExecutor 就会路由到 SQL 执行步骤。
重要前提 :SQL步骤不是独立存在的,它是 Plan 计划中的一个步骤。只有 Planner 生成的计划里包含 SQL_GENERATE_NODE 类型的步骤时,才会执行这条链路。PlanExecutor 负责按计划顺序调度,SQL步骤执行完后结果存入 state,然后回到 PlanExecutor 继续下一个步骤。
Planner(生成计划,计划中包含SQL步骤)
│
▼
PlanExecutor(读取当前步骤,toolToUse=SQL_GENERATE_NODE)
│
▼
┌─────────────────────────────────────────────────┐
│ SQL执行步骤(本文档) │
│ │
│ SQL生成 → 语义一致性校验 → SQL执行 │
│ │ │ 不通过 │ 失败 │
│ │ ▼ ▼ │
│ │ 重新生成SQL 重新生成SQL │
│ │ (最多N次) (最多N次) │
│ │ │
│ └─────────── 通过 ──────► 成功 ──► 结果存入state │
│ │ │
└──────────────────────────────────────────┼──────────┘
│
▼
PlanExecutor(下一步)
1.2 三个节点的职责
| 节点 | 行数 | 职责 |
|---|---|---|
SqlGenerateNode |
211 | 根据当前步骤的 instruction 生成 SQL,支持重试生成 |
SemanticConsistencyNode |
123 | 校验生成的 SQL 与用户需求的语义一致性,不通过则回到 SQL 生成 |
SqlExecuteNode |
307 | 执行 SQL,存储结果,生成图表配置,失败则回到 SQL 生成 |
1.3 与 Plan 和 PlanExecutor 的紧密关联
- 步骤来源 :SQL步骤的 instruction 来自 Planner 生成的 Plan 中当前步骤的
toolParameters.instruction,不是用户原始问题 - 步骤号维护 :
PLAN_CURRENT_STEP由 PlanExecutor 维护,SQL执行成功后递增 - 结果存储 :SQL执行结果以
step_N为 key 存入SQL_EXECUTE_NODE_OUTPUT,供后续 Python/报告步骤读取 - sql_query回填 :SQL生成后回填到 Plan 当前步骤的
toolParameters.sqlQuery字段 - 重试计数 :
SQL_GENERATE_COUNT记录当前步骤的 SQL 生成次数,超过最大次数则 END - 回到 PlanExecutor :SQL执行成功后通过
SQLExecutorDispatcher路由回PLAN_EXECUTOR_NODE,继续下一个步骤
二、完整执行链路(用「统计上月各部门销售额」展开)
2.1 前置状态
PlanExecutor 路由到 SQL 步骤时,state 中的关键状态:
PLAN_CURRENT_STEP = 1(当前是第1步)
PLANNER_NODE_OUTPUT = {
"execution_plan": [
{ "step": 1, "tool_to_use": "SQL_GENERATE_NODE",
"tool_parameters": { "instruction": "从 sales_order 表关联 department 表..." } },
{ "step": 2, "tool_to_use": "REPORT_GENERATOR_NODE", ... }
]
}
TABLE_RELATION_OUTPUT = 精选后的 Schema(sales_order + department)
EVIDENCE = 证据召回的输出
DB_DIALECT_TYPE = "mysql"
SQL_GENERATE_COUNT = 0
SQL_REGENERATE_REASON = empty()
2.2 第一步:SQL生成(SqlGenerateNode)
2.2.1 检查重试次数
java
int count = state.value(SQL_GENERATE_COUNT, 0);
if (count >= properties.getMaxSqlRetryCount()) {
// 超过最大重试次数,返回错误并 END
return Map.of(SQL_GENERATE_OUTPUT, generator); // generator 中设置 SQL_GENERATE_OUTPUT = END
}
本例:count = 0 < 最大次数 → 继续
2.2.2 获取当前步骤的 instruction
java
// 获取planner分配的当前执行步骤的sql任务要求,每个步骤的sql任务是不同的。
// 不要拿 user query 这个总体的大任务。
String promptForSql = getCurrentExecutionStepInstruction(state);
// promptForSql = "从 sales_order 表关联 department 表(sales_order.dept_id = department.dept_id),
// 筛选 ship_time >= '2026-07-01' 且 ship_time < '2026-08-01'、status = '已发货' 的记录,
// 按 department.dept_name 分组,返回 department.dept_name AS 部门名称 和 SUM(sales_order.amount) AS 销售总额。"
关键:这里用的是当前步骤的 instruction,不是用户原始问题。Planner 在生成计划时已经把大任务拆解成每个步骤的具体 SQL 需求。
2.2.3 判断是否为重试生成
java
SqlRetryDto retryDto = StateUtil.getObjectValue(state, SQL_REGENERATE_REASON, SqlRetryDto.class, SqlRetryDto.empty());
String failedSql = StateUtil.getStringValue(state, SQL_GENERATE_OUTPUT, "");
if (retryDto.sqlExecuteFail()) {
// SQL执行失败重试
sqlFlux = handleRetryGenerateSql(state, failedSql, retryDto.reason(), promptForSql);
} else if (retryDto.semanticFail()) {
// 语义校验失败重试
sqlFlux = handleRetryGenerateSql(state, failedSql, retryDto.reason(), promptForSql);
} else {
// 首次生成
sqlFlux = handleGenerateSql(state, promptForSql);
}
本例:retryDto = empty() → 首次生成 → handleGenerateSql
2.2.4 构建 SQL 生成参数
handleGenerateSql 调用 handleRetryGenerateSql(state, null, null, promptForSql):
java
private Flux<String> handleRetryGenerateSql(OverAllState state, String originalSql, String errorMsg, String executionDescription) {
String evidence = StateUtil.getStringValue(state, EVIDENCE);
SchemaDTO schemaDTO = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
String userQuery = StateUtil.getCanonicalQuery(state);
String dialect = StateUtil.getStringValue(state, DB_DIALECT_TYPE);
String previousStepResults = buildPreviousStepResults(state); // 前序步骤结果,本例 currentStep=1 → "无"
SqlGenerationDTO sqlGenerationDTO = SqlGenerationDTO.builder()
.evidence(evidence)
.query(userQuery)
.schemaDTO(schemaDTO)
.previousStepResults(previousStepResults)
.sql(originalSql) // 首次生成时为 null
.exceptionMessage(errorMsg) // 首次生成时为 null
.executionDescription(executionDescription) // 当前步骤的 instruction
.dialect(dialect)
.build();
return nl2SqlService.generateSql(sqlGenerationDTO);
}
2.2.5 LLM 生成 SQL
nl2SqlService.generateSql 使用 new-sql-generate.txt 模板调用 LLM,生成 SQL。
本例生成的 SQL:
sql
SELECT d.dept_name AS 部门名称, SUM(o.amount) AS 销售总额
FROM sales_order o
JOIN department d ON o.dept_id = d.dept_id
WHERE o.ship_time >= '2026-07-01' AND o.ship_time < '2026-08-01' AND o.status = '已发货'
GROUP BY d.dept_name
ORDER BY 销售总额 DESC
2.2.6 防重复执行检测
java
private boolean isUnchangedExecutionRetry(SqlRetryDto retryDto, String failedSql, String generatedSql) {
return retryDto.sqlExecuteFail() && StringUtils.isNotBlank(failedSql) && StringUtils.isNotBlank(generatedSql)
&& normalizeSql(failedSql).equals(normalizeSql(generatedSql));
}
如果是执行失败重试,且新生成的 SQL 和失败的 SQL 完全相同(去掉末尾分号后比较),则停止重试,避免无限循环。
本例:首次生成,不触发此检测。
2.2.7 写入 state
java
Map<String, Object> result = new HashMap<>(Map.of(
SQL_GENERATE_OUTPUT, StateGraph.END, // 临时值,后面会被覆盖
SQL_GENERATE_COUNT, count + 1, // 0 → 1
SQL_REGENERATE_REASON, SqlRetryDto.empty() // 清空重试原因
));
// 流式输出完成后,设置 SQL_GENERATE_OUTPUT = 生成的SQL
result.put(SQL_GENERATE_OUTPUT, sql);
2.3 第二步:语义一致性校验(SemanticConsistencyNode)
2.3.1 结构校验(非 LLM)
java
Optional<String> structuralValidationError = SqlUtil.findGeneratedSqlValidationError(sql, dialect);
if (structuralValidationError.isPresent()) {
return buildStructuralValidationFailure(state, sql, structuralValidationError.get());
}
先做 SQL 结构校验(语法、写操作检测等),不通过则直接回到 SQL 生成,不调用 LLM。
本例:SQL 结构合法 → 继续
2.3.2 构建语义校验参数
java
SemanticConsistencyDTO semanticConsistencyDTO = SemanticConsistencyDTO.builder()
.dialect(dialect)
.sql(sql) // 生成的 SQL
.executionDescription(getCurrentExecutionStepInstruction(state)) // 当前步骤的 instruction
.schemaInfo(buildMixMacSqlDbPrompt(schemaDTO, true)) // Schema
.userQuery(userQuery) // 规范化查询
.evidence(evidence) // 证据
.build();
2.3.3 LLM 语义校验
使用 semantic-consistency.txt 模板调用 LLM,输出 SemanticConsistencyOutputDTO:
isPassed:是否通过reason:不通过的原因
本例:SQL 与 instruction 语义一致 → isPassed = true
2.3.4 校验结果处理
java
private Map<String, Object> buildValidationResult(boolean passed, String validationResult) {
if (passed) {
return Map.of(SEMANTIC_CONSISTENCY_NODE_OUTPUT, true);
} else {
return Map.of(
SEMANTIC_CONSISTENCY_NODE_OUTPUT, false,
SQL_REGENERATE_REASON, SqlRetryDto.semantic(validationResult) // 设置语义失败原因
);
}
}
- 通过 →
SEMANTIC_CONSISTENCY_NODE_OUTPUT = true→ 进入 SQL 执行 - 不通过 → 设置
SQL_REGENERATE_REASON = semantic(reason)→ 回到 SQL 生成(重试)
本例:通过 → 进入 SQL 执行
2.4 第三步:SQL执行(SqlExecuteNode)
2.4.1 获取 SQL 和数据源配置
java
Integer currentStep = PlanProcessUtil.getCurrentStepNumber(state); // 1
String sqlQuery = StateUtil.getStringValue(state, SQL_GENERATE_OUTPUT); // 生成的 SQL
sqlQuery = nl2SqlService.sqlTrim(sqlQuery); // 去掉代码围栏、末尾分号等
String agentIdStr = StateUtil.getStringValue(state, Constant.AGENT_ID);
Long agentId = Long.valueOf(agentIdStr);
DbConfigBO dbConfig = databaseUtil.getAgentDbConfig(agentId); // 动态获取数据源配置
2.4.2 执行业务逻辑(先执行 SQL,再做流式输出)
java
// 业务逻辑优先:先实际执行 SQL
Mono<ExecutedSqlResult> sqlExecution = Mono.fromCallable(() ->
executeAndStoreResult(state, currentStep, sqlQuery, dbConfig, dbQueryParameter, dbAccessor, result)
).subscribeOn(Schedulers.boundedElastic());
设计亮点:采用"业务逻辑优先"模式------先实际执行 SQL 并存储结果,再创建流式输出给用户看。这样即使流式输出中断,业务结果已经存好了。
2.4.3 executeAndStoreResult(核心)
java
private ExecutedSqlResult executeAndStoreResult(...) throws Exception {
// 1. 执行 SQL
ResultSetBO resultSetBO = dbAccessor.executeSqlAndReturnObject(dbConfig, dbQueryParameter);
String strResultSetJson = JsonUtil.getObjectMapper().writeValueAsString(resultSetBO);
// 2. 写入 state
result.put(SQL_REGENERATE_REASON, SqlRetryDto.empty()); // 清空重试原因
result.put(SQL_RESULT_LIST_MEMORY, resultSetBO.getData()); // 结果数据(供Python读取)
result.put(PLAN_CURRENT_STEP, currentStep + 1); // 1 → 2,步骤号递增
result.put(SQL_GENERATE_COUNT, 0); // 重置SQL生成计数
// 3. 存入步骤结果集合
Map<String, String> existingResults = StateUtil.getObjectValue(state, SQL_EXECUTE_NODE_OUTPUT, Map.class, new HashMap<>());
Map<String, String> updatedResults = PlanProcessUtil.addStepResult(existingResults, currentStep, strResultSetJson);
result.put(SQL_EXECUTE_NODE_OUTPUT, updatedResults);
// SQL_EXECUTE_NODE_OUTPUT = { "step_1": "{结果JSON}" }
// 4. 回填 sql_query 到 Plan 的当前步骤
ExecutionStep.ToolParameters currentStepParams = PlanProcessUtil.getCurrentExecutionStep(state).getToolParameters();
currentStepParams.setSqlQuery(sqlQuery);
}
本例执行结果:
step_1 = {
"columns": ["部门名称", "销售总额"],
"data": [
["销售一部", 1250000.00],
["销售二部", 980000.00],
["销售三部", 760000.00]
]
}
PLAN_CURRENT_STEP = 2
2.4.4 图表配置生成
SQL 执行成功后,还会调用 LLM 生成图表配置信息(DisplayStyleBO),填充到 ResultSetBO 中,用于前端展示。
2.4.5 执行失败处理
java
.onErrorResume(e -> {
String errorMessage = e.getMessage();
result.put(SQL_REGENERATE_REASON, SqlRetryDto.sqlExecute(errorMessage)); // 设置执行失败原因
return Flux.just(ChatResponseUtil.createResponse("SQL执行失败: " + errorMessage));
});
执行失败时设置 SQL_REGENERATE_REASON = sqlExecute(errorMessage),然后通过 SQLExecutorDispatcher 回到 SQL 生成(重试)。
2.5 回到 PlanExecutor
SQL 执行成功后,SQLExecutorDispatcher 路由:
java
public String apply(OverAllState state) {
SqlRetryDto retryDto = StateUtil.getObjectValue(state, SQL_REGENERATE_REASON, SqlRetryDto.class);
if (retryDto.sqlExecuteFail()) {
return SQL_GENERATE_NODE; // 执行失败 → 重新生成 SQL
} else {
return PLAN_EXECUTOR_NODE; // 执行成功 → 回到 PlanExecutor
}
}
本例:执行成功 → 回到 PlanExecutor,PLAN_CURRENT_STEP = 2 → 路由到报告生成步骤。
三、SQL生成节点详解
3.1 new-sql-generate.txt 模板解析
模板分为 6 个部分:
| 部分 | 作用 |
|---|---|
# 角色 |
定义 LLM 为精通指定方言的 SQL 查询工程师 |
# 指令边界 |
防 Prompt 注入,Schema 是唯一依据 |
# 输入 |
6 个输入:Schema、Evidence、用户原始问题、当前执行步骤、前序步骤结果、方言 |
# SQL 约束 |
8 条约束:只读、表名列名必须存在、精确完成当前步骤、不使用 SELECT * 等 |
# 输出 |
只输出 SQL 语句本身 |
3.2 关键约束
- 只读限制 :只允许
SELECT或WITH ... SELECT,禁止 INSERT/UPDATE/DELETE/DDL 等 - Schema 唯一依据:所有表名、列名、关联关系必须存在于 Schema,不得猜测
- 精确完成当前步骤:不遗漏过滤条件,不额外解决其他步骤
- 半开区间 :时间列包含完整结束日时,优先使用
< 下一日 - 方言一致性:MySQL 用反引号,PG/Oracle 用双引号,SQL Server 用方括号
- 不输出末尾分号:为兼容执行器
3.3 前序步骤结果的使用
java
private String buildPreviousStepResults(OverAllState state) {
int currentStep = PlanProcessUtil.getCurrentStepNumber(state);
if (currentStep <= 1) return "无"; // 第一步没有前序结果
// 遍历 step_1 到 step_{currentStep-1},拼接结果
// 最多 8000 字符,超过则截断
}
如果当前步骤依赖前序 SQL 结果(比如第二步需要用第一步的结果做过滤),LLM 会使用前序结果中真实存在的列和值替换占位符。
3.4 重试生成的区别
首次生成和重试生成使用同一个模板,但重试时多传入两个参数:
sql:上一版失败的 SQLexceptionMessage:失败原因(执行错误或语义校验不通过原因)
LLM 会根据失败原因修正 SQL,而不是重新生成。
四、语义一致性校验节点详解
4.1 两层校验
语义一致性校验节点做了两层校验:
-
结构校验(非 LLM) :
SqlUtil.findGeneratedSqlValidationError(sql, dialect)- 语法检查
- 写操作检测(INSERT/UPDATE/DELETE 等)
- 不通过则直接回到 SQL 生成,不调用 LLM(节省成本)
-
语义校验(LLM) :使用
semantic-consistency.txt模板- 校验 SQL 是否与当前步骤的 instruction 语义一致
- 校验 SQL 是否与用户原始问题一致
- 输出
isPassed+reason
4.2 校验输入
语义校验传入 6 个参数:
dialect:数据库方言sql:生成的 SQLexecutionDescription:当前步骤的 instruction(核心对比对象)schemaInfo:Schema 信息userQuery:规范化查询(全局背景)evidence:业务知识
4.3 校验结果路由
- 通过 →
SEMANTIC_CONSISTENCY_NODE_OUTPUT = true→ 进入 SQL 执行 - 不通过 → 设置
SQL_REGENERATE_REASON = semantic(reason)→ 回到 SQL 生成(带原因重试)
五、SQL执行节点详解
5.1 业务逻辑优先模式
SqlExecuteNode 采用"业务逻辑优先"的设计模式:
1. 先实际执行 SQL(Mono.fromCallable,在 boundedElastic 线程池)
2. 处理和存储结果(写入 state)
3. 创建流式输出给用户看(Flux)
这样即使流式输出中断,业务结果已经存好了,不会丢失。
5.2 结果存储的 4 个状态 Key
SQL 执行成功后,写入 4 个状态 Key:
| Key | 值 | 作用 |
|---|---|---|
SQL_REGENERATE_REASON |
empty() |
清空重试原因 |
SQL_RESULT_LIST_MEMORY |
resultSetBO.getData() |
最近一次 SQL 结果数据(供 Python 步骤读取) |
PLAN_CURRENT_STEP |
currentStep + 1 |
步骤号递增,告诉 PlanExecutor 执行下一步 |
SQL_GENERATE_COUNT |
0 |
重置 SQL 生成计数 |
另外还有两个重要操作:
SQL_EXECUTE_NODE_OUTPUT:添加step_N结果到集合(供后续步骤读取所有历史结果)toolParameters.sqlQuery:回填生成的 SQL 到 Plan 的当前步骤
5.3 图表配置生成
SQL 执行成功后,还会调用 LLM 生成图表配置信息(DisplayStyleBO),包括:
- 图表类型(柱状图/折线图/饼图等)
- X 轴/Y 轴映射
- 排序方式
- 展示样式
这些配置填充到 ResultSetBO 中,用于前端自动渲染图表。
5.4 执行失败处理
执行失败时:
- 设置
SQL_REGENERATE_REASON = sqlExecute(errorMessage) - 流式输出错误信息给用户
SQLExecutorDispatcher检测到执行失败 → 路由回SQL_GENERATE_NODE(重试)
六、三层失败重试机制
SQL 执行步骤有三层失败重试,每一层失败都会回到 SQL 生成节点重新生成:
SQL生成
│
├─ 超过最大重试次数 → END(放弃)
│
▼
语义一致性校验
│
├─ 不通过 → SQL_REGENERATE_REASON = semantic(reason) → 回到 SQL生成
│
▼
SQL执行
│
├─ 失败 → SQL_REGENERATE_REASON = sqlExecute(error) → 回到 SQL生成
│
▼
成功 → 回到 PlanExecutor
6.1 第一层:SQL生成重试计数
SQL_GENERATE_COUNT 记录当前步骤的 SQL 生成次数,每次进入 SqlGenerateNode 递增。超过 properties.getMaxSqlRetryCount() 则 END。
注意:这个计数是每个步骤独立的,SQL 执行成功后重置为 0。
6.2 第二层:语义校验不通过
语义校验不通过时,设置 SQL_REGENERATE_REASON = semantic(reason),SqlGenerateNode 检测到 retryDto.semanticFail(),使用重试模式生成 SQL(传入失败 SQL + 原因)。
6.3 第三层:SQL执行失败
SQL 执行失败时,设置 SQL_REGENERATE_REASON = sqlExecute(errorMessage),SqlGenerateNode 检测到 retryDto.sqlExecuteFail(),使用重试模式生成 SQL。
6.4 防重复执行检测
如果是执行失败重试,且新生成的 SQL 和失败的 SQL 完全相同(normalizeSql 比较,去掉末尾分号),则停止重试,避免无限循环:
java
if (isUnchangedExecutionRetry(retryDto, failedSql, generatedSql)) {
result.put(SQL_GENERATE_OUTPUT, StateGraph.END); // 停止重试
}
七、结果传递机制
7.1 SQL结果如何传递给后续步骤
SQL 执行成功后,结果通过两个渠道传递:
-
SQL_EXECUTE_NODE_OUTPUT(Map) :所有步骤的历史结果集合,key =step_N,value = 结果 JSON- 报告生成节点读取所有步骤结果
- Python 生成节点读取前序步骤结果
-
SQL_RESULT_LIST_MEMORY(Object):最近一次 SQL 执行的结果数据- Python 执行节点直接读取这个字段
7.2 sql_query 回填
SQL 生成后,回填到 Plan 当前步骤的 toolParameters.sqlQuery 字段:
java
ExecutionStep.ToolParameters currentStepParams = PlanProcessUtil.getCurrentExecutionStep(state).getToolParameters();
currentStepParams.setSqlQuery(sqlQuery);
这样 Plan 中就记录了每个 SQL 步骤实际生成的 SQL,便于追溯和展示。
7.3 步骤号递增
PLAN_CURRENT_STEP 递增后,PlanExecutor 下一次进入时就会读取下一个步骤:
java
int currentStep = PlanProcessUtil.getCurrentStepNumber(state); // 递增后的值
ExecutionStep executionStep = executionPlan.get(currentStep - 1); // 下一个步骤
八、设计亮点
- 业务逻辑优先模式:先执行 SQL 存储结果,再做流式输出,即使输出中断也不丢失结果
- 三层重试机制:生成计数 + 语义校验 + 执行失败,每层失败都回到 SQL 生成,提高成功率
- 防重复执行检测:执行失败重试时,如果新 SQL 和旧 SQL 相同则停止,避免无限循环
- 结构校验前置:语义校验前先做非 LLM 的结构校验,节省成本
- 步骤级 instruction:SQL 生成使用当前步骤的 instruction,不是用户原始问题,确保每个步骤只完成自己的任务
- 前序结果注入:多步骤 SQL 时,前序结果注入到生成 Prompt 中,支持跨步骤依赖
- 结果双渠道传递:历史结果集合 + 最近结果内存,兼顾多步骤读取和单步快速访问
- sql_query 回填:生成的 SQL 回填到 Plan 中,便于追溯和前端展示
九、潜在问题
- 重试次数配置不透明 :
properties.getMaxSqlRetryCount()的默认值和配置方式需要查看配置类,文档中未明确 - 语义校验可能误判:LLM 语义校验可能误判正确的 SQL 为不通过,导致不必要的重试
- 前序结果截断:前序结果最多 8000 字符,超过则截断,可能导致后续步骤缺少必要数据
- 图表配置生成增加延迟:SQL 执行后还要调用 LLM 生成图表配置,增加响应时间
- 结构校验覆盖有限 :
SqlUtil.findGeneratedSqlValidationError的校验规则可能不全面,某些写操作可能漏检 - 重试时未区分失败类型:语义失败和执行失败都回到同一个 SQL 生成节点,虽然传入了原因,但处理逻辑相同
- PLAN_CURRENT_STEP 递增时机:在 SQL 执行成功后立即递增,如果后续图表配置生成失败,步骤号已经递增,可能导致状态不一致
- SQL_RESULT_LIST_MEMORY 只存最近一次:如果有多个 SQL 步骤,Python 步骤只能读到最近一次的结果,需要从 SQL_EXECUTE_NODE_OUTPUT 读历史结果
十、总结
SQL执行步骤是 PlanExecutor 路由的三种步骤类型之一,由 SQL生成 → 语义一致性校验 → SQL执行 三个节点组成:
- 承上 :接收 PlanExecutor 路由,从 Plan 当前步骤的
instruction获取 SQL 需求 - 启下:执行成功后结果存入 state,步骤号递增,回到 PlanExecutor 继续下一个步骤
核心设计是三层重试 + 业务逻辑优先 + 结果双渠道传递:
- 三层重试:生成计数限制 + 语义校验不通过重试 + 执行失败重试,每层都回到 SQL 生成
- 业务逻辑优先:先执行 SQL 存储结果,再做流式输出
- 结果双渠道:历史结果集合(SQL_EXECUTE_NODE_OUTPUT)+ 最近结果内存(SQL_RESULT_LIST_MEMORY)
用「统计上月各部门销售额」的例子:
- SQL生成:根据步骤1的 instruction 生成 SELECT 查询
- 语义校验:校验 SQL 与 instruction 语义一致 → 通过
- SQL执行:执行查询,结果存入 step_1,PLAN_CURRENT_STEP→2,回填 sql_query
- 回到 PlanExecutor → 路由到报告生成步骤
这个模块的设计体现了 DataAgent 的核心理念:用 LLM 生成 SQL,用语义校验保证质量,用多层重试提高成功率,用业务逻辑优先保证结果可靠,用结构化状态传递实现多步骤协作。