“用调评”一体化:生成上下文数据集,改善AI测试生成质量

代码为聘礼 2024-02-20 03:46:25

最近,我们在围绕 AutoDev 开源插件,构建完整的端到端开源辅助编程方案。即:

结合 IDE 插件微调开放二进制大语言模型(所谓 “开源”模型)。

构建对应开放对应的模型与数据集

构建针对于微调的开源数据工程:Unit Eval 。

简单来说,就是依旧在 Unit Eval 开源项目中设计的:“用调评”一体化(即 AI 工具-模型微调-模型评测一体化),以构建更贴合于不同组织现状的编码方案。

如何让 AI 写好测试?

在大部分编码场景中,AI 辅助单元测试的效果是相对最好的。但是这也并不简单,其中的难点在于如何构建好的上下文。

什么是好的 AI 测试上下文?

作为一个经常刷测试覆盖率,以及在 ArchGuard 中构建了测试坏味道分析检查工具的工程师,我大抵可以算得上是一个经验老道的单元测试专家。

在 AI 生成的背景之下,我们预期一个好的测试上下文,它应该包括:

类的构造信息(constructor)。

接口、函数的输入和输出。以使得测试能构建出正确的输入和输出

测试框架的相关信息。诸如于采用的是 JUnit 4 还是 JUnit 5,使用的是哪个 Mock 框架

测试代码规范。诸如于命名方式等

如下是一个经典的,用于练习的测试示例:

// ....

import org.junit.jupiter.api.Test;

@SpringBootTest

@AutoConfigureMockMvc

class BlogControllerTest {

@Autowired

private MockMvc mockMvc;

@MockBean

private BlogRepository blogRepository;

@Test

public void should_return_correct_blog_information_when_post_item() throws Exception {

BlogPost mockBlog = new BlogPost("Test Title", "Test Content", "Test Author");

....

}

}

在这个普通的测试里,如果缺乏上述的一系列信息,那么会导致一些有意思的错误:

使用 JUnit 4 来编码 JUnit 5 的测试

使用错误的 Mock 框架

生成的 BlogPost 构建函数是错误的

直接调用 private 方法,而非绕过其它的方式

测试函数的命名是不规范的,不遵循内部的开发规范。

其它的可能还有诸如于缺少测试断言等其它的测试坏味道,所以上述的信息就变得非常有必要。基于我们积累下来的单元测试与 IDE 插件经验,便需要考虑一体化的工程思路。

为什么基于开源模型微调?

依照我们设计的 “一大一中一微” 的三模型体系,为了解决测试代码的生成问题,我们采用了 DeepSeek 6.7B 模型作为代码生成模型基础模型,以满足代码补全、测试生成两个高频高响应速度的需求。而为了在 AutoDev 中为了提升生成结果的准确度,使用静态代码分析生成更精准的上下文。其中包含了:

技术栈上下文

测试技术栈上下文

代码块(类、函数)的输入和输出信息

如下是在 AutoDev 中精简化后的 Prompt 示例:

Write unit test for following code.

${context.testFramework}

${context.coreFramework}

${context.testSpec}

${context.related_model}

```${context.language}

${context.selection}

```

而在我们测试了一些开源模型之后,发现理解 prompt 以及上下文的能力是有限的,甚至可能无法理解,以至于生成不了测试代码。为此,我们就需要围绕于测试提示词的上下文,构造微调数据集。

“用调评”一体化:围绕于提示词的上下文工程

在 Unit Eval 中设计的三个核心原则:

统一提示词(Prompt)。统一工具-微调-评估底层的提示词。

代码质量管道(Pipeline)。诸如于代码复杂性、代码坏味道、测试坏味道、API 设计味道等。

可扩展的质量阈。自定义规则、自定义阈值、自定义质量类型等。

基于这三个原则,再融合我们的测试经验,Unit Eval 中便有了测试生成的数据工程能力。然而,这并非一件简单事情,在测试这个场景之下,我们可以再看看如何实现。

统一提示词:识别基础元素

在 AutoDev 中,会从 Project 中读取依赖管理工具中的 LibraryData 进而构建出对应的测试框架等信息。如下所示:

You are working on a project that uses Spring MVC,Spring WebFlux,JDBC to build RESTful APIs.

This project uses JUnit 5, you should import `org.junit.jupiter.api.Test` and use `@Test` annotation.

...

为了让模型能更好地理解对应的代码指令,在对应的测试数据集中,也需要构建对应的框架信息出来。在 Unit Eval 中会先调用 ArchGuard SCA 的分析工具,从中解析中依赖列表,进而生成的上下文信息。

代码质量管理:测试代码坏味道

而为了更好的结合的管理生成质量,我们还是需要控制一下数据集中的质量。因此,我们使用 ArchGuard Rule 来扫描测试代码,只放入生成比较好的测试。诸如于:

没有断言的测试

包含 Sleep 的测试 —— 说明测试写得并不好

过多调试打印信息的测试用例

过多 Assert 语句

……

当然了,根据不同的组织信息,我们还需要添加好更多的规划。

可扩展的质量阈:函数长度控制

在测试上,我们还需要进一步控制输入的测试的质量,诸如于测试代码函数的长度。当然了,现在 Unit Eval 控制的是整个类的长度,在未来将添加对于测试函数的控制。

一体化示例:AutoDev 与 Unit Eval

根据不同的微调场景,如基于内部代码库生成数据、让开源模型理解指令,所需要的数据集大小是不一样的。在这里的场景,是让开源模型能更好地理解 AutoDev 的指令。

结合规范的持续演进

结合上述的原则,我们构建了新的 Unit Eval 测试数据集:https://github.com/unit-mesh/unit-eval/releases/tag/v0.2.0 ,并进行了微调。

如下是,结合微调生成的测试用例示例:

@Test

public void testCreateBlog() {

BlogPost blogDto = new BlogPost("title", "content", "author");

when(blogRepository.save(blogDto)).thenReturn(blogDto);

BlogPost blog = blogService.createBlog(blogDto);

assertEquals("title", blog.getTitle());

assertEquals("content", blog.getContent());

assertEquals("author", blog.getAuthor());

}

虽然,生成的测试构建函数都是正确的,测试也是可运行的。但是,在这里,我们可以发现一个明显的问题:生成的函数名没有符合规范。

前面在 prompt 中要求的是类似于 should_return_correct_blog_information_when_post_item 方式命名的,这也会导致后续生成的测试方法都失去对应的准确性。因此,我们需要基于上述的原则,重新检查是否符合我们的命名规范,再进行微调。

保持提示词一致

在总结并编写这篇文章的时候,还发现了一些提示词不合理之处,并更正错误。诸如于:框架的提示词、命名方式等等。如下是更新完后的上下文信息:

- Test should be named `snake_case`.

- You are working on a project that uses Spring Boot, Spring Boot Web

- This project uses JUnit, assertj, mockito, Spring Boot Test, Spring Test to test code.

当然了,还需要依旧测试的类型进一步演进。

总结

与编写一个可以用的 AI 辅助编码工具,如何持续演进整体的架构更有挑战。

PS:更详细可以参见:https://github.com/unit-mesh/unit-eval 项目的 README。

0 阅读:0

代码为聘礼

简介:感谢大家的关注