在本系列的前两篇文章《LLM 赋能的研发效能》、《LLM 赋能的软件架构》里,介绍了我们在 LLM 结合 BizDevOps、软件架构的一系列试验。围绕于这两大类探索,我们构建了两个开源 LLM 工具:AI 辅助编程工具 AutoDev(IDEA 插件 )、架构治理工具 ArchGuard Co-mate。
先看看,基于我们所理解 LLM + 软件开发的 AutoDev 自动 CRUD 视频:
引子:如何利用 LLM 有限的能力解决复杂问题?
再让我们先看看几个不同的 LLM 使用场景:
特定格式文章总结
作为一个每周写文章总结的开发者,在我过往的文章【AI 总结】里发现一个事实:LLM 对我文章的总结能力是极差的,多数时候非常的鸡肋。在足够长的情况下,总结错误的可能性就更高。
然而,我的文章往往看看大标题、一级标题、二级标题就可以有初步印象。所以如果只把几级标题交给 LLM 去写,效果远远比 LLM 阅读全文要高效得多。
DSL 作为语言接口
作为一个探索过结合 LLM 应用的开发者,我想你也已经尝试过使用 DSL(领域特定语言)来作为 LLM 的 API。诸如于采用 JSON 作为 LLM API 的返回结果,又或者是类似于 Co-mate 采用的内部 DSL,还可能是类似于 LangChain 的流式 DSL 方式:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [introduce_system]
Action Input: the input to the action
...
不论是何种方式,我们都在探索基于特定数据结构的 API 返回。
流程与工序中的 LLM
我们都知道 LLM 的上下文能力(并不单指 token 限制,还有对复杂问题的理解能力)是有限的,它无法一次解决复杂问题 —— 需要先我们做好 Tasking(拆解好步骤),再交由不同的 LLM 去完成。
在工序或者流程不明确的情况下,我们可以构建类似于 AutoGPT 的方式来自动完成 task。但是,由于工序本身的不确定性,以及过程结果的准确率问题,所以结果往往是不可靠的。诸如于,假设我们的问题,需要拆解为 6 步,而LLM 生成的准确率是 80%,而 0.8^6 = 0.262144,也就是只有 1/4 的可能性会生成我们想要的。
而在软件工程领域里,在工序足够规范的情况下,可以通过抽象来解决上下文过长的问题。所以,我们在探索开发者工具时,在不考虑向量数据库能力有限的情况下,我们并不需要类似于 LangChain 工具来动态构建上下文,它会浪费我的 token(钱)。
工序指的是开发过程中的各个环节和步骤,例如需求分析、设计、编码、测试等。将工序融入AIGC触点,意味着在每个工序中都可以使用 LLM 和其他工具来辅助开发。
简单来说,人类如何解决复杂问题,那么就需要 AI 如何解决复杂问题。
要素 1:编码软件工序,扩大 AIGC 触点
软件开发是一个复杂的群体智力活动。正如,我们先前在 LLM 结合 BizDevOps 的探索一样,我们需要打开看已有的软件开发流程,并尽可能的细化子任务,将它们与 AI 相结合,才能看到明显的效果。
即,在过去是通过建立标准化的软件工程、开发工序,才摆脱集市的模式 ,走向了大教堂模式。而在标准化的工序/流水线,它是易于被自动化的。
标准化工序的代码化
在 AutoDev 中,为了自动化对于 CRUD 的代码编写,从抽象层面是分为五步的。即代码中的 CrudFlowAction 所描述的:
interface CrudFlowAction {fun getOrCreateStoryDetail(id: String): Stringfun updateOrCreateDtoAndEntity(storyDetail: String)fun fetchSuggestEndpoint(storyDetail: String): TargetEndpointfun updateOrCreateEndpointCode(target: TargetEndpoint, storyDetail: String)fun updateOrCreateServiceAndRepository()
}
同样是写代码,我们需要完整的模拟人的编码过程,拆到足够细的粒度:
根据需求,更新或者创建新的 DTO
根据需求,寻找合适的 Controller 作为修改入口(Endpoint)
根据需求,更新 Controller 代码或者创建新的 Controller
根据 Controller,创建或者更新对应的 Service 和 Repository 代码。
而这其中最 trick 的一点是,如果现有的工序缺少一环的话,那么就会导致生成的结果出现过多问题。诸如于,我们缺少 DTO 的生成,那么会导致 Controller 无法正确生成,并出现调用错误的 DTO 方法。
扩大工序中的 AIGC 触点
在 AIGC 结合软件开发流程中,我们可以简单划出三个重要的阶段:需求、设计、编码(含自动化测试)。为了实现上述的 AutoDev 自动化,我们需要两个关键输入:实例化需求和 API 接口设计。
实例化需求是指根据具体项目的需求和规范,将需求描述转化为具体的示例、用例或者场景,以便于后续的开发和测试工作。
API 接口设计是指根据需求和系统设计,定义和规划系统的接口,包括接口的输入、输出、参数等信息。
为了这两个关键的输入,那么生成的代码的可用性会更高。而其前提是:有人类介入到生成结果中。即在生成实例化需求时,需要有机制来检查结果是否准确;在生成 API 接口设计时,需要有机制来检查 API 是否合理。而这个机制,可以是由人来检查,也可以是 AIGC 结合规范来检查。
而一旦,我们的 AIGC 止步于代码阶段,而没有需求阶段和设计阶段时,那么人类可能因为一天只有 24 小时,而没有给出实例化的需求,或者是 API 接口。导致 AIGC 无法进一步向下生成,所以提升的结果是非常有限的。
要素 2:围绕副驾驶设计,完善开发者体验
回到我们开头介绍的 AutoDev 视频,在起步阶段会去获取在 GitHub 上的 issue,根据 issue ID 如 #1 的内容如下:
用户故事:新建博客
作为一位博客作者
我想在InnoRev网站上新建博客
以便于我可以发布我的文章
AC 1: 新建博客成功
假设用户已经登录,并且填写了正确的博客信息
当用户点击“新建博客”按钮
于是系统创建一个新的博客,并返回博客的id
AC 2: 博客标题过长
假设用户已经登录,并且填写了超过50个字符的博客标题
当用户点击“新建博客”按钮
于是系统返回“博客标题过长”的错误信息
AC 3:...
AC 4:...
// {API Method} POST {API URI} /blogs
// CreateBlogRequest: { "title": "My First Blog", "content": "Hello world!" }
// CreateBlogResponse: { "id": 1, "title": "My First Blog", "content": "Hello world!" }
这个 comments 由用户故事和 API 设计两部分组成。有了 API 设计和 response 作为输入和输出,结果现有的代码作为上下文,这时不论是交给谁来设计代码,最后输入和输出只要是符合要求的,那么 AIGC 就是合格的。
围绕副驾驶(Copilot)设计
不论是开发人员使用的 IDE,还是产品经理、架构师、质量测试的工具,我们都需要围绕于这些研发人员的日常来设计。
在 GitHub Copilot 里,我们会习惯于它提供的 inlays 模式来提供智能的代码填充。而只有这一个核心能力是不够的,在日常的开发中,我们很多的时间是花在 debug 上的,又或者是其它的问题上,我们还需要围绕这些环节设计。
于是,诸如于 GitHub Copilot X,又或者是 JetBrains 的新版本 IDE 里都提供了一系列的 AI 能力:New chat using selection、Write documentation、Explain code、Suggest refactoring、Find potential problems 等等。
当然了,在 AutoDev 中我们也提供了相应的能力,并且还有更强大的辅助 CRUD 的功能。
完善开发者体验
现有的很多 LLM 模型厂商是不负责的,他们只提供模型本身的编码能力,而不提供配套的编码工具。在这里,LLM 本身就是一个鸡肋,企业依旧需要构建 IDE 插件来提升开发者的体验。
而如果我们可以自己设计 IDE 工具,那么我们应该尽可能加一些增强开发者体验的功能。诸如于,直接选择开发、运行时的错误日记来问 LLM,以快速修复这个问题。
又或者是在 AutoDev 里,可以在编写提交信息时,让 LLM 根据代码变更来生成建议。如下是对应的 prompt:
suggest 10 commit messages based on the following diff:
commit messages should:- follow conventional commits- message format should be: <type>[scope]: <description>
examples:- fix(authentication): add password regex pattern- feat(storage): add new test cases
在我们提供了基本的规范和示例之后,它生成的提交信息是越来越标准了。
要素 3:内建开发规范,提升软件质量
众所周知,GitHub Copilot 会根据我们当前的编码习惯,来生成新的代码。这就有可能导致:因为原有的代码是不规范的,所以生成的代码也是不规范的。因此,在那篇《LLM 赋能的软件架构》里,我们提及了架构、编码规范应该内建到 AIGC 工具中。基于此,才能提高软件开发的 AIGC 质量,强化代码的可用性。
在先前的《上下文工程:基于 Github Copilot 的实时能力分析与思考》,我们分享了 GitHub 上下文在实现自动填充的基本策略:
defaultPriorities.json = ["BeforeCursor","SimilarFile","ImportedFile","PathMarker","LanguageMarker"
]
由此,它只会根据用户的行为计算,根据结果带上相关的代码。相似的,我们也可以通过 getMostRecentFiles
来计算相关文件,再通过 tokenLevelJaccardSimilarity
,最后生成 SimilarChunkContext
。
而为了写出的代码,也是规范的,我们需要加上一些特定的规则,如针对于业务系统编程时,就可以:
when {isController -> {val spec = PromptConfig.load().spec["controller"]additionContext += mvcContextService.controllerPrompt(file)}isService -> {val spec = PromptConfig.load().spec["service"]additionContext += mvcContextService.servicePrompt(file)}
}
而规范本身应该是可配置的:
{"spec": {"controller": "- 在 Controller 中使用 BeanUtils.copyProperties 进行 DTO 转换 Entity\n- 禁止使用 Autowired\n-使用 Swagger Annotation 表明 API 含义\n-Controller 方法应该捕获并处理业务异常,不应该抛出系统异常。","service": "- Service 层应该使用构造函数注入或者 setter 注入,不要使用 @Autowired 注解注入。","entity": "- Entity 类应该使用 JPA 注解进行数据库映射\n- 实体类名应该与对应的数据库表名相同。实体类应该使用注解标记主键和表名,例如:@Id、@GeneratedValue、@Table 等。","repository": "- Repository 接口应该继承 JpaRepository 接口,以获得基本的 CRUD 操作","ddl": "- 字段应该使用 NOT NULL 约束,确保数据的完整性"}
}
不同的团队根据其团队的业务场景和需求,去稍微修改自己的规范配置。
小结
由 ChatGPT 总结:
LLM 赋能的研发工具通过编码软件工序、完善开发者体验和内建开发规范,提高了软件开发的效率和质量。它们涵盖端到端链路,以自动化和智能化的方式辅助开发人员完成任务。通过细化工序、提供智能代码填充和提示,以及内置规范,这些工具使开发人员能够更高效地编写代码,并确保生成的代码符合规范。LLM赋能的研发工具将人工智能与软件开发流程结合,为开发者提供更好的开发体验和更高质量的代码。