第2006篇:PAL模式实现——让AI用代码而非语言解决数学推理问题
2026/4/30大约 5 分钟
第2006篇:PAL模式实现——让AI用代码而非语言解决数学推理问题
适读人群:构建AI数据分析和计算功能的工程师 | 阅读时长:约18分钟 | 核心价值:理解PAL(Program-Aided Language Models)的工程价值,在Java中实现可靠的代码执行推理
接了个需求:让AI回答业务分析问题。比如"上个月退货率最高的品类是什么?退货率是多少?"
一开始我直接用LLM回答,感觉挺好的。但有一天,一个财务经理盯着AI给出的数字问我:"这个17.3%是怎么算出来的?"
我一查,LLM算错了。它用"语言推理"来处理数学问题,而语言模型做算术本来就不可靠。
真实数字是14.7%。AI的信口开河差点影响了实际业务决策。
这就是PAL(Program-Aided Language Models)要解决的问题——让LLM不是直接"说出"答案,而是"写一段代码"来计算答案,然后实际执行代码,把代码的执行结果作为最终答案。
PAL的核心思路
LLM的职责从"计算答案"变成了"写代码",把实际计算交给确定性的代码执行器。
Java中的PAL实现
@Service
@Slf4j
@RequiredArgsConstructor
public class PalService {
private final ChatClient codingClient; // 专门用于代码生成的LLM
private final ChatClient explainClient; // 用于解释结果的LLM
private final CodeExecutor codeExecutor;
private final DataSourceSchemaProvider schemaProvider;
/**
* PAL模式的完整流程:提问 -> 生成代码 -> 执行 -> 解释
*/
public PalResult answer(String question, String dataSourceId) {
// 1. 获取数据源的Schema信息(让LLM知道有哪些表和字段)
String schema = schemaProvider.getSchema(dataSourceId);
// 2. 让LLM生成SQL(不是Python,因为数据在数据库里)
String generatedCode = generateCode(question, schema);
log.debug("生成的代码:\n{}", generatedCode);
// 3. 在安全沙箱中执行代码
CodeExecutionResult execResult = codeExecutor.execute(generatedCode, dataSourceId);
if (!execResult.isSuccess()) {
// 代码执行失败,让LLM根据错误信息修复
String fixedCode = fixCode(generatedCode, execResult.getErrorMessage(), schema);
execResult = codeExecutor.execute(fixedCode, dataSourceId);
}
// 4. 让LLM用自然语言解释执行结果
String explanation = explainResult(question, generatedCode, execResult);
return PalResult.builder()
.question(question)
.generatedCode(generatedCode)
.rawResult(execResult.getOutput())
.explanation(explanation)
.succeeded(execResult.isSuccess())
.build();
}
private String generateCode(String question, String schema) {
String codeGenPrompt = """
你是一个数据分析SQL专家。根据以下数据库Schema和问题,生成一个SQL查询。
数据库Schema:
%s
问题:%s
要求:
1. 只返回SQL代码,不要任何解释
2. SQL要精确回答问题,不要返回多余的字段
3. 使用标准SQL语法(兼容MySQL 8.0)
4. 注意空值处理(使用COALESCE等函数)
5. 如果需要计算百分比,保留两位小数
SQL查询:
""".formatted(schema, question);
String response = codingClient.prompt().user(codeGenPrompt).call().content();
// 清理代码块标记
return cleanCodeBlock(response);
}
private String fixCode(String originalCode, String errorMessage, String schema) {
String fixPrompt = """
下面的SQL执行失败了,请修复它:
原始SQL:
%s
错误信息:
%s
数据库Schema:
%s
请返回修复后的SQL(只返回SQL,不要解释):
""".formatted(originalCode, errorMessage, schema);
return cleanCodeBlock(codingClient.prompt().user(fixPrompt).call().content());
}
private String explainResult(String question, String code, CodeExecutionResult result) {
if (!result.isSuccess()) {
return "抱歉,查询执行失败,无法回答这个问题。错误:" + result.getErrorMessage();
}
String explainPrompt = """
用户问了一个数据分析问题,我执行了SQL查询并得到了结果。
请用简洁的自然语言解释这个结果,直接回答用户的问题。
用户问题:%s
SQL查询:
%s
查询结果:
%s
请直接回答用户问题,引用具体数字,不要提及SQL细节。
""".formatted(question, code, result.getOutput());
return explainClient.prompt().user(explainPrompt).call().content();
}
private String cleanCodeBlock(String response) {
response = response.trim();
if (response.startsWith("```sql")) response = response.substring(6);
else if (response.startsWith("```")) response = response.substring(3);
if (response.endsWith("```")) response = response.substring(0, response.length() - 3);
return response.trim();
}
}安全的SQL执行器
这是PAL里最关键的安全设计。不能让LLM生成任意SQL然后无限制地执行:
@Component
@Slf4j
public class SafeSqlExecutor implements CodeExecutor {
private final DataSource readOnlyDataSource; // 只读数据源!
// SQL语句白名单:只允许SELECT
private static final List<String> ALLOWED_STATEMENT_TYPES = List.of("SELECT", "WITH");
// 危险关键词黑名单
private static final List<String> FORBIDDEN_KEYWORDS = List.of(
"DROP", "DELETE", "UPDATE", "INSERT", "ALTER", "CREATE",
"TRUNCATE", "EXEC", "EXECUTE", "CALL", "GRANT", "REVOKE"
);
// 单次查询最大返回行数
private static final int MAX_ROWS = 1000;
// 查询超时时间
private static final int QUERY_TIMEOUT_SECONDS = 30;
@Override
public CodeExecutionResult execute(String sql, String dataSourceId) {
// 1. SQL安全校验
String validationError = validateSql(sql);
if (validationError != null) {
return CodeExecutionResult.error("SQL安全校验失败: " + validationError);
}
// 2. 在只读连接上执行
try (Connection conn = readOnlyDataSource.getConnection();
Statement stmt = conn.createStatement()) {
stmt.setMaxRows(MAX_ROWS);
stmt.setQueryTimeout(QUERY_TIMEOUT_SECONDS);
ResultSet rs = stmt.executeQuery(sql);
String output = resultSetToString(rs);
return CodeExecutionResult.success(output);
} catch (SQLTimeoutException e) {
return CodeExecutionResult.error("查询超时(超过" + QUERY_TIMEOUT_SECONDS + "秒)");
} catch (SQLException e) {
return CodeExecutionResult.error("SQL执行错误: " + e.getMessage());
}
}
private String validateSql(String sql) {
String upperSql = sql.toUpperCase().trim();
// 检查是否以允许的语句类型开头
boolean startsWithAllowed = ALLOWED_STATEMENT_TYPES.stream()
.anyMatch(upperSql::startsWith);
if (!startsWithAllowed) {
return "只允许SELECT查询,不允许修改数据的操作";
}
// 检查是否包含危险关键词(用单词边界匹配,避免字段名误判)
for (String keyword : FORBIDDEN_KEYWORDS) {
if (upperSql.matches(".*\\b" + keyword + "\\b.*")) {
return "包含不允许的操作关键词: " + keyword;
}
}
// 检查多语句(防止SQL注入:SELECT 1; DROP TABLE xxx)
if (sql.contains(";") && !sql.trim().endsWith(";")) {
return "不允许多语句执行";
}
return null;
}
private String resultSetToString(ResultSet rs) throws SQLException {
ResultSetMetaData meta = rs.getMetaData();
int colCount = meta.getColumnCount();
// 表头
StringBuilder sb = new StringBuilder();
List<String> headers = new ArrayList<>();
for (int i = 1; i <= colCount; i++) {
headers.add(meta.getColumnLabel(i));
}
sb.append(String.join(" | ", headers)).append("\n");
sb.append("-".repeat(headers.stream().mapToInt(String::length).sum() + headers.size() * 3)).append("\n");
// 数据行
int rowCount = 0;
while (rs.next()) {
List<String> values = new ArrayList<>();
for (int i = 1; i <= colCount; i++) {
Object val = rs.getObject(i);
values.add(val != null ? val.toString() : "NULL");
}
sb.append(String.join(" | ", values)).append("\n");
rowCount++;
}
sb.append("\n共 ").append(rowCount).append(" 行");
if (rowCount == MAX_ROWS) {
sb.append("(已达到最大返回行数限制)");
}
return sb.toString();
}
}与普通LLM回答的对比
我们在系统里做了对比测试:
| 场景 | 直接LLM回答准确率 | PAL回答准确率 |
|---|---|---|
| 简单计数 | 92% | 99% |
| 百分比计算 | 71% | 98% |
| 多表关联 | 65% | 95% |
| 排名类问题 | 78% | 97% |
PAL在数字计算类问题上的提升非常明显,尤其是涉及多步计算的场景。
代价是响应时间增加了:需要额外的SQL生成和执行时间。但对于财务分析、数据报表类场景,准确性远比速度更重要。
