Blog

Blog

PHODAL

如何设计 AI 辅助编程的 PROMPT 策略?

在研究了 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 会自动加入之后,别的差别也不大。

BeforeCursor:一个简易的、快速的 PROMPT

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 的优点是,由于输入少,所以返回特别快。

SimilarFile:相似文件

不论是 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

ImportedFile:依赖文件

在 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 有时在生成构造参数很灵光有时就不行。

Token 压缩

由于,我们不可能把所有的代码都发送给 GPT,一来,上下文过长,GPT 会失焦;二来,上下文太长,响应速度特别慢。

所以,需要:

  1. 根据是否超过 token 上限来添加上下文。
  2. 由于代码是结构化的,可以为 UML 或者相似的方式来压缩上下文。

诸如于 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: @phodal     微博:@phodal     知乎:@phodal    

微信公众号(Phodal)

围观我的Github Idea墙, 也许,你会遇到心仪的项目

QQ技术交流群: 321689806
comment

Feeds

RSS / Atom

最近文章

关于作者

Phodal Huang

Engineer, Consultant, Writer, Designer

ThoughtWorks 技术专家

工程师 / 咨询师 / 作家 / 设计学徒

开源深度爱好者

出版有《前端架构:从入门到微前端》、《自己动手设计物联网》、《全栈应用开发:精益实践》

联系我: h@phodal.com

微信公众号: 最新技术分享

标签