
模型已经是优秀的程序员。Flex 把程序本身交给模型,于是优化器改写的就不只是你的提示词,而是你的代码。
本文是客座文章,作者 Michael Isaac(卡内基梅隆大学软件工程博士生),在 cmpnd 实习期间完成。Michael 在 DSPy 中实现了本文要介绍的 Flex 模块。
DSPy 的核心主张是:你可以只定义一次任务,并让它在 AI 生态演进的过程中不断被重新实现。这些“重新实现”的历史,很大程度上就是模型能力的历史:我们绕开了哪些短板,又放大了哪些优势:
- 2022 年,模型还需要你先示范任务长什么样,于是 BootstrapFewShot 这类优化器把少样本(few-shot)示例的挑选自动化了。
- 之后,模型成长为合格的提示词作者,于是 MIPROv2 和 GEPA 这类优化器可以靠重写指令来改进程序。
- 而现在,模型已经变成了非常出色的程序员。
本周,我们向 DSPy 正式引入 Flex。它利用模型的编程能力,重写的不再只是程序里的指令,而是代码本身。
Flex 让 GEPA 优化代码本身
dspy.Flex(YourSignature) 是一个 DSPy 模块,可以直接放进你现有的 Predict、ReAct 或 RLM 程序中。比如:
my_signature = "question -> answer"
my_program = dspy.Predict(my_signature)
my_program = dspy.Flex(my_signature)
无论运行哪一个版本,得到的结果都一样。在优化之前,Flex 只是一个 Predict 模块(如果你提供了工具,它就是 RLM)。
Flex 与 Predict 的区别在于它向优化器暴露的内容:除了指令之外,Flex 还把自己的代码交了出来。把 Flex 模块交给 dspy.GEPA,反思模型就能拆解你的程序、编写辅助函数、实现路由逻辑,还能重写你的提示词。最终得到的是一个在你给定的指标上表现最优的优化程序。
下面就是用 GEPA 优化 Flex 模块的方法。这里的 SamePlace 是位置融合(location conflation)任务的签名,下一节我们会详细演示这个任务:
program = dspy.Flex(SamePlace) # was: dspy.Predict(SamePlace)
# cheap LM to use during inference
dspy.configure(lm=dspy.LM("anthropic/claude-haiku-4-5"))
optimized = dspy.GEPA(
metric=make_metric(penalty=0.2),
reflection_lm=big_lm,
max_metric_calls=400,
).compile(program, trainset=train, valset=val)
优化结束后,optimized.save("program.json") 会把生成的源码保存下来,dspy.Flex(SamePlace).load(...) 则负责重新加载。最终产物就是一个普通文件,你可以打开它、阅读它、对它做 diff,也可以仔细琢磨它。
你拿到的,是反思模型(reflection model,负责生成并优化代码的模型)为了在你的指标上拿高分而写出的一个程序。随之而来的往往有两个结果:有时它压根不会调用大模型,因为它发现这个判断完全可以用代码解决;而当它确实调用时,调用也更有针对性——模块已经先完成了解析和比较,抛给模型的只是一个更窄的问题。调用次数更少、目标更准,最终得到的程序表现也会超过你原本交给它的那个。
模型写的代码仍然是不可信代码,所以默认情况下,它绝不会在你的进程里运行。Flex 会在一个沙箱解释器中执行生成的源码。只有预测器调用和你显式提供的工具,才能与宿主进程建立连接;max_predictor_calls 上限则限制了每次 forward(前向执行)过程中可以跨越这条连接的次数。
让优化器多一个旋钮:从改提示到改代码
这个任务的难点在于:表面相似不代表同一地点,拼接信息又容易出现歧义。“KIN CAFE” 和 “KIN” 可以指向同一家店,但同一个地址前缀下的“CONCESSION #2 KEN MERCER SPORTS PARK” 和 “KEN MERCER SPORTS PARK”却是两个不同的设施。模型必须学会区分“省略简称”和“信息缺失”之间的微妙差别。
基线表现
作为对比基准,我们用原有的 dspy.Predict 模块对每条记录单独调用一次模型:准确率为 90.4%,每处理 1,000 条记录的成本约 0.98 美元。这是后续所有优化的起点。
纯提示优化的瓶颈
如果我们只用 GEPA 优化提示词、不引入 Flex,准确率确实能提升到 92.5%。但问题在于,提示优化器唯一的“调节旋钮”就是指令文本本身。为了榨取更多准确率,它生成了冗长得多的提示,每条记录在推理时都要为这些额外 token 买单。最终结果是:每千条记录的成本飙升至 2.88 美元,是基线的 2.9 倍,延迟也增加了 48%。
单纯的提示优化就像在限速 30 的路上猛踩油门——效率提不上去,油耗倒是先上来了。
Flex 带来的转折
Flex 给了优化器第二个调节杠杆:模块内部的代码逻辑。在 Flex 程序上运行同一个 GEPA 优化器,结果完全不同:
- 准确率从 90.4% 提升至 95.0%;
- 每千条记录成本降至 0.70 美元,比基线便宜 28%;
- 推理速度比基线快 40%。
原理并不复杂:许多成对的地点记录,仅凭代码规则就能判断是否为同一地点,根本不需要调用大模型。优化器的反思模型(reflection model,负责审视和重写代码的模型)会自动识别这些简单匹配,将其路由到普通的 Python 函数中处理。最终,大模型的调用次数减少了 75%。
用惩罚系数控制“调用欲望”
Flex 的另一个优势在于,它允许我们把“大模型调用次数”也纳入优化目标。GEPA 的评估指标本来只返回准确率分数和自然语言反馈,但结合 Flex,优化器还能看到每条记录实际调用了多少次大模型。我们可以据此在反馈中加一个成本惩罚项:
score = max(0.0, correct - PENALTY * n_llm_calls)
- 当惩罚系数 λ = 0 时,大模型调用是“免费”的,优化器只需要追求准确率。
- 当 λ 增大时,每一次大模型调用都必须带来足够的准确率提升来“回本”,优化器被迫优先用 Python 代码把能定论的场景先定下来,只把真正模糊的案例留给大模型。
- 当 λ 超过 1.0,一次调用永远无法自己回本——刻度盘那端的意思就是“干脆别调模型”。
为了观察这个旋钮的实际效果,我们对 λ 取 0、0.05、0.1、0.2、0.4 做了一组扫描。执行模型用 Haiku 4.5(最弱也最便宜的 Claude 型号),反思模型用 Opus 5(更强的推理模型)来重写代码。用最弱的执行模型恰恰让成本惩罚的价值更突出——因为弱模型容易犯错,你得权衡到底让它多干活还是少干活。
图:随着 λ 增大,成本与准确率的权衡变化
执行模型 claude-haiku-4-5 · 反思模型 claude-opus-5 · n = 240 条保留记录 · 误差线为 95% Wilson 置信区间| 方案 | λ | 准确率 | 大模型调用次数/条 | 每千条成本 | 平均延迟* |
|---|---|---|---|---|---|
| dspy.Predict 基线 | 无 | 90.4% | 1.00 | $0.98 | 1,924 ms |
| GEPA 仅优化提示 | 无 | 92.5% | 1.00 | $2.88 | 2,841 ms |
| Flex + GEPA | 0 | 95.0% | 0.25 | $0.70 | 1,155 ms |
| Flex + GEPA | 0.05 | 94.6% | 0.17 | $0.45 | 726 ms |
| Flex + GEPA | 0.1 | 90.8% | 0.07 | $0.18 | 347 ms |
| Flex + GEPA | 0.2 | 91.7% | 0.08 | $0.09 | 135 ms |
| Flex + GEPA | 0.4 | 92.1% | 0.004 | $0.01 | 65 ms |
*8 路并发下的单请求平均延迟。
有趣的是,当 λ 从 0 增加到 0.05 时,准确率只掉了 0.4 个百分点,成本却下降了约 36%,延迟缩短了近 40%。这说明在“什么场景交给代码、什么场景交给模型”的分配上,存在一个高性价比的甜区。
本站短评:对 AI 编程而言,Flex 的核心价值在于把“判断逻辑”和“语言模型”分开来优化——用代码兜底、用模型处理模糊。这和我们在日常开发中追求“快速失败、尽早返回”的工程直觉是一致的。
关键要点
- 纯提示优化有天花板:只改提示词,准确率提升有限,且成本可能暴涨至近 3 倍。
- Flex 让优化器多了一个维度:代码逻辑与提示词可以同时被优化,大模型调用次数减少了 75%。
- 惩罚系数 λ 是成本与准确率的调节旋钮:调高 λ 可以大幅降低调用成本和延迟,准确率代价可控。
- 甜区存在:本实验中 λ ≈ 0.05 时,成本降低 36% 而准确率仅下降 0.4 个百分点,性价比最高。
在 Flex 的评测中,即使完全不用模型、只靠代码逻辑,也能达到更高的准确率;而在成本和延迟受限时,程序只需调用一次模型就能维持与基线相当的表现。本文展示优化器写出的代码骨架,以及背后的取舍逻辑。
关键结果:代码写得越好,模型用得越少
即使模型调用完全免费(λ=0),优化器仍然写出了代码。 评测函数只按准确率打分,唯一的引导来自评测反馈里的文字提示——让它尽量用代码处理可判定的情况。它找到的最优方案把 75% 的记录交给确定性的 Python 代码处理,准确率反而比每条记录都调用模型更高:95.0% 对 90.4%(McNemar 检验 p=0.019),同时更快、更便宜。规则代码处理简单样本的效果比小模型更好,模型只需要看到那些真正需要判断的样本。
在 λ=0.4 时,程序处理完 240 条记录只调用了一次模型。 准确率维持在 92.1%,与每次都调用模型的基线在统计上没有显著差异,而成本约为后者的百分之一,延迟只有约三十分之一。
其他高惩罚系数设置下的结果也如出一辙:准确率与基线持平,成本却只有原来的几分之一——大多数生产系统都会欣然接受这种取舍。
阅读它写出的代码
当 λ=0.4 时,程序包含约两百行 Python 代码,均由反思模型写成。压缩成骨架来看: