Simon Willison 用 DSPy 优化 Datasette Agent 提示词:提示工程正在变成可测试的软件工程
来源参考:Simon Willison 的博客条目 Research: Using DSPy to evaluate and improve Datasette Agent’s SQL system prompts,以及对应的 research 仓库报告 Using DSPy to evaluate and improve Datasette Agent’s SQL system prompts。
文章目录
提示词不再只是“写一句更聪明的系统指令”。
Simon Willison 在 2026 年 7 月 2 日发布的一条 research 笔记,表面上是在试验 DSPy 能不能帮助优化 Datasette Agent 的 SQL 系统提示词。真正值得注意的地方却更大:他没有把提示词优化当成一次灵感写作,而是把它放进了一个接近软件工程的循环里:真实生产提示词、真实工具实现、真实 SQLite 数据库、自动生成的评测集、可复跑的指标、训练集和测试集切分,以及优化后的回归分析。
这篇文章不是复现实验教程,而是一次技术解读。我们关心的问题是:为什么这次小实验能说明 Prompt Engineering 正在走向 Prompt Software Engineering?为什么一个看似简单的 schema 列表,能决定 Text-to-SQL Agent 是否可靠?以及,为什么自动优化提示词并不等于“把提示词写得更长”?
这次实验到底做了什么
Datasette 是 Simon Willison 长期维护的数据发布和探索工具。Datasette Agent 则是在 Datasette 之上加入大模型能力,让用户可以用自然语言提问,Agent 再通过只读 SQL 查询回答问题。
这类能力听起来很熟悉:用户问“哪个客户消费最多?”“过去一个月销量最高的书是什么?”模型需要理解数据库结构,选择表,写出 SQL,执行查询,再把结果解释成人能读懂的答案。
难点也很典型。模型如果不知道表里有哪些列,就会猜。模型如果不理解工具参数的语义,就可能把自己需要读取的数据隐藏起来。模型如果只被最终答案评分,而不看工具调用轨迹,就很难判断错误到底来自 SQL、schema 信息、工具输出,还是评测指标本身。
Simon 让 Claude Code for web 执行了一个异步 research task:安装最新 Datasette alpha、datasette-agent 和 dspy,研究如何用 DSPy 评估并改进 Datasette Agent 用于只读 SQL 问答功能的主系统提示词。最终生成的报告保存在 simonw/research 仓库中。
这次实验使用的版本是:
| 组件 | 版本 |
|---|---|
| Datasette | 1.0a35 |
| datasette-agent | 0.3a0 |
| DSPy | 3.2.1 |
实验关注的不是一个玩具 prompt,而是 Datasette Agent 实际生产代码里的系统提示词。报告里提到,主系统提示词来自 datasette_agent/agent.py 中的 _build_system_prompt(datasette, actor),工具描述来自 datasette_agent/sql_tools.py 里的 get_default_tools()。工具包括列出数据库和表、描述表、执行只读 SQL 查询。SQL 查询工具还有一个很关键的 display 参数,可以控制结果是给模型看、给用户看,还是两边都看。
这个设置很重要。很多 prompt 优化演示会把系统抽象成一个简化任务:给一段输入,让模型输出一段文本,然后用一个字符串指标评分。Simon 的实验更接近真实 Agent:DSPy 评估的是生产提示词,调用的是生产工具实现,运行的是一个真实的进程内 Datasette 实例。换句话说,优化器面对的不是纸面题,而是一个会调用工具、会受到工具语义约束、会遇到真实数据库结构的 Agent。
DSPy 在这里扮演什么角色
DSPy 的价值,不是帮你把“你是一个 SQL 专家”改写成更华丽的句子。它更像一个把提示词、示例、指标和优化器组织起来的框架。
在这次实验里,DSPy 的工作流可以粗略拆成四步。
第一步,抽取真实提示词。实验把 Datasette Agent 生产系统提示词里的静态部分提取出来,用作 DSPy ReAct signature 的 instruction。动态部分,也就是每次请求注入的数据库和表信息,被作为 schema_hint 输入传进去。这样做的好处是,优化出来的 instruction 理论上可以再贴回生产提示词里,而不是只在实验脚本中有效。
第二步,接入真实工具。DSPy Agent 调用的不是临时写的模拟函数,而是 Datasette Agent 已有的 _sql_query、_describe_table 和 _list_databases_and_tables。工具输出也经过生产代码里的过滤逻辑,隐藏那些模型不该直接看到的 _ 前缀 side-channel 字段。
第三步,构造评测数据。实验脚本生成了一个小型书店数据库 books.db,包含作者、书籍、客户、订单、订单条目等表,也包含 NULL、折扣、取消订单、没有下单的客户等边界情况。自然语言问题一共 30 个,gold answer 不是手写猜测,而是通过执行 SQL 从生成数据库里算出来的。数据集切成 20 个训练问题和 10 个 held-out 测试问题。
第四步,用指标和优化器闭环。指标检查 gold answer 中关键值是否出现在用户可见输出里。对于 GEPA 优化器,指标还会返回文字反馈,包括问题、正确答案、一个可行 SQL 查询以及缺失值。GEPA 利用这些轨迹和反馈反思并重写提示词。
这里的优化器是 DSPy 3.x 中的 dspy.GEPA,配置为 auto="light"。任务模型使用 gpt-4.1-nano,反思模型使用 gpt-5-mini。报告也比较了 gpt-4.1-mini 和 gpt-4.1-nano 的基线表现:gpt-4.1-mini 已经接近天花板,训练集 95.0、测试集 96.7;gpt-4.1-nano 的基线训练集 90.0、测试集 81.7,这让它更适合作为“便宜模型上还有优化空间”的实验对象。需要注意的是,这组模型选择分数使用的是第一版指标,后面指标修复后,核心 GEPA 对比使用的是修正指标。
结果并不是“优化后全面变强”
如果只看“用了 DSPy 优化提示词”,很容易期待一个漂亮结论:自动优化后训练集和测试集都提升。但这次实验恰恰有意思在于,结果更像真实工程,而不是宣传页。
在修正后的指标下,核心结果是:
| 程序 | 训练集 20 题 | Held-out 测试集 10 题 |
|---|---|---|
| 生产基线提示词 | 90.0 | 95.0 |
| GEPA 优化提示词 | 95.0 | 85.0 |
也就是说,GEPA 确实修复了训练集上暴露出来的一个失败点,训练集提升 5 分。但它也在测试集引入了一个回归,测试集下降 10 分。
这不是“DSPy 没用”。相反,这正是它有工程价值的地方:它把失败暴露出来了,而且暴露得很具体。优化器不是神谕,它只是基于训练轨迹和指标反馈提出候选提示词。训练集太小、基线已经很强、指标还有噪声时,它完全可能过拟合。Prompt optimizer 也需要评测集质量、回归测试和人工审查。
更值得看的是那次回归到底怎么发生的。
GEPA 优化后的提示词加入了一条看起来很合理的建议:如果不确定订单状态,可以先查询不同的 status 值。这个建议本身没错。问题在于示例中使用了 display='user'。在 Datasette Agent 的语义里,display='user' 的意思是把查询结果渲染给用户,但不要把行数据给模型看。它是为了节省 token、避免模型看到过多数据而设计的。
于是,在一个 held-out 测试问题里,模型照着优化提示词的建议去查询订单状态,却把结果设置成了只给用户显示。用户能看到表,模型自己看不到行内容,只知道大概有几行。接着它重复执行相同查询,耗尽 ReAct 迭代预算,最后没有给出数字答案。
这就是 Agent 提示词最麻烦的地方:一句局部正确的建议,可能和同一提示词里另一个工具语义冲突。人读起来“先查状态”当然合理,但 Agent 执行时需要知道“我自己之后要用的数据,不能只发给用户”。这类错误不是靠把提示词写得更长就能避免的,它需要工具语义、评测轨迹和回归测试共同约束。
从这个回归可以抽出一条非常实用的生产规则:如果模型自己还需要读取查询结果,就不要使用只面向用户显示的模式。更一般地说,工具参数不是注释,它们是 Agent 行为的一部分。优化提示词时必须把工具语义当成接口契约,而不是可有可无的说明文字。
最关键的发现:schema 列表只给表名还不够
用户给出的摘要里提到一个重点:基线提示因 schema 信息不完整导致列名猜测错误,改进建议是在 schema 列表中直接包含列名。这个点确实是整件事里最有迁移价值的发现之一。
Datasette Agent 的动态提示会告诉模型有哪些数据库和表。问题是,如果列表只包含表名,而系统提示又建议“如果已经有信息,就不要调用 describe_table”,模型就容易进入一个尴尬状态:它知道有哪些表,却不知道表里有哪些列;它又被鼓励不要多调用工具,于是开始猜列名。
报告中提到的 baseline trace 里出现过类似 page_count、o.order_id、first_name 这样的列名猜测,然后触发 SQL 错误和重试循环。对 Text-to-SQL 来说,这不是小问题。SQL 是结构化语言,列名错一个字符,整个查询就失败。模型“理解了用户问题”并不等于它“知道数据库结构”。
这给 Agent 设计者一个朴素但很硬的提醒:不要让模型在结构信息缺失时靠语言常识补全 schema。一个图书数据库里可能叫 pages,也可能叫 page_count;客户姓名可能拆成 first_name 和 last_name,也可能只有 name;订单金额可能存在订单表,也可能需要从订单条目表聚合。自然语言常识能给出候选,但数据库执行只接受真实列名。
所以,改进方向不是单纯写一句“不要猜列名”。更好的做法是改变上下文提供方式:在 schema 列表里直接包含表名和列名,或者至少软化“已有信息就不要 describe_table”的建议,让模型在缺少列名时主动调用 describe_table。这不是 prompt 文案问题,而是 Agent 可用信息边界的问题。
在很多企业内部 Text-to-SQL 场景里,同样的错误很常见。系统提示写得很严厉,告诉模型要准确、不要臆造、只读查询,但给它的 schema 只有几个表名,甚至只有业务系统的中文名称。模型当然会生成看似合理的 SQL,因为它没有更好的选择。最终用户看到的是“AI 写 SQL 不靠谱”,但真正的问题是上下文合同没有设计好。
指标 bug 比优化器更值得警惕
这次实验还有一个很重要的反转:第一版测试分数里有两类“失败”后来被确认是指标问题,而不是 Agent 真的答错。
第一类是语义上的 0。gold answer 是 0,但模型用自然语言回答“没有书从未被订购”之类的表达。这在语义上等价于 0,却没有出现字面数字 0,于是简单的字符串检查会误判。
第二类是并列名次。某个 top-3 问题存在真实并列,gold checks 只接受了一种排序或一个候选,导致正确答案被指标排除。后来通过 any-of check groups 修复。
指标修复后,基线测试集分数从第一版的 81.7 上升到 95.0。这件事的启发非常直接:在优化提示词之前,先调试你的评测指标。否则优化器会认真地优化错误目标。
这也是为什么很多 AI Agent 项目到了生产阶段会卡住。团队知道要做 eval,也写了几十个测试样例,但指标只是“答案里是否包含某个字符串”。这种指标可以作为起点,却很容易把正确答案打成错误,也可能把格式正确、语义错误的答案打成正确。如果再把这样的指标交给自动优化器,它会把指标的偏见放大。
一个更健康的流程应该是:先收集真实失败轨迹,人工看几轮模型输出和工具调用,再修指标;确认指标能区分关键失败后,再让优化器参与。优化器不是用来替代判断的,而是用来扩大搜索范围和暴露候选改法的。
为什么这不是传统提示工程
传统提示工程经常像调音:加一句角色设定,改一个语气,塞几个规则,观察输出变好没有。这个过程在原型阶段很有用,但它有三个问题。
第一,它很难复现。今天你觉得改得更好,明天换几个问题可能又坏了。
第二,它很难定位原因。输出错了,是模型没理解,还是 schema 不够,还是工具说明有歧义,还是答案格式没被指标识别?
第三,它很难维护。提示词越写越长,每次加规则都可能和旧规则冲突。没有回归测试时,提示词就会变成一个没人敢动的长文档。
Simon 这次实验展示的是另一种方向:把提示词当作生产资产来管理。
生产资产意味着它有版本,有测试,有指标,有已知失败,有回归风险。你可以用 DSPy 生成候选改法,但候选改法必须过 held-out 测试。你可以让优化器扩展提示词,但你也要检查它有没有破坏工具参数语义。你可以把评测集做得很小先跑通流程,但不能因为 20 个训练问题上的提升就宣布生产可靠。
这和软件工程里的单元测试、集成测试、性能测试很像。提示词本身不是代码,但 Agent 的行为由提示词、工具、模型、上下文和外部数据共同决定。只改提示词,也可能造成系统级回归。
对 Text-to-SQL Agent 的实践启发
如果你正在做数据库问答、BI Copilot、内部数据 Agent,这个案例至少给出六条很实用的建议。
第一,schema 信息要足够具体。只给表名通常不够,至少要给列名。对于复杂库,还要考虑列注释、主外键关系、枚举值、时间字段含义、金额字段口径。不要让模型猜结构。
第二,把“何时调用 describe table”写成明确策略。比如:当问题涉及未知列、聚合口径、用户可见名称、金额计算、状态过滤时,应先查看表结构。不要用一句模糊的“已有信息就不要调用工具”压住必要探索。
第三,工具输出模式必须和模型任务对齐。如果一个参数会让数据只显示给用户、不返回给模型,那提示词里必须明确:模型后续需要推理的数据不能用这种模式。节省 token 的规则不能牺牲推理所需信息。
第四,评测集要覆盖真实麻烦。NULL、取消订单、重复名次、0 值、top-N、日期过滤、金额折扣、跨表 join、ID 转人名,这些都是 Text-to-SQL 的常见坑。只测“列出所有客户”这种简单问题,很难发现生产风险。
第五,指标要先被调试。不要只看分数,要看被判错的样例。至少抽样检查:模型答案是否语义正确但没命中字符串?是否有多个正确答案?是否有格式差异?是否因为 markdown 转义、千分位、小数字英文表达导致误判?
第六,优化后必须做人工 diff review。DSPy 或 GEPA 可能提出很有价值的新规则,也可能引入过度具体的建议。尤其是长提示词,容易把训练集里的一个失败修成通用规则,最后在测试集上反咬一口。
一个可落地的工作流
如果把 Simon 这次实验抽象成团队可用流程,可以这样落地。
先选一个真实的 Agent 能力,不要从玩具任务开始。比如“只读 SQL 回答业务数据问题”。把生产系统提示词、工具描述、工具实现都接进评测环境,确保评测跑的是同一套行为。
然后做一个小但硬的数据库和问题集。数据可以合成,但问题要覆盖真实业务边界。每个问题都配 gold SQL 或 gold checks,并且 gold answer 通过执行 SQL 得到,而不是凭印象手写。
接着写指标。指标一开始不用完美,但必须能解释失败原因。对 Agent 来说,最好记录最终答案、工具调用、用户可见表格、模型可见输出、SQL 错误和迭代预算消耗。只有最终答案,没有轨迹,调试会很痛苦。
再跑 baseline。不要急着优化,先看 baseline 错在哪里。如果 10 个失败里 6 个是指标误判,先修指标。如果失败主要来自 schema 信息缺失,先改上下文结构。如果失败来自模型能力不足,再考虑换模型或给更多规则。
最后引入 DSPy 这类优化器。让它基于训练集提出候选提示词,再用 held-out 测试集评估。训练集提升、测试集下降,不代表优化器无效,而是告诉你这个候选有回归。把它当作 code review:保留有价值的规则,删掉与工具语义冲突的建议,补充新的测试样例。
这个流程不华丽,但可靠。它不会保证每次自动优化都带来提升,却能让团队知道为什么提升、为什么下降、下一步该修什么。
不要把一次小实验夸大成万能结论
这次实验规模很小:30 个问题,20 个训练,10 个测试。报告自己也指出,20 个训练问题配上 90% 的 baseline,留给优化器学习的空间非常有限;10 个测试问题里每道题都占 10 分,单个回归会显著影响分数。
所以,合理结论不是“DSPy 一定能自动提升所有 Agent 提示词”。更准确的结论是:DSPy 可以把提示词优化放进一个可评测、可复盘的流程里;GEPA 能根据失败轨迹提出候选改法;但优化质量高度依赖评测集、指标、训练覆盖和人工审查。
这和我们对机器学习的常识一致。数据集太小,指标有 bug,训练分布不覆盖测试失败,再好的优化器也不可能稳定泛化。Prompt optimizer 只是把这个问题带到了提示词工程领域。
这件事为什么重要
过去一年,很多团队已经从“写提示词”走向“写 Agent”。Agent 一旦能调用工具、读数据库、执行代码、修改文件、触发工作流,它的提示词就不再是文案,而是行为约束的一部分。
Datasette Agent 的例子很小,却很典型。一个 display 参数的语义误用,可以让模型看不到自己刚查出来的数据。一个 schema 列表少了列名,可以让模型猜错 SQL。一个评测指标没处理语义 0 和并列答案,可以把正确行为标成失败。一个自动优化器加入的合理建议,也可能在 held-out 问题上造成回归。
这就是 AI Agent 工程的真实样子:不是一句神奇提示词解决一切,而是上下文、工具、指标和模型行为之间的协同。
对开发者来说,这篇 research 笔记最值得带走的不是某个优化后的长 prompt,而是一套思维方式:
- 提示词要接真实工具测,而不是只在聊天窗口里试。
- schema 要作为接口设计,而不是背景知识。
- eval 要先被调试,再被用于优化。
- 自动优化得到的是候选改动,不是免审发布。
- Agent 提示词需要像代码一样做回归测试。
如果说早期 Prompt Engineering 更像写作,那么这类工作流已经明显转向工程。下一阶段真正有价值的能力,可能不是“谁能写出最玄妙的一段提示词”,而是谁能建立一套让提示词可评估、可回归、可维护的系统。
这就是 Simon Willison 这次 DSPy + Datasette Agent 实验给我的最大启发:Prompt Engineering 正在变成 Prompt Software Engineering。它仍然需要语言直觉,但更需要测试、指标、接口意识和对失败轨迹的耐心。
更多推荐



所有评论(0)