如果你也经常阅读技术博客,大概率和我有同样的感受:我们的行业正处在一场剧烈的变革之中。如今超过半数的技术讨论,都在围绕一个核心问题展开——当能够编写代码的大型语言模型(LLM)出现后,整个软件行业该如何适应?
关于LLM编程的伦理、最佳实践以及AI Agent的使用方式,已经有了大量的探讨。本文想提供一个补充视角:在一个需要“确定性”的工程体系中,我们如何合理地使用本质上“非确定性”的LLM?
这并不是说这是唯一正确的用法,但它绝对值得我们将其视为工具箱中一项重要的工具。
数学证明与 LLM:一个极具启发的先例
在深入软件开发领域之前,我们不妨先看看另一个同样被LLM深刻影响的领域:数学。数学界面对AI时所做出的选择,对我们开发者而言,非常有启发性。
LLM实际上相当擅长生成“看起来像数学证明”的文本。早在2024年9月,菲尔兹奖得主陶哲轩就曾将监督一个LLM的工作形容为:
“就像在指导一个水平一般、但不算完全无能的研究生。”
这个比喻非常精准。要知道,能够表现得像“普通数学研究生”的人类本身就不多,而LLM在过去一年半里还在持续进步。
然而,问题在于:LLM的本质是基于海量已有文档生成“高度相似”的文本,它会产生幻觉(hallucination)。在数学领域,这尤其危险。数学证明往往依赖于极其微妙的逻辑差异,一个听起来头头是道的论证,很容易让人误信为真。
因此,数学家们得出了一个明确的结论:
- 不能直接相信LLM写出的证明是正确的。
- 也不能指望人类通过逐行阅读LLM生成的证明来捕捉所有错误。
于是,他们转向了另一种工具:形式化证明系统,例如 Lean 。
从理论上讲,数学证明就是从公理出发,通过严格的逻辑推理得出结论。但现实中的数学家并不会写出每一步逻辑细节——否则一篇5页的论文可能变成数千页,既难以阅读也毫无洞见。
但计算机可以。Lean能够生成严格、每一步都可验证的形式化证明。但新的问题是:编写Lean证明本身极其困难,这使得它并未在专业数学家中广泛普及。
接下来的发展你或许能猜到。2026年1月,一个研究团队成功利用LLM及相关工具,解决了一个此前未被人类解决的有难度的数学问题——这个问题来自保罗·埃尔德什提出的系列难题之一。
他们的工作流程是:
- 创造性草稿:使用 ChatGPT 生成证明的大致思路和草稿(事后验证,初稿中确实存在逻辑漏洞)。
- 确定性修补与验证:使用另一套AI工具(如 Aristotle)来修补这些逻辑漏洞,并将整个证明转化为 Lean 可以验证的形式。
- 格式化输出:再次使用 ChatGPT 将经过验证的 Lean 证明翻译成符合人类阅读习惯、可用于发表的论文格式。
这个案例的关键启示并不在于“LLM独立解决了问题”,而在于它确立了一种范式:
LLM 负责创造性的草稿构思,而确定性的验证系统负责最终的逻辑正确性保障。
这种分工,为我们将LLM引入软件开发的核心流程提供了绝佳的范本。
编程中的“确定性”问题
现在让我们回到软件行业。
今天,我们已经拥有了诸如 Claude Code、Gemini Code Assist 这样的AI编程助手,它们能够以“ vibe coding ”的风格生成中等复杂度的应用程序。它们的水平,大致相当于一位普通的初级工程师。
这些工具正在迅速改变整个行业的生产方式。但我们这里要深入探讨的,是一个更具体、也更本质的问题:LLM 与软件开发所追求的“确定性”之间的关系。
为什么我们偏爱自动化脚本?
一个普遍的共识是:自动化部署远优于手动部署。
编写部署脚本所花费的时间,可能比手动执行一次正确的部署更长。但在多次部署之后,脚本带来的效率提升会收回成本——这是从效率层面考虑的理由。
但更重要的是,软件工程领域早就认识到一个更深层的事实:
自动化的核心价值不只是效率,更是其带来的“确定性”。
一次成功的手动部署依赖于执行者当时的状态、记忆力和专注度。而脚本一旦被正确编写,其每次执行的结果都是完全一致、可预期的。这种确定性,是构建可靠系统的基础。
LLM 所处的尴尬中间地带
LLM 恰恰处于人类与确定性程序之间的一个“尴尬”地带。
它不像人类那样会疲倦、会分心,但——它也不是确定性的。
LLM 的核心工作机制是:
- 根据其庞大的训练权重,预测下一个“token”(可以理解为词或字)的概率分布。
- 按照这个概率分布进行随机采样来生成输出。
因此,即使输入相同的提示(prompt),LLM 每次生成的结果都可能在细节上存在差异。这意味着:
让 LLM 直接执行部署或生产任务,看似方便,实则并非理想选择。
它可能成功99次,但只要有1次失败,就足以引发严重的事故。在追求稳定性的生产环境中,这种不确定性是难以接受的。
何时必须追求绝对的确定性?
我们可以将开发任务按确定性需求进行分类:
1. 一次性任务
例如:将数据从旧系统A迁移到新系统B、导入某个特定格式的Excel表格、为某次汇报临时生成一组图表。
这类工作“只做一次”,结果不需要被反复验证或依赖。对于它们,LLM 是完全合适的工具,可以快速产出结果,效率优先。
2. 写一次,用很多次
例如:用户登录验证模块、提供核心服务的API、实现某种业务逻辑的关键算法。
这类代码本身需要确定性地运行,但它的编写过程可以是非确定性的。我们可以利用LLM快速生成代码草稿,然后由人类工程师或通过严格的测试来确保其最终版本的确定性。
3. 最棘手的“规范执行”类任务
真正挑战在于下面这类问题:
某个开发规范必须在整个代码库的每一次修改、每一处代码中被正确执行。
比如:
- 所有涉及用户输入的地方都必须防止 SQL 注入。
- 所有错误日志都必须包含完整的堆栈跟踪信息。
- 所有打开的文件句柄都必须在
finally 块或使用 try-with-resources 语法中确保关闭。
- 所有变量、函数、类的命名都必须遵循团队约定的规范。
以防范SQL注入为例:它不是在项目开始时做一次安全审计就能解决的,而是要求每一次使用用户输入进行字符串拼接以构造SQL语句时,都必须进行正确的转义或使用参数化查询。
数十年的软件工程经验告诉我们:
- 人类工程师无法保证100%不出错,人会有疏忽。
- LLM 同样无法保证100%不出错,因为它有随机性。
即使我们做了以下努力:
- 在团队的
AGENTS.md 或 PROMPT_GUIDE.md 中反复强调这条规范。
- 为LLM提供大量正确的示例代码作为参考。
- 编写详尽无比的“技能说明文档”。
但由于LLM生成结果的非确定性,我们依然无法获得100%的保证。试图通过不断修改和优化提示词(prompt)来解决这个问题,是治标不治本的。
正确的工程化姿势是什么?
实际上,软件行业对这个经典问题早已有了成熟且优雅的解决方案,那就是——用(确定性的)程序来约束(可能出错的)程序。
| 工具 |
机制 |
保障强度 |
| 类型系统 (Type System) |
编译期约束 |
最强 |
| Lint工具 |
静态代码分析 |
中 |
| 单元测试/安全扫描 |
构建与CI流程中校验 |
基础 |
这些机制最关键的优势在于:
它们被集成在每一次代码构建和持续集成(CI)流程中,自动执行。
它们不依赖于开发者的记忆,也不依赖于某次LLM输出的“好心情”。
当开发者尝试提交一段含有潜在SQL注入的代码时,静态分析工具会直接报错;当代码中的文件操作没有正确关闭资源时,代码检查(Lint)规则会给出警告并阻止合并。这是一种前置的、确定性的防护。
那么,LLM 真正该用来做什么?
答案是:让LLM去做那些构建确定性约束系统本身的工作。
编写复杂的Lint规则、设计更严谨的类型、生成覆盖边界情况的测试脚本——这些工作通常具有以下特点:
- 结构清晰:规则定义明确,输入输出要求具体。
- 逻辑可描述:虽然实现可能繁琐,但目标可以被清晰定义。
- 耗时且容易厌倦:需要大量样板代码和重复劳动。
而这,不正是LLM的强项吗?
因此,更合理、更工程化的协作模式应该是:
当你需要在代码中贯彻一致性时,不要反复祈求LLM在每次生成代码时都“自觉”遵守规则。
而是,让LLM为你编写一个能够“自动检查并强制该规则的程序”(比如一个自定义的Lint规则或一组测试用例)。
然后,把这个程序放入你的自动化构建链条中,让它为所有后续的代码(无论是人写的还是AI生成的)提供确定性的保障。
最终原则总结
让我们提炼一下核心原则:
- LLM 不知疲倦,但也不具确定性:不能将其等同于可靠的自动化脚本。
- 规范不能依赖提示词来保证:提示词的引导效果是概率性的,不是强制性的。
- 一致性必须交给可验证的确定性系统:如类型检查、Lint规则、自动化测试套件。
- LLM 擅长生成工具,而非替代约束:用它来创造维护确定性的工具,而不是让它直接承担需要确定性的任务。
一句话总结:
将创造性的探索和草稿生成交给 LLM,将最终的一致性和正确性验证交给确定性的工程系统。
真正的能力,不在于让LLM模仿人类一样“思考”并祈祷它不出错,而在于:
巧妙地将LLM嵌入到一个可靠、可验证、可重复的软件工程体系之中,让它在体系中发挥创造性,同时由体系本身来确保产出的确定性。
这种结合了 人工智能 创造力与经典软件工程严谨性的新范式,或许才是我们应对未来复杂软件开发挑战的更优解。对于此类深度探讨软件研发范式与基础原理的话题,也欢迎你在 云栈社区 与其他开发者继续交流碰撞。