这个系列写到第83期,后台收到最多的留言不是“哪个工具好用”,而是“为什么我用AI写出来的代码老是不能直接用”。以前我遇到这种问题,第一反应是提示词没写清楚,后来想得深了才发现,真相比提示词更底层的多——我们大多数人,其实根本没分清自然语言和编程语言在底层逻辑上的差别,也没想明白自己到底是在“写意图”还是在“写格式”。
一句话可以戳破窗户纸:传统编程语言“认格式不认意图”,大模型驱动的自然语言“认意图不认格式”。这两句话背后,是两套完全不同的通信协议,也是“灵活性”和“确定性”长期博弈的根源。这篇文章我把它们的本质差异、两种编程模式的运作逻辑,以及实际工程里怎么选型、怎么避坑,一次性拆透。
1. 一次需求描述引发的“翻车”:人与机器各吃哪一套?
1.1 说给同事听的话,为什么一句都不能给电脑用
有次我让一位同事帮忙把导入功能做一下容错处理,当时我心里想的是“文件里有坏行就跳过,但要在日志里记录是哪一行”。我没说那么细,因为我觉得以我们共事半年的默契,他肯定能猜到。结果他交付的东西是“发现坏行就整批回滚,并且弹窗提示”,完全两个方向。问题出在哪?出在我们用的自然语言信息密度太低,低到必须靠共同背景补全。
后来我又试了一次,把同样的话原封不动给一个代码生成大模型,说“导入文件时如果某一行解析失败,跳过这一行并记录日志,继续处理后续行”。模型确实生成了几乎能跑的Python代码,try-except包着每一行的解析,日志也写了。这一次的体验让我意识到:真正的问题不是“机器听不懂人话”,而是我们大部分人还没学会把“给同事说的话”切换成“给机器说的话”。
人理解一句话,靠的是场景、语气、上下文和常识。机器理解一句话(尤其是传统编程语言的语义),靠的是格式,格式不允许任何模糊。你要让机器按你的意图干活,就得先把意图翻译成它认识的格式;而要让大模型按你的意图干活,你就得先想清楚:模型是从意图里猜格式的那一方,它猜得对不对,你要不要验证。
1.2 自然语言和编程语言,本质是两种不同的“通信协议”
我习惯打一个比方:自然语言是高语境语言,编程语言是低语境语言。
高语境的意思是,信息很大一部分藏在环境里、藏在说话人和听话人的共同认知里。你说“把那个东西拿来”,在办公室里大家都知道“那个东西”是桌角的订书机;但换成机器,“那个东西”就彻底抓瞎。低语境则反过来,所有信息必须显式编码,标点、空格、大小写、类型注解全都能改变语义,一个分号错了,整段话就废了。
所以自然语言天然是意图友好的:语法灵活、容忍歧义、允许信息缺失,因为接收方是人类,人类会脑补。编程语言天然是格式友好的:语法严格、拒绝歧义、不允许信息缺失,因为接收方是CPU,CPU只会照着指令逐条执行,不会脑补。这不是某一种语言比另一种高级,而是它们服务的对象从根本上不同。
用一个表格来看会比较清楚:
| 维度 | 自然语言 | 编程语言 |
|---|---|---|
| 语义来源 | 意图 + 上下文 + 常识 | 语法 + 格式 + 显式定义 |
| 歧义态度 | 容忍,甚至允许利用歧义 | 拒绝,歧义直接导致错误 |
| 上下文依赖 | 极强,省略大量信息 | 极弱,信息必须写全 |
| 容错机制 | 听者会自动纠错、脑补 | 无容错,一个符号错就报错 |
| 信息密度 | 低,一句话可以有很多种理解 | 高,一行代码就是一个精确指令 |
| 执行主体 | 人类 | 机器/解释器/编译器 |
理解这个区别,就能理解为什么很多人第一次用AI生成代码会觉得“AI神了”,因为它第一次让机器主动去脑补你的意图;也能理解为什么老程序员总觉得AI生成的代码“不靠谱”,因为脑补这件事,本身就是确定性的对立面。
1.3 两边都别妖魔化:格式不是敌人,意图也不是玄学
我见过两类极端的人。一类是完全拥抱AI,觉得以后不用学编程了,天天用大白话让AI写代码,结果遇到一次幻觉、一次静默错误就崩溃;另一类是坚决抵制AI,觉得代码必须自己一行行敲,AI生成的东西是不可控的垃圾。这两种态度我都理解,但都有点浪费机会。
关键不在于“要不要用”,而在于你手里拿着的这个工具,到底在帮你承担哪一部分工作。传统编程里,承担“意图到格式翻译”的是程序员自己,所以程序员必须懂语法、懂类型、懂数据流。大模型编程里,模型替你做了大量翻译工作,但代价是翻译结果需要你来校验,而且是要用一种全新的方式来校验。
这就像以前你亲自当翻译官,把老板的意图翻译成机器能执行的外语;现在你请了一个AI翻译官,你只需要提要求、看翻译结果,但你要有本事判断翻译得对不对。那这“判断对不对”的能力从哪来?还是得懂一点格式的底层逻辑。所以自然语言不会取代编程语言,它只是改变了编程的重心。
2. 传统编程为何“认格式不认意图”:确定性背后藏着的代价
2.1 格式即语义:编程语言没有“大概”这个词
传统编程语言之所以“认格式不认意图”,是因为它的执行机制就这么设计的。CPU不关心你是不是想排序,它只关心你给的指令是不是合法的、能不能执行。编译器也一样,它在乎的是你的代码有没有语法错误、类型对不对、作用域对不对,至于这行代码“想表达什么”,编译器完全不在乎。
举几个常见到溢出的例子。JavaScript里==和===只差一个字符,但前者会自动类型转换,后者不会;C语言里if (x = 1)是赋值而不是比较,程序会静默地执行完全错误的分支;Python里缩进错了直接无法运行。这些都不是“意图”问题,纯粹是格式问题。你心里想的是A,手写出来的格式却暗指B,机器就真的执行成了B。
这种“格式即语义”的设计,带来了一个很多人没意识到的副产品:代码的确定性非常强。同一段代码,在一百万台机器上跑,结果完全一致。可以单测、可以静态检查、可以审计、可以预测行为。这对于金融结算、飞行控制、操作系统内核这种场景是绝对必要的。如果一句指令因为“理解不同”而各跑各的,整个工业文明都建立不起来。
2.2 程序员真正的核心工作:把意图编码进格式
既然编程语言不认意图,那意图从哪来?答案是从程序员脑子里来。程序员的工作,本质上是需求翻译官:把产品经理嘴里那句“把用户列表按最近活跃排序”翻译成数据库能执行的SQL,再把SQL的中间结果翻译成前端能渲染的结构化数据。
这个过程里最消耗精力的,从来不是“写代码”这个动作,而是“把模糊意图拆成精确步骤”。格式层面的东西,比如括号打没打、分号漏没漏,反而相对机械。以前我带新人的时候常说一句话:你纠结报错信息之前,先想清楚——是你没告诉机器你要干嘛,还是你没按机器要求的方式告诉它?绝大多数时候,是后者。
这句“按机器要求的方式告诉它”,就是传统编程范式下所有难点的源头。一个业务功能要落地,你要先选数据结构,再设计函数签名,再拆解逻辑流,还要考虑异常分支。每一步都是在把“用户意图”翻译成“机器格式”。翻译得越好,程序越稳;翻译得不好,轻则运行时报错,重则悄悄跑出错误结果。
2.3 确定性的代价:编译报错、语义走样、需求迁移成本
确定性不是白来的,它收了三笔税。
第一笔税是学习成本。你至少要花几个月到一年才能看懂一门语言的各种格式规则,再加上框架、工具链、部署环境,完全上手怎么也得两三年。很多人就是因为这个门槛被挡在了编程门外。而自然语言的门槛几乎为零,谁都会说话。
第二笔税是报错不可读。编译器的报错信息是给格式错误准备的,不是给意图错误准备的。比如你写漏了一个花括号,编译器提示“unexpected token”,它根本不知道你本来想干嘛。但如果你把需求本身理解错了,写出来的代码格式完美、逻辑自洽、运行正常,只是功能完全不是用户要的,编译器不会抱怨一个字。这种“语义走样”是传统开发里最贵的一种错误,因为它要等到验收阶段才会暴露。
第三笔税是需求迁移成本。用户需求一变,你就要重新改一大堆格式细节。改一个字段名,可能要同步改数据库脚本、后端实体、前端表单验证、接口文档,任何一个地方漏改,系统就行为异常。格式层面的连锁反应会吃掉大量人力。
我并不是说确定性不好,而是想强调:确定性是有边界的,它不是免费的午餐。当需求变更频繁、探索性质强、试错成本低时,传统编程的高确定性反而变成了一种拖累。
3. 大模型“认意图不认格式”:新范式改变的最关键一环
3.1 从“描述结果”到“生成实现”:意图成为第一等公民
大模型驱动的自然语言编程,最大的变化不是“输入方式变了”——你还是在打字,而是“语义的主导权变了”。
在传统编程里,意图是隐形的,所有东西都必须编码成格式才能被机器看到。但在大模型编程里,意图变得可见了:你说的每一句话,模型都在尝试从中还原你想要的实现。你不需要告诉它变量怎么命名、函数怎么拆分、异常怎么处理,你只需要说“我要什么”,然后它替你决定“怎么做”。
这就像以前你拍照必须自己对焦、调光圈、选快门速度(格式),现在你只需要告诉相机“我要拍一辆飞驰的赛车”(意图),相机会把快门调快、开连拍、自动追踪对焦。相机不知道赛车为什么重要,但它能通过大量训练数据知道“拍运动场景该怎么设置”。大模型也一样,它不懂业务,但它见过海量的“意图—实现”配对,可以推断出“这种需求大概会对应什么样的代码结构”。
你可能会问:那这不就是“高级代码自动补全”吗?不。自动补全是在格式的框架里帮你完成局部,而意图驱动是在语义的层面上直接生成完整的格式。这个区别很关键:前者默认格式是你主导的,后者默认意图是你主导的。当意图成为第一等公民时,一个完全不懂编程的人,只要能清楚描述目标、约束和可接受的边界,也能驱动机器生成可用的软件。
3.2 以“做一个评分接口”为例:模型默默替我做完了什么
我实际试过用一个非常模糊的需求让模型写代码,原话大概是:“帮我写一个接口,输入一个人的年龄和名字,输出一个评价,年龄大于60就说‘优秀’,小于18就说‘年轻有为’,其他就说‘普通’。”
注意,这句话里我没讲接口用什么语言、有没有框架、要不要数据库、参数怎么传、返回结构长什么样。如果是传统编程,至少要先拍板十来个问题才能动手。但模型直接生成了一段Python的Flask接口代码,定义了/evaluate路由,从JSON里取age和name,做了逻辑判断,返回了统一的JSON结构,甚至连HTTP状态码都分了:参数缺失返回400,正常返回200。
它替我做了一堆隐含决策:用了Flask而不是FastAPI(大概率因为Flask在训练语料里更普及)、把名字和年龄放进POST请求体、返回结构里带了code和message字段。这些决策不一定是最优的,但它证明了:格式层面的大量工作可以自动化完成。我真正写的是什么?只有那两句“大于60”“小于18”的业务规则——这就是意图。
但这个例子里也藏着坑。它没有对“年龄是负数”做校验,也没有处理“名字是空字符串”的情况。模型默认你描述的需求就是完整边界,你说大于60,它就只判断大于60,剩下的是它从训练数据里“猜”出来的常见模式。猜对了是运气,猜错了就是缺陷。
3.3 幻觉与过度推断:意图式编程的代价清单
既然模型是“认意图不认格式”,那它自然有对应的几个毛病,我用一张表把这些代价列清楚:
| 风险 | 具体表现 | 对应传统编程 |
|---|---|---|
| 幻觉代码 | 生成了不存在的库函数、错误API用法 | 基本不会发生,编译期就拦截 |
| 过度推断 | 你只说A,它擅自补了B、C、D | 不会发生,所有行为显式书写 |
| 格式不稳定 | 同一需求生成两次,结构不一致 | 格式完全可控 |
| 错误沉默化 | 逻辑错了但代码能跑,不报错 | 逻辑错但代码能跑的情况也存在 |
| 边界缺失 | 需求没提边界,模型也忽略边界 | 需求没提边界,代码也忽略边界 |
注意,最后一条传统编程同样存在,因为边界本来就是需求的一部分,不是编程语言分内的事。但前三条是大模型编程特有的,它们都源于同一个原因:模型在“猜”意图的边界,而不是“执行”意图的定义。
我举一个真实的翻车案例。有人让模型生成一段“把目录下所有文件名改成日期前缀”的脚本,模型确实写出来了,但它在遍历时用了一个错误的路径拼接方式,导致在某个操作系统上生成的是20240101_/absolute/path/file.txt这种严重错误的名字。脚本能运行,但结果完全不能用。如果这段代码是人手写的,路径拼接这种错误大概率一眼就能看出来;但因为是模型写的,而且跑起来没有报错,你很容易信任它。
这就是“认意图不认格式”的另一面:它主动帮你处理了格式,但格式错了它也不会负责。
4. 灵活性×确定性的博弈:这场“范式革命”革的是“翻译权”
4.1 翻译权转移:从人到模型
前面两章分别讲了两套范式的逻辑,这一章想把“革命”这个词说透。很多人说大模型编程是革命,我一开始不太认同,因为代码还是代码,运行环境还是那套运行环境,到底革了什么命?后来想明白:革的是“意图到格式的翻译权”。
传统编程范式中,翻译权归属于程序员。程序员是唯一能把模糊的商业意图变成精确语法的人,所以编程能力等于翻译能力。大模型编程范式中,翻译权被移交给了模型。你只要给出意图,模型负责把它翻译成格式,而且翻译得又快又便宜。这让过去需要花3天写的功能,现在可能3分钟就生成了初稿。
翻译权转移的直接后果,就是编程的门槛从“会写格式”变成了“会提意图”。你不会语法没关系,模型会;但你得会判断翻译结果对不对,哪里跑偏了,这个判断力本质上比“会写代码”要求更高。以前代码写错了,编译器会拦你;现在模型帮你写错了,没人拦你,只有线上事故拦你。
这也是为什么很多老程序员反而对AI编程有比较重的戒心——他们不是怕学不会提示词,而是太清楚“意图翻译”这件事有多容易出错。他们以前要做完整个翻译链才能交付,现在模型直接把第一版翻译拍在桌上,你还是得从头到尾审一遍代码,甚至比以前更仔细,因为模型可能用了一个你没见过的写法。
4.2 四种编程模式的定位对比:从手写指令到意图驱动
为了说得更清楚,我把当前能看到的编程模式排成一条光谱:
| 模式 | 意图谁负责 | 格式谁负责 | 灵活性 | 确定性 | 适用场景 |
|---|---|---|---|---|---|
| 传统手写编码 | 程序员 | 程序员 | 低(改需求要改代码) | 高(完全可控) | 核心引擎、算法、基础设施 |
| 低代码/规则引擎 | 配置者部分负责 | 平台负责 | 中 | 中 | 业务流程、表单场景 |
| AI辅助补全(Copilot式) | 程序员 | 大部分程序员,模型补局部 | 中 | 中高 | 有经验的开发者提效 |
| 意图驱动生成(对话式) | 使用者 | 模型 | 高(一句话改方向) | 低(需验证) | 原型、脚本、胶水代码、学习探索 |
这张表很直观地展示了“灵活性”和“确定性”的博弈。从左到右,确定性在下降,灵活性在上升。传统手写编码,任何结构变化都要动代码,灵活度极低;意图驱动生成,你想改逻辑只需要换一句话,模型立刻生成新的结构,但你说的每个字都可能被模型翻译出偏差。
最容易被忽视的是这四者完全可以同时存在。我不认为谁要取代谁。低代码没取代手写,Copilot也没取代低代码,意图驱动同样不会消灭后面三者。它们各自占据着“确定性需求”和“灵活性需求”光谱上的不同段位。
4.3 革命没有消灭确定性,只是把它搬到了验证环节
还记得我之前说的“传统编程的确定性是花钱买来的”吗?在大模型编程里,确定性这笔开销并没有消失,它只是换了位置:从“写代码时保证格式正确”变成了“生成之后验证结果正确”。
传统编程,你要在编码阶段反复和编译器搏斗,把格式磨到完全正确,程序基本就八九不离十了。意图驱动编程,模型瞬间给你格式正确的代码,但你还要考虑这代码对不对,所以验证工作反而变成了主战场。
我之前帮别人做一个数据清洗脚本,需求是“把CSV里电话号为空的行标出来”。模型生成的代码里,判断空值用的是if row[3] is None,但CSV里空值其实是一个空字符串'',这个判断直接就失效了。如果这是手写的,我大概不会犯这种错误;但因为是模型生成的,我花了五分钟才意识到它把“空值”理解成了None而不是空字符串。这个过程总结下来就是:模型负责了格式,我负责了验证语义的一致性。
所以,“编程”这个动词的语义也在变。以前编程等于“写代码”,现在编程的一半变成了“审代码”和“测场景”。谁能建立一套高效的验证体系出来,谁才能真正用好意图驱动。
5. 实际工程里我的选型框架与踩坑心得
5.1 用一张决策表判断:这个任务该手写还是该指挥大模型
聊了这么多本质差异,落到实际工作里,大家最关心的还是“我到底该怎么办”。我总结了一个简单的决策表,每次拿到需求先过一遍这四个问题:
| 判断维度 | 倾向传统手写 | 倾向意图驱动 |
|---|---|---|
| 需求确定性 | 明确、稳定、长期不变 | 模糊、探索、频繁调整 |
| 失败后果 | 严重(线上交易、数据安全) | 轻微(demo、分析脚本、原型) |
| 逻辑复杂度 | 高(状态机、并发控制、复杂算法) | 低到中(CRUD、文本处理、小工具) |
| 使用者的技术背景 | 核心开发者自己维护 | 非开发人员、跨团队协作 |
这个表不复杂,核心就是一条:核心资产用确定性,探索边界用灵活性。比如一个项目的核心算法、账务逻辑、权限模型,我会倾向于手写、单元测试覆盖到死角;但一个“帮我把这个Excel里的数据格式整理成JSON”的一次性任务,我直接用意图驱动生成,跑完就扔,根本不想手写。
当然,这个决策不是静态的。一个探索性的脚本如果逐渐长成核心服务,我会把它重写成传统模式,加上类型标注、测试和错误处理,把它从“灵活”收缩回“确定”。从灵活走向确定,而不是从确定走向灵活——后者几乎总是灾难。
5.2 三个最容易翻车的场景与对策
下面这三个坑是我在过去几十期分享里反复见到的,也是我自己踩得最多的,单独拿出来说一下。
第一个坑是模型自信地生成“看起来没问题但边界全错”的代码。对策只有一个:边界条件要显式写进提示词。比如上面那个CSV空值问题,我现在会在提示词里直接写明“空值包括空字符串、None、以及全空格字符串”。把你能想到的边界预先告诉模型,它就不太容易自由发挥。模型自由发挥不可怕,可怕的是它在边界条件上自由发挥。
第二个坑是复杂需求一次生成,中途悄悄丢掉某个子需求。需求要说完整,但一次说太多又容易互相干扰。我的习惯是把大需求拆成小需求,逐个生成,生成完先跑通,再串起来。比如做一个报表系统,我不会直接说“给我一个带权限、导出、筛选的完整系统”,我会先让模型生成一个基础的列表页,再让它加筛选,再加导出,每一步都验证一次。这其实和传统开发里的增量迭代是同一个道理,只是节奏快了十倍。
第三个坑是生成的代码没有测试,你也不知道它跑对了没有。传统开发里,测试通常在代码写完以后;意图驱动编程里,我建议测试要在生成的同时完成——直接让模型帮你写几个测试用例,或者你亲手造几个特殊的输入来验证。有些服务甚至可以让模型直接生成单元测试。关键不是“测试由谁写”,而是“输出必须要有验证闭环”,否则你只是把原先写代码的精力转移到了排线上。
5.3 混合使用的个人习惯:让意图做胶水,让代码做骨架
我目前在生产环境里的做法,是让两种范式各管一段,像搭积木一样拼接。
系统的骨架——比如微服务的框架、数据库访问层、核心领域的模型定义——还是我自己手写。这些地方结构稳定、语义严谨,用确定性来兜底,能让我睡得着觉。骨架定下来以后,那些“粘合逻辑”比如消息格式转换、定时任务的数据汇总、几个API之间的编排,我大量地让模型生成初稿,然后我审一遍再合入。这一层需求变化快、逻辑又不算深,最适合用灵活性来对冲,模型一天能帮我出好几版方案供挑选。
有人管这个叫“AI辅助编程”,但我的体感更像“双语言工作制”:计算机底层只认格式,我用代码去跟它沟通;模型擅长理解意图,我用自然语言去跟它沟通。我作为中间人的主要工作,是决定哪句话给谁听、哪件事让谁干、最后谁来验收。
最后再分享一个个人习惯:我每让大模型生成一段代码,都会要求它把实现思路用三句话讲给我听。一方面逼着模型把隐含假设显式化,另一方面也逼着自己去复查那些假设是否正确。“认意图不认格式”的工具用得越顺手,我们越需要在意图和格式之间反复横跳,越不能放弃对自己代码的控制力。这大概就是新范式下,一个写代码的人最需要练的本事。