AIDocumentLibraryChat 项目介绍

AIDocumentLibraryChat 是一个能够为 GitHub 上的公开项目生成测试代码的工具。该项目支持 Java 语言,并且已经通过了测试。它可以通过提供的类 URL 加载相应的类,分析其导入关系,并加载项目中依赖的类。这为使用大型语言模型(LLM)生成测试模拟提供了便利,使得测试代码能够更贴近源代码的实际使用场景。

功能特点

  • 类加载与分析:项目可以加载指定 URL 的类,并分析其依赖关系。- 测试代码生成:基于 LLM,生成贴近实际的测试代码。- 模型支持:支持 granite-code 和 deepseek-coder-v2 模型。

使用方法要使用 AIDocumentLibraryChat 生成测试代码,你需要进行以下步骤:

1. 提供类 URL你需要提供一个指向要测试类的 URL。

2. 配置模型在 application-ollama.properties 文件中配置所需的 LLM 模型。这个文件位于项目的后端资源目录下。

3. 生成测试通过提供的 testUrl,AIDocumentLibraryChat 将使用选定的 LLM 模型生成测试代码。

配置示例以下是 application-ollama.properties 文件的一个配置示例:

properties# 选择 LLM 模型llm.model=granite-code

测试模型granite-code 和 deepseek-coder-v2 是两个已经过测试的模型,它们可以帮助开发人员更高效地创建测试代码。

结论AIDocumentLibraryChat 项目为开发人员提供了一个强大的工具,以自动化测试代码的生成过程,提高开发效率和代码质量。

