我们会在 GitHub 上持续更新这个教程: https://github.com/phodal/build-ai-coding-assistant,欢迎在 GitHub 上谈论。
2023 年,天生式 AI 的火爆,让越来越多的组织开始引入 AI 赞助编码。与在 2021 年发布的 GitHub Copilot 稍有差异的是,代码补全只是重多场景中的一个。大量的企业内部在探索结合需求天生完全代码、代码审查等场景,也引入天生式 AI,来提升开拓效率。
在这个背景下,我们(Thoughtworks)也开拓了一系列的开源工具,以帮助更多的组织构建自己的 AI 赞助编码助手:
AutoDev,基于 JetBrains 平台的全流程 AI 赞助编码工具。
Unit Eval,代码补全场景下的高质量数据集构建与天生工具。
Unit Minions,在需求天生、测试天生等测试场景下,基于数据蒸馏的数据集构建工具。
由于,我们设计 AutoDev 时,各种开源模型也在不断演进。在这个背景下,它的步骤是:
构建 IDE 插件与度量体系设计。基于公开模型 API,编写和丰富 IDE 插件功能。
模型评估体系与微调试验。
环绕意图的数据工程与模型演进。
也因此,这个教程也是环绕于这三个步骤展开的。除此,基于我们的履历,本教程的示例技能栈:
插件:Intellij IDEA。AutoDev 是基于 Intellij IDEA 构建的,并且自带静态代码剖析能力,以是基于它作为示例。我们也供应了 VSCode 插件的参考架构,你可以在这个根本上进行开拓。
模型:DeepSeek Coder 6.7b。基于 Llama 2 架构,与 Llama 生态兼容
微调:Deepspeed + 官方脚本 + Unit Eval。
GPU:RTX 4090x2 + OpenBayes。(PS: 用我的专用约请链接,注册 OpenBayes,双方各得到 60 分钟 RTX 4090 利用时长,支持累积,永久有效:https://openbayes.com/console/signup?r=phodal_uVxU )
由于,我们在 AI 方面的履历相比拟较有限,难免会有一些缺点,以是,我们也希望能够与更多的开拓者一起,来构建这个开源项目。
功能设计:定义你的 AI 助手结合 JetBrains 2023《开拓者生态系统》报告的人工智能部分 ,我们可以总结出一些通用的场景,这些场景反响了在开拓过程中天生式 AI 可以发挥浸染的领域。以下是一些紧张的场景:
代码自动补全:在日常编码中,天生式 AI 可以通过剖析高下文和学习代码模式,供应智能的代码自动补全建议,从而提高开拓效率。
阐明代码:天生式 AI 能够阐明代码,帮助开拓者理解特定代码片段的功能和实现办法,供应更深层次的代码理解支持。
天生代码:通过学习大量的代码库和模式,天生式 AI 可以天生符合需求的代码片段,加速开拓过程,尤其在重复性事情中发挥主要浸染。
代码审查:天生式 AI 能够进行代码审查,供应高质量的建媾和反馈,帮助开拓者改进代码质量、遵照最佳实践。
自然措辞查询:开拓者可以利用自然措辞查询与天生式 AI 进行交互,提出问题或要求,以获取干系代码片段、文档或阐明,使得开拓者更轻松地获取须要的信息。
其它。诸如于重构、提交信息天生、建模、提交总结等。
而在我们构建 AutoDev 时,也创造了诸如于创建 SQL DDL、天生需求、TDD 等场景。以是。我们供应了自定义场景的能力,以让开发者可以自定义自己的 AI 能力,详细见:https://ide.unitmesh.cc/customize。
场景驱动架构设计:平衡模型速率与能力在日常编码时,会存在几类不同场景,对付 AI 相应速率的哀求也是不同的(仅作为示例):
PS:这里的 32B 仅作为一个量级表示,由于在更大的模型下,效果会更好。
因此,我们将其总结为:一大一中一微三模型,供应全面 AI 赞助编码:
高质量大模型:32B~。用于代码重构、需求天生、自然措辞代码搜索与阐明等场景。
高相应速率中模型:6B~。用于代码补全、单元测试天生、文档天生、代码审查等场景。
向量化微模型:~100M。用于在 IDE 中进行向量化,诸如:代码相似度、代码干系度等。
重点场景先容:补全模式AI 代码补全能结合 IDE 工具剖析代码高下文和程序措辞的规则,由 AI 自动天生或建议代码片段。在类似于 GitHub Copilot 的代码补全工具中, 常日会分为三种细分模式:
行内补全(Inline)
类似于 FIM(fill in the middle)的模式,补全的内容在当前行中。诸如于: BlotPost blogpost = new
,补全为: BlogPost();
, 以实现: BlogPost blogpost = new BlogPost();
。
我们可以 Deepseek Coder 作为例子,看在这个场景下的效果:
<|fim▁begin|>def quick_sort(arr):
if len(arr) <= 1:
return arr
pivot = arr[0]
left = []
right = []
<|fim▁hole|>
if arr[i] < pivot:
left.append(arr[i])
else:
right.append(arr[i])
return quick_sort(left) + [pivot] + quick_sort(right)<|fim▁end|>
在这里,我们就须要结合光标前和光标后的代码。
块内补全(InBlock)
通过高下文学习(In-Context Learning)来实现,补全的内容在当前函数块中。诸如于,原始的代码是:
fun createBlog(blogDto: CreateBlogDto): BlogPost{
}
补全的代码为:
val blogPost = BlogPost(
title = blogDto.title,
content = blogDto.content,
author = blogDto.author
)
return blogRepository.save(blogPost)
块间补全(AfterBlock)
通过高下文学习(In-Context Learning)来实现,在当前函数块之后补全,如:在当前函数块之后补全一个新的函数。诸如于,原始的代码是:
fun createBlog(blogDto: CreateBlogDto): BlogPost{
//...
}
补全的代码为:
fun updateBlog(id: Long, blogDto: CreateBlogDto): BlogPost{
//...
}
fun deleteBlog(id: Long) {
//...
}
在我们构建对应的 AI 补全功能时,也须要考虑运用到对应的模式数据集,以提升补全的质量,供应更好的用户体验。
编写本文里的一些干系资源:
Why your AI Code Completion tool needs to Fill in the Middle
Exploring Custom LLM-Based Coding Assistance Functions
重点场景先容:代码阐明代码阐明旨在帮助开拓者更有效地管理和理解大型代码库。这些助手能够回答关于代码库的问题、 供应文档、搜索代码、识别缺点源头、减少代码重复等, 从而提高开拓效率、降落缺点率,并减轻开拓者的事情包袱。
在这个场景下,取决于我们预期的天生质量,常日会由一大一微或一中一微两个模型组成,更大的模型在天生的质量上结果更好。结合,我们在 Chocolate Factory 工具中的设计履历,常日这样的功能可以分为几步:
理解用户意图:借助大模型理解用户意图,将其转换为对应的 AI Agent 能力调用或者 function calling 。
转换意图搜索:借助模型将用户意图转换为对应的代码片段、文档或阐明,结合传统搜索、路径搜索和向量化搜索等技能,进行搜索及排序。
输出结果:交由大模型对末了的结果进行总结,输出给用户。
作为一个 RAG 运用,其分为 indexing 和 query 两个部分。
在 indexing 阶段,我们须要将代码库进行索引,并涉及到文本分割、向量化、数据库索引等技能。个中最有寻衅的一个内容是拆分,我们参考的折分规则是:https://docs.sweep.dev/blogs/chunking-2m-files 。即:
代码的均匀 Token 到字符比例约为1:5(300 个 Token),而嵌入模型的 Token 上限为 512 个。
1500 个字符大约对应于 40 行,大致相称于一个小到中等大小的函数或类。
寻衅在于尽可能靠近 1500 个字符,同时确保分块在语义上相似且干系高下文连接在一起。
在不同的场景下,我们也可以通过不同的办法进行折分,如在 Chocolate Factory 是通过 AST 进行折分,以担保天生高下文的质量。
在 querying 阶段,须要结合我们一些传统的搜索技能,如:向量化搜索、路径搜索等,以担保搜索的质量。同时,在中文场景下,我们也须要考虑到转换为中文 的问题,如:将英文转换为中文,以担保搜索的质量。
干系工具:https://github.com/BloopAI/bloop
干系资源:
Prompt 策略:代码库 AI 助手的语义化搜索设计
其它:日常赞助对付日常赞助来说,我们也可以通过天生式 AI 来实现,如:自动创建 SQL DDL、自动创建测试用例、自动创建需求等。这些只须要通过自定义提示词, 结合特定的领域知识,便可以实现,这里不再赘述。
架构设计:高下文工程除了模型之外,高下文也是影响 AI 赞助能力的主要成分。在我们构建 AutoDev 时,我们也创造了两种不同的高下文模式:
干系高下文:基于静态代码剖析的高下文天生,可以构建更好质量的高下文,以天生更高质量的代码和测试等,依赖于 IDE 的静态代码剖析能力。
相似高下文:基于相似式搜索的高下文,可以构建更多的高下文,以天生更多的代码和测试等,与平台能力无关。
大略比拟如下:
在支持 IDE 有限时,干系高下文的才会带来更高的性价高。
相似高下文架构:GitHub Copilot 案例GitHub Copilot 采取了相似高下文的架构模式,其精略的架构分层如下:
监听用户操作(IDE API )。监听用户的 Run Action、快捷键、UI 操作、输入等,以及最近的文档操作历史(20 个文件)。
IDE 胶水层(Plugin)。作为 IDE 与底层 Agent 的胶水层,处理输入和输出。
高下文构建(Agent)。JSON RPC Server,处理 IDE 的各种变革,对源码进行剖析,封装为 “prompt” (疑似) 并发送给做事器。
做事端(Server)。处理 prompt 要求,并交给 LLM 做事端处理。
在 “公开” 的 Copilot-Explorer 项目的研究资料里,可以看到 Prompt 是如何构建出来的。如下是发送到的 prompt 要求:
{
\"大众prefix\"大众: \公众# Path: codeviz\\app.py\n#....\"大众,
\公众suffix\"大众: \"大众if __name__ == '__main__':\r\n app.run(debug=True)\"大众,
\"大众isFimEnabled\"大众: true,
\公众promptElementRanges\"大众: [
{
\"大众kind\"大众: \"大众PathMarker\公众,
\"大众start\"大众: 0,
\公众end\"大众: 23
},
{
\公众kind\公众: \"大众SimilarFile\公众,
\公众start\公众: 23,
\"大众end\"大众: 2219
},
{
\"大众kind\公众: \"大众BeforeCursor\"大众,
\"大众start\"大众: 2219,
\"大众end\"大众: 3142
}
]
}
个中:
用于构建 prompt 的 prefix
部分,是由 promptElements 构建了,个中包含了: BeforeCursor
, AfterCursor
, SimilarFile
, ImportedFile
, LanguageMarker
, PathMarker
, RetrievalSnippet
等类型。从几种 PromptElementKind
的名称,我们也可以看出其真正的含义。
用于构建 prompt 的 suffix
部分,则是由光标所在的部分决定的,根据 tokens 的上限(2048 )去打算还有多少位置放下。而这里的 Token 打算则是真正的 LLM 的 token 打算,在 Copilot 里是通过 Cushman002 打算的,诸如于中文的字符的 token 长度是不一样的,如: { context: \公众console.log('你好,天下')\公众, lineCount: 1, tokenLength: 30 }
,个中 context 中的内容的 length 为 20,但是 tokenLength 是 30,中笔墨符共 5 个(包含 ,
)的长度,单个字符占的 token 便是 3。
如下是一个更详细的 Java 运用的高下文示例:
// Path: src/main/cc/unitmesh/demo/infrastructure/repositories/ProductRepository.java
// Compare this snippet from src/main/cc/unitmesh/demo/domain/product/Product.java:
// ....
// Compare this snippet from src/main/cc/unitmesh/demo/application/ProductService.java:
// ...
// @Component
// public class ProductService {
// //...
// }
//
package cc.unitmesh.demo.repositories;
// ...
@Component
publicclassProductRepository{
//...
在打算高下文里,GitHub Copilot 采取的是 Jaccard 系数 (Jaccard Similarity) ,这部分的实现是在 Agent 实现,更详细的逻辑可以参考: 花了大半个月,我终于逆向剖析了Github Copilot。
干系资源:
高下文工程:基于 Github Copilot 的实时能力剖析与思考
干系高下文架构:AutoDev 与 JetBrains AI Assistant 案例如上所述,干系代码依赖于静态代码剖析,紧张借助于代码的构造信息,如:AST、CFG、DDG 等。在不同的场景和平台之下,我们可以结合不同的静态代码剖析工具, 如下是常见的一些静态代码剖析工具:
TreeSitter,由 GitHub 开拓的用于天生高效的自定义语法剖析器的框架。
Intellij PSI (Program Structure Interface),由 JetBrains 开拓的用于其 IDE 的静态代码剖析接口。
LSP(Language Server Protocol),由微软开拓的用于 IDE 的通用措辞做事器协议。
Chapi (common hierarchical abstract parser implementation) ,由笔者(@phodal)开拓的用于通用的静态代码剖析工具。
在补全场景下,通过静态代码剖析,我们可以得到当前的高下文,如:当前的函数、当前的类、当前的文件等。如下是一个 AutoDev 的天生单元测试的高下文示例:
// here are related classes:
// 'filePath: /Users/phodal/IdeaProjects/untitled/src/main/java/cc/unitmesh/untitled/demo/service/BlogService.java
// class BlogService {
// blogRepository
// + public BlogPost createBlog(BlogPost blogDto)
// + public BlogPost getBlogById(Long id)
// + public BlogPost updateBlog(Long id, BlogPost blogDto)
// + public void deleteBlog(Long id)
// }
// 'filePath: /Users/phodal/IdeaProjects/untitled/src/main/java/cc/unitmesh/untitled/demo/dto/CreateBlogRequest.java
// class CreateBlogRequest ...
// 'filePath: /Users/phodal/IdeaProjects/untitled/src/main/java/cc/unitmesh/untitled/demo/entity/BlogPost.java
// class BlogPost {...
@ApiOperation(value = \公众Create a new blog\公众)
@PostMapping(\"大众/\"大众)
publicBlogPost createBlog(@RequestBodyCreateBlogRequest request) {
在这个示例中,会剖析 createBlog
函数的高下文,获取函数的输入和输出类: CreateBlogRequest
、 BlogPost
信息,以及 BlogService 类信息,作为高下文(在注释中供应)供应给模型。在这时,模型会天生更准确的布局函数,以及更准确的测试用例。
由于干系高下文依赖于对不同措辞的静态代码剖析、不同 IDE 的 API,以是,我们也须要针对不同的措辞、不同的 IDE 进行适配。在构建本钱上,相对付相似高下文本钱更高。
步骤 1:构建 IDE 插件与度量体系设计IDE、编辑器作为开拓者的紧张工具,其设计和学习本钱也相比拟较高。首先,我们可以用官方供应的模板天生:
IDEA 插件模板
VSCode 插件天生
然后,再往上添加功能(是不是很大略),当然不是。以下是一些可以参考的 IDEA 插件资源:
Intellij Community 版本源码
IntelliJ SDK Docs Code Samples
Intellij Rust
当然了,更得当的是参考AutoDev 插件。
JetBrains 插件可以直策应用官方的模板来天生对应的插件:https://github.com/JetBrains/intellij-platform-plugin-template
对付 IDEA 插件实现来说,紧张是通过 Action 和 Listener 来实现的,只须要在 plugin.xml
中注册即可。详细可以参考官方文档:IntelliJ Platform Plugin SDK
由于我们前期未 AutoDev 考虑到对 IDE 版本的兼容问题,后期为了兼容旧版本的 IDE,我们须要对插件进行兼容性处理。以是,如官方文档:Build Number Ranges 中所描述,我们可以看到不同版本,对付 JDK 的哀求是不一样的,如下是不同版本的哀求:
并配置到 gradle.properties
中:
pluginSinceBuild = 223
pluginUntilBuild = 233.
后续配置兼容性比较麻烦,可以参考 AutoDev 的设计。
补全模式:Inlay在自动代码补全上,海内的厂商紧张参考的是 GitHub Copilot 的实现,逻辑也不繁芜。
采取快捷键办法触发
其紧张是在 Action 里监听用户的输入,然后:
采取自动触发办法
其紧张通过 EditorFactoryListener
监听用户的输入,然后:根据不同的输入,触发不同的补全结果。核心代码如下:
classAutoDevEditorListener: EditorFactoryListener{
override fun editorCreated(event: EditorFactoryEvent) {
//...
editor.document.addDocumentListener(AutoDevDocumentListener(editor), editorDisposable)
editor.caretModel.addCaretListener(AutoDevCaretListener(editor), editorDisposable)
//...
}
classAutoDevCaretListener(val editor: Editor) : CaretListener{
override fun caretPositionChanged(event: CaretEvent) {
//...
val wasTypeOver = TypeOverHandler.getPendingTypeOverAndReset(editor)
//...
llmInlayManager.disposeInlays(editor, InlayDisposeContext.CaretChange)
}
}
classAutoDevDocumentListener(val editor: Editor) : BulkAwareDocumentListener{
override fun documentChangedNonBulk(event: DocumentEvent) {
//...
val llmInlayManager = LLMInlayManager.getInstance()
llmInlayManager
.editorModified(editor, changeOffset)
}
}
}
再根据不同的输入,触发不同的补全结果,并对构造进行处理。
渲染补全代码
随后,我们须要实现一个 Inlay Render,它继续自 EditorCustomElementRenderer
。
结合 IDE 的接谈锋能,我们须要添加对应的 Action,以及对应的 Group,以及对应的 Icon。如下是一个 Action 的示例:
<add-to-group group-id=\"大众ShowIntentionsGroup\公众 relative-to-action=\公众ShowIntentionActions\"大众 anchor=\"大众after\"大众/>
如下是 AutoDev 的一些 ActionGroup:
在编写 ShowIntentionsGroup 时,我们可以参考 AutoDev 的实现来构建对应的 Group:
<groupid=\"大众AutoDevIntentionsActionGroup\"大众class=\"大众cc.unitmesh.devti.intentions.IntentionsActionGroup\"大众
icon=\公众cc.unitmesh.devti.AutoDevIcons.AI_COPILOT\公众searchable=\"大众false\"大众>
<add-to-groupgroup-id=\"大众ShowIntentionsGroup\公众relative-to-action=\"大众ShowIntentionActions\公众anchor=\公众after\"大众/>
</group>
由于 Intellij 的平台策略,使得运行于 Java IDE(Intellij IDEA)与在其它 IDE 如 Python IDE(Pycharm)之间的差异性变得更大。我们须要供应基于多平台产品的兼容性,详细先容可以参考:Plugin Compatibility with IntelliJ Platform Products
首先,将插件的架构进一步模块化,即针对付不同的措辞,供应不同的模块。如下是 AutoDev 的模块化架构:
java/ # Java 措辞插件
src/main/java/cc/unitmesh/autodev/ # Java 措辞入口
src/main/resources/META-INF/plugin.xml
plugin/ # 多平台入口
src/main/resources/META-INF/plugin.xml
src/ # 即核心模块
main/resource/META-INF/core.plugin.xml
在 plugin/plugin.xml
中,我们须要添加对应的 depends
,以及 extensions
,如下是一个示例:
<idea-pluginpackage=\"大众cc.unitmesh\"大众xmlns:xi=\"大众http://www.w3.org/2001/XInclude\公众allow-bundled-update=\"大众true\公众>
<xi:includehref=\公众/META-INF/core.xml\"大众xpointer=\"大众xpointer(/idea-plugin/)\"大众/>
<content>
<modulename=\公众cc.unitmesh.java\"大众/>
<!-- 其它模块 -->
</content>
</idea-plugin>
而在 java/plugin.xml
中,我们须要添加对应的 depends
,以及 extensions
,如下是一个示例:
<idea-pluginpackage=\公众cc.unitmesh.java\"大众>
<!--suppress PluginXmlValidity -->
<dependencies>
<pluginid=\"大众com.intellij.modules.java\"大众/>
<pluginid=\"大众org.jetbrains.plugins.gradle\"大众/>
</dependencies>
</idea-plugin>
随后,Intellij 会自动加载对应的模块,以实现多措辞的支持。根据我们预期支持的不同措辞,便须要对应的 plugin.xml
,诸如于:
cc.unitmesh.javascript.xml
cc.unitmesh.rust.xml
cc.unitmesh.python.xml
cc.unitmesh.kotlin.xml
cc.unitmesh.java.xml
cc.unitmesh.go.xml
cc.unitmesh.cpp.xml
末了,在不同的措辞模块里,实现对应的功能即可。
高下文构建为了简化这个过程,我们利用 Unit Eval 来展示如何构建两种类似的高下文。
静态代码剖析通过静态代码剖析,我们可以得到当前的函数、当前的类、当前的文件等。再结合路径相似性,探求最贴进的高下文。
private fun findRelatedCode(container: CodeContainer): List<CodeDataStruct> {
// 1. collects all similar data structure by imports if exists in a file tree
val byImports = container.Imports
.mapNot {
context.fileTree[it.Source]?.container?.DataStructures
}
.flatten()
// 2. collects by inheritance tree for some node in the same package
val byInheritance = container.DataStructures
.map {
(it.Implements+ it.Extend).mapNot { i ->
context.fileTree[i]?.container?.DataStructures
}.flatten()
}
.flatten()
val related = (byImports + byInheritance).distinctBy { it.NodeName}
// 3. convert all similar data structure to uml
return related
}
classRelatedCodeStrategyBuilder(private val context: JobContext) : CodeStrategyBuilder{
override fun build(): List<TypedIns> {
// ...
val findRelatedCodeDs = findRelatedCode(container)
val relatedCodePath = findRelatedCodeDs.map { it.FilePath}
val jaccardSimilarity = SimilarChunker.pathLevelJaccardSimilarity(relatedCodePath, currentPath)
val relatedCode = jaccardSimilarity.mapIndexed { index, d ->
findRelatedCodeDs[index] to d
}.sortedByDescending {
it.second
}.take(3).map {
it.first
}
//...
}
}
上述的代码,我们可以通过代码的 Imports 信息作为干系代码的一部分。再通过代码的继续关系,来探求干系的代码。末了,通过再路径相似性,来探求最贴近的高下文。
干系代码剖析先探求,再通过代码相似性,来探求干系的代码。核心逻辑所示:
fun pathLevelJaccardSimilarity(chunks: List<String>, text: String): List<Double> {
//...
}
fun tokenize(chunk: String): List<String> {
return chunk.split(Regex(\"大众[^a-zA-Z0-9]\公众)).filter { it.isNotBlank() }
}
fun similarityScore(set1: Set<String>, set2: Set<String>): Double{
//...
}
详细见:SimilarChunker
VSCode 插件TODO
TreeSitterTreeSitter 是一个用于天生高效的自定义语法剖析器的框架,由 GitHub 开拓。它利用 LR(1)解析器,这意味着它可以在 O(n)韶光内解析任何措辞,而不是 O(n²)韶光。它还利用了一种称为“语法树的重用”的技能,该技能使其能够在不重新解析全体文件的情形下更新语法树。
由于 TreeSitter 已经供应了多措辞的支持,你可以利用 Node.js、Rust 等措辞来构建对应的插件。详细见:TreeSitter。
根据我们的意图不同,利用 TreeSitter 也有不同的办法:
解析 Symbol
在代码自然措辞搜索引擎 Bloop 中,我们利用 TreeSitter 来解析 Symbol,以实现更好的搜索质量。
;; methods
(method_declaration
name: (identifier) @hoist.definition.method)
随后,根据不同的类型来决定如何显示:
pub static JAVA: TSLanguageConfig= TSLanguageConfig{
language_ids: &[\公众Java\"大众],
file_extensions: &[\"大众java\公众],
grammar: tree_sitter_java::language,
scope_query: MemoizedQuery::new(include_str!(\"大众./scopes.scm\"大众)),
hoverable_query: MemoizedQuery::new(
r#\"大众
[(identifier)
(type_identifier)] @hoverable
\"大众#,
),
namespaces: &[&[
// variables
\公众local\"大众,
// functions
\公众method\"大众,
// namespacing, modules
\"大众package\公众,
\"大众module\"大众,
// types
\"大众class\公众,
\"大众enum\"大众,
\"大众enumConstant\"大众,
\"大众record\公众,
\"大众interface\"大众,
\"大众typedef\公众,
// misc.
\公众label\"大众,
]],
};
Chunk 代码
如下是 Improving LlamaIndex’s Code Chunker by Cleaning Tree-Sitter CSTs 中的 TreeSitter 的利用办法:
from tree_sitter importTree
def chunker(
tree: Tree,
source_code: bytes,
MAX_CHARS=512 3,
coalesce=50# Any chunk less than 50 characters long gets coalesced with the next chunk
) -> list[Span]:
# 1. Recursively form chunks based on the last post (https://docs.sweep.dev/blogs/chunking-2m-files)
def chunk_node(node: Node) -> list[Span]:
chunks: list[Span] = []
current_chunk: Span= Span(node.start_byte, node.start_byte)
node_children = node.children
for child in node_children:
if child.end_byte - child.start_byte > MAX_CHARS:
chunks.append(current_chunk)
current_chunk = Span(child.end_byte, child.end_byte)
chunks.extend(chunk_node(child))
elif child.end_byte - child.start_byte + len(current_chunk) > MAX_CHARS:
chunks.append(current_chunk)
current_chunk = Span(child.start_byte, child.end_byte)
else:
current_chunk += Span(child.start_byte, child.end_byte)
chunks.append(current_chunk)
return chunks
chunks = chunk_node(tree.root_node)
# 2. Filling in the gaps
for prev, curr in zip(chunks[:-1], chunks[1:]):
prev.end = curr.start
curr.start = tree.root_node.end_byte
# 3. Combining small chunks with bigger ones
new_chunks = []
current_chunk = Span(0, 0)
for chunk in chunks:
current_chunk += chunk
if non_whitespace_len(current_chunk.extract(source_code)) > coalesce \
and\"大众\n\"大众in current_chunk.extract(source_code):
new_chunks.append(current_chunk)
current_chunk = Span(chunk.end, chunk.end)
if len(current_chunk) > 0:
new_chunks.append(current_chunk)
# 4. Changing line numbers
line_chunks = [Span(get_line_number(chunk.start, source_code),
get_line_number(chunk.end, source_code)) for chunk in new_chunks]
# 5. Eliminating empty chunks
line_chunks = [chunk for chunk in line_chunks if len(chunk) > 0]
度量体系设计常用指标
return line_chunks
代码接管率
AI 天生的代码被开拓者接管的比例。
入库率
AI 天生的代码被开拓者入库的比例。
开拓者体验驱动如微软和 GitHub 所构建的:DevEx: What Actually Drives Productivity: The developer-centric approach to measuring and improving productivity
步骤 2:模型评估体系与微调试验
评估数据集:HumanEval
模型选择与测试在结合公开 API 的大措辞模型之后,我们就可以构建基本的 IDE 功能。随后,该当进一步探索适宜于内部的模型,以适宜于组织内部的效果。
模型选择现有的开源模型里采取 LLaMA 架构相比拟较多,并且由于其模型的质量比较高,其生态也相比拟较完善。因此,我们也采取 LLaMA 架构来构建,即:DeepSeek Coder。
OpenBayes 平台支配与测试随后,我们须要支配模型,并供应一个对应的 API,这个 API 须要与我们的 IDE 接口保持同等。这里我们采取了 OpenBayes 平台来支配模型。详细见:
code/server
目录下的干系代码。
pip install -r requirements.txt
python server-python38.py
如下是适用于 OpenBayes 的代码,以在后台供应公网 API:
if __name__ == \"大众__main__\"大众:
try:
meta = requests.get('http://localhost:21999/gear-status', timeout=5).json()
url = meta['links'].get('auxiliary')
if url:
print(\"大众打开该链接访问:\"大众, url)
exceptException:
pass
uvicorn.run(app, host=\"大众0.0.0.0\"大众, port=8080)
随后,在 IDE 插件中,我们就可以结合他们来测试功能。
大规模模型支配结合模型量化技能,如 INT4,可以实现 6B 模型在消费级的显卡上进行本地支配。
(TODO)
模型微调有监督微调(SFT)是指采取预先演习好的神经网络模型,并针对你自己的专门任务在少量的监督数据上对其进行重新演习的技能。
数据驱动的微调方法结合 【SFT最佳实践 】中供应的权衡考虑:
样本数量少于 1000 且需看重基座模型的通用能力:优先考虑 LoRA。
如果特界说务数据样本较多且紧张看重这些任务效果:利用 SFT。
如果希望结合两者上风:将特界说务的数据与通用任务数据进行稠浊配比后,再利用这些演习方法能得到更好的效果。
这就意味着:
任务类型 样本数量 通用编码数据集 IDE AI 功能支持少于 1000须要内部代码补整年夜于 10,000不须要IDE + 代码补整年夜于 10,000须要常日来说,我们测试是结合 IDE 的功能,以及代码补全的功能,因此,我们须要合并两个数据集。
数据集构建根据不同的模型,其所须要的指令也是不同的。如下是一个基于 DeepSeek + DeepSpeed 的数据集示例:
{
\公众instruction\"大众: \"大众Write unit test for following code.\n<SomeCode>\"大众,
\"大众output\"大众: \公众<TestCode>\公众
}
下面是 LLaMA 模型的数据集示例:
{
\"大众instruction\公众: \"大众Write unit test for following code.\"大众,
\公众input\"大众: \"大众<SomeCode>\"大众,
\"大众output\"大众: \"大众<TestCode>\"大众
数据集构建
}
我们构建 Unit Eval 项目,以天生更适宜于 AutoDev 的数据集。
代码补全。行内(Inline)、块内(InBlock)、块间(AfterBlock)三种场景。
单元测试天生。天生符合高下文的单元测试。
而为了供应 IDE 中的其他功能支持,我们结合了开源数据集,以及数据蒸馏的办法来构建数据集。
开源数据集在 GitHub、HuggingFace 等平台上,有一些开源的数据集。
Magicoder: Source Code Is All You Need 中开源的两个数据集:
https://huggingface.co/datasets/ise-uiuc/Magicoder-OSS-Instruct-75K
https://huggingface.co/datasets/ise-uiuc/Magicoder-Evol-Instruct-110K
在 License 得当的情形下,我们可以直策应用这些数据集;在不得当的情形下,我们可以拿来做一些实验。
数据蒸馏数据蒸馏。过去的定义是,即将大型真实数据集(演习集)作为输入,并输出一个小的合成蒸馏数据集。但是,我们要做的是直接用 OpenAI 这一类公开 API 的模型:
天生符合预期的数据集。
对数据集进行筛选,以担保数据集的质量。
对数据集进行扩充,以担保数据集的多样性。
对数据集进行标注,以担保数据集的可用性。
微调示例:OpenBayes + DeepSeek在这里我们利用的是,以及 DeepSeek 官方供应的脚本来进行微调。
云 GPU: OpenBayes
GPU 算力:4090x2 (目测和微调参数有关,但是我试了几次 4090 还是弗成)
微调脚本:https://github.com/deepseek-ai/DeepSeek-Coder
数据集:6000
我在 OpenBayes 上传了的 DeepSeek 模型:OpenBayes deepseek-coder-6.7b-instruct,你可以在创建时直策应用这个模型。
数据集信息
由 Unit Eval + OSS Instruct 数据集构建而来:
150 条补全(Inline,InBlock,AfterBlock)数据集。
150 条单元测试数据集。
3700 条 OSS Instruct 数据集。
而从结果来看,如何保持高质量的数据是最大的寻衅。
测试视频:开源 AI 赞助编程方案:Unit Mesh 端到端打通 v0.0.1 版本
参数示例:
DATA_PATH=\公众/openbayes/home/summary.jsonl\公众
OUTPUT_PATH=\"大众/openbayes/home/output\"大众
MODEL_PATH=\公众/openbayes/input/input0/\公众
!cd DeepSeek-Coder/finetune && deepspeed finetune_deepseekcoder.py \
--model_name_or_path $MODEL_PATH \
--data_path $DATA_PATH \
--output_dir $OUTPUT_PATH \
--num_train_epochs 1 \
--model_max_length 1024 \
--per_device_train_batch_size 8 \
--per_device_eval_batch_size 1 \
--gradient_accumulation_steps 1 \
--evaluation_strategy \"大众no\"大众 \
--save_strategy \公众steps\"大众 \
--save_steps 375 \
--save_total_limit 10 \
--learning_rate 1e-4 \
--warmup_steps 10 \
--logging_steps 1 \
--lr_scheduler_type \"大众cosine\"大众 \
--gradient_checkpointing True \
--report_to \"大众tensorboard\公众 \
--deepspeed configs/ds_config_zero3.json \
--bf16 True
运行日志:
`use_cache=True` is incompatible with gradient checkpointing. Setting`use_cache=False`...
0%| | 0/375[00:00<?, ?it/s]`use_cache=True` is incompatible with gradient checkpointing. Setting`use_cache=False`...
{'loss': 0.6934, 'learning_rate': 0.0, 'epoch': 0.0}
{'loss': 0.3086, 'learning_rate': 3.0102999566398115e-05, 'epoch': 0.01}
{'loss': 0.3693, 'learning_rate': 4.771212547196624e-05, 'epoch': 0.01}
{'loss': 0.3374, 'learning_rate': 6.020599913279623e-05, 'epoch': 0.01}
{'loss': 0.4744, 'learning_rate': 6.989700043360187e-05, 'epoch': 0.01}
{'loss': 0.3465, 'learning_rate': 7.781512503836436e-05, 'epoch': 0.02}
{'loss': 0.4258, 'learning_rate': 8.450980400142567e-05, 'epoch': 0.02}
{'loss': 0.4027, 'learning_rate': 9.030899869919434e-05, 'epoch': 0.02}
{'loss': 0.2844, 'learning_rate': 9.542425094393248e-05, 'epoch': 0.02}
{'loss': 0.3783, 'learning_rate': 9.999999999999999e-05, 'epoch': 0.03}
其它:
详细的 Notebook 见:code/finetune/finetune.ipynb
微调参数,详细见:Trainer
步骤 3:环绕意图的数据工程与模型演进Unit Tools Workflow Unit Eval 是一个针对付构建高质量代码微调的开源工具箱。其三个核心设计原则:
统一提示词(Prompt)。统一工具-微调-评估底层的提示词。
代码质量管道。诸如于代码繁芜性、代码坏味道、测试坏味道、API 设计味道等。
可扩展的质量阈。自定义规则、自定义阈值、自定义质量类型等。
即要办理易于测试的数据集天生,以及易于评估的模型评估问题。
IDE 指令设计与蜕变AutoDev 早期采取的是 OpenAI API,其模型能力较强,因此在指令设计上比较强大。而当我们须要微调里,我们须要更大略、易于区分的指令来构建。
模板指令如下是在 AutoDev 中精简化后的 Prompt 示例:
Write unit test for following code.
${context.testFramework}
${context.coreFramework}
${context.testSpec}
```${context.language}
${context.related_model}
${context.selection}
```
个中包含了:
技能栈高下文
测试技能栈高下文
代码块(类、函数)的输入和输出信息
而这个模板指令,也是我们在 Unit Eval 中所采取的指令。
统一指令模板为了实现统一的指令模板,我们引入了 Apache Velocity 模板引擎来实现,并通过 Chocolate Factory 实现底层的通用逻辑:
工具侧。在 IDE 插件中,直接通过 Velocity 模板引擎、基于 Chocolate Factory 来实现指令的天生。
数据集成。在 Unit Eval 中,天生适用于模板的数据集。
结果评估。基于 Chocolate Factory 的实现,对模板的结果进行评估。
高质量数据集天生年初(2023 年 4 月),我们做了一系列的代码微调探索, 在那篇 《AI 研发提效的精确姿势:开源 LLM + LoRA 》里,企业该当开始着力于:
规范与流程标准化
工程化的数据准备
高质量的脱敏数据
只有微调是不足的,模型须要与工具紧密相结合。
质量流水线设计示例Code Quality Workflow 基于 Thoughtworks 在软件工程的丰富履历,以及 Thoughtworks 的架构管理开源工具 ArchGuard 作为根本举动步伐。在 UnitEval 中,我们也将代码质量的筛选构建成 pipeline 的办法:
代码繁芜度。在当前的版本设计里,可以直接通过代码繁芜度来决定是否放代码文件进入数据库。
不同的坏味道检讨类型。诸如于代码坏味道、测试坏味道等。
特定的规则检讨。Controller 的 API 设计、Repository 的 SQL 设计 等。
而基于 ArchGuard 中所供应的丰富代码质量和架构质量剖析能力,诸如 OpenAPI、 SCA(软件依赖剖析)能力,我们也在思考未来是否也加入干系的设计。
实现高质量数据集天生如下是 Unit Eval 0.3.0 的紧张代码逻辑:
val codeDir = GitUtil
.checkoutCode(config.url, config.branch, tempGitDir, config.gitDepth)
.toFile().canonicalFile
logger.info(\"大众start walk $codeDir\"大众)
val languageWorker = LanguageWorker()
val workerManager = WorkerManager(
WorkerContext(
config.codeContextStrategies,
config.codeQualityTypes,
config.insOutputConfig,
pureDataFileName = config.pureDataFileName(),
config.completionTypes,
config.maxCompletionEachFile,
config.completionTypeSize,
qualityThreshold = InsQualityThreshold(
complexity = InsQualityThreshold.MAX_COMPLEXITY,
fileSize = InsQualityThreshold.MAX_FILE_SIZE,
maxLineInCode = config.maxLineInCode,
maxCharInCode = config.maxCharInCode,
maxTokenLength = config.maxTokenLength,
)
)
)
workerManager.init(codeDir, config.language)
随后是根据不同的质量门禁,来进行不同的质量检讨:
fun filterByThreshold(job: InstructionFileJob) {
val summary = job.fileSummary
if(!supportedExtensions.contains(summary.extension)) {
return
}
// limit by complexity
if(summary.complexity > context.qualityThreshold.complexity) {
logger.info(\"大众skip file ${summary.location} for complexity ${summary.complexity}\"大众)
return
}
// like js minified file
if(summary.binary || summary.generated || summary.minified) {
return
}
// if the file size is too large, we just try 64k
if(summary.bytes > context.qualityThreshold.fileSize) {
logger.info(\"大众skip file ${summary.location} for size ${summary.bytes}\"大众)
return
}
// limit by token length
val encoded = enc.encode(job.code)
val length = encoded.size
if(length > context.qualityThreshold.maxTokenLength) {
logger.info(\"大众skip file ${summary.location} for over ${context.qualityThreshold.maxTokenLength} tokens\"大众)
println(\公众| filename: ${summary.filename} | tokens: $length | complexity: ${summary.complexity} | code: ${summary.lines} | size: ${summary.bytes} | location: ${summary.location} |\"大众)
return
}
val language = SupportedLang.from(summary.language)
val worker = workers[language] ?: return
worker.addJob(job)
}
在过虑之后,我们就可以由不同措辞的 Worker 来进行处理,诸如 JavaWorker、PythonWorker 等。
val lists = jobs.map { job ->
val jobContext = JobContext(
job,
context.qualityTypes,
fileTree,
context.insOutputConfig,
context.completionTypes,
context.maxCompletionInOneFile,
project = ProjectContext(
compositionDependency = context.compositionDependency,
),
context.qualityThreshold
)
context.codeContextStrategies.map { type ->
val codeStrategyBuilder = type.builder(jobContext)
codeStrategyBuilder.build()
}.flatten()
}.flatten()
根据用户选择的高下文策略,我们就可以构建出不同的高下文,如:干系高下文、相似高下文等
在高下文策略中检讨代码质量SimilarChunksStrategyBuilder 紧张逻辑如下
利用配置中指定的规则检讨以识别存在问题的数据构造。
网络所有具有相似数据构造的数据构造。
为每个被识别的数据构造中的函数构建完成天生器。
过滤掉具有空的前置和后置光标的完成天生器。
利用JavaSimilarChunker打算块补全的相似块。
为每个完成天生器创建SimilarChunkIns工具,包括措辞、前置光标、相似块、后置光标、输出和类型的干系信息。
返复天生的SimilarChunkIns工具的列表。
在规则检讨里,我们可以通过不同的规则来检讨不同的代码质量问题,如:代码坏味道、测试坏味道、API 设计味道等。
fun create(types: List<CodeQualityType>, thresholds: Map<String, Int> = mapOf()): List<QualityAnalyser> {
return types.map { type ->
when(type) {
CodeQualityType.BadSmell-> BadsmellAnalyser(thresholds)
CodeQualityType.TestBadSmell-> TestBadsmellAnalyser(thresholds)
CodeQualityType.JavaController-> JavaControllerAnalyser(thresholds)
CodeQualityType.JavaRepository-> JavaRepositoryAnalyser(thresholds)
CodeQualityType.JavaService-> JavaServiceAnalyser(thresholds)
}
}
其它
}
我们会在 GitHub 上持续更新这个教程: https://github.com/phodal/build-ai-coding-assistant,欢迎在 GitHub 上谈论。