想象你走进一家私房菜馆,不看菜单,只对主厨说:“我要一碗面,不要太辣,最好加颗溏心蛋。”主厨根据这句话,反推出完整做法。程序合成做的正是这件事:不要求你逐行写出代码,而是把“想要什么”表达出来——可以是几张输入输出示例,也可以是一张留有空白的菜谱草稿——让计算机自动把缺失的逻辑填出来。这项技术里有两个响当当的名字:Sketch 和 FlashFill。Sketch 像一位谨慎的主厨,先画草稿再请试菜员验证;FlashFill 则像 Excel 里的小助手,你示范三五行“把姓名改成缩写”,它立刻学会整列文本变换。本文就用“主厨反推菜谱”这条线索,带你从零看懂程序合成与验证的核心思路。

岗位关联

掌握程序合成,意味着你能用“示例即需求”的方式自动生成可靠代码,在数据清洗、代码补全和开发工具智能化中创造出真正落地的 AI 产品价值。

目录
  1. 可视化演示
  2. 认识程序合成
  3. Sketch核心原理
  4. FlashFill原理
  5. 关键算法
  6. 性能与优化
  7. 应用与场景
  8. 挑战与前瞻
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

认识程序合成

你不需要背出油盐酱醋的精确克数,只要对主厨说“我想要一碗带溏心蛋的面”,他就能把整道菜做出来。传统编程就像你事无巨细地指挥主厨:“先放油,再放葱,三分钟后加水……”程序合成则把指挥权交给系统:你描述“要什么”,系统自动写出“怎么做”。

从技术上说,程序合成(Program Synthesis)是在一个由合法程序构成的空间里,自动寻找满足用户意图的程序。用户的意图可以是输入输出示例、自然语言描述、形式化规约,或者一张不完整的草图。Sketch 和 FlashFill 是这个领域最有名的两个代表:前者面向通用代码补全,后者面向表格文本变换。

用一个公式可以概括合成问题的核心:

Sketch 像酒店后厨的大师傅:先写一个粗略流程,然后让助手补齐细节。FlashFill 则像街边小摊老板:看一眼客人比划的样例,马上做出同样风格的小吃。它们共享同一个目标——从“模糊的需求”走向“确定的程序”。

∃P∈D, ∀(xi,yi)∈E,P(xi)=yi\displaystyle \exists P \in \mathcal{D},\ \forall (x_i,y_i)\in E,\quad P(x_i)=y_i
这个公式说:在菜谱库 D 里,存在一份菜谱 P,它能把每一个示例输入 x_i 都变成期望输出 y_i。
  1. 存在量词先限定我们在一个菜谱库中找候选菜谱,这个菜谱库就是合法的程序集合。
    ∃P∈D\exists P \in \mathcal{D}
  2. 全称量词找到的菜谱不能只对一道菜有效,必须对所有示例都成立。
    ∀(xi,yi)∈E\forall (x_i,y_i) \in E
  3. 条件等式“把输入变成输出”这一动作,被表达成程序 P 在输入 x_i 上的运行结果等于 y_i。
    P(xi)=yiP(x_i)=y_i

Sketch核心原理

Sketch 的灵感来自“先画草图,再请人完善”。主厨写菜谱时,会留下“加少许____”这样的空格;Sketch 把这些空格称为占位符(Hole),通常用 ?? 表示。用户给出一个不完整的程序骨架,合成器负责把每个洞填上具体表达式。

这个“填洞”过程不能乱来。Sketch 使用语法引导搜索(Syntax-Guided Synthesis,SyGuS):先定义一组允许出现的语法,比如只能使用加、减、乘和常量,再枚举候选填充方案。把候选填进草图后,验证器会检查它是否满足全部约束。

验证失败时,最漂亮的一招出现了:CEGIS(Counterexample-Guided Inductive Synthesis,反例引导的归纳合成)。验证器不是简单地说“不行”,而是返回一个反例,比如“输入为 3 时你输出了 7,但期望是 8”。合成器把反例加入示例集,重新搜索,直到没有反例或超时。整个过程可以写成:

这个流程像主厨反复试菜:先按暂定菜谱做一道,试菜员尝出“太咸”,他就在下一版减少盐量。CEGIS 的价值在于,每次失败都会带来一条新信息,让搜索越来越有方向。

