返回 2026-06-10
🤖 AI / ML

大语言模型与“差不多好”的代码LLMs and almost good code

顶尖大语言模型在处理简单任务时生成的代码,通常会比实际需要的复杂大约 10%。由于这些代码能够立竿见影地解决眼前的问题,开发者往往容易接受这种额外的复杂性。然而,这种为了短期便利而妥协的代码质量,可能会给系统的长期维护带来严重的负面后果。

kqr

tl;dr: 我现在的新认知是,顶级的 LLM 在处理简单任务时,生成的代码可能比实际需要的复杂 10%。我还认为,我们太容易接受这种复杂性了,因为这些代码就在眼前,立刻就能解决眼下的问题。这可能会对长期的代码维护产生影响。

发现这一情况的背景是,我在一个工作项目中需要做一些基础的 CRUD 管道工作。这是一个简单的修改,大部分是对现有功能的镜像复制。根据我的经验,这非常适合 LLM 来做,所以我使用了一个前沿模型来生成代码。最终的修改总共只有 200 多行,主要是新增代码。

我们要讨论的生成代码部分是一个 24 行的函数,它将任意(用户提供的)字符串转换为安全的 HTTP 头部值。11将其编码为安全值是必要的,这不仅能避免令人困惑的错误,还能防止 HTTP 头部注入攻击。

In[1]:

toHeaderValue :: Text -> Text
toHeaderValue raw =
  let
    attrChars = "!#$&+-.^_`|~"
    padHex t = if Text.length t < 2 then "0" <> t else t
    percentEncode c =
      if (isAscii c && isAlphaNum c) || elem c attrChars then
        Text.singleton c
      else
        Text.concat
          [ "%" <> padHex (Text.toUpper (Text.pack (showHex b "")))
          | b <- ByteString.unpack (encodeUtf8 (Text.singleton c))
          ]
    rfc5987Encode = Text.concatMap percentEncode
    isPrintable c = c >= ' ' && c /= '\DEL'
    replacePathSeparator c =
      if c == '/' || c == '\\' then
        '_'
      else
        c
    cleaned =
      Text.map replacePathSeparator (Text.filter isPrintable raw)
  in
    rfc5987Encode cleaned

像这样单独审视这个函数时,它显然有些过于复杂了,但回想一下,这仅仅是 200 行代码改动中的 24 行而已。我确认了其底层思路是正确的,而且生成的测试覆盖了我希望看到的所有边界情况。这段代码看起来并不优雅,但已被测试证明是正确的。

更重要的是,它具有高度的局部性。如果这段代码有任何需要替换的地方,可以直接替换,而不会影响其他任何部分。初级程序员会同等程度地担心各处的代码质量;我很久以来就想写一篇名为《别担心,它是局部的》的文章,告诉这些程序员,只要糟糕的代码质量被局限在一个小范围内且自包含,那就没问题。22我之所以还没写那篇文章,是因为我还在等文章需要用到的一堆例子。但我总是忘记去收集,所以到目前为止我收集到的例子数量为零。

我接受了这段代码。我需要可用的实现,而这段代码显然是可用的。它就在眼前,立刻就能解决问题。不接受它才显得愚蠢!接受它是个轻松的选择,当然也不是个坏决定。

然而,命运的巧合令人惊喜,这个项目的 CI 流水线有一个强制性的语句测试覆盖率检查33我虽然不是语句覆盖率的忠实粉丝,但那是另一篇我拖延了多年没写的文章。,而这段代码没能通过该检查。看看你能不能找出问题出在哪里以及为什么。

我给你个提示:这与 padHex 函数有关,该函数接收 0x0–0xff 范围内的十六进制值,如果小于 0x10,就会给它补零。

传入 padHex 的数据已经经过了 isPrintable 过滤器的处理,该过滤器会移除所有低于 0x20 的字节。因此,传给 padHex 的值永远不会低于 0x10,它也永远不会执行任何填充操作!它始终是一个空操作(no-op)。语句覆盖率检查对 padHex 的填充分支发出了警告,因为没有测试能够执行到该分支。事实上,在测试中也不可能执行到它。

这让人有些头疼:

  • 一方面,我们不应该假设 percentEncode 总是被传入大于 0x1f 的字符,即使目前碰巧是这样。这种假设依赖于“幽灵般的超距作用”(隐式依赖)——即使它只存在于该函数的局部范围内——也是我们想要避免的。
  • 另一方面,覆盖率报告也是对的:这整个结构设计确实有些别扭。
  • 所以我亲自动手,编写了自己的实现。最终交付的实现更接近下面这样:

    In[2]:

    toHeaderValue :: Text -> Text
    toHeaderValue =
      let
        retainPrintable = Text.filter (\c -> c >= ' ' && c /= '\DEL')
        replacePathSeparators = Text.replace "/" "_" . Text.replace "\\" "_"
        -- URL encoding is also legal RFC5987 encoding.
        rfc5987Encode = decodeUtf8 . urlEncode True . encodeUtf8
      in
        rfc5987Encode . replacePathSeparators . retainPrintable

    这减少了 15 行的复杂度。大约占总修改量的 8%。

    LLM 并没有生成糟糕的代码。44 从某种意义上说,它的代码甚至更好。rfc 5987 编码比 url 编码更宽松,因此严格来说我的实现属于过度编码。它只是生成了比实际所需复杂 8% 的代码而已。这在目前算不上灾难,而且在面临发布压力时,人们很容易接受它,因为它就在眼前,并且能解决问题。我接受了,并且差点就发布了复杂度超标 8% 的代码。只是碰巧我进行了更深入的研究,才意识到了其中的问题。

    这段经历给我留下了一连串我找不到答案的问题。

  • 那些同样不必要地复杂、但我还是照单全收的其他修改又该怎么算?
  • 如果这只是个简单的案例,而当我们让 LLM 去处理更复杂的任务时,它生成的代码复杂度远超 8%,比如 20%、40%,甚至比实际需要复杂 3 倍,那会怎样?
  • 当我们拿到如此不必要地复杂的代码时,我们会坚决拒绝吗?还是说我们会接受它,因为这在今天算不上灾难,而且它此时此刻就在眼前?
  • 如果一两年后,我们还在持续发布复杂度总是超出实际需要的代码,会发生什么?
  • 一方面,这让我感到担忧。另一方面,显而易见的反驳是,编写代码的机器人改进速度极快,以至于两年后当这成为问题时,它们早就知道该如何应对了。

    也许吧。但我并没有被说服。

    需要完整排版与评论请前往来源站点阅读。