tl;dr
自然语言、伪代码和代码并不是竞争的工具。它们是不同层次精确度的语言。
在AI辅助编程的时代,学习编程不仅仅是手动编写每一行代码。它还涉及到知道何时一个想法需要更加精确,如何控制这种转换,以及如何区分人类做出的决策和AI系统填充的决策。
一段来自 Richard Feynman 的引言
要理解为什么编程不会因为AI而消失,有助于从更一般性的区别开始:清晰与精确的区别。
Richard Feynman 曾在谈论数学时解释了这个想法:
语言问题的真正问题不在于语言的精确性。问题在于语言的清晰性。愿望是让想法清晰地传达给对方。只有在对短语的含义存在疑问时,才需要精确,而精确应放在疑问存在的地方。
用绝对精确的语言说任何事情都是不可能的,除非这个事物被抽象到脱离现实世界,以至于不再代表任何真实的事物。纯粹的数学就是对现实世界的抽象,而纯粹的数学确实有一种特殊的精确语言来处理其特殊和技术性的问题。
但是,如果处理的是现实世界中的物体,这种精确语言就并不精确,除非有某些特殊的微妙之处需要仔细区分,否则使用它只会显得做作且令人困惑。
有趣的是,费曼并不是在批评数学。他是在批评那种认为更多的形式化意味着更多理解的想法。
他的观点很简单:
- 清晰比精确更重要;
- 精确性应该只在存在真实歧义的地方引入;
- 纯数学可以完美精确,因为它本身定义了抽象;
- 当我们谈论现实世界时,总是存在近似、假设和不完美的定义。
虽然费曼是在谈论数学,但这种区分对思考编程也很有用。不是因为编程和纯数学一样,而是因为编程也在自然语言过于模糊的地方引入了形式化的精确性。
这篇文章的核心思想是这样的:
学习编程仍然很重要,因为硬问题不仅仅是生产代码。硬问题是控制从模糊的人类意图到一个没有常识的形式系统之间的翻译。
人类的语言和计算机的语言
编程是一种传达过程的方式。它是一种告诉机器我们想要它做什么的方式。
CPU,即中央处理器,不理解英语、西班牙语、Python或C++,就像人一样。在最低层,它执行非常简单的指令:读取数字,比较它们,加法,将数据从内存的一个位置复制到另一个位置,以及从一个指令跳转到另一个指令。
编程语言是描述过程的一个更人性化的层次,这些过程后来会被解释或编译成机器可以执行的指令。
在自然语言中,像这样的句子:
洗牌。
留下了许多可以解释的地方。
它假设接收指令的人知道什么是牌,什么是排序,使用哪种排序标准,哪些牌比其他牌高,花色是否重要,以及可接受的时间或努力程度。
在编程中,这些决策必须在系统中某个地方定义。
我们并不总是手动编写每一个细节,因为我们使用编程语言、库、框架和抽象,这些已经封装了许多决策。但某个人,某个层次,必须精确地定义这些决策。
自然语言和编程语言并不是同一种事物的更好或更差的版本。它们是不同的工具。问AI是否意味着我们不再需要学习编程,就像问因为我们知道如何使用放大镜,是否就不需要理解显微镜的用途一样。
软件公司如何创建软件
在软件公司中,几乎一切都要从自然语言开始。
客户解释他们想要什么:
我需要一个应用程序来管理订单。
或者:
我需要一个网站来销售产品。
或者:
我需要一个系统来管理预约。
这个最初的描述很少是精确的。不是因为客户表达得不好,而是因为他们试图将大量的领域知识压缩成几句句子。
这种知识可能包括习惯、例外、业务规则、偏好、边缘情况、以前的问题和隐含的决策。
这就是为什么中间层存在。
产品经理、业务分析师或功能分析师会与客户交流,提问,发现模糊之处,并将最初的请求转化为更清晰的需求。
然后,技术负责人、架构师或程序员会将这些需求转化为越来越具体的决策,最终变成代码。
每一层可能会发现一些问题或提出一些更改。但没有一层应该默默地覆盖更高层次的决策。
如果程序员发现矛盾,他们应该提出。如果产品经理重新表述了需求,这种重新表述应能追溯到原始的客户请求。
用三种方法将想法转化为代码
以下图表比较了三种工作流程:传统的软件开发、直接基于LLM的代码生成,以及一个带有显式中间层的提议工作流程。
传统流程
没有方法论的LLM
黑箱方法
当一个人使用LLM生成代码时会发生什么
当一个人直接与LLM交互以生成代码时,中间层并不会完全消失。它变得隐含了。
模型会解读提示,补充缺失的细节,优先处理指令,并做出实现决策。但许多这些决策并未被记录在任何稳定的场所。
这就是为什么许多AI编程工具都有类似计划模式的东西。想法是让模型在实现之前先思考。
问题在于,这个计划通常是临时的。它可能被困在对话中,与最终代码断开连接,或者被用户忽略。
当用户是程序员时,他们通常会花很多时间编写规格说明、需求文档、架构笔记和上下文文件,以指导LLM。但这又带来了另一个问题:数十个分散的文件,每个文件都有不同的权威性、优先级和新鲜度。
即使规格说明被仔细编写,问题仍然存在。
LLM可能会遇到矛盾,填补空白,或者做出与要求不完全一致的决定。有时这是因为自然语言的歧义。有时是因为模型读取文件的顺序。有时是因为指令冲突。有时是因为LLM是概率系统。
问题不在于LLM做出决定。
问题在于不知道它做出了哪些决定,为什么做出这些决定,或者这些决定是否与人类之前已经决定的内容相矛盾。
假代码作为中间层
假代码可以扮演在传统软件开发中分散在客户、经理、架构师和程序员之间的作用。
它不是纯粹的自然语言,因为它有结构。
但它也不是可执行代码,因为它仍然允许我们用意图、模块和责任来表达。
这个中间层对于LLM来说尤其有用,因为它将两个通常被快速合并的时刻分开了:
- 决定系统应该做什么;
- 决定如何实现它。
例如:
IF user selects a subscription plan
CALL create_checkout_session
IF payment succeeds
CALL activate_subscription
CALL send_confirmation_email
ELSE
CALL show_payment_error
这个伪代码尚未决定我们将使用Stripe、Paddle、Mercado Pago、自定义API还是特定库。
但它确实定义了一个结构:
- 有一个计划;
- 有一个结账会话;
- 有一个订阅激活步骤;
- 有一个支付成功路径;
- 有一个错误路径。
这已经比模糊的请求更精确了,比如:
在应用中添加支付功能。
但它仍然没有像最终实现代码那样严格。
建议:黑盒方法
所提出的 methodology 是简单的:
将系统中的每个部分视为可细化的黑盒,具有明确的边界、中间伪代码和明确的来源。
每个应用程序项目都会得到一个 blackboxes/ 文件夹,包含:
- YAML 盒子;
- 共享的数据结构;
- 一个 LLM 在被明确调用时必须遵循的技能文件;
- 一个来源日志;
- 可选地,一个可视化器,可以显示盒子树。
目标不是取代代码。
目标是使从人类意图到代码的转换变得可见、可编辑和可审计。
它试图解决的问题
1. 人类决策与 AI 决策
每个盒子和每个数据字段都追踪来源:
human_definedai_definedmixed
一旦人类确认了某事,AI 就不能默默地将其降级回猜测。
例如,如果 LLM 决定使用 Stripe,这个决定最初可以被标记为 ai_defined。
如果人类确认了这个决定,它就成为人类批准的。从那时起,模型不应在没有明确提出该变更的情况下将 Stripe 替换为其他提供商。
2. 伪代码作为中间层
盒子使用混合伪代码:
CALL create_checkout_session
IF payment succeeds
WHILE there are pending invoices
这为分解系统提供了足够的结构,但不要多到让我们在准备之前就淹没在实现细节中。
3. 模块边界
系统一次一个黑盒进行细化。
祖先是只读的上下文。只有选定的子树可以更改。
这很重要,因为像这样的请求:
修复支付流程。
不应允许LLM重写认证层、数据库架构和通知系统,除非这些更改被明确连接和批准。
4. 人类可读的视图
可视化工具可以以比数十个零散的规格文件更简单的方式展示盒树。
重点不在于仅仅文档。重点在于保持决策边界。
最小示例
以下是一个简化版的支付流程黑盒示例:
box: payment_flow
goal: allow the user to pay for a subscription
provenance: human_defined
inputs:
- selected_plan
- user_id
pseudocode:
- IF user selects plan
- CALL create_checkout_session
- IF payment succeeds
- CALL activate_subscription
- ELSE show_error_message
decisions:
payment_provider:
value: Stripe
provenance: ai_defined
status: needs_human_confirmation
constraints:
- Do not modify auth_flow without human approval
- Do not store sensitive card data
有趣的部分不在于YAML本身。
有趣的部分在于每个决策都有其位置。系统知道人类已经指定了什么,AI推断了什么,还有什么仍需要确认。
中间层不是官僚主义。它是一种防止AI将模糊性转化为不可见决策的方式。
那么现在学习编程意味着什么?
也许学习编程不再意味着记忆语法或从头开始编写每一个函数。
也许它意味着学习如何在精确的层次上思考。
这意味着知道何时自然语言就足够,何时需要伪代码,何时需要可执行代码,以及何时必须记录决策,因为这将影响整个系统。
学习编程也教会了:
- 如何分解问题;
- 如何识别边缘情况;
- 如何建模数据;
- 如何批判性地阅读生成的代码;
- 如何调试;
- 如何评估权衡;
- 如何思考诸如内存、延迟、安全、并发、权限和成本等约束。
AI可以显著加速代码生成。
但生成的代码越多,就越重要去阅读它、质疑它,并将其与它所要实现的人类决策联系起来。
结论
问题"我们是否仍然需要学习编程?"可能被错误地提出。
如果编程意味着记忆语法和手动编写每一行代码,那么随着时间的推移,我们可能将需要做更少的这种事。
但如果说编程意味着理解如何将模糊的意图转化为一个正式系统,该系统做出决策并执行操作,那么答案显然是肯定的。
AI可以编写代码。
但仍然需要有人来控制翻译。
要控制翻译,你需要至少理解两种语言的一部分。
本文中描述的黑箱方法是一个概念验证实验。github.com/amiune/theblackboxapproach