∃σ∈Σ, ∀x∈I,Ph[σ](x)=yx\displaystyle \exists \sigma \in \Sigma,\ \forall x \in I,\quad P_h[\sigma](x)=y_x
它的意思是:存在一种对洞的填充 σ,使得填充后的草图 P_h 在输入集 I 中每个 x 上都能得到期望输出 y_x。
  1. 填充方案 σσ 是给每个洞选出的具体表达式,来自有限的填充集合 Σ。
    σ∈Σ\sigma \in \Sigma
  2. 所有输入 x验证范围是输入集 I,通常包含用户示例和多次迭代中收到的反例。
    ∀x∈I\forall x \in I
  3. 填入后的程序P_h 是带洞草图,[σ] 表示把 σ 填进洞里,最后要求输出满足 y_x。
    Ph[σ](x)=yxP_h[\sigma](x)=y_x

FlashFill原理

FlashFill 是一道更家常的菜:在 Excel 里,用户手动示范“John Smith → J. Smith”,再按一下回车,整列都能自动生成类似缩写。它不要求用户写代码,只要求提供输入输出示例,因此也叫“编程通过示例”(Programming by Example,PBE)。

为什么机器能“照葫芦画瓢”?因为 FlashFill 为字符串变换设计了一个专属 DSL。这个领域特定语言限制了食材种类,让搜索不必漫无目的。常见的表达式语法是:

有了 DSL,FlashFill 会枚举所有候选表达式,再用示例过滤。最后它按“长度短、常量少、结构简单”的规则给候选打分,把最像人类手写的表达式排在前面。这正是主厨从三碗成品中猜出统一做法:先限定可用调料,再挑最简单说得通的版本。

e::=x  ∣  Const(s)  ∣  SubStr(x,p1,p2)  ∣  Concat(e1,e2)\displaystyle e ::= x \;|\; \text{Const}(s) \;|\; \text{SubStr}(x,p_1,p_2) \;|\; \text{Concat}(e_1,e_2)
这条语法规则定义了“可以有哪些操作”:直接用原字符串、拼一个固定字符串、按位置截取子串、把两个表达式拼起来。
  1. 原字符串 x最基本原料,表示当前单元格的输入文本。
    e::=xe ::= x
  2. 固定字符串 Const(s)例如 "."、"-" 这类保持不变的分隔符。
    Const(s)\text{Const}(s)
  3. 截取子串 SubStr(x,p1,p2)从 x 的第 p1 个位置取到第 p2 个位置,比如取第一个字符。
    SubStr(x,p1,p2)\text{SubStr}(x,p_1,p_2)
  4. 拼接 Concat(e1,e2)把两个表达式的结果首尾相连,组成更复杂的转换。
    Concat(e1,e2)\text{Concat}(e_1,e_2)

关键算法

主厨手里有很多半成品:切好的葱花、调好的酱汁、煮到七分的面条。程序合成器也一样,擅长把已经验证过的子表达式拼接成更大的表达式。版本空间代数(Version Space Algebra)就是一套高效管理“候选集合”的方法。

普通初学者可能会把每个候选程序单独存起来,但程序空间会爆炸。版本空间代数的聪明之处在于:用集合的交、并、笛卡尔积运算来表示一大片候选,而不必逐个列出。例如,当已知左边候选集合 S1 和右边候选集合 S2 后,拼接表达式的集合可以写成:

在实际合成中,系统还会做一致性检验:如果一个表达式在某些示例上失败,就把它从版本空间里剔除;如果两个候选相互冲突,就保留更符合多数示例的一方。这就像主厨面对两个客人的口味矛盾,选择一套能同时满足最多人的固定流程。

S1⊕S2={Concat(e1,e2)∣e1∈S1,e2∈S2}\displaystyle S_1 \oplus S_2 = \{ \text{Concat}(e_1,e_2) \mid e_1 \in S_1, e_2 \in S_2 \}
这个公式表示:把 S1 中任意一个左边子表达式和 S2 中任意一个右边子表达式拼接,所有可能组合合在一起就是新的候选空间。
  1. 候选集合 S1、S2它们是之前从不同示例中整理出来的“部分菜谱”。
    S1, S2S_1,\ S_2
  2. 合并算子 ⊕它不是一个普通加法,而是把两个集合里的元素两两组合。
    S1⊕S2S_1 \oplus S_2
  3. 拼接表达式对每一对 e1、e2 都生成 Concat(e1,e2),并收集成集合。
    Concat(e1,e2)\text{Concat}(e_1,e_2)
  4. 成员条件e1 必须来自 S1,e2 必须来自 S2,保证每个组合都有依据。
    e1∈S1, e2∈S2e_1 \in S_1,\ e_2 \in S_2

性能与优化

菜谱越复杂,厨房要尝试的组合就越多。程序合成最怕的是搜索空间爆炸:明明只有几十种基本操作,组合起来却像天文数字。Sketch 和 FlashFill 都在拼命“省火省时”。

