想象你走进一家私房菜馆,不看菜单,只对主厨说:“我要一碗面,不要太辣,最好加颗溏心蛋。”主厨根据这句话,反推出完整做法。程序合成做的正是这件事:不要求你逐行写出代码,而是把“想要什么”表达出来——可以是几张输入输出示例,也可以是一张留有空白的菜谱草稿——让计算机自动把缺失的逻辑填出来。这项技术里有两个响当当的名字:Sketch 和 FlashFill。Sketch 像一位谨慎的主厨,先画草稿再请试菜员验证;FlashFill 则像 Excel 里的小助手,你示范三五行“把姓名改成缩写”,它立刻学会整列文本变换。本文就用“主厨反推菜谱”这条线索,带你从零看懂程序合成与验证的核心思路。
掌握程序合成,意味着你能用“示例即需求”的方式自动生成可靠代码,在数据清洗、代码补全和开发工具智能化中创造出真正落地的 AI 产品价值。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
认识程序合成
你不需要背出油盐酱醋的精确克数,只要对主厨说“我想要一碗带溏心蛋的面”,他就能把整道菜做出来。传统编程就像你事无巨细地指挥主厨:“先放油,再放葱,三分钟后加水……”程序合成则把指挥权交给系统:你描述“要什么”,系统自动写出“怎么做”。
从技术上说,程序合成(Program Synthesis)是在一个由合法程序构成的空间里,自动寻找满足用户意图的程序。用户的意图可以是输入输出示例、自然语言描述、形式化规约,或者一张不完整的草图。Sketch 和 FlashFill 是这个领域最有名的两个代表:前者面向通用代码补全,后者面向表格文本变换。
用一个公式可以概括合成问题的核心:
Sketch 像酒店后厨的大师傅:先写一个粗略流程,然后让助手补齐细节。FlashFill 则像街边小摊老板:看一眼客人比划的样例,马上做出同样风格的小吃。它们共享同一个目标——从“模糊的需求”走向“确定的程序”。
- 存在量词先限定我们在一个菜谱库中找候选菜谱,这个菜谱库就是合法的程序集合。
- 全称量词找到的菜谱不能只对一道菜有效,必须对所有示例都成立。
- 条件等式“把输入变成输出”这一动作,被表达成程序 P 在输入 x_i 上的运行结果等于 y_i。
Sketch核心原理
Sketch 的灵感来自“先画草图,再请人完善”。主厨写菜谱时,会留下“加少许____”这样的空格;Sketch 把这些空格称为占位符(Hole),通常用 ?? 表示。用户给出一个不完整的程序骨架,合成器负责把每个洞填上具体表达式。
这个“填洞”过程不能乱来。Sketch 使用语法引导搜索(Syntax-Guided Synthesis,SyGuS):先定义一组允许出现的语法,比如只能使用加、减、乘和常量,再枚举候选填充方案。把候选填进草图后,验证器会检查它是否满足全部约束。
验证失败时,最漂亮的一招出现了:CEGIS(Counterexample-Guided Inductive Synthesis,反例引导的归纳合成)。验证器不是简单地说“不行”,而是返回一个反例,比如“输入为 3 时你输出了 7,但期望是 8”。合成器把反例加入示例集,重新搜索,直到没有反例或超时。整个过程可以写成:
这个流程像主厨反复试菜:先按暂定菜谱做一道,试菜员尝出“太咸”,他就在下一版减少盐量。CEGIS 的价值在于,每次失败都会带来一条新信息,让搜索越来越有方向。
- 填充方案 σσ 是给每个洞选出的具体表达式,来自有限的填充集合 Σ。
- 所有输入 x验证范围是输入集 I,通常包含用户示例和多次迭代中收到的反例。
- 填入后的程序P_h 是带洞草图,[σ] 表示把 σ 填进洞里,最后要求输出满足 y_x。
FlashFill原理
FlashFill 是一道更家常的菜:在 Excel 里,用户手动示范“John Smith → J. Smith”,再按一下回车,整列都能自动生成类似缩写。它不要求用户写代码,只要求提供输入输出示例,因此也叫“编程通过示例”(Programming by Example,PBE)。
为什么机器能“照葫芦画瓢”?因为 FlashFill 为字符串变换设计了一个专属 DSL。这个领域特定语言限制了食材种类,让搜索不必漫无目的。常见的表达式语法是:
有了 DSL,FlashFill 会枚举所有候选表达式,再用示例过滤。最后它按“长度短、常量少、结构简单”的规则给候选打分,把最像人类手写的表达式排在前面。这正是主厨从三碗成品中猜出统一做法:先限定可用调料,再挑最简单说得通的版本。
- 原字符串 x最基本原料,表示当前单元格的输入文本。
- 固定字符串 Const(s)例如
"."、"-"这类保持不变的分隔符。 - 截取子串 SubStr(x,p1,p2)从 x 的第 p1 个位置取到第 p2 个位置,比如取第一个字符。
- 拼接 Concat(e1,e2)把两个表达式的结果首尾相连,组成更复杂的转换。
关键算法
主厨手里有很多半成品:切好的葱花、调好的酱汁、煮到七分的面条。程序合成器也一样,擅长把已经验证过的子表达式拼接成更大的表达式。版本空间代数(Version Space Algebra)就是一套高效管理“候选集合”的方法。
普通初学者可能会把每个候选程序单独存起来,但程序空间会爆炸。版本空间代数的聪明之处在于:用集合的交、并、笛卡尔积运算来表示一大片候选,而不必逐个列出。例如,当已知左边候选集合 S1 和右边候选集合 S2 后,拼接表达式的集合可以写成:
在实际合成中,系统还会做一致性检验:如果一个表达式在某些示例上失败,就把它从版本空间里剔除;如果两个候选相互冲突,就保留更符合多数示例的一方。这就像主厨面对两个客人的口味矛盾,选择一套能同时满足最多人的固定流程。
- 候选集合 S1、S2它们是之前从不同示例中整理出来的“部分菜谱”。
- 合并算子 ⊕它不是一个普通加法,而是把两个集合里的元素两两组合。
- 拼接表达式对每一对 e1、e2 都生成
Concat(e1,e2),并收集成集合。 - 成员条件e1 必须来自 S1,e2 必须来自 S2,保证每个组合都有依据。
性能与优化
菜谱越复杂,厨房要尝试的组合就越多。程序合成最怕的是搜索空间爆炸:明明只有几十种基本操作,组合起来却像天文数字。Sketch 和 FlashFill 都在拼命“省火省时”。
第一招是剪枝:遇到一个子表达式在某条示例上已经失败,就整棵剪掉,不再尝试它的各种上层组合。第二招是缓存复用:同一个字符串片段可能被多个候选用到,与其反复计算,不如把结果缓存起来,像提前批量切好的菜。第三招是多列协同合成:电子表格里常常同时需要改姓名、日期、金额,合成器让多列共享中间变换,一起逼近最终格式。
搜索代价可以粗略表示成:
剪枝和缓存还能继续互相配合:缓存让剪枝判断更快,剪枝让缓存里存的东西更少。主厨的灶台也讲究“顺手”:把最常用的高汤提前备好,把不合适的配方直接扔掉,整个后厨才能高效运转。
- 基础组件集合 B指 DSL 里允许出现的原子操作,比如常量、截取函数等。
- 深度 d表达式的嵌套层数,每多一层,组合可能性就翻一次倍。
- 累计求和把从 1 到 D 层的所有候选数量加起来,就是整个搜索规模。
- 指数增长|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 演示
# 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')
(x*x)+1 的数学程序。exprs = ['x'] + list(range(0, 10))初始原料:输入变量和常量 0 到 9。if size(e) == d:只保留指定复杂度的候选表达式,防止无限膨胀。if all(evaluate(expr, x) == y for x, y in examples):用全部示例验证候选,不满足就淘汰。valid.sort(key=lambda e: (size(e), to_string(e)))按“更短、更直观”排名,类似 FlashFill 的排序偏好。print('synthesized:', to_string(expr))输出最终合成出的表达式。
常见误区
误区很多初学者认为,程序合成就是给 AI 一个提示词,它随机生成代码,写对了就用,写错了再重试。
正解程序合成更像主厨在限定食材库里找菜谱:合成器必须在 DSL 约束下搜索,Sketch 还会用验证器证明候选程序在全部示例上成立,而不是靠猜。
为何常见:因为大模型生成代码的宣传太普遍,大家容易把“生成”和“合成”混为一谈,忽略了验证机制。
误区为了保险,用户一次性录入几十行示例,以为例子越多合成结果越准。
正解示例多但同质化,会加剧搜索成本,还可能让排名更难选择。真正重要的是示例多样性,比如覆盖空格、标点、长度差异这些边界情况。
为何常见:直觉上“数据越多越好”,但忽略了程序合成需要的是信息增益,而不是重复劳动。
误区FlashFill 不过是微软做的一个方便功能,背后没什么算法。
正解它背后有完整的程序合成技术栈:领域特定语言 DSL、版本空间代数、候选表达式排序,是“面向终端用户的程序合成”标杆。
为何常见:产品界面太简洁,用户看到的是“一键完成”,看不到内部的搜索与验证引擎。
误区只要 Sketch 输出了一个程序,它就是数学上绝对正确的,不需要再测试。
正解正确性依赖验证器覆盖的语义和输入集合。验证通过只说明在已建模的约束下成立,如果规格本身有遗漏,程序仍可能在真实场景出问题。
为何常见:把“验证器证明”想象成万无一失,却忘了验证器只能证明它被要求证明的东西。
面试题
Q1. 程序合成最核心的目标是什么?
- A.从需求自动生成程序← 正确答案
- B.用更多内存换速度
- C.自动优化数据库索引
- D.生成美观的用户界面
答案:A。程序合成强调从高层需求自动推导出可运行程序;优化内存、数据库索引和界面都不是它的核心目标。就像主厨从“想吃面”的模糊需求推出完整菜谱,而不是只调调味。
Q2. FlashFill 使用什么作为程序合成的输入?
- A.一段自然语言描述
- B.一个输入输出示例← 正确答案
- C.一份完整软件文档
- D.一个单元测试报告
答案:B。FlashFill 是“编程通过示例”的典型代表,它从输入输出示例中学习变换规则,而不是从自然语言或软件文档中理解需求。
Q3. Sketch 中的占位符通常被称为什么?
- A.未知变量声明
- B.待补全的洞← 正确答案
- C.运行时异常
- D.编译器优化点
答案:B。Sketch 允许用户在代码骨架中留下 ??,称为 Hole,翻译过来就是“待补全的洞”。它不是普通变量,也不是异常或优化点,而是合成器要填充的位置。
Q4. CEGIS 迭代中,验证器发现错误时的动作是?
- A.把反例加入约束集← 正确答案
- B.删除当前全部示例
- C.启动图形调试器
- D.立即终止整个流程
答案:A。CEGIS 的核心是“反例驱动”:验证器发现候选程序不满足某条输入时,会把该反例加入示例集,让合成器重新搜索。这就像试菜员说“太咸”,主厨据此改良,而不是推倒重来。
Q5. FlashFill 的 DSL 中 SubStr 表达式的功能是?
- A.按位置截取字符串片段← 正确答案
- B.在多个表间建立索引
- C.把字符串转成浮点数
- D.删除工作表中的空行
答案:A。SubStr 表示“从原字符串的某个位置取到另一个位置”,比如取第一个字符或某段数字。它是 FlashFill 处理姓名缩写、提取账号等任务的基础操作。
Q6. 版本空间代数的主要作用是什么?
- A.紧凑保存候选程序集合← 正确答案
- B.生成图形化用户界面
- C.压缩电子表格文件大小
- D.加密传输用户隐私数据
答案:A。版本空间代数用集合运算表示大量候选程序,避免逐个枚举,大幅提升搜索效率。它不负责界面、压缩或加密,只关心“如何高效表示可能正确的菜谱集合”。
Q7. 以下哪些属于 FlashFill 的典型处理场景?
- A.拆分姓名和日期← 正确答案
- B.批量提取邮箱账号← 正确答案
- C.训练图像识别模型
- D.调整页边距和字号
答案:AB。FlashFill 擅长字符串变换,比如从“张三 2024-01-02”中拆出姓名和日期,或从文本中提取邮箱账号。图像识别和页面排版不属于它的字符串变换范畴。
Q8. Sketch 合成过程中通常包含哪些要素?
- A.带洞的草图程序← 正确答案
- B.程序语言的语法限制← 正确答案
- C.验证器返回的反例← 正确答案
- D.编译后目标机器码
答案:ABC。Sketch 需要带洞草图、语法引导搜索和验证器反例三要素配合:先有洞,再按语法枚举填充,最后用反例迭代修正。目标机器码是传统编译产物,不是 Sketch 的必要输入。
Q9. 程序合成目前面临的主要挑战有哪些?
- A.候选搜索空间爆炸← 正确答案
- B.生成结果难以解释← 正确答案
- C.屏幕分辨率不够高
- D.示例规格存在歧义← 正确答案
答案:ABD。搜索空间爆炸让组合数迅速失控,可解释性弱让用户难以信任生成结果,示例歧义则可能让程序学到错误模式。屏幕分辨率与合成算法无关。
Q10. 多列协同合成的主要收益是什么?
- A.共享中间转换表达式← 正确答案
- B.提升显示器的刷新率
- C.减少键盘的按键次数
- D.增加表格的行列数量
答案:A。多列协同让不同列之间共享中间结果和变换模式,比如先统一提取省份,再提取城市。它能降低整体搜索成本,但不会影响显示器、键盘或表格大小。