介绍 Flex:让模型编写代码(16 分钟阅读)

TLDR AI 工具

摘要

介绍 Flex,这是一个新的 DSPy 模块,让语言模型重写程序代码本身,而不仅仅是提示词,从而实现更好的优化、更少的模型调用以及通过沙箱实现更安全的执行。

Flex 利用模型的编码能力,不仅重写程序的指令,还重写代码本身。它在沙箱解释器中执行生成的源代码。Flex 通过优化提示词和代码,生成更便宜、更快的程序。
查看原文
查看缓存全文

缓存时间: 2026/08/06 13:34

# 介绍 Flex:让模型编写代码 — cmpnd 来源: https://www.cmpnd.ai/blog/let-the-model-write-the-code.html 模型已经成为出色的程序员。Flex 将程序本身交给模型,让优化器重写你的代码,而不仅仅是你的提示词。 *本文客座作者为 Michael Isaac (https://www.linkedin.com/in/michael-isaac-mi/),卡内基梅隆大学软件工程博士生,本文撰写于他在 cmpnd (https://www.cmpnd.ai/) 实习期间。Michael 为 DSPy 实现了 Flex,即本文介绍的这个模块。* DSPy 的核心主张是:你可以一次性地定义任务,使其能够随着 AI 生态系统的进步而被重新实现 (https://www.cmpnd.ai/blog/separating-task-from-model.html)。这些重新实现的历史可以理解为模型的历史:我们绕过的弱点和我们利用的强项: - 在 2022 年,模型需要被展示任务是什么样的,因此像 BootstrapFewShot (https://dspy.ai/api/optimizers/BootstrapFewShot/) 这样的优化器自动化了少样本示例的选择。 - 随后,模型成长为能够编写提示词的作者,因此像 MIPROv2 (https://dspy.ai/api/optimizers/MIPROv2/) 和 GEPA (https://dspy.ai/api/optimizers/GEPA/overview/?h=gepa) 这样的优化器可以通过重写指令来改进程序。 - 近来,模型已经成为出色的程序员。 本周,我们向 DSPy 介绍 `Flex` (https://dspy.ai/diving-deeper/flex/),它利用模型的编码能力,不仅重写程序的指令,还重写代码本身。 ## Flex 让 GEPA 优化代码 `dspy.Flex(YourSignature)` (https://dspy.ai/diving-deeper/flex/) 是一个 DSPy 模块,可以无缝嵌入到你现有的 `Predict`、`ReAct` 或 `RLM` 程序中。例如: ``` my_signature = "question -> answer" my_program = dspy.Predict(my_signature) # 让它变成 Flex! my_program = dspy.Flex(my_signature) ``` 如果我们运行这两个程序中的任何一个,都会得到相同的结果。在优化之前,Flex 只是一个 Predict 模块(如果你提供了工具,则是一个 RLM)。 Flex 的不同之处在于它向优化器暴露了什么:`Flex` 除了暴露指令之外,还暴露了它的代码。将一个 Flex 模块交给 `dspy.GEPA`,反思模型可能会分解你的程序、编写辅助函数、实现路由逻辑,*并且*重写你的提示词。输出的是一个针对你给定的度量标准表现最佳的优化后程序。 下面是你如何用 GEPA 优化 Flex 模块的方法。这里的 `SamePlace` 是我们在下一节将要介绍的地址同一性(location conflation)任务的签名: ``` program = dspy.Flex(SamePlace) # 原来是: dspy.Predict(SamePlace) # 推理时使用的廉价 LM dspy.configure(lm=dspy.LM("anthropic/claude-haiku-4-5")) # 用于编写代码和指令的大 LM big_lm = dspy.LM("anthropic/claude-opus-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 和推理的文件。 你得到的程序是反思模型为了在你的度量标准上尽可能得高分而编写的。通常会伴随两件事:有时它完全不调用模型,因为它发现了可以用代码解决的情况;而当它确实调用时,调用也更有针对性,因为模块已经完成了解析和比较,交给模型的是一个更窄的问题。调用更少、调用质量更高,而且程序性能超过你交给它的那个。 模型编写的代码仍然是不可信代码,因此默认情况下**它绝不会在你的进程中运行**。Flex 在沙箱化解释器中执行生成的源代码。只有预测器调用和你显式提供的工具可以桥接回宿主进程,并且 `max_predictor_calls` 上限限制了每次前向传播可以跨越该桥接的次数。 ## 地址同一性任务 去年,Drew 在 Data + AI Summit (https://www.dbreunig.com/2025/06/10/let-the-model-write-the-prompt.html) 上用一个地理空间同一性任务演示了提示词优化:给定两个地点条目,判断它们是否是同一个物理地点。这在长尾情况下非常棘手。KIN CAFE 和位于同一地址的 KIN 是同一个地方。CONCESSION #2 KEN MERCER SPORTS PARK 和 KEN MERCER SPORTS PARK 位于同一地址则不是。 我们用 `Flex` 替换了 `Predict`,并在这个任务上运行了 GEPA:1,029 对带标签的数据,在 240 条保留记录上评估(类别均衡,因此 50% 是随机水平)。整个过程中禁用了缓存,因此下面的成本和延迟数字是生产环境冷流量需要支付的。 **基线**是原始的 `dspy.Predict`,每条记录一次模型调用:准确率 90.4%,每千条记录成本 $0.98。 **仅用 GEPA 优化提示词**(不使用 Flex)将准确率提升到 92.5%。但提示词优化器唯一的杠杆是指令,因此它编写了一个更长的提示词,每条记录在推理时都要为这些额外的 token 付费:每千条记录 $2.88,是基线成本的 2.9 倍,速度慢 48%。 **Flex 给了优化器第二个杠杆:模块代码。**在 Flex 程序上运行不变的 GEPA,将准确率从 90.4% 提升到 95.0%,每千条记录成本为 $0.70。通过同时优化提示词*和*代码,Flex 生成了一个比基线便宜 28%、快 40% 的程序。 这是如何实现的?首先,许多被比较的地点可以仅用代码来评估。我们的反思模型编写了代码来识别这些问题,并将这些简单匹配路由到纯 Python 函数,结果 **LLM 调用减少了 75%**。 我们可以通过更新度量标准来进一步利用这种行为。GEPA 的度量标准返回一个分数以及自然语言反馈。使用 Flex,它还可以看到生成的程序在每条记录上进行了多少次 LM 调用。我们可以用这个值来惩罚反馈: ``` score = max(0.0, correct - PENALTY * n_llm_calls) ``` 在 PENALTY = 0 时,调用是免费的,优化器只追求准确率。随着惩罚(以下称为 λ)上升,每次 LM 调用都必须买回超过其成本的准确率,优化器被迫用 Python 解决简单情况,并将模型保留给真正模糊的情况。(当 λ > 1.0 时,调用永远无法收回成本;这一端意味着从不调用模型。)我们在 λ = 0、0.05、0.1、0.2 和 0.4 之间进行了扫描,程序运行使用 **Haiku 4.5**,而反思模型重写则使用 **Opus 5**。Haiku 是最弱、最便宜的 Claude,这正是惩罚成为一个有趣权衡的原因。 成本与准确率对比,随着 LLM 调用惩罚上升 基线 普通 GEPA(仅提示词) Flex + GEPA 执行 claude-haiku-4-5 · 反思 claude-opus-5 · n = 240 条保留记录 · 误差条为 95% Wilson 置信区间 *\*\*8 路并发下的平均每请求延迟。\*\** **即使在调用免费的情况下(λ=0),优化器仍然编写了代码。**度量函数只按准确率评分。唯一的推动力来自度量标准的文本反馈,要求尽可能用代码解决情况。它找到的最佳程序将 75% 的记录路由到确定性 Python,并且比每次调用模型的准确率更高,达到 **95.0%** 对比 90.4%(McNemar p=0.019),同时**更快且更便宜**。规则在简单情况上比小模型处理得更好,而模型只看到真正需要判断的情况。 **在 λ=0.4 时,程序在 240 条记录中只调用了一次模型。**准确率保持在 92.1%,与始终调用的基线在统计上无法区分,而成本约为基线的百分之一,延迟约为三十分之一。其他高惩罚值也以同样的方式落地:准确率与基线持平,价格却只有一小部分,这是大多数生产系统都会接受的交易。 ## 阅读它编写的代码 在 λ=0.4 时,程序包含约两百行 Python 代码,由反思模型编写。压缩到骨架后如下: ``` class SamePlaceModule(dspy.Module): def __init__(self): super().__init__() # LLM 是最后手段的备用方案:只在确定性信号真正冲突的 # 窄带情况下才咨询它(例如,明显的相同品牌/名称, # 但门牌号不匹配且距离中等)。 self.judge = dspy.Predict(dspy.Signature( "input_name: str, input_address: str, " "match_name: str, match_address: str, " "distance: float, name_similarity: float, " "address_analysis: str -> is_same: bool", "你对企业/兴趣点列表执行实体解析。[...] " "3. 门牌号是最强的地址信号。[...] " "4. 相同的品牌名称相隔很远意味着两个不同的分支 => 不是同一个地方。" )) def forward(self, **inputs): import re, difflib # 约 150 行辅助函数:规范化名称(去除 '#30696'、 # 'LLC'、CAFE/GRILL 等通用词),将地址解析为 # 门牌号 + 街道核心,对区分性 token 计算模糊相似度... name_same = (nsim >= 0.87) or (containment and shared_len >= 5) name_diff = (not name_same) and (nsim < 0.62) decision = None if name_same: if addr_same and (d is None or d <= 400.0): decision = True # 名称相同,地址相同,距离近 elif hn_diff and d is not None and d > 120.0: decision = False # 门牌号不同 => 分支 elif d is not None and d > 500.0: decision = False # 相距很远 => 不同分支 # ... elif name_diff: decision = False # 区分性名称部分不一致 else: # 灰色地带 (nsim 0.62–0.87):让地址来决定 if addr_same and (d is None or d <= 150.0): decision = True elif hn_diff or (d is not None and d > 200.0): decision = False # ... if decision is None: # 规则无法决定 -> 询问模型 out = self.judge(**inputs, name_similarity=round(nsim, 3), address_analysis=analysis) decision = to_bool(out.is_same) return dspy.Prediction(is_same=bool(decision)) ``` 顶部的注释“LLM 是最后手段的备用方案”是优化器关于自身架构写下的。其底层的算法有三个阶段: 1. **规范化:**名称被转成大写,去除特许编号(“#30696”)、法律后缀(LLC, INC)、标点符号,以及大约四十个通用商业词(CAFE、RESTAURANT、MARKET、GRILL……)。剩下的是名称中具有区分性的部分:对于 KIN CAFE,就是 KIN。地址也会被解析为门牌号和街道核心,丢弃单元标识(STE 4, APT B)和街道类型词,这样 AVE 与 AVENUE 永远不会导致不匹配。 2. **比较:**区分性的名称 token 用模糊相似度从零到一评分,并分成三个桶:确定匹配、确定不匹配、不确定。地址逐部分比较,门牌号与街道名称分开处理,因为反思模型认定门牌号本身在数据中是一个强信号。 3. **决定:**每个桶都有自己的规则,综合考虑名称判定、地址数据和两个地理编码点之间的距离。例如,如果名称和地址匹配且距离在 400 米以内:是同一个地方。如果名称相同但门牌号不同且相距超过 120 米:是同一个品牌的两个分支。 只有当“决定”阶段的规则*全部*没有触发时——例如相同品牌名称但门牌号不匹配且距离中等——该记录才会被交给模型。而且它不是独自去的:模块将其自身的分析作为额外的输入字段转发(解析后的门牌号、街道核心和相似度分数),而裁判(judge)的指令则将其学到的知识提炼为少量编号的领域规则。在 λ=0.4 时,该备用方案在 240 条记录中只触发了一次。 ## 确定性的还是随机的?让度量标准来决定 每个使用 AI 构建的人都在不断重新回答同一个问题:你把什么交给代码,把什么交给模型?这与你在编码智能体中看到的直觉相同,它们越来越多地编写 Python 脚本来完成工作,而不是逐 token 地做。 有了 Flex,我们可以让一个模型在从学生模型和度量标准获得反馈时探索这个空间。它可能变得相当复杂。例如,我们将 Flex 和 GEPA 指向 SWE-bench Pro,这是一个由 GitHub Issue 组成的编码基准。给定一个高层任务——阅读一个 issue 并生成修复——以及几个工具,Haiku 4.5 在 12 个抽样 issue 中解决了 0 个。 然后我们在我们的 Flex 程序上运行了 GEPA,`max_metric_calls` 设置为适中的 60。这个优化后的程序解决了 12 个问题中的 4 个,它设计了一个混合 Python 和 LLM 调用的软件工程工作流,用于研究、起草、评估、修复并提交最终答案。它自行编写的指令包含针对特定失败模式的防护。 这个实验是一个试点项目,应该这样看待。但观察一个 harness 在几轮迭代中进化到 12 个中解决 4 个,对比 Haiku 在一个成熟、手工构建的 harness 中被报道可达到的 39%,是令人瞩目的。 今天,我们的大多数 harness 是一次性编写,然后在问题出现时进行编辑。有了 Flex,我们可以持续编译我们的 harness,在新增数据、模型和策略时平衡哪些用代码编写、哪些交给 LM。作为产品构建者,我们可以扫描模型并调整度量标准,为我们的应用找到成本、延迟和准确率之间的最佳平衡。 在我们观察 GEPA 跨多种任务类型重写程序的过程中,有四种动作反复出现: 1. **分解。**注意到一个任务有步骤(解析、规范化、比较、决定),并为每个步骤提供独立的实现。 2. **方法选择。**为每个步骤在确定性代码和模型调用之间做出选择,并在选择调用时挑选正确的模块(Predict、ChainOfThought、RLM)。 3. **路由。**认识到不同的*输入*是不同的任务:明确的情况走廉价路径,模糊的情况交给裁判。 4. **进化。**一旦结构稳定下来,就优化其内部:分解后各部分的签名和指令,以及代码本身。 手工编写的 harness 也会做这些动作。但它们不会随着新模型、新数据集或新策略而进化,除非你专门回去重写它们。模型在编程方面已经足够出色,我们现在可以随着获得更多数据、更好的度量标准和更好的模型,持续编译我们的 harness。 ## 这一切的来源 Flex 建立在几条研究线索之上: - **GEPA。**Lakshya Agrawal (https://lakshyaaagrawal.github.io/) 和团队的反思式提示词进化,来自 UC Berkeley 的 Sky Computing Lab。它表明,一个读取执行轨迹和度量反馈的模型,可以在 rollout 效率上超越强化学习优化。 - **Meta-Harness。**Yoonho Lee (https://yoonholee.com/) 和团队的斯坦福工作,将模型周围的 harness 视为一个可学习对象。 - **RLM。**Alex Zhang (https://alexzhang13.github.io/) 和团队在 MIT 关于递归语言模型(Recursive Language Models)的工作,其中模型以编程方式探索大型输入,而不是整体吞入。RLM 是 Flex 优化器在某个步骤需要时可以使用的原语之一。

相似文章

语言模型代理的自我编程执行

arXiv cs.AI

本文介绍了自我编程执行(SPE),这是一种代理架构,其中语言模型生成其自身的编排程序,而非依赖固定的外部框架。文章提出了“Spell”,一种基于 Lisp 的语言,支持自我编辑和重新求值,并展示了前沿模型能够利用该方法成功执行代理任务。