第一招是剪枝:遇到一个子表达式在某条示例上已经失败,就整棵剪掉,不再尝试它的各种上层组合。第二招是缓存复用:同一个字符串片段可能被多个候选用到,与其反复计算,不如把结果缓存起来,像提前批量切好的菜。第三招是多列协同合成:电子表格里常常同时需要改姓名、日期、金额,合成器让多列共享中间变换,一起逼近最终格式。

搜索代价可以粗略表示成:

剪枝和缓存还能继续互相配合:缓存让剪枝判断更快,剪枝让缓存里存的东西更少。主厨的灶台也讲究“顺手”:把最常用的高汤提前备好,把不合适的配方直接扔掉,整个后厨才能高效运转。

Cost(D)=∑d=1D∣B∣d\displaystyle \text{Cost}(D) = \sum_{d=1}^{D} |\mathcal{B}|^d
这个公式告诉我们:候选数量随表达式的嵌套深度 d 指数增长,所以限制最大深度 D、减少基础组件数量 B,是控制搜索成本的关键。
  1. 基础组件集合 B指 DSL 里允许出现的原子操作,比如常量、截取函数等。
    B\mathcal{B}
  2. 深度 d表达式的嵌套层数,每多一层,组合可能性就翻一次倍。
    d≤Dd \le D
  3. 累计求和把从 1 到 D 层的所有候选数量加起来,就是整个搜索规模。
    ∑d=1D\sum_{d=1}^{D}
  4. 指数增长|B| 的 d 次方说明基础组件一旦增多,搜索成本会急剧上升。
    ∣B∣d|\mathcal{B}|^d

应用与场景

Sketch 和 FlashFill 不只活在论文里,它们已经悄悄出现在你每天使用的工具中。最典型的是表格数据处理:几万行地址需要统一成“省-市-区”格式,手写公式又长又容易错;用 FlashFill 风格的工具,只要手动改两行示例,系统就能生成一整列转换规则。

代码自动化也是重头戏。工程师写排序函数时,可以只画出整体框架,把实现细节留成洞,交给 Sketch 类工具补全。数据清洗管道同样受益:ETL 流程里常见的日期格式转换、手机号脱敏、姓名拆分,都能靠“示例即规则”快速生成,而不是写一堆 if else。

教学场景同样有趣:学生输入“3 → 9、4 → 16”,可以让系统自动合成一个平方函数,帮助理解抽象逻辑。对 AI 从业者来说,这些应用意味着一个共同趋势:把“写代码”变成“描述代码”,而程序合成就是这套自动化的核心引擎。

挑战与前瞻

主厨也会遇到难题:客人只给三个例子,却要求覆盖一百种口味。程序合成目前最大的挑战,是如何处理复杂约束与海量搜索之间的矛盾。规格越复杂,验证越慢;示例太少,又容易学到过度拟合的错误程序。

可解释性同样棘手。系统给出一个能通过所有示例的表达式,但用户往往想问“为什么是这个而不是另一个”。如果合成器能提供简单的排名理由,比如“这个表达式最短、用到的常量最少”,信任感会大大增强。数据隐私也值得关注,因为合成过程常接触敏感表格数据,直接在本地端侧完成合成,会是一大趋势。

与大模型结合是新的希望:大模型擅长理解自然语言和生成候选代码,Sketch/FlashFill 擅长用验证器保证正确性。两者结合,就像主厨先用经验快速设计菜式,再让试菜员系统把关。未来的程序合成,会离“只表达意图,不关心实现”的梦想越来越近。

AI 应用场景

表格数据清洗助手

阶段:数据预处理与自动化

解决的问题:传统方式需要人工编写大量 Excel 公式或 Python 脚本,遇到“手机号统一为 3-4-4 格式”“日期改为 yyyy-mm-dd”这类任务时,效率低且容易出错。基于示例的程序合成只需用户示范几行,就能自动生成清洗规则。

具体案例:电商运营拿到十万行收货地址,第一列是名字、第二列是电话、第三列是省市区。运营手动把前三行改成目标格式,合成工具自动生成“提取首字 + 连接点号 + 截取电话后四位”的转换表达式,并在剩余数据上批量执行。

代码智能补全与草图填充

阶段:代码生成与质量验证

解决的问题:开发者画好函数骨架后,关键实现往往卡住。代码智能补全工具虽能续写,但不保证正确。Sketch 风格的程序合成把骨架当成“带洞草图”,用语法约束和验证器共同保障输出正确。

