大型语言模型(LLM)已成为软件开发领域的变革性技术,能够自动化并加速包括代码生成、补全和翻译在内的各种任务。¹ 开发领域见证了专门的代码 LLM 的激增,例如 CodeLlama、GPT-4、CodeGeeX 和 Amazon CodeWhisperer,它们越来越多地被集成到开发人员的工作流程中。¹ 这些模型在根据自然语言描述生成代码片段方面表现出卓越的能力,有可能提高生产力,并使新手程序员也能阐明需求并获得功能性代码。² 像 HumanEval 这样的性能基准显示,这些模型的能力随着时间的推移取得了显著进步。²
尽管取得了这些进步,但在将 LLM 应用于开发或修改大型代码库时,出现了一个重大挑战。正如从业者所阐述和研究观察到的那样,虽然 LLM 可以快速生成初始代码段(例如,最多几百行),但随着代码库的增长,它们的性能通常会大幅下降 [用户查询]。修改变得更慢,生成或修改的代码质量往往会下降,表现出复杂性增加、可维护性降低和潜在的不正确性等问题。¹ 这种现象在处理超过几百行的代码时尤其明显,限制了 LLM 在构建和演进大型、真实世界软件系统方面的实际效用。⁶ 核心问题似乎是一个可伸缩性瓶颈,即模型难以在大型、复杂的代码结构中维持上下文和连贯性。
本报告研究了一种缓解这些限制的潜在策略:应用成熟的软件重构原则来更有效地构建 LLM 生成的代码。具体来说,它检验了这样一个假设:采用诸如“按概念拆分”(主要解释为关注点分离 (SoC) 和单一职责原则 (SRP))以及领域驱动设计 (DDD) 等技术可以创建更易于管理的代码结构。其核心前提是,通过将代码库分解为更小、定义明确、概念上连贯的模块或上下文,LLM 或许能够更有效地运行,即使在整个项目扩展时也能保持更好的上下文理解并生成更高质量的代码。这种结构化的方法可能克服在大型项目中使用 LLM 时观察到的性能下降和质量问题。
存在一个潜在的矛盾:LLM 在快速生成初始代码方面表现出优势,但在面对大多数软件项目所特有的持续演进和扩展时却步履维艰。¹ 这表明,旨在加速的工具遇到了其旨在加速的过程中固有的复杂性障碍。困难不仅在于生成速度,还在于模型在复杂性增加时有效保留和利用上下文的能力。⁷ 这表明架构结构可能是一个目前在大型 LLM 辅助开发工作流程中未被充分强调的关键因素。
本报告的主要目标是批判性地评估使用 SoC/SRP 和 DDD 重构技术来增强 LLM 辅助软件开发的可伸缩性和可维护性的可行性和有效性。这包括:
报告接下来将检查 LLM 的局限性,详细说明重构原则,分析其应用和影响,比较各种方法,讨论挑战,最后评估整体可行性并提出建议。
大型语言模型尽管在生成代码方面表现出色,但在处理大型或复杂的代码库时,会暴露出一些特别严重的局限性。这些局限性涵盖了性能下降、代码质量问题以及修改现有系统的困难。
影响 LLM 在大型代码库上性能的一个基本约束是有限的“上下文窗口”——模型在生成或修改过程中可以同时处理的最大文本量(代码、指令、历史记录)。虽然现代 LLM 拥有越来越大的上下文窗口,例如 Claude 3.5 Sonnet 提到的 200K token 容量⁶,但性能问题仍然可能出现。有证据表明,即使有大的窗口,超过某些阈值(例如,Sonnet 3.5 的 100K token)也可能导致性能下降。⁶
对于大型软件项目来说,这个限制的实际影响是显著的,因为这些项目不可避免地包含比最大上下文窗口所能容纳的多得多的代码。这迫使开发人员手动筛选相关的代码片段以提供给 LLM,否则模型可能会在信息不完整的情况下运行。当必要的上下文超过窗口时,LLM 可能会有效地“忘记”或未能考虑代码库的关键部分,导致输出不一致、逻辑错误或先前建立的信息被破坏,使得 LLM 在需要对整个系统状态有深入、持久理解的任务中变得不可靠。⁷
除了上下文限制之外,LLM 生成的代码通常还存在各种质量缺陷,并且随着规模和复杂性的增加,这些缺陷往往会恶化。
这些记录在案的质量问题——低效、可维护性差、错误——不仅仅是孤立事件。它们往往随着代码库的增长而复合。少量冗余或复杂性在小型脚本中可能是可以容忍的 ¹,但当 LLM 生成数千行代码时,这些小缺陷会迅速累积。这种累积会产生显著的技术债务 ⁸,使整个系统变得脆弱、难以推理,并且难以可靠地修改——无论是对人类还是对后续的 LLM 交互而言。这种复合效应很可能显著导致了在超过某个复杂性阈值后观察到的 LLM 效用的急剧下降。
核心冲突似乎在于 LLM 固有的局部优化倾向(基于直接上下文预测下一个最佳 token)与大型软件系统中全局一致性要求之间的矛盾。主要基于序列预测训练的 LLM,本身并不优先考虑架构完整性、长程依赖或遵守软件设计原则。¹ 重构的目的恰恰是强加这种缺失的全局结构,可能创建更小、更连贯的“局部”上下文,这可能更符合 LLM 的操作优势。
使用 LLM 修改现有大型代码库带来了超出初始生成的具体挑战:
重构是重新构造现有计算机代码的过程——改变其构造——而不改变其外部行为。它是一种在代码编写完成后改进代码库设计的规范化技术。与管理 LLM 生成代码中观察到的复杂性相关的两组关键原则是关注点分离/单一职责原则和领域驱动设计。
“按概念拆分”的思想与旨在通过模块化管理复杂性的基本软件工程原则密切相关。
领域驱动设计提供了一种更全面的方法,特别适用于在复杂业务领域内运行的软件系统。
战略设计: DDD 的一个关键方面,特别是对于大型系统,是战略设计,它侧重于领域模型的宏观组织。¹⁹ 关键模式包括:
限界上下文 (Bounded Contexts): DDD 通过将大型领域模型划分为多个限界上下文来处理复杂性。²¹ 限界上下文定义了一个明确的边界(例如,子系统或团队的职责),在该边界内,特定的领域模型及其相关语言是精确和一致的。²⁰ 这种划分允许大型系统的不同部分独立演进,并在各自边界内具有清晰的模型完整性,从而缓解了因不同业务领域之间术语或概念冲突而引起的问题。²⁵
上下文映射图 (Context Maps): 这些图表可视化了大型系统中不同限界上下文之间的关系和集成,阐明了依赖关系和交互模式(例如,共享内核、客户/供应商、防腐层)。²²
战术设计: DDD 还提供了一套用于在限界上下文中实现领域模型的构建块模式。¹⁹ 这些包括:
聚合 (Aggregates): 聚合是一组相关的领域对象(实体和值对象),在数据更改时被视为单个单元。¹⁹ 每个聚合都有一个根实体,它是聚合外部唯一可访问的成员。聚合定义了一致性边界,确保跨越聚合内多个对象的业务规则被原子地执行。²⁵
仓库 (Repositories): 这些为持久化和检索聚合提供了一个抽象层,将领域模型与特定的数据存储技术解耦。¹⁹
对复杂性的好处: DDD 通过战略设计划分为限界上下文和战术设计的建模模式,为管理大型业务领域固有的复杂性提供了一个强大的框架。¹⁹ 它有助于创建更模块化、可维护且与它们所服务的业务需求紧密结合的系统。对边界和清晰定义的强调对于管理沟通和系统复杂性至关重要,尤其是在大型或长期运行的项目中。²⁶
虽然 SoC/SRP 和 DDD 都旨在管理复杂性,但它们在不同的抽象层次上运作。SoC/SRP 为组织模块或类中的代码提供了基础性的、通常是局部的指导方针,侧重于技术职责。¹³ 相比之下,DDD,特别是战略设计,提供了一种更高级别的、以领域为中心的策略,用于根据业务能力划分整个系统。²¹ 重要的是,DDD 通常在其限界上下文和聚合中使用 SoC/SRP 原则作为良好战术实现的一部分。它们不是相互排斥的,而是实现结构良好系统的互补方法。
此外,认识到重构通常不是一次性活动,而是一个持续改进的过程至关重要。⁸ 诸如“先让它工作,再让它正确,最后让它快”和“童子军规则”(让代码比你发现时更整洁)等原则强调持续优化以保持代码质量和管理技术债务。¹¹ 这种持续性在考虑 LLM 生成的代码时尤其重要,这些代码可能需要迭代的人工指导或 LLM 辅助优化才能达到并保持理想的结构。
将像领域驱动设计这样具体的、复杂的重构策略直接应用于管理 LLM 生成代码的可伸缩性,似乎是一个显式研究和既定最佳实践正在形成而非完全成熟的领域。关于 LLM 在代码生成中应用的调查涵盖了数据整理、评估和下游任务等各种主题,但对这一特定交叉点的详细探索似乎不太普遍。¹ 对自动化 LLM 代码中低效检测和重构工具的需求进一步表明这是一个活跃的开发领域。¹ 开发人员正在积极探索如何在大型现有项目的背景下利用 LLM 进行重构。²⁹
尽管缺乏广泛的专门研究,但 SoC/SRP 和 DDD 的原则可以通过几种方法在概念上应用于 LLM 生成的代码:
一个关键的潜在协同作用在于,根据 SoC/DDD 原则结构化的代码可能天生更适合 LLM 处理。由符合 SRP 的类或 DDD 的限界上下文定义的清晰边界和集中职责可以创建更小、语义上更连贯的代码单元。这些定义明确的单元可能更符合 LLM 有限的上下文窗口和处理能力,即使在整个项目变大时,也能实现更有效的迭代开发和修改。注意到 AI 助手有时会因为缺乏“全局观”而导致技术债务;通过 SoC/DDD 强加架构结构可以作为一种关键的对策,充当“避免技术债务的基石”。⁸
然而,应用这些策略提出了一个实际问题:是应该首先重构通常混乱的初始 LLM 输出,为后续的 LLM 交互创建一个更好的基础,还是投入复杂的提示和指导来鼓励 LLM 从一开始就生成结构良好的代码?前者需要大量的前期手动工作,可能抵消 LLM 的速度优势,而后者则挑战了当前 LLM 能力和提示工程的极限。⁸ 这表明通常可能需要一种混合方法:初始 LLM 生成,然后是人工主导的战略重构(可能由 LLM 辅助执行战术任务),创建一个结构化的基线,后续的 LLM 交互可以更有效地在此基础上构建。
思考 LLM 与结构化代码之间的交互,使用 SoC/DDD 进行重构可以被视为不仅仅是代码清理。它主动构建了一个上下文的“脚手架”。通过强加清晰的边界(模块、类、限界上下文)和分离关注点 ⁸,代码库被预处理成逻辑上不同的单元。这种结构为 LLM 提供了导航辅助,帮助它在其固有限制内更有效地运行,特别是在处理大型、未分化上下文的困难方面。⁶ 重构后的结构实质上创建了 LLM 可以更连贯地处理的可管理块。
假设使用关注点分离、单一职责原则和领域驱动设计等原则来构建 LLM 生成的代码,会对 LLM 的性能和有效性产生积极影响,特别是在大型和不断演进的代码库的背景下。
根据 SoC/SRP(产生更小、更集中的类或模块)或 DDD(产生不同的限界上下文和聚合)良好结构化的代码,自然地将代码库分割成更易于管理的单元。当 LLM 被赋予在特定的、定义明确的模块或限界上下文中工作的任务时,与该任务直接相关的代码更有可能适应其操作上下文窗口。
这种改进背后的机制涉及减少 LLM 的认知负荷。清晰的边界最大限度地减少了模型需要同时处理的不相关代码和信息的数量。这使得 LLM 能够将其注意力和计算资源集中在手头的特定任务上,可能导致更高保真度的输出,并减少在大型、非结构化上下文中观察到的上下文损坏或信息丢失的风险。⁷ 这些定义明确的单元之间的显式依赖关系(例如,模块之间的接口、限界上下文之间的关系)可能通过在提示中有针对性地包含或利用诸如检索增强生成 (RAG) 之类的技术来动态提供相关的跨边界信息而得到更有效的管理。¹⁰
具有清晰职责 (SRP) 并与业务领域 (DDD) 对齐的代码库拥有更明确的语义结构。通过其定义的角色和边界,单个代码段的目的变得更加清晰。这种增强的清晰度可能提高 LLM “理解”代码并生成与现有设计和逻辑更相关、更一致的添加或修改的能力。
例如,与处理多个不相关任务的单体 ApplicationManager 相比,负责修改遵守 SRP 的 UserService 类的 LLM 在更清晰的功能范围内操作。类似地,限制在 Ordering 限界上下文中的指令比应用于未分化代码库的模糊指令提供了更强的领域约束。这种改进的结构和语义清晰度可能导致 LLM 在处理代码时产生更可靠的内部表示,可能与生成输出中更高的质量和正确性相关,这与探索内部 LLM 状态以获取可靠性信号的研究一致。³
重构使 LLM 能够进行更有针对性的干预。开发人员可以将模型引导到特定的类、模块、聚合或特定的限界上下文中操作,而不是可能将大量、未分化的代码块输入 LLM 的上下文。然后,提示可以只包含必要的上下文:正在修改的代码单元、其直接依赖项(例如,接口、关键协作者)以及所需更改的自然语言描述。
这种专注的方法减少了提供给 LLM 的输入中的噪声和歧义,增加了生成尊重既定架构的准确、局部化更改的可能性。它允许开发人员在更大的系统内以更可控和可预测的方式利用 LLM 的代码生成能力。
一个经过良好重构的代码库可以作为一种隐式提示。一致的组织、对设计原则的遵守、有意义的命名约定(例如 DDD 的通用语言 ²⁰)以及清晰的边界充当了结构性线索。在包含具有类似模式的代码的大量数据集上训练的 LLM ¹,很可能识别并遵循这些既定结构。这种固有的结构一致性可以引导 LLM 的预测朝着生成自然融入现有架构的代码方向发展,即使在每个提示中没有明确的架构指令。
此外,通过模块化和清晰边界(SoC/SRP 和 DDD 固有)实现的隔离可以有效减少潜在 LLM 错误的“爆炸半径”。通过最小化组件之间的耦合 ¹¹,LLM 在一个封装良好的模块或限界上下文 ¹ 中引入的错误或缺陷不太可能传播并在整个系统中引起意外后果。与在高度耦合的“意大利面条式代码” ¹³ 或单体的“大泥球” ²⁰ 中排除故障相比,这种遏制简化了调试和纠正。
虽然关注点分离/单一职责原则 (SoC/SRP) 和领域驱动设计 (DDD) 都旨在通过模块化来管理复杂性,但它们在范围、重点和对 LLM 交互的潜在影响方面有所不同。
对 LLM 的影响:
SoC/SRP: 应用 SoC/SRP 可以创建更小、更集中的代码单元(类/模块)。这对于在 LLM 有限的上下文窗口内工作非常有益,并可以提高局部代码生成的清晰度。⁷ 然而,它本身可能无法为 LLM 提供足够的宏观上下文来理解跨模块的交互或整体业务流程。
DDD: DDD 的限界上下文 ²¹ 提供了更高级别的结构单元。这可能有助于 LLM 理解系统的不同业务部分以及它们之间的关系(通过上下文映射图 ²²)。在限界上下文内操作可以为 LLM 提供强大的领域约束,从而可能产生更符合业务需求的输出。然而,单个限界上下文可能仍然很大,并且其内部实现仍然需要 SoC/SRP 原则才能实现有效的局部上下文管理。
实施复杂性: 与 DDD 相比,SoC/SRP 通常更容易理解和应用。它们是许多开发人员熟悉的基础设计原则。⁸ DDD,特别是战略设计,需要更深入地理解业务领域,与领域专家的协作,以及对限界上下文、聚合和通用语言等概念的把握。¹⁹ ²⁰ ²⁴ 这使得 DDD 的实施更具挑战性,尤其是在没有明确领域专业知识或组织支持的情况下。
总之,SoC/SRP 提供了基本的代码级组织,这可能有助于 LLM 的局部上下文管理。DDD 提供了一个更全面的、以业务为中心的框架,用于构建大型系统,这可能有助于 LLM 的领域理解和更高层次的上下文。对于旨在利用 LLM 处理大型、复杂、面向业务的系统的团队来说,结合使用这两种方法可能是最有益的,利用 DDD 进行战略划分,利用 SoC/SRP 进行战术实现。
虽然通过重构(使用 SoC/SRP 或 DDD)来构建 LLM 生成的代码以提高可伸缩性很有希望,但也存在一些挑战和需要考虑的因素:
克服这些挑战可能需要一种多方面的方法,结合改进的 LLM 能力(更好地理解架构)、更智能的开发工具(用于 LLM 辅助重构和验证)、仔细的提示策略以及人类开发人员在指导、验证和维护架构完整性方面的持续监督。
大型语言模型为加速软件开发提供了巨大的潜力,但在处理大型代码库时,它们面临着显著的可伸缩性挑战,表现为性能下降和代码质量问题,如复杂性增加、可维护性降低和潜在的不正确性。¹ ⁶ 这些问题似乎源于 LLM 有限的上下文窗口和它们在大型、非结构化代码中维持全局一致性的困难。⁷
应用成熟的软件重构原则,特别是关注点分离/单一职责原则 (SoC/SRP) 和领域驱动设计 (DDD),为缓解这些挑战提供了一条有希望的途径。通过将代码库分解为更小、定义明确、概念上连贯的单元(无论是技术模块/类还是业务限界上下文),重构可以创建更易于 LLM 处理的结构。⁸ ¹⁹
重构后的结构有望通过以下方式提高 LLM 的性能:增强上下文管理(使相关代码适应上下文窗口)、改进代码理解(提供更清晰的语义结构)以及实现更专注的生成和修改(将 LLM 定向到特定单元)。⁷ ¹⁰ 这种结构还可以作为一种隐式提示,引导 LLM 生成更符合既定架构的代码,并限制潜在错误的传播范围。
然而,实施这些策略并非没有挑战。重构所需的前期投入可能会抵消 LLM 的速度优势,LLM 理解和遵循复杂架构指令的能力仍在发展中,并且需要持续努力来维护架构完整性。⁸
建议:
通过有意识地将 SoC/SRP 和 DDD 等重构原则整合到 LLM 辅助的开发工作流程中,团队可以更好地利用 LLM 的优势,同时减轻其在处理大型、复杂软件系统时的局限性,从而构建更具可伸缩性、可维护性和可靠性的应用程序。
围观我的Github Idea墙, 也许,你会遇到心仪的项目