闭上眼睛想象一个场景:你请来一位神秘的侦探,只给他看几组录像——每组录像有两段画面:第一段,厨房台面上摆着食材(鸡蛋、番茄、葱花);第二段,一盘番茄炒蛋已经端上桌。再换一组食材,土豆和牛肉,画面切到一盘土豆炖牛腩。侦探看完五六组这样的“食材→成品”录像后,走进厨房,亲手还原出了那本传说中的菜谱。这个故事听起来像侦探小说,但它正是人工智能领域一个真实的研究方向——程序合成(Program Synthesis)。程序合成要让计算机从“输入输出示例”或“任务描述”里反推出一个程序:这个程序像菜谱一样,把给定原材料(输入)变成期待的成品(输出)。Excel 里的 FlashFill 和科研界的 DeepCoder,就是这个方向最有名的两个代表:前者是细心的学徒,靠观察示例总结字符串整理规则;后者是阅菜无数的老侦探,用神经网络练出一身“直觉”来加速寻找菜谱。读完这篇文章你会明白,“让机器自己写代码”并非魔法,而是一场关于搜索、语言和直觉的精密博弈。
程序合成是 AI 从“会写代码”走向“自动写代码”的桥梁。理解 FlashFill 与 DeepCoder 的合成思路,能帮你站在代码智能的前沿,设计出真正可落地的自动化编程工具。
可视化演示
展示四种微服务架构模式:API网关、服务网格、事件驱动和Saga事务。切换模式查看不同架构的服务拓扑和通信方式。
本节配有交互动效(进入视口后自动加载,约 3KB)。
API网关模式 Gateway 所有请求通过统一网关入口路由到后端服务 网关负责鉴权、限流、负载均衡和协议转换 Client → Gateway → Services
合成概念
假设你把一份食材清单和一张成品菜照片同时交给一位“菜谱侦探”。侦探没有进过你的厨房,只看过三五组这样的“输入—输出”,却要倒推出完整菜谱。听起来像天方夜谭,但程序合成就是为此而生的一门技术:它把“写菜谱”这个原本属于大厨的工作,交给一个自动化的搜索过程。
程序合成(Program Synthesis)指从规格(specification)或示例(examples)出发,自动生成满足要求的程序。规格可能是形式化描述,示例则是“输入 x 对应输出 y”的数据对。和传统编程最大的区别是:我们不再逐行告诉计算机怎么做,而是告诉它“把什么变成什么”,剩下的由合成器自己想办法。
合成的经典建模方式是把它看作一个搜索问题:在一个由领域特定语言(DSL)定义的候选程序空间里,找一个程序 P,让它在所有已知示例上表现正确。最简单的目标函数是“错误数最少”:
侦探并不关心菜谱写得多花哨,只关心它能不能在已知的几道菜上分毫不差。但“少犯错”并不等于“一直正确”:候选空间有多大?搜索如何进行?这正是 FlashFill 与 DeepCoder 登场的舞台。
- 候选程序 PP 是领域语言 \mathcal{D} 里的一条“菜谱”,由若干算子组合而成,代表一种可能的转换方式。
- 示例集合(x_i, y_i) 表示第 i 组“原材料→成品”证据,共 n 组,来自用户提供或自动生成。
- 指示函数\mathbb{1}[·] 是计数开关,括号里的条件成立记 1、不成立记 0,用来数“做错了几道菜”。
- 最小化目标P^* 是所有候选里犯错最少的程序,也就是最贴合手上证据的那本菜谱。
归纳式合成
表哥是个表格菜鸟,整理通讯录时总要把 “ada lovelace“ 改成 ”ADA LOVELACE”。Excel 里有个叫 FlashFill 的功能:只要你在旁边打出两三个期望结果,它就会自动总结规则,把整列数据一次性收拾干净。它就像侦探身边那个手脚麻利的学徒,看几个例子就能举一反三。
FlashFill 的背后是“归纳式合成”(inductive synthesis):从有限的输入输出对出发,推断出一个能解释所有示例的通用规则。关键点在“通用”二字——死记硬背不算本事,能处理没见过的名字才算真正学会了规则。
FlashFill 定义的字符串变换 DSL 中包含 Concat(拼接)、SubStr(截取子串)、ToUpper/ToLower(大小写转换)等算子。一个典型程序的形状如下:
学徒观察得越仔细,猜的规则越靠谱;但如果示例太少或误导,他也会学出一本“只对两盘菜有效”的假菜谱。这种“死记样本”的毛病在机器学习里有个熟悉的名字——过拟合,后面我们会展开。
- SubStr 切菜SubStr(x, l, u) 表示从输入 x 的第 l 位取到第 u 位,相当于用菜刀从食材上切下需要的片段。
- Concat 拼盘Concat 把多个片段按顺序拼回一个完整字符串,相当于把切好的食材按顺序摆进盘里。
- 位置参数l_1、u_1、l_2、u_2 是切菜的位置,合成器在枚举中不断尝试,示例决定了哪些位置“看起来对”。
- 整体形状P(x) 是一条“切两刀、拼一盘”的菜谱——名字被拆成姓和名,再按统一格式重组。
领域语言设计
侦探的厨房不是无限自由的:他只有几样固定工具——切菜刀(SubStr)、拼盘(Concat)、加热调色(ToUpper/ToLower)、换料(Replace)。没有蒸笼,他就永远做不出“蒸菜”。这就是领域特定语言(DSL)的隐喻:DSL 定义了一组合法算子,也就划定了“可能写出的程序”的边界。
程序合成必须在某个 DSL 中进行,因为在无约束的通用编程语言里,“找一个满足示例的程序”在理论上不可判定。DSL 是合成器唯一会说的“语言”,它决定了表达能力的上限——好比侦探只会用厨房里有的工具,绝无可能凭空变出一台蒸箱。
FlashFill 的 DSL 就是一个小巧的字符串变换语言:
所以说,设计 DSL 就像给侦探配工具:工具太少,很多菜谱无法还原;工具太多,他在工具间里挑到天黑也找不出正确步骤。好的 DSL 是一门“够用且好搜”的平衡艺术。
- 算子集合\mathcal{D} 是合成搜索空间的定义,花括号里的每个元素都是一个可调用的转换动作。
- 每个算子的职责Concat 负责拼接、SubStr 负责截取、ToUpper/ToLower 负责大小写、Replace 负责替换——对应厨房里不同的“刀工火候”。
- 组合产生无限程序算子本身有限,但通过反复嵌套组合,可以表达出无数个不同程序,搜索空间由此而来。
- 设计与平衡DSL 越丰富表达力越强,但搜索越困难;FlashFill 选择小而贴合表格场景的语言,换来了可用的响应速度。
搜索与排序
有了 DSL,侦探要面对最现实的问题:怎么从成千上万的候选菜谱里找到正确的那一本?如果傻乎乎地把所有组合都试一遍——切三次、拼两次、换个料——他可能从天亮试到天黑。这就像在图书馆里既不按索引也不按分类,从第一本硬翻起。
枚举式搜索就是这种朴素做法:按程序深度从小到大枚举候选,用全部示例验证,第一个全部通过的候选就是答案。FlashFill 早期版本正是这样工作的。枚举虽然简单,但候选数量随程序深度指数增长,直接导致“搜索爆炸”:
于是我们需要更聪明的策略:排序与剪枝。排序指按照某种启发(程序长度优先、算子似然优先)决定先搜哪个候选;剪枝指一旦发现某个前缀已经无法满足示例,就放弃整棵子树,不再浪费时间。
这就像侦探先凭经验判断“八成是先切再拼”,从那几本菜谱开始翻,而不是从第一本翻起;发现某条路走不通,立刻折返。搜索策略的优劣,直接决定合成是“秒回”还是“等到地老天荒”。
- 程序深度 kk 表示菜谱里动作的步数,比如切一刀再拼一盘就是 k = 2。
- 每层候选数每多一步,候选数量就要乘一次算子总数 |\mathcal{D}|,因为每一步都可以选任意一种工具。
- 逐层累加把 1 步到 K 步的所有可能程序都加起来,才是完整的假设空间大小。
- 指数爆炸当 |\mathcal{D}| = 10、K = 6 时,6 层以内就有一百多万个候选,纯枚举很快吃不消。
神经程序合成
光靠经验和剪枝还不够快,于是 DeepCoder 请来了一位阅谱无数的老侦探——一个预训练的神经网络。老侦探瞄一眼食材和成品,就能凭直觉说:“这个八成要先焯水、再爆炒。”他不能直接写出菜谱,但能大幅缩小翻书范围。
DeepCoder 的思路是“神经引导搜索”:把输入输出示例编码成特征向量,用神经网络预测每个 DSL 算子在正确程序里出现的概率,再把这些概率当作先验,重排搜索顺序。
注意,老侦探的直觉只是“导航”,不是“终点”。最终程序仍要靠搜索验证——神经网络的功劳是让搜索像装了 GPS 一样直奔目标,而不是漫无目的地绕路。这就是 DeepCoder 给程序合成带来的关键升级。
- 程序算子集合 ops(P)一条菜谱由若干动作组成,ops(P) 就是这条菜谱用到的所有工具清单。
- 神经网络先验 p_\theta老侦探(神经网络)根据示例 \mathcal{E} 预测某个工具 f 会被用到的概率,\theta 是网络参数,从大量合成任务中训练而来。
- 得分累加候选程序的总得分等于它包含工具的概率求和,直觉上“越可能被大师选中的工具组合”得分越高。
- 引导排序合成器按得分从高到低搜索,正确菜谱往往出现在很靠前的位置,搜索时间因此大幅缩短。
挑战与局限
侦探再神,也有翻车的时候。给一盘菜他能还原菜谱,给他十盘相似菜他反而可能学出一本“只对这十盘有效”的假菜谱。程序合成至今仍有几道硬门槛。
第一是搜索爆炸:程序步骤稍长、算子稍多,候选空间就指数膨胀,哪怕有神经网络引导,深程序依然难产。第二是规格模糊:示例通常不足以唯一确定程序,存在大量“都能满足示例”的候选,机器不知道用户真正想要哪一本。
第三是泛化问题:验证只能在有限例子上进行,而合成器真正该保证的是在新输入上也正确。用留出集衡量的准确率,才是菜谱是否真正学会的标尺:
这些局限提醒我们:程序合成不是万能的“自动编程机”,而是在 DSL 边界、示例质量和搜索效率三者之间走钢丝。理解了钢丝在哪里,才能在实践中避开陷阱。
- 留出示例(x_j, y_j) 是合成时没见过的“新菜”,共 m 份,用来检验菜谱是否真学会了,而不是背下了旧题。
- 逐个判断指示函数对每份新菜做判断,做对了记 1、做错了记 0。
- 求平均把所有判断结果求和再除以 m,得到 0 到 1 之间的准确率。
- 与训练对比训练示例上全对只说明“背下来了”,留出集上的准确率才反映“学会了”。两者差距越大,过拟合越严重。
应用与前景
侦探出诊的价值,已经在真实世界里兑现。最早也最广为人知的,是 Excel 的 FlashFill——如今每个用表格的人都能体验“输入两三个例子,整列数据自动清洗”的魔法。它本质就是一个在数秒内完成枚举搜索的归纳式合成器。
更前沿的方向包括:代码修复——用“出错代码 + 期望行为”作为示例,自动合成最小补丁;编程教育——自动生成参考解法,或对学习者代码做等价性验证;以及大模型时代的新合成范式——用语言模型生成候选程序,再用搜索器验证修正,两者互为补充。
可以预见,未来的程序合成会走向“人机协作”:人类负责提供 DSL、示例和约束,合成器负责搜索与验证。它不会取代程序员,而是把程序员从重复琐碎的转换劳动中解放出来,去写更具创造性的代码。
从菜谱侦探到合成器,变的只是载体,不变的是同一个信念:只要把“想要什么”讲清楚,机器就能自己找到“怎么做”的路。
AI 应用场景
表格数据清洗的零代码魔法
阶段:数据预处理 / 数据工程
解决的问题:传统方式需要手写正则表达式或 VBA 宏,门槛高、易出错。程序合成只靠几个输入输出示例就能自动生成转换程序,让非技术人员也能完成清洗。
具体案例:某银行运营团队要整理上千行客户名单,把 "ZHANG WEI" 统一成全大写规范格式。在 Excel 中输入两三个期望结果,FlashFill 自动合成转换程序,一键应用到整列,原先需要半天的工程压缩到几十秒。
自动化代码修复与补丁生成
阶段:软件工程 / DevOps 质量保障
解决的问题:人工定位并修补 bug 耗时费力,且容易引入新问题。程序合成以“出错状态→期望状态”为示例,在 DSL 中搜索最小修改程序,自动生成候选补丁,把开发者的工作从“写补丁”变成“审补丁”。
具体案例:持续集成流水线检测到一处数组越界异常,合成工具根据函数调用前后的输入输出对,生成一段最小补丁并附带测试用例,开发者在代码评审页面确认后即可合并。
编程教育的智能批改与反馈
阶段:教育科技 / 智能教学系统
解决的问题:开放编程题的批改难以自动化,人工批改反馈慢。程序合成可以自动生成参考解,并用差异化测试验证学生代码与参考解的等价性,精准定位逻辑漏洞。
具体案例:在线判题系统对学生的排序算法实现运行一组带标签样例,合成工具基于输入输出示例自动构造新测试用例,发现学生代码在边界输入上的隐藏 bug,并给出针对性提示。
Python 演示
# -*- coding: utf-8 -*-import itertools OPS = ['toupper', 'tolower', 'trim', 'first3', 'last3', 'title'] def apply(prog, s): """按合成出的菜谱(程序)解释执行字符串变换""" out = s for op in prog: if op == 'toupper': out = out.upper() elif op == 'tolower': out = out.lower() elif op == 'trim': out = out.strip() elif op == 'first3': out = out[:3] elif op == 'last3': out = out[-3:] elif op == 'title': out = out.title() return out def synthesize(examples, max_depth=3): """归纳式合成: 在 DSL 中枚举候选, 用全部示例验证""" for depth in range(1, max_depth + 1): for prog in itertools.product(OPS, repeat=depth): if all(apply(prog, x) == y for x, y in examples): return prog return None def main(): examples = [ (' Ada LoveLace ', 'ADA LOVELACE'), (' Alan Turing ', 'ALAN TURING'), ] prog = synthesize(examples) if prog is None: print('没有找到符合示例的菜谱') return print('合成出的程序:', list(prog)) for name in [' Grace Hopper ', ' Linus Torvalds']: print('%-16s -> %s' % (name.strip(), apply(prog, name))) if __name__ == '__main__': main()
OPS = [...]定义了迷你 DSL 的算子清单,相当于侦探厨房里的全部工具def apply(prog, s)解释执行程序:依次把每个算子作用到字符串上,得到输出itertools.product(OPS, repeat=depth)枚举搜索空间,生成所有长度为 depth 的候选程序all(apply(prog, x) == y for x, y in examples)用全部示例验证候选,有一例不符就淘汰return prog返回第一个通过全部示例验证的程序,这就是“合成出的菜谱”
常见误区
误区我听说 AI 能自动写代码了,那是不是把需求扔给它,它就能帮我写完整个电商系统?以后程序员都要失业了。
正解程序合成是在受限的 DSL 里搜索满足示例的程序,本质上是“在给定的工具集里拼出答案”。它擅长的是小而明确的任务,比如字符串清洗、小函数修复、表达式补全。大型软件涉及架构设计、需求理解、模块协调,远超合成器能力。程序合成更像自动生成一道菜谱,而不是给你开一家餐厅。
为何常见:新闻常把“AI 写代码”渲染成万能,混淆了代码生成(写一段新代码)和程序合成(在受约束空间里搜索),导致期望被过度拉高。
误区那我多给点输入输出对,合成器肯定能找到更好的程序,而且不会出错。
正解更多示例确实能缩小假设空间、降低歧义,但每次验证候选程序都要在所有示例上跑一遍,示例过多反而拖慢搜索速度。真正决定合成质量的是示例的“覆盖度”:是否覆盖了边界情况、是否排除了错误假设。三四个精心挑选的示例,往往胜过一百个冗余重复的示例。
为何常见:机器学习领域“数据越多越好”的印象深入人心,但程序合成的瓶颈往往在搜索成本而非信息量,两者逻辑并不相同。
误区DeepCoder 不就是用神经网络把输入输出直接映射成程序吗?像机器翻译一样,输入示例,输出代码。
正解DeepCoder 的神经网络只负责“打分”:预测哪些算子更可能出现在正确程序里,从而给搜索排序。最终程序仍由枚举搜索枚举、验证并确认。神经网络是导航员,搜索才是驾驶员,两者分工不同。把深度学习的作用理解成“直接写程序”,是对“神经引导搜索”这一混合范式的误读。
为何常见:大模型时代的“端到端代码生成”过于流行,人们自然把老一代神经程序合成也想象成同一种模式。
误区DSL 里多加几个算子不是更好吗?功能更多,能解决的问题也更多。
正解DSL 越强,候选程序空间越大,搜索爆炸越严重。合成器需要在“表达力”和“可搜索性”之间取平衡:FlashFill 的 DSL 之所以小巧,正是为了在表格场景里保证秒级响应。无限强的 DSL(图灵完备)甚至会让“找满足示例的程序”变得不可判定。工具多不等于好用,关键在于能否快速找到答案。
为何常见:日常经验里“功能多=产品好”,但程序合成是搜索问题,功能多直接转化为搜索代价,这是反直觉的地方。
面试题
Q1. 程序合成最典型的输入是什么?
- A.自然语言的用户描述
- B.规格说明或输入输出示例← 正确答案
- C.编译完成的二进制程序
- D.人工标注的图片数据集
答案:B。程序合成的输入是规格(形式化约束)或示例(输入输出对)。FlashFill 靠用户给出的输入输出示例来工作;自然语言描述不是传统合成器的直接输入,二进制程序和图片数据集则与此无关。像侦探靠“食材+成品”录像破案一样,示例就是合成器最重要的破案线索。
Q2. Excel 的 FlashFill 属于下面哪种合成思路?
- A.演绎式的符号推理合成
- B.示例驱动的归纳式合成← 正确答案
- C.端到端的神经网络生成
- D.基于模板的静态实例化
答案:B。FlashFill 从用户提供的输入输出示例中归纳出字符串变换规则,属于典型的归纳式合成(inductive synthesis)。它不做端到端神经网络生成,也不是从一个通用定理演绎推导,更不是套固定模板。这正是“学徒看例子学规则”的工作方式。
Q3. 在程序合成里,领域特定语言(DSL)的核心作用是什么?
- A.划定候选程序搜索空间的范围← 正确答案
- B.加快处理器的运算速度
- C.存储用户的全部历史数据
- D.渲染程序运行的图形界面
答案:A。DSL 定义了合法算子的集合,也就划定了“可能写出哪些程序”的边界,搜索只在这个空间内进行。它不负责加速 CPU、存储数据或渲染界面。就像侦探厨房里的工具清单,决定了能还原哪些菜谱、不能还原哪些菜谱。
Q4. 下面哪句正确描述了“搜索空间爆炸”?
- A.候选程序数量随深度指数级增长← 正确答案
- B.内存不足导致合成进程崩溃
- C.网络带宽被合成工具占满
- D.磁盘读写速度逐渐变慢
答案:A。搜索空间爆炸是指候选程序数量随 DSL 算子数和程序深度指数级增长。比如 10 个算子、6 层程序就有上百万候选。它描述的是算法复杂性难题,不是内存、网络或磁盘资源问题。这就是为什么侦探不能把所有组合都试一遍的原因。
Q5. DeepCoder 用神经网络改进搜索的方式包括哪些?
- A.用神经网络为每个算子预测概率← 正确答案
- B.从输入输出示例中提取统计特征← 正确答案
- C.完全放弃领域特定语言的使用
- D.按照预测得分重排搜索顺序← 正确答案
答案:ABD。DeepCoder 的神经网络从输入输出示例中提取特征,预测每个 DSL 算子出现的概率,并用这些概率给候选程序打分,从而重排搜索顺序。它始终依赖 DSL 和搜索,并未放弃 DSL。相当于老侦探用直觉排定翻书的先后,但书还是要一本本翻。
Q6. 归纳合成中出现“过拟合”,指的是哪种状态?
- A.程序只符合训练示例但新输入失败← 正确答案
- B.训练过程消耗过长的编译时间
- C.合成出的程序代码结构过于庞大
- D.示例数据集规模太小导致欠拟合
答案:A。过拟合指合成出的程序在已知示例上全对,但无法推广到未见输入。代码庞大或训练慢是诱因或伴生现象,不是过拟合的定义。这就好比学徒背下了两盘菜的造型,却不会做第三盘相似的菜。解决思路是增加覆盖边界情况的示例,或用留出集验证泛化。
Q7. 程序合成目前面临的核心挑战有哪些?
- A.候选程序的搜索空间往往十分巨大← 正确答案
- B.用户提供的示例规格常常不够完整← 正确答案
- C.更强表达能力与搜索难度存在矛盾← 正确答案
- D.合成出的程序解释性总是完美无缺
答案:ABC。三大核心挑战正是:搜索空间爆炸、示例规格不完备导致歧义、DSL 表达力与可搜索性的矛盾。合成程序的解释性并非总是完美,往往还偏弱,因此 D 不成立。侦探既怕工具太多翻不完,又怕线索太少猜不准。
Q8. 搜索过程里的“剪枝”操作具体在做什么?
- A.提前排除不可能满足示例的候选分支← 正确答案
- B.增加更多候选程序供用户挑选
- C.扩大领域特定语言的算子范围
- D.忽略输入输出示例中的某一部分
答案:A。剪枝是搜索优化技术:当某条候选分支的前缀已经与示例矛盾时,它的所有后续扩展也必然矛盾,因此整棵子树可以被丢弃。剪枝既不加候选、也不扩 DSL、更不丢示例。相当于侦探发现“这条路第一步就错了”,立刻折返,而不是走到底再后悔。
Q9. 合成出候选程序后,怎样验证它真正学到了规则?
- A.用未见过的新输入测试泛化能力← 正确答案
- B.检查合成程序源代码的字符长度
- C.随机猜测程序是否正确满足需求
- D.比较程序运行前后占用内存的大小
答案:A。验证合成的关键是泛化测试:程序在训练示例上全对只说明“背下来了”,必须用未参与合成的留出输入测试,看它能否正确处理。代码长度、内存占用与“是否学到规则”无关。只有第二盘菜也做对了,才说明侦探真的会做这道菜。
Q10. 关于 FlashFill 的 DSL(如 Concat/SubStr),哪些说法正确?
- A.DSL 决定了程序表达能力的边界← 正确答案
- B.DSL 的选择会直接影响搜索难度← 正确答案
- C.DSL 与搜索效率完全没有关系
- D.DSL 能完美解决一切字符串变换问题
答案:AB。DSL 决定“能表达哪些程序”,也直接决定搜索空间大小与搜索难度,因此 A、B 正确、C 错误。任何 DSL 都有表达边界,FlashFill 的 DSL 并不能解决所有字符串问题,D 夸大其词。工具清单决定了侦探能还原的菜谱范围,也决定他要翻多大的书库。