具体案例:嵌入式工程师写一个 CRC 校验函数,只写出输入输出和校验步骤,用 ?? 表示校验算法核心。合成器根据功能约束生成候选实现,验证器用边界测试逐个检查,最终返回通过全部测试的代码。

数据清洗管道生成

阶段:特征工程与 ETL 流程构建

解决的问题:传统 ETL 清洗规则需要人工维护,字段格式一变就要改代码。用程序合成,用户只需给一组“旧值 → 新值”示例,系统就能生成对应的转换函数,减少重复劳动。

具体案例:数据团队从多个系统采集日期字段,格式有 01/02/2024 和 03/04/2024。用户写出两条期望转换示例,合成器自动生成“拆分月日年→拼接为 yyyy-mm-dd”的函数,接入管道后每天自动清洗。

Python 演示

python56 行
# coding: utf-8"""A mini Sketch-style synthesizer:learn arithmetic expression from input-output examples.""" def evaluate(expr, x):    if isinstance(expr, int):        return expr    if expr == 'x':        return x    left, op, right = expr    if op == '+':        return evaluate(left, x) + evaluate(right, x)    if op == '-':        return evaluate(left, x) - evaluate(right, x)    if op == '*':        return evaluate(left, x) * evaluate(right, x)    raise ValueError('unknown expression') def size(expr):    if isinstance(expr, int) or expr == 'x':        return 1    return 1 + size(expr[0]) + size(expr[2]) def to_string(expr):    if isinstance(expr, int):        return str(expr)    if expr == 'x':        return 'x'    return '(' + to_string(expr[0]) + expr[1] + to_string(expr[2]) + ')' def synthesize(examples, max_size=5):    exprs = ['x'] + list(range(0, 10))    for d in range(2, max_size + 1):        base = list(exprs)        for left in base:            for op in ['+', '-', '*']:                for right in base:                    e = (left, op, right)                    if size(e) == d:                        exprs.append(e)    valid = []    for expr in exprs:        if all(evaluate(expr, x) == y for x, y in examples):            valid.append(expr)    valid.sort(key=lambda e: (size(e), to_string(e)))    return valid[0] if valid else None if __name__ == '__main__':    examples = [(2, 5), (3, 10), (4, 17)]    expr = synthesize(examples)    if expr is not None:        print('synthesized:', to_string(expr))        for x, y in examples:            print('f(%d) = %d' % (x, evaluate(expr, x)))    else:        print('no expression found')
目的:实现了一个迷你版 Sketch 思想——从输入输出示例中枚举候选表达式,自动合成形如 (x*x)+1 的数学程序。
  1. exprs = ['x'] + list(range(0, 10))初始原料:输入变量和常量 0 到 9。
  2. if size(e) == d:只保留指定复杂度的候选表达式,防止无限膨胀。
  3. if all(evaluate(expr, x) == y for x, y in examples):用全部示例验证候选,不满足就淘汰。
  4. valid.sort(key=lambda e: (size(e), to_string(e)))按“更短、更直观”排名,类似 FlashFill 的排序偏好。
  5. print('synthesized:', to_string(expr))输出最终合成出的表达式。

常见误区

误区很多初学者认为,程序合成就是给 AI 一个提示词,它随机生成代码,写对了就用,写错了再重试。

正解程序合成更像主厨在限定食材库里找菜谱:合成器必须在 DSL 约束下搜索,Sketch 还会用验证器证明候选程序在全部示例上成立,而不是靠猜。

为何常见:因为大模型生成代码的宣传太普遍,大家容易把“生成”和“合成”混为一谈,忽略了验证机制。

误区为了保险,用户一次性录入几十行示例,以为例子越多合成结果越准。

正解示例多但同质化,会加剧搜索成本,还可能让排名更难选择。真正重要的是示例多样性,比如覆盖空格、标点、长度差异这些边界情况。

为何常见:直觉上“数据越多越好”,但忽略了程序合成需要的是信息增益,而不是重复劳动。

误区FlashFill 不过是微软做的一个方便功能,背后没什么算法。

正解它背后有完整的程序合成技术栈:领域特定语言 DSL、版本空间代数、候选表达式排序,是“面向终端用户的程序合成”标杆。

为何常见:产品界面太简洁,用户看到的是“一键完成”,看不到内部的搜索与验证引擎。

误区只要 Sketch 输出了一个程序,它就是数学上绝对正确的,不需要再测试。

正解正确性依赖验证器覆盖的语义和输入集合。验证通过只说明在已建模的约束下成立,如果规格本身有遗漏,程序仍可能在真实场景出问题。

