随着大语言模型在软件开发中的应用越来越广泛,传统的软件工程实践开始被重新关注和提及。在诸如于编写清晰的文档、进行代码审查和单元测试等领域,我们可以看到 LLM(大语言模型) 能带来极多在提升。而在其它的一些领域,诸如于辅助接口设计、辅助架构设计、架构治理,我们看到人们有了越来越多的尝试。
而不论是架构设计,还是接口设计,最后还依赖于需求的表达上。而从需求到模型的标准化设计,正是经典软件工程(如 UML,即统一建模语言)等特别擅长的方式。在过去,我们对于编写详细的需求疲惫不堪,而 LLM 正好能在一定程度上帮助我们。
这是否意味着经典软件工程的复兴?又是否我们需要新一代的软件工程方式?
你是否试过提供一个详细的需求或者 API 接口,以让 ChatGPT 绘制 PlantUML(支持 UML 实现的开源软件) 图?
我刚在学编程那会,学校用的二手计算机还是蓝底白字的 DOS。由于那时计算机很慢,脑子也不灵光,一个小小的函数,老师也会要求你先在纸和笔上写写画画,然后再输入到计算机里。上大学的时候,搞的是嵌入式开发,写个只带任务调度的 OS 都得先理论验证一波,再扔到芯片上跑跑。今天,设计个 Web 系统、框架,还都是 PoC 验证起步,写个功能都是一个测试起手。
印象中,不同层次、不同领域的实现方式里,我很少能做到设计与实现是完全一致的,所以我对于经典的软件工程方法一直都是不看好。
在 Web 应用开发上,我们一直在追求一种快速验证的方式,诸如于:
在追求稳定的基础设施,诸如于运行在资源受限设备的嵌入领域里,由于用户同样在期待类似互联网的交付方式,所以也受到一系列的冲击。像过去我们认为的汽车软件,也搞起了 OTA 升级方式,以出现、也避免你在路上因操作系统升级导致抛锚。
当工作以后,我开始习惯于标准的敏捷软件开发流程,它意味着在开发项目里,我需要(由 ChatGPT 生成):
在敏捷软件开发里,我们会强调:工作的软件 高于 详尽的文档。而现在我们多了一个新的团队成员:LLM,这个新的团队成员它需要文档、详尽的文档、更种详细的文档。(PS:当然了文档的方式是多种多样的,比如代码信息也是文档的一部分。)
随后,我们尝试在软件开发领域引入 LLM 之后。在进行了一系列的内部头脑风暴之后,我们认为它会在不同的成熟阶段,扮演不同的角色:
回到经典软件工程开发上,我们为什么不愿意去画 UML 图呢?一来,它只适合提供参考;二来,学习成本不低。简单来说,就是性价比太低。
而恰好 LLM 能在一定程度上解决这两个问题,LLM 可以作为一个 Copilot 解决“我懒得做”及“我重复做”的事儿,诸如于你可以让它生成 UML,虽然不是那么靠谱,但是改一改也就能用。
而在有了 “改一改就能用” 的这一基础,那么剩下的事情就变得非常简单了。
在探索了 LLM + 软件工程的一系列实践与应用开发之后,我们着手构建 ArchGuard Co-mate 用于指导软件架构设计与软件架构治理。
位于其背后的瓶颈是:如何动态的构建软件开发所需要的上下文?
多数人或许都尝试过让 ChatGPT 生成 RESTful API,修改过一个又一个的 prompt,诸如于:
您是一个软件架构师,请生成 博客 entity 的所有 API。使用表格返回,格式:方法、路径、请求参数、返回参数、状态码。
然后,ChatGPT 就开始生成 API 了,这时候你发现了,它生成的 API 可能并不符合内部的 API 规范。于是,我们尝试和它对话去生成更准确的 API,然而,这时可能并不如你直接修改来得快。又或者,我们可以提供一个精炼的 API 规范给它,诸如于我们在 Co-mate 中所设计的:
您是一个软件架构师,请生成 博客 entity 的所有 API。要求:
1. 使用表格返回,格式:方法、路径、请求参数、返回参数、状态码。
2. 你需要参考 API 规范生成。
API 规范如下:
###
rest_api {
uri_construction {
pattern("/api\\/[a-zA-Z0-9]+\\/v[0-9]+\\/[a-zA-Z0-9\\/\\-]+")
example("/api/petstore/v1/pets/dogs")
}
http_action("GET", "POST", "PUT", "DELETE")
status_code(200, 201, 202, 204, 400, 401, 403, 404, 500, 502, 503, 504)
security(
"""
Token Based Authentication (Recommended) Ideally, microservices should be stateless so the service instances can be scaled out easily and the client requests can be routed to multiple independent service providers. A token based authentication mechanism should be used instead of session based authentication
""".trimIndent()
)
}
###
然后,随着我们提供越来越多的上下文之后,ChatGPT 终于可以像你一样工作了。尽管,这时结合 ChatGPT 生成 API 的时间已经远超过我们动手去设计 API 的时间 —— 因为我们一直需要提供上下文所需要的时间,我们一直在将知识进行显性化。
在这时,你就会发现:“哦,LLM 就像我们团队新来的毕业生”。你需要教给他一系列的知识:
顺便给了毕业生一堆文档,让他花两天的时间阅读。随后,你开始让他去实现某个功能,以让他去练手。最后,你发现 10 个毕业生里有 9 个写不出符合要求的代码。还有一个写得出来的是因为,他在这个团队实习过。
思考一下,我们在实现一个 API 的功能时,分别需要:
没错,每一小步我们都需要一个精确的 spec 才能写出符合要求的代码。而在多数的团队里,这些都是隐性的知识,又或者是由过时的文档所维护。(PS:所以,事实上,就算工作几年的团队成员,也不一定能写出符合规范的代码)
所以,我们更倡导采用结对编程的方式来分享知识,以让团队新人更快上手。
现在,在绕了一大圈之后,让我们回到文章的主题。我们把 LLM 看成是一个团队的新人,它需要知道团队的上下文,才能辅助我们更高效的完成工具。
在构建架构治理平台 ArchGuard 时,我们围绕三态架构(设计态、开发态、运行态)的思想所实现。对于软件来说,它也是颇为相似的,我们会基于初期的需求来设计架构,也就是设计态架构。而在实现时,是基于细化的、响应市场变化的架构,也就是开发态架构。如果想具备快速的市场响应力,我们往往会平衡花在两部分上的时间,所以往往两者不会完全一致。
相信大部分人没用过 GitHub Copilot 写代码,但是大部分人都用过 ChatGPT 写代码。我想大家都会得到一个结论:当我们给定足够精确的上下文时,AI 能与出非常准确的代码,尽管还存在一定的随机性。(PS:当然,第二个结论还是先前提到的那个:如果我给了足够精准的上下文,那我早写完了。)
所以,为了让 AI 更自动化的写代码,我们就需要探索实现过程标准化,诸如于:
在当前的软件开发流程之下,我们只能让 LLM 模拟现在的流程工作。这也就是我们创建了 AutoDev 的初衷,用 ChatGPT 分解需求,将分析需求流程编写到工具中,以让 ChatGPT 去分析单个的需求,基于此来自动写代码。
而在这时,我们会发现另外一个问题:ChatGPT 缺乏一种全局观。它只拿到了单个的需求,表现得就是一个新人一样。它还需要更多的设计、规范相关的信息。
作为一个 AI + 软件工程的实践者,我并不相信文档能帮助 LLM 解决这个问题。因为文档总是落后的,缺少人维护的,而且无法自动化。
所以,我们在 Co-mate 中探索的是规范 DSL 化,即在原先 ArchGuard 规范代码化的基础上进行了二次封装。即可以让 LLM 按 DSL 来生成设计,还可以通过 DSL 来检查生成的设计是否符合规范。
诸如于在 Co-mate 的 Foundation Spec 里,我们可以用如下的方式来检查命名:
naming {
class_level {
style("CamelCase")
pattern(".*") { name shouldNotBe contains("$") }
}
function_level {
style("CamelCase")
pattern(".*") { name shouldNotBe contains("$") }
}
}
而在生成代码里,也可以以此作为 LLM 的上下文提供。由于它是一个 DSL,而不是一个文档,所以可以动态地拿出作为上下文的一部分。
在过去,我们的行业积累了一系列的 DSL,诸如于大量的 ADL(架构设计语言)、UML(统一建模语言)、BDD 语言(如 Cucumber)等等。
Cucumber 是背后的 Gherkin 是一种很有意思的 DSL,特别适合于与 LLM 结合。它也符合那篇《[语言接口:探索大模型优先架构的新一代 API 设计](https://www.phodal.com/blog/language-api-llm-first-api/)》所提及的新一代流式(Streaming) DSL 格式。如下:
Feature: OKR协作与管理
用户可以创建和管理OKR,跟踪目标和关键结果的进展。
Scenario: 创建OKR
Given 用户已登录到OKR协作与管理系统
When 用户进入系统主界面
And 用户选择创建OKR
And 用户填写目标和关键结果的详细信息
And 用户设置时间周期和权重
And 用户点击保存按钮
Then 系统应成功创建并保存OKR
所以,我们可以通过上述的方式将需求格式化。
但是我们又遇到了一个问题,如何去表述更宏观的需求呢?
所以,我又从经典的工程方法里,找到了 UML。我依旧还是“相信”,很多人已经尝试过让 LLM 生成 PlantUML,以辅助进行架构设计。尽管有一定的概率生成的 UML 不生效,或者不准确,但是都觉得挺好玩的。
因为,我一直不擅长标准的 UML 写法,所以我并不看好它。而因为大部分后端开发人员都写过 Gradle 配置,所以我觉得类似于 Kotlin DSL 的方式,更方便于理解和修改:
caseflow("MovieTicketBooking", defaultRole = "User") {
// activity's should consider all user activities
activity("AccountManage") {
// task part should include all user tasks under the activity
task("UserRegistration") {
// you should list key steps in the story
story = listOf("Register with email", "Register with phone")
}
task("UserLogin") {
story += "Login to the website"
}
}
activity("MovieSelection") {}
// ...
activity("PaymentCancel") {
task("ConfirmCancel") {
role = "Admin" // if some task is role-specific, you can specify it here
//...
}
}
}
我一直尝试在平衡用例与用户故事,并尝试将它们结合在一起,以为未来生成代码时,提供一种动态的上下文。
为了更好将 LLM 应用于软件开发过程,那么我们需要:
而这一些还需要相当长的时间。
围观我的Github Idea墙, 也许,你会遇到心仪的项目