在研究了 GitHub Copilot 与 JetBrains AI Assistant 的实现原理,并以某种方式参考了他们的代码之后,我尝试在 AutoDev 实现相关的 prompt 策略。
PS:为了能复现本文的内容,你需要将 temperature
设为 0
故事从 JetBrains 里的 AI Assistant 的 jar 包中的一个 JSON 文件说起:
defaultPriorities.json = [
"BeforeCursor",
"SimilarFile",
"ImportedFile",
"PathMarker",
"LanguageMarker"
]
这个文件简直和 GitHub Copilot 像极了,除了 PathMarker、LanguageMarker 会自动加入之后,别的差别也不大。
在 temperature
为 0 的情况下,我们只需要把代码直接扔给 ChatGPT,它就可以直接帮我们生成后续的代码。如下是一个不完整的代码段示例:
this.blogService = blogService;
}
// create blog
@ApiOperation(value = "Create a new blog")
@PostMapping("/create")
public BlogPost createBlog(@RequestBody CreateBlogRequest request) {
BlogPost blogPost = new BlogPost
在这时,LLM 返回的内容便会继续先前的内容:
();
blogPost.setTitle(request.getTitle());
blogPost.setContent(request.getContent());
...
所以,在 JetBrains AI Assistant 的早期版本设计里,直接取了光标前的 256 个字符,直接扔给 GPT。
这个 Prompt 的优点是,由于输入少,所以返回特别快。
不论是 GitHub Copilot 还是 AI Assistant 都会优先基于最近的 20 个文件,或者是基于打开的 tab 打开的文件。如下:
private fun getMostRecentFiles(element: PsiElement): List<VirtualFile> {
val recentFiles: List<VirtualFile> = EditorHistoryManager.getInstance(element.project).fileList.filter { file ->
file.isValid && file.fileType == element.containingFile?.fileType
}
val start = (recentFiles.size - maxRelevantFiles + 1).coerceAtLeast(0)
val end = (recentFiles.size - 1).coerceAtLeast(0)
return recentFiles.subList(start, end)
}
再基于相关的文件计算相关的代码块相似度,诸如于 JaccardSimilarity
算法:
private fun tokenLevelJaccardSimilarity(chunks: List<List<String>>, element: PsiElement): List<List<Double>> {
val currentFileTokens: Set<String> = tokenize(element.containingFile.text).toSet()
return chunks.map { list ->
list.map {
val tokenizedFile: Set<String> = tokenize(it).toSet()
jaccardSimilarity(currentFileTokens, tokenizedFile)
}
}
}
不过,JetBrains 的相似算法实现还相当的粗糙,还处于不可用的阶段。然后结合注释的方式以发送给 GPT,类似于:
// Compare this snippet from java/cc/unitmesh/untitled/demo/controller/BlogController.java
// package cc.unitmesh.untitled.demo.controller;
@RestController
@RequestMapping("/blog")
public class BlogController {
BlogService blogService;
详细可以见 AutoDev 的 “拷贝部分”:SimilarChunkContext
。
在 AI Assistant 还没有基于这个方式进行相关的 prompt 设计,而在 GitHub Copilot 则是会基于特定的语言将其 import
的文件也发送给 Codex。
于是乎,在设计 AutoDev 时,针对于日常的 CRUD 进行优化,会在编写 Controller 时,分析 import:
val allImportStatements = serviceFile.importList?.allImportStatements
val entities = filterImportByRegex(allImportStatements, domainRegex)
再将 DTO、Entity 和 Service 的代码浓缩成 UML 发送给服务端:
// package: cc.unitmesh.untitled.demo.dto
// getter/setter: title: String, content: String, author: String
//class CreateBlogDto {
//}
//
// package: cc.unitmesh.untitled.demo.model
// getter/setter: id: Long, title: String, content: String, author: String
//class BlogPost {
// + constructor(title: String, content: String, author: String)
//}
如此一来,后续生成的代码所需要的变更就更加的少。GitHub Copilot 有时在生成构造参数很灵光有时就不行。
由于,我们不可能把所有的代码都发送给 GPT,一来,上下文过长,GPT 会失焦;二来,上下文太长,响应速度特别慢。
所以,需要:
诸如于 AI Assistant 在生成文档上,也会构建出一个浓缩的上下文:
class name: BlogController
class fields: blogService
class methods: public BlogController(BlogService blogService)
@PostMapping("/blog") public BlogPost createBlog(CreateBlogDto blogDto)
@GetMapping("/blog") public List<BlogPost> getBlog()
super classes: []
这样一来,即能减少 token 消耗,还能提升响应速度。
欢迎大家来入坑,一起开发 AutoDev:https://github.com/unit-mesh/auto-dev
可以看看我们最近的 CRUD DEMO 视频
围观我的Github Idea墙, 也许,你会遇到心仪的项目