为何常见:把“验证器证明”想象成万无一失,却忘了验证器只能证明它被要求证明的东西。

面试题

Q1. 程序合成最核心的目标是什么?

单选题 · easy

  • A.从需求自动生成程序← 正确答案
  • B.用更多内存换速度
  • C.自动优化数据库索引
  • D.生成美观的用户界面

答案:A。程序合成强调从高层需求自动推导出可运行程序;优化内存、数据库索引和界面都不是它的核心目标。就像主厨从“想吃面”的模糊需求推出完整菜谱,而不是只调调味。

Q2. FlashFill 使用什么作为程序合成的输入?

单选题 · easy

  • A.一段自然语言描述
  • B.一个输入输出示例← 正确答案
  • C.一份完整软件文档
  • D.一个单元测试报告

答案:B。FlashFill 是“编程通过示例”的典型代表,它从输入输出示例中学习变换规则,而不是从自然语言或软件文档中理解需求。

Q3. Sketch 中的占位符通常被称为什么?

单选题 · mid

  • A.未知变量声明
  • B.待补全的洞← 正确答案
  • C.运行时异常
  • D.编译器优化点

答案:B。Sketch 允许用户在代码骨架中留下 ??,称为 Hole,翻译过来就是“待补全的洞”。它不是普通变量,也不是异常或优化点,而是合成器要填充的位置。

Q4. CEGIS 迭代中,验证器发现错误时的动作是?

单选题 · mid

  • A.把反例加入约束集← 正确答案
  • B.删除当前全部示例
  • C.启动图形调试器
  • D.立即终止整个流程

答案:A。CEGIS 的核心是“反例驱动”:验证器发现候选程序不满足某条输入时,会把该反例加入示例集,让合成器重新搜索。这就像试菜员说“太咸”,主厨据此改良,而不是推倒重来。

Q5. FlashFill 的 DSL 中 SubStr 表达式的功能是?

单选题 · hard

  • A.按位置截取字符串片段← 正确答案
  • B.在多个表间建立索引
  • C.把字符串转成浮点数
  • D.删除工作表中的空行

答案:A。SubStr 表示“从原字符串的某个位置取到另一个位置”,比如取第一个字符或某段数字。它是 FlashFill 处理姓名缩写、提取账号等任务的基础操作。

Q6. 版本空间代数的主要作用是什么?

单选题 · easy

  • A.紧凑保存候选程序集合← 正确答案
  • B.生成图形化用户界面
  • C.压缩电子表格文件大小
  • D.加密传输用户隐私数据

答案:A。版本空间代数用集合运算表示大量候选程序,避免逐个枚举,大幅提升搜索效率。它不负责界面、压缩或加密,只关心“如何高效表示可能正确的菜谱集合”。

Q7. 以下哪些属于 FlashFill 的典型处理场景?

多选题 · mid

  • A.拆分姓名和日期← 正确答案
  • B.批量提取邮箱账号← 正确答案
  • C.训练图像识别模型
  • D.调整页边距和字号

答案:AB。FlashFill 擅长字符串变换,比如从“张三 2024-01-02”中拆出姓名和日期,或从文本中提取邮箱账号。图像识别和页面排版不属于它的字符串变换范畴。

Q8. Sketch 合成过程中通常包含哪些要素?

多选题 · hard

  • A.带洞的草图程序← 正确答案
  • B.程序语言的语法限制← 正确答案
  • C.验证器返回的反例← 正确答案
  • D.编译后目标机器码

答案:ABC。Sketch 需要带洞草图、语法引导搜索和验证器反例三要素配合:先有洞,再按语法枚举填充,最后用反例迭代修正。目标机器码是传统编译产物,不是 Sketch 的必要输入。

Q9. 程序合成目前面临的主要挑战有哪些?

多选题 · hard

  • A.候选搜索空间爆炸← 正确答案
  • B.生成结果难以解释← 正确答案
  • C.屏幕分辨率不够高
  • D.示例规格存在歧义← 正确答案

答案:ABD。搜索空间爆炸让组合数迅速失控,可解释性弱让用户难以信任生成结果,示例歧义则可能让程序学到错误模式。屏幕分辨率与合成算法无关。

Q10. 多列协同合成的主要收益是什么?

单选题 · mid

  • A.共享中间转换表达式← 正确答案
  • B.提升显示器的刷新率
  • C.减少键盘的按键次数
  • D.增加表格的行列数量

答案:A。多列协同让不同列之间共享中间结果和变换模式,比如先统一提取省份,再提取城市。它能降低整体搜索成本,但不会影响显示器、键盘或表格大小。