跳到正文
Hacker News· reverseblade2·· 7 天前AI 评分36

软件开发中的渐进式确定化:用 LLM 弱点定位该写成普通代码的部分

Progressive Determinization in Software Development

AI 导读

开发者提出"渐进式确定化"方法:先用一个 prompt 让 LLM 完成整个任务,再通过运行观察其出错、重复推理、耗时十秒等问题,逐步把汇率查询、VAT 计算、科目校验等环节抽成确定性代码,让 prompt 缩小、代码增长。该方法需记录每次模型调用的输入、输出、成本与耗时,并以 BDD 风格的 given/when/then 测试保证抽取后的行为正确。

正文 · AI 翻译

如果 LLM 总是正确、即时且免费,很多软件都会坍缩成一步:说明输入什么,说明应该输出什么,中间的一切交给模型处理。

但它们并非如此。它们会编造内容,对同一个问题给出不同的答案,响应需要数秒,而且每次调用都要花钱。关于用 LLM 构建应用的大部分讨论,都是围绕如何应对这些问题展开的。

我一直从相反的角度来看待这件事。这些弱点其实是有用的信息:它们指向了程序中本应是普通代码的部分。我把这种方法称为渐进式确定化。这个名字借自自动机理论,在那里,确定化把非确定性自动机转变为确定性自动机。这个类比并不严谨,但方向是一致的。

从一个提示词开始

通常你会从架构开始:领域模型、服务、算法。而在这里,你从一个完成全部工作的提示词开始。输入进去,模型弄清楚该做什么,输出出来。第一天,这个应用几乎完全是概率性的。

注意模型在这里扮演的角色。它不是在帮某人编写软件;起初,它就是软件。这就是它与 AI 辅助编程的区别,后者的模型输出是代码,然后由开发者接手。

然后你运行它,观察它在哪里出错。它算错算术。它在每个请求上都重复同样的推理。一个本该十毫秒完成的操作却要花十秒。

这些都是可以抽取为确定性代码的候选对象:一个函数、一条验证规则、一张查找表、一个状态机、一个模型可以调用的工具。模型保留尚未被抽取出来的部分。

持续这样做,代码会增长,而提示词会缩小。模型最终可能只占系统的一小部分,或者完全消失。不过这并不是目标。目标是让分界线落在正确的位置。

1 · 第一版

LLM

2 · 抽取部分行为

LLM 代码 + 工具

3 · 以代码为主

LLM 代码 + 工具

4 · 可能的最终状态

代码 + 工具

系统占比随时间的变化。这些比例仅作示意,并非实测,且第 4 阶段是可选的。

循环

通常的周期大致是这样的:

specify → design → implement → test → refine

而这一种是这样的:

specify → run → observe → determinize → repeat

设计这一步的位置变了。你不必在写任何东西之前就理解整个领域。你从某个能运行但很糟糕的东西开始,然后把那些最终需要可靠性、速度或可预测性的部分形式化。运行系统会告诉你这些部分在哪里,而不是事先去猜测。

  1. Specify 你想要的行为
  2. Run 它:LLM 加上目前已有的任何代码
  3. Observe 结果、失败和成本
  4. Is this piece worth turning into code?

    yes把它写成代码,并依据真实的正确性标准进行验证

    no暂时把它留给模型

无论哪种方式,都回到第 2 步,再次运行。

观察需要的不仅仅是偶尔读取一次输出。我会记录每一次模型调用:输入、输出、成本、耗时,以及之后是否有人修正了结果。由于我来自事件溯源(event sourcing)的背景,我把这份日志看作一个事件流。“哪些供应商被修正得最多?”或“这一步每月花费多少?”这类问题都是对它的投影,而它们的答案会告诉你下一步该提取什么。

一个例子:从发票到凭证

以记账为例。每张发票都需要一张日记账分录凭证,也就是一个借方账户、一个贷方账户、一个增值税代码,以及能对上的金额。

第一个版本就是一个提示词。把 PDF 和会计科目表给它,然后要求返回一张凭证:

Invoice  Cloud hosting, Germany
         EUR 60.00, no VAT charged
Rate     11.70 NOK/EUR
Debit    6553 Software     702.00
Credit   2400 Payables     702.00
VAT      Reverse charge, 25%
         175.50 in / 175.50 out