spring.ai.ollama.base-url=${OLLAMA-BASE-URL:http://localhost:11434}
spring.ai.ollama.embedding.enabled=false
spring.ai.embedding.transformer.enabled=true
document-token-limit=150
embedding-token-limit=500
spring.liquibase.change-log=classpath:/dbchangelog/db.changelog-master-ollama.xml

...

# generate code
#spring.ai.ollama.chat.model=granite-code:20b
#spring.ai.ollama.chat.options.num-ctx=8192

spring.ai.ollama.chat.options.num-thread=8
spring.ai.ollama.chat.options.keep_alive=1s

spring.ai.ollama.chat.model=deepseek-coder-v2:16b
spring.ai.ollama.chat.options.num-ctx=65536

在配置和使用LLM(Large Language Model,大型语言模型)时,我们需要注意几个关键的设置项,以确保模型能够高效地运行并满足我们的需求。以下是一些重要的配置参数及其说明:

  1. 模型选择spring.ai.ollama.chat.model 用于指定要使用的LLM代码模型。选择合适的模型对于确保生成内容的质量和准确性至关重要。
  2. 上下文窗口设置: - spring.ollama.chat.options.num-ctx 用于设置上下文窗口中的令牌数量。这个窗口包括请求和响应所需的令牌,对模型理解上下文和生成连贯的回应非常关键。 - spring.ollama.chat.options.num-thread 如果Ollama没有选择正确数量的核心,可以通过此参数进行调整,以优化多线程处理能力。
  3. 上下文保留时间spring.ollama.chat.options.keep_alive 设置上下文窗口保留的秒数。这决定了模型在生成回应之前可以维持的上下文信息的时间长度。
  4. 控制器接口:控制器提供了获取源和生成测试的接口,这是与模型交互的主要方式。 通过合理配置这些参数,可以确保LLM模型在对话和文本生成任务中发挥最佳性能。
@RestController
@RequestMapping("rest/code-generation")
public class CodeGenerationController {
  private final CodeGenerationService codeGenerationService;

  public CodeGenerationController(CodeGenerationService 
    codeGenerationService) {
    this.codeGenerationService = codeGenerationService;
  }

  @GetMapping("/test")
  public String getGenerateTests(@RequestParam("url") String url,
    @RequestParam(name = "testUrl", required = false) String testUrl) {
    return this.codeGenerationService.generateTest(URLDecoder.decode(url, 
      StandardCharsets.UTF_8),
    Optional.ofNullable(testUrl).map(myValue -> URLDecoder.decode(myValue, 
      StandardCharsets.UTF_8)));
  }

  @GetMapping("/sources")
  public GithubSources getSources(@RequestParam("url") String url, 
    @RequestParam(name="testUrl", required = false) String testUrl) {
    var sources = this.codeGenerationService.createTestSources(
      URLDecoder.decode(url, StandardCharsets.UTF_8), true);
    var test = Optional.ofNullable(testUrl).map(myTestUrl -> 
      this.codeGenerationService.createTestSources(
        URLDecoder.decode(myTestUrl, StandardCharsets.UTF_8), false))
          .orElse(new GithubSource("none", "none", List.of(), List.of()));
    return new GithubSources(sources, test);
  }
}

CodeGenerationController 功能概述

CodeGenerationController 类提供了生成测试代码的功能。它包含两个主要的方法:getSourcesgetGenerateTests。以下是这两个方法的详细描述:

方法一:getSources- 功能:获取源代码和测试示例。- 参数: - url:必须,用于定位要测试的类。 - testUrl:可选,用于定位示例测试。- 处理流程: 1. 对请求参数进行解码。 2. 调用 createTestSources 方法,生成测试源代码。

方法二:getGenerateTests- 功能:生成测试代码。- 参数: - url:必须,指向测试类的 URL。 - testUrl:可选,用于 URL 解码。- 处理流程: 1. 调用 CodeGenerationServicegenerateTests 方法。

服务:CodeGenerationService- 功能:从 Github 收集类并生成测试代码。- 操作:使用提示信息进行服务描述。

服务描述提示- 服务名称:CodeGenerationService- 功能描述:收集 Github 上的类并生成相应的测试代码。

@Service
public class CodeGenerationService {
  private static final Logger LOGGER = LoggerFactory
    .getLogger(CodeGenerationService.class);
  private final GithubClient githubClient;
  private final ChatClient chatClient;
  private final String ollamaPrompt = """
    You are an assistant to generate spring tests for the class under test. 
    Analyse the classes provided and generate tests for all methods. Base  
    your tests on the example.
    Generate and implement the test methods. Generate and implement complete  
    tests methods.
    Generate the complete source of the test class.
 
    Generate tests for this class:
    {classToTest}

    Use these classes as context for the tests:
    {contextClasses}

    {testExample}
  """;
  private final String ollamaPrompt1 = """
    You are an assistant to generate a spring test class for the source 
    class.
    1. Analyse the source class
    2. Analyse the context classes for the classes used by the source class
    3. Analyse the class in test example to base the code of the generated 
    test class on it.
    4. Generate a test class for the source class, use the context classes as 
    sources for it and base the code of the test class on the test example. 
    Generate the complete source code of the test class implementing the 
    tests.

    {testExample}

    Use these context classes as extension for the source class:
    {contextClasses}

    Generate the complete source code of the test class implementing the  
    tests.
    Generate tests for this source class:
    {classToTest}
  """;
  @Value("${spring.ai.ollama.chat.options.num-ctx:0}")
  private Long contextWindowSize;

  public CodeGenerationService(GithubClient githubClient, ChatClient 
    chatClient) {
    this.githubClient = githubClient;
    this.chatClient = chatClient;
  }

这是带有“GithubClient”和“ChatClient”的“CodeGenerationService”。GithubClient 用于从公开可用的存储库中加载源代码,ChatClient 是用于访问 AI/LLM 的 Spring AI 接口。

“ollamaPrompt”是 IBM Granite LLM 的提示,其上下文窗口为 8k 个令牌。
‘{classToTest} 被替换为被测类的源代码。“{contextClasses}”可以替换为被测类的依赖类,“{testExample}”是可选的,可以替换为可以用作代码生成示例的测试类。

‘ollamaPrompt2’ 是 Deepseek Coder V2 的提示LLM。这LLM可以“理解”或与思维链提示一起工作,并具有超过 64k 个令牌的上下文窗口。“{…}”占位符的工作方式与“ollamaPrompt”中的工作方式相同。
长上下文窗口允许添加用于代码生成的上下文类。

Spring 注入了 ‘contextWindowSize’ 属性,用于控制上下文窗口LLM是否足够大以将 ‘{contextClasses}’ 添加到提示符中。

方法 ‘createTestSources(…)’ 收集并返回 AI/LLM 提示的源:

public GithubSource createTestSources(String url, final boolean 
  referencedSources) {
  final var myUrl = url.replace("https://github.com", 
    GithubClient.GITHUB_BASE_URL).replace("/blob", "");
  var result = this.githubClient.readSourceFile(myUrl);
  final var isComment = new AtomicBoolean(false);
  final var sourceLines = result.lines().stream().map(myLine -> 
      myLine.replaceAll("[\t]", "").trim())
    .filter(myLine -> !myLine.isBlank()).filter(myLine -> 
      filterComments(isComment, myLine)).toList();
  final var basePackage = List.of(result.sourcePackage()
    .split("\\.")).stream().limit(2)
    .collect(Collectors.joining("."));
  final var dependencies = this.createDependencies(referencedSources, myUrl, 
    sourceLines, basePackage);
  return new GithubSource(result.sourceName(), result.sourcePackage(), 
    sourceLines, dependencies);
}

private List<GithubSource> createDependencies(final boolean 
  referencedSources, final String myUrl, final List<String> sourceLines, 
  final String basePackage) {
  return sourceLines.stream().filter(x -> referencedSources)
    .filter(myLine -> myLine.contains("import"))
    .filter(myLine -> myLine.contains(basePackage))
    .map(myLine -> String.format("%s%s%s", 
      myUrl.split(basePackage.replace(".", "/"))[0].trim(),
myLine.split("import")[1].split(";")[0].replaceAll("\\.", 
          "/").trim(), myUrl.substring(myUrl.lastIndexOf('.'))))
    .map(myLine -> this.createTestSources(myLine, false)).toList();
}

private boolean filterComments(AtomicBoolean isComment, String myLine) {
  var result1 = true;
  if (myLine.contains("/*") || isComment.get()) {
    isComment.set(true);
    result1 = false;
  }
  if (myLine.contains("*/")) {
    isComment.set(false);
    result1 = false;
  }
  result1 = result1 && !myLine.trim().startsWith("//");
  return result1;
}

在软件开发过程中,源代码的管理和测试是至关重要的。以下是使用GitHub源代码来创建测试源的步骤概述:

  1. 源代码获取: - 首先,我们需要定义一个URL(例如:myUrl),用于获取特定类的原始源代码。
  2. 源代码读取: - 使用githubClient,将源文件作为字符串读取出来。
  3. 源代码处理: - 通过filterComments(...)方法,将源字符串转换为不含格式设置和注释的源行。
  4. 依赖类处理: - 识别项目中的依赖类,特别是那些位于基础包ch.xxx中的类。 - 使用createDependencies(...)方法,为这些依赖类创建GithubSource记录。
  5. 递归调用: - 通过设置basePackage参数,过滤掉不需要的类,并递归调用createTestSources(...)方法。 - 在递归调用中,将referencedSources设置为false以终止递归。
  6. 测试源生成: - 最后,使用generateTest(...)方法,结合AI/LLM技术为被测类创建测试源。 这个过程确保了源代码的完整性和测试的准确性,同时通过自动化的方式提高了开发效率。
public String generateTest(String url, Optional<String> testUrlOpt) {
  var start = Instant.now();
  var githubSource = this.createTestSources(url, true);
  var githubTestSource = testUrlOpt.map(testUrl -> 
    this.createTestSources(testUrl, false))
      .orElse(new GithubSource(null, null, List.of(), List.of()));
  String contextClasses = githubSource.dependencies().stream()
    .filter(x -> this.contextWindowSize >= 16 * 1024)
    .map(myGithubSource -> myGithubSource.sourceName() + ":"  + 
      System.getProperty("line.separator")
      + myGithubSource.lines().stream()
        .collect(Collectors.joining(System.getProperty("line.separator")))
      .collect(Collectors.joining(System.getProperty("line.separator")));
  String testExample = Optional.ofNullable(githubTestSource.sourceName())
    .map(x -> "Use this as test example class:" + 
      System.getProperty("line.separator") +  
      githubTestSource.lines().stream()
        .collect(Collectors.joining(System.getProperty("line.separator"))))
    .orElse("");
  String classToTest = githubSource.lines().stream()
    .collect(Collectors.joining(System.getProperty("line.separator")));
  LOGGER.debug(new PromptTemplate(this.contextWindowSize >= 16 * 1024 ? 
    this.ollamaPrompt1 : this.ollamaPrompt, Map.of("classToTest", 
      classToTest, "contextClasses", contextClasses, "testExample", 
      testExample)).createMessage().getContent());
  LOGGER.info("Generation started with context window: {}",  
    this.contextWindowSize);
  var response = chatClient.call(new PromptTemplate(
    this.contextWindowSize >= 16 * 1024 ? this.ollamaPrompt1 :  
      this.ollamaPrompt, Map.of("classToTest", classToTest, "contextClasses", 
      contextClasses, "testExample", testExample)).create());
  if((Instant.now().getEpochSecond() - start.getEpochSecond()) >= 300) {
    LOGGER.info(response.getResult().getOutput().getContent());
  }
  LOGGER.info("Prompt tokens: " + 
    response.getMetadata().getUsage().getPromptTokens());
  LOGGER.info("Generation tokens: " + 
    response.getMetadata().getUsage().getGenerationTokens());
  LOGGER.info("Total tokens: " + 
    response.getMetadata().getUsage().getTotalTokens());
  LOGGER.info("Time in seconds: {}", (Instant.now().toEpochMilli() - 
    start.toEpochMilli()) / 1000.0);
  return response.getResult().getOutput().getContent();
}

为此,使用“createTestSources(…)”方法创建带有源行的记录。然后创建字符串 ‘contextClasses’ 来替换提示符中的 ‘{contextClasses}’ 占位符。
如果上下文窗口小于 16k 个标记,则字符串为空,以便有足够的标记用于所测试的类和测试示例类。然后创建可选的“testExample”字符串来替换提示符中的“{testExample}”占位符。
如果未提供“testUrl”,则字符串为空。然后创建“classToTest”字符串以替换提示符中的“{classToTest}”占位符。

调用“chatClient”以将提示发送到 AI/LLM。提示是根据“contextWindowSize”属性中上下文窗口的大小选择的。“PromptTemplate”将占位符替换为准备好的字符串。

“response”用于记录提示令牌、生成令牌和总令牌的数量,以便能够检查上下文窗口边界是否得到遵守。然后记录生成测试源的时间,并返回测试源。
如果生成测试源的时间超过 5 分钟,则会记录测试源,以防止浏览器超时。

 结论

两种模型都已经过测试,可以生成 Spring Controller 测试和 Spring 服务测试。测试网址是:

<a href="http://localhost:8080/rest/code-generation/test?url=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/adapter/controller/ActorController.java&amp;testUrl=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/adapter/controller/MovieControllerTest.java">http://localhost:8080/rest/code-generation/test?url=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/adapter/controller/ActorController.java&amp;testUrl=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/adapter/controller/MovieControllerTest.java</a>
<a href="http://localhost:8080/rest/code-generation/test?url=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/usecase/service/ActorService.java&amp;testUrl=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/usecase/service/MovieServiceTest.java">http://localhost:8080/rest/code-generation/test?url=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/main/java/ch/xxx/moviemanager/usecase/service/ActorService.java&amp;testUrl=https://github.com/Angular2Guy/MovieManager/blob/master/backend/src/test/java/ch/xxx/moviemanager/usecase/service/MovieServiceTest.java</a>

测试结果分析

1. Ollama LLM ‘granite-code:20b’ 测试结果- 上下文窗口: 8k 令牌- 问题: 窗口过小,无法同时提供contextClasses和足够的令牌进行响应。- 生成测试: 为Spring服务生成了基础测试,但测试不完整,需要补充缺失的上下文类。- Spring Controller 测试: 测试结果不理想,遗漏了大量代码。- 测试生成时间: 在中等性能的笔记本电脑CPU上,耗时超过10分钟。

2. Ollama LLM ‘deepseek-coder-v2:16b’ 测试结果- 上下文窗口: 超过64k令牌- 优势: 能够将contextClasses添加到提示中,并与思维链提示符协同工作。- 生成测试: 大部分Spring服务测试有效,提供了良好的工作基础,缺失部分易于修复。- Spring Controller 测试: 存在错误,但提供了有用的起始基础。- 测试生成时间: 在中等性能的笔记本电脑CPU上,耗时不到10分钟。

综合意见

  • Deepseek-Coder-V2 LLM: 有助于编写Spring应用程序的测试,但为了实现生产使用,需要GPU加速。- 代码理解限制: LLMs不理解代码,代码对它们而言只是字符序列。如果不理解语言语法,生成的代码质量将受限。- 开发者角色: 开发者需要能够修复测试中的错误,LLMs仅能节省一些输入测试的时间。

结论- LLM帮助: 是的,LLM可以提供帮助。- 时间节省: 否,虽然提供了帮助,但并非显著节省时间。

建议- 考虑使用具有更大上下文窗口的LLM,以便更好地处理复杂任务。- 优化测试生成流程,减少开发者修复错误所需的时间。- 探索GPU加速的可能性,以提高测试生成的效率。