大语言模型与“差不多好”的代码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 的填充分支发出了警告,因为没有测试能够执行到该分支。事实上,在测试中也不可能执行到它。
这让人有些头疼:
所以我亲自动手,编写了自己的实现。最终交付的实现更接近下面这样:
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% 的代码。只是碰巧我进行了更深入的研究,才意识到了其中的问题。
这段经历给我留下了一连串我找不到答案的问题。
一方面,这让我感到担忧。另一方面,显而易见的反驳是,编写代码的机器人改进速度极快,以至于两年后当这成为问题时,它们早就知道该如何应对了。
也许吧。但我并没有被说服。
需要完整排版与评论请前往来源站点阅读。