这招出奇地经常管用。不过,拿几个月的发票跑一遍,问题就很容易列出来了。它会把增值税拆分算错几分钱。它会在三月把同一笔托管账单记到与二月不同的账户上。偶尔它会用一个不存在的账号,或者猜测汇率。

我会一个一个地修。汇率从发票日期对应的公布汇率中查询;模型绝不应该去猜它。增值税计算只是算术,所以放进一个函数里。模型不应该编造账号,所以把会计科目表作为固定列表给它,其他任何东西都被拒绝。在保存凭证之前,会有一项检查确保借贷平衡。而且大多数供应商每个月都一样,所以把它们连同账户和增值税代码放进一张表里。如果供应商在表里,就不调用模型。

在那之后,模型只会看到表里不知道的东西,主要是新供应商和零星的收据。一旦有人确认了一个新供应商应该如何入账,它就会进入表中,下个月这个供应商也不会再交给模型处理了。

测试才是让它安全的原因

没有测试,这一切都行不通。每一次提取都是对行为的一次改动,而它以前大多数时候能正常工作,你需要知道它现在始终能正常工作,并且你没有破坏它旁边的任何东西。

同样值得说清楚的是,确定性并不意味着正确。一个把发票记到错误账户的函数,每次都会把它记到错误账户。把某些东西从提示词中移出来会让它变得可预测。它是否正确仍然取决于测试以及测试背后的领域知识。

我喜欢为此使用行为风格的测试,也就是 BDD 意义上的:以 given/when/then 写成的场景,关注系统应该做什么,而不是怎么做:

Scenario: Remotely delivered service from abroad, no VAT charged
  Given we are registered for VAT in Norway
  And an invoice for software or hosting from a supplier outside Norway
  And the invoice charges no VAT
  When it is booked
  Then the expense is debited to 6553 Software
  And the VAT code is reverse charge, 25%
  And debits equal credits

我把这些场景当作该功能的契约,它们被用在两个地方。当然,它们是测试。但它们也会进入提示词,这样模型就不必去猜如何为一张没有增值税的国外软件发票入账。它会被告知,用的正是测试将要检查的同一套措辞。同一段文本最终既是规格说明,又是测试,还是提示词的一部分。

这样,系统变化时它依然有效。同样的场景既针对第一天的提示词运行,也针对取代它的代码运行。一旦某个场景由代码处理,你就可以把它从提示词中移除,而测试仍然会用代码来约束它。

运行这些场景还能告诉你接下来该提取什么。把它们针对提示词运行二十次,看看哪些偶尔失败。那些就是候选。一旦某样东西变成了代码,它就必须每次都通过。

不一定非要用 Gherkin。普通的单元测试,或者一张输入与预期输出的表格,也可以,尽管可读的场景更容易放进提示词里。重要的是,预期答案来自懂这个领域的人——会计师或税法规则——而不是来自模型之前生成的内容。模型过去的答案里包含它的错误。它们不是规范。

代码是一种投资

确定性代码不是免费的。得有人设计它、编写它、测试它并维护它。所以每次提取都应该物有所值:更少的错误、更低的运行成本、更快的响应,或者模型无法给出的保证。

很多行为达不到这个标准。如果某件事很罕见、很混乱、很模糊或者不断变化,它可以无限期地留在提示词里,这也没问题。

为什么我认为这很重要

你可以把这看作一种原型设计技巧:先用提示词拼凑出来,然后再好好重写。我认为不止于此。它改变了你需要理解问题的时机。与其在构建之前设计整个解决方案,不如先构建一个粗略版本,让实际使用告诉你工程投入应该放在哪里。架构来自你观察到的东西,而不是来自在任何人使用之前写好的设计文档。

它也不是一种架构。在任何给定时刻,系统可能看起来和其他带工具的 LLM 没什么两样。不同之处在于它是如何走到这一步的,以及你如何决定下一步移动什么。

它还给了你一个通常没有的数字。仍然交给模型处理的部分,就是你尚未建模、或者决定不去建模的那部分领域。

从意图开始。在经验表明需要的地方加入确定性。

来源:Hacker News · novian.works