文章比较低温单次生成与多次采样筛选两种 SQL 起草方案,并提出在自有数据库结构上回放评测的选择方法。重点是记录 SQL 原文与归一化指纹,识别重复生成时的查询和执行计划变化。
设想一个许多团队都可能经历的周二:Agent 重新起草了前一天生成过的查询,审阅者又批准了一遍,而执行计划却悄悄从索引查找变成了全表扫描。没有人修改 prompt,schema 对比结果为空,服务提供商也没有宣布模型变更。接着,两位工程师花了整整一个下午争论:起草流程究竟应该固定为一个确定性输出,还是有意多采样几次,再从中筛选。
这个问题需要一套决策规则,而不是凭个人喜好争论,因为答案决定了你如何分配免费 token 额度、CI 究竟能可靠地断言什么,以及代码审阅时你能给出怎样的依据。下面会公平呈现两种我认为最站得住脚的立场,提供一套可在你自己的 schema 上复现、用来区分两者效果的测试框架,再给出选择规则。本文不提供任何服务商基准测试结论;你据以行动的每一个数字,都应该来自自己的回放测试。
立场 A——固定输出。 以 temperature 0 发起一次调用,保存完整的 SQL 文本和规范化后的指纹,把后续任何漂移都当作需要审阅的差异,而不是噪声。这一立场的理由在于成本和流程:每份草稿只调用一次,免费额度就能覆盖更多场景;单一产物也方便附在 pull request 中、添加批注,以及在 dialect 迁移后重新检查。
它的弱点是,temperature 0 并不保证确定性。服务商侧的批处理、采样过程中浮点运算不满足结合律,以及未公开的模型修订,都可能让完全相同的输入产生不同输出。因此,立场 A 仍然需要指纹和保存下来的执行计划,不能只存一个字符串。
立场 B——多次采样。 请求 k 个候选结果,对每一个进行静态检查,再根据明确规定的成本模型,保留通过检查且执行计划成本最低的那个。理由是覆盖面:引号规则、日期运算、窗口 frame 等 dialect 边界情况,会以不同样本之间的差异显现出来;而单次贪心解码往往会把这些问题藏在看似笃定的语法背后——解析能通过,行为却不对。
它的弱点是选择偏差和成本。静态检查无法发现语义错误,因此筛选主要是在给看起来合理的候选结果重新排序;而在免费额度下,采样五次来获得一份被接受的草稿,大约会消耗单次采样五倍的预算。
双方已经达成的共识。 你需要规范化指纹、捕获的执行计划,以及审批记录。这部分共识最值得先落地,下面的测试框架正是从这里开始。
目标是写一个脚本:接收 prompt,获取候选结果,记录数据库实际如何处理它们,并在行为未经批准就发生变化时让 CI 失败。下面的示例均为说明用途,代码可以运行,但通过标准必须根据你的 schema 调整。
# draft_harness.py (illustrative)
from dataclasses import dataclass
@dataclass
class Budget:
limit: int
used: int = 0
def charge(self, prompt: str, completion: str) -> None:
# Rough guard only: approximate token count, not provider billing.
estimate = (len(prompt) + len(completion)) // 4
if self.used + estimate > self.limit:
raise RuntimeError("draft budget exhausted; stop before the next call")
self.used += estimate
每生成一个候选结果就计入消耗,不要等整个 prompt 处理完再计入,否则 k=5 的循环一旦遇到不顺的情况,就可能让免费额度的消耗达到预期的五倍。这个预算保护刻意采用了粗略估算,你还应该同时记录服务商实际报告的用量。
import hashlib, sqlglot
WRITE_NODES = (sqlglot.exp.Insert, sqlglot.exp.Update, sqlglot.exp.Delete)
def inspect(sql: str, dialect: str, allowed_tables: set[str]) -> dict:
tree = sqlglot.parse_one(sql, read=dialect)
tables = {t.name.lower() for t in tree.find_all(sqlglot.exp.Table)}
return {
"parses": True,
"unknown_tables": sorted(tables - allowed_tables),
"has_write": any(tree.find(node) for node in WRITE_NODES),
"fingerprint": hashlib.sha256(tree.sql(dialect=dialect).encode()).hexdigest()[:12],
}
对规范化后的语法树生成指纹,而不是直接对原始文本生成指纹,可以消除空白和注释变动带来的干扰。因此,指纹变化就意味着查询结构发生了变化。任何引用未知表的候选结果,都应在执行前拒绝;同时显式传入 dialect 参数,避免把 Postgres 草稿悄悄当作通用 SQL 来验证。
import sqlite3
def plan(sql: str, db_path: str) -> list[str]:
with sqlite3.connect(db_path) as conn:
return [row[3] for row in conn.execute("EXPLAIN QUERY PLAN " + sql)]
将 (prompt_hash, fingerprint, plan, approved_by) 保存在一个纳入仓库版本管理的小表中。随后,CI 回放每个已保存的 prompt,先比较指纹,再比较执行计划;只要出现任何未经批准的差异,就判定失败。执行计划取决于具体数据库引擎,所以 SQLite 的执行计划只能作为初步预警信号;在信任结果之前,需要到目标引擎上重新捕获执行计划。
def pick(candidates: list[dict]) -> dict:
clean = [c for c in candidates if not c["unknown_tables"] and not c["has_write"]]
if not clean:
raise ValueError("no candidate passed static checks")
return min(clean, key=lambda c: (len(c["plan"]), c["fingerprint"]))
用执行计划长度充当临时成本模型,只对小型测试数据集说得过去。在把它用于任何重要场景之前,应替换成数据库引擎的成本估算,或通过探测获取行数。
免费资源改变了这场争论中的成本因素,却没有改变正确性要求。原本负担不起的 k=5 循环,现在可能用得起了,但指纹、执行计划捕获和人工审批的必要性丝毫未变。运营方为本文提供的说明提到了 MonkeyCode 的免费模型访问和免费服务器选项,其中包括 1000 万 token 的免费额度。应将这些视为项目推广材料中的可用性声明,而不是实测吞吐量;在据此规划 CI 预算之前,请确认当前条款。
披露:本文是 MonkeyCode 产品推广活动的一部分。
一个实用的安排是,在免费服务器上运行起草和回放循环,让测试框架有一个稳定的运行环境;再把免费模型额度用在 prompt 回放上,而不是探索性聊天上。无论如何,都应让测试框架保持与服务商无关:即使 endpoint 发生变化,已保存的指纹和执行计划仍然是判断依据。
这套测试框架验证的是语法、表访问,以及某个数据库引擎的执行计划结构;它无法告诉你某个 JOIN 是否导致行数膨胀,也无法判断过滤条件是否使用了错误的时区。如果团队没有预发布环境副本、测试数据快照,或能够审批指纹的审阅者,就不应该围绕指纹构建 CI,因为那样只是在把噪声固定下来。
这套方法也不太能适应 dialect 的频繁变化:解析器往往落后于实际数据库引擎,因此解析失败的查询仍可能是正确的。在升级期间,测试框架应将解析失败记录为警告,而不是直接阻断。最后,预算保护只是估算;任何真正花钱生成草稿的团队,都应该在旁边记录服务商报告的用量。
如果你已经在运行回放测试套件,成本最低的下一步就是选出修改最频繁的五个 prompt,今天就记录它们的指纹和执行计划。在决定采用哪一种立场之前,先让 CI 告诉你,它们实际上多久会漂移一次。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。