我们会在 GitHub 上持续更新这个教程: https://github.com/phodal/build-ai-coding-assistant,欢迎在 GitHub 上讨论。
2023 年,生成式 AI 的火爆,让越来越多的组织开始引入 AI 辅助编码。与在 2021 年发布的 GitHub Copilot 稍有差异的是,代码补全只是重多场景中的一个。大量的企业内部在探索结合需求生成完整代码、代码审查等场景,也引入生成式 AI,来提升开发效率。
在这个背景下,我们(Thoughtworks)也开发了一系列的开源工具,以帮助更多的组织构建自己的 AI 辅助编码助手:
由于,我们设计 AutoDev 时,各类开源模型也在不断演进。在这个背景下,它的步骤是:
也因此,这个教程也是围绕于这三个步骤展开的。除此,基于我们的经验,本教程的示例技术栈:
由于,我们在 AI 方面的经验相对比较有限,难免会有一些错误,所以,我们也希望能够与更多的开发者一起,来构建这个开源项目。
结合 JetBrains 2023《开发者生态系统》报告的人工智能部分 ,我们可以总结出一些通用的场景,这些场景反映了在开发过程中生成式 AI 可以发挥作用的领域。以下是一些主要的场景:
而在我们构建 AutoDev 时,也发现了诸如于创建 SQL DDL、生成需求、TDD 等场景。所以。我们提供了自定义场景的能力,以让开发者可以自定义自己的 AI 能力,详细见:https://ide.unitmesh.cc/customize。
场景驱动架构设计:平衡模型速度与能力
在日常编码时,会存在几类不同场景,对于 AI 响应速度的要求也是不同的(仅作为示例):
PS:这里的 32B 仅作为一个量级表示,因为在更大的模型下,效果会更好。
因此,我们将其总结为:一大一中一微三模型,提供全面 AI 辅助编码:
重点场景介绍:补全模式
AI 代码补全能结合 IDE 工具分析代码上下文和程序语言的规则,由 AI 自动生成或建议代码片段。在类似于 GitHub Copilot 的代码补全工具中, 通常会分为三种细分模式:
行内补全(Inline)
类似于 FIM(fill in the middle)的模式,补全的内容在当前行中。诸如于: ,补全为: , 以实现: 。
我们可以 Deepseek Coder 作为例子,看在这个场景下的效果:
在这里,我们就需要结合光标前和光标后的代码。
块内补全(InBlock)
通过上下文学习(In-Context Learning)来实现,补全的内容在当前函数块中。诸如于,原始的代码是:
补全的代码为:
块间补全(AfterBlock)
通过上下文学习(In-Context Learning)来实现,在当前函数块之后补全,如:在当前函数块之后补全一个新的函数。诸如于,原始的代码是:
补全的代码为:
在我们构建对应的 AI 补全功能时,也需要考虑应用到对应的模式数据集,以提升补全的质量,提供更好的用户体验。
编写本文里的一些相关资源:
重点场景介绍:代码解释
代码解释旨在帮助开发者更有效地管理和理解大型代码库。这些助手能够回答关于代码库的问题、 提供文档、搜索代码、识别错误源头、减少代码重复等, 从而提高开发效率、降低错误率,并减轻开发者的工作负担。
在这个场景下,取决于我们预期的生成质量,通常会由一大一微或一中一微两个模型组成,更大的模型在生成的质量上结果更好。结合,我们在 Chocolate Factory 工具中的设计经验,通常这样的功能可以分为几步:
作为一个 RAG 应用,其分为 indexing 和 query 两个部分。
在 indexing 阶段,我们需要将代码库进行索引,并涉及到文本分割、向量化、数据库索引等技术。其中最有挑战的一个内容是拆分,我们参考的折分规则是:https://docs.sweep.dev/blogs/chunking-2m-files 。即:
在不同的场景下,我们也可以通过不同的方式进行折分,如在 Chocolate Factory 是通过 AST 进行折分,以保证生成上下文的质量。
在 querying 阶段,需要结合我们一些传统的搜索技术,如:向量化搜索、路径搜索等,以保证搜索的质量。同时,在中文场景下,我们也需要考虑到转换为中文 的问题,如:将英文转换为中文,以保证搜索的质量。
其它:日常辅助
对于日常辅助来说,我们也可以通过生成式 AI 来实现,如:自动创建 SQL DDL、自动创建测试用例、自动创建需求等。这些只需要通过自定义提示词, 结合特定的领域知识,便可以实现,这里不再赘述。
除了模型之外,上下文也是影响 AI 辅助能力的重要因素。在我们构建 AutoDev 时,我们也发现了两种不同的上下文模式:
简单对比如下:
在支持 IDE 有限时,相关上下文的才会带来更高的性价高。
相似上下文架构:GitHub Copilot 案例
GitHub Copilot 采用了相似上下文的架构模式,其精略的架构分层如下:
在 “公开” 的 Copilot-Explorer 项目的研究资料里,可以看到 Prompt 是如何构建出来的。如下是发送到的 prompt 请求:
其中:
如下是一个更详细的 Java 应用的上下文示例:
在计算上下文里,GitHub Copilot 采用的是 Jaccard 系数 (Jaccard Similarity) ,这部分的实现是在 Agent 实现,更详细的逻辑可以参考: 花了大半个月,我终于逆向分析了Github Copilot。
相关资源:
相关上下文架构:AutoDev 与 JetBrains AI Assistant 案例
如上所述,相关代码依赖于静态代码分析,主要借助于代码的结构信息,如:AST、CFG、DDG 等。在不同的场景和平台之下,我们可以结合不同的静态代码分析工具, 如下是常见的一些静态代码分析工具:
在补全场景下,通过静态代码分析,我们可以得到当前的上下文,如:当前的函数、当前的类、当前的文件等。如下是一个 AutoDev 的生成单元测试的上下文示例:
在这个示例中,会分析 函数的上下文,获取函数的输入和输出类: 、 信息,以及 BlogService 类信息,作为上下文(在注释中提供)提供给模型。在这时,模型会生成更准确的构造函数,以及更准确的测试用例。
由于相关上下文依赖于对不同语言的静态代码分析、不同 IDE 的 API,所以,我们也需要针对不同的语言、不同的 IDE 进行适配。在构建成本上,相对于相似上下文成本更高。
IDE、编辑器作为开发者的主要工具,其设计和学习成本也相对比较高。首先,我们可以用官方提供的模板生成:
然后,再往上添加功能(是不是很简单),当然不是。以下是一些可以参考的 IDEA 插件资源:
当然了,更合适的是参考AutoDev 插件。
JetBrains 插件
可以直接使用官方的模板来生成对应的插件:https://github.com/JetBrains/intellij-platform-plugin-template
对于 IDEA 插件实现来说,主要是通过 Action 和 Listener 来实现的,只需要在 中注册即可。详细可以参考官方文档:IntelliJ Platform Plugin SDK
版本兼容与兼容架构
由于我们前期未 AutoDev 考虑到对 IDE 版本的兼容问题,后期为了兼容旧版本的 IDE,我们需要对插件进行兼容性处理。所以,如官方文档:Build Number Ranges 中所描述,我们可以看到不同版本,对于 JDK 的要求是不一样的,如下是不同版本的要求:
并配置到 中:
后续配置兼容性比较麻烦,可以参考 AutoDev 的设计。
补全模式:Inlay
在自动代码补全上,国内的厂商主要参考的是 GitHub Copilot 的实现,逻辑也不复杂。
采用快捷键方式触发
其主要是在 Action 里监听用户的输入,然后:
采用自动触发方式
其主要通过 监听用户的输入,然后:根据不同的输入,触发不同的补全结果。核心代码如下:
再根据不同的输入,触发不同的补全结果,并对结构进行处理。
渲染补全代码
随后,我们需要实现一个 Inlay Render,它继承自 。
日常辅助功能开发
结合 IDE 的接口能力,我们需要添加对应的 Action,以及对应的 Group,以及对应的 Icon。如下是一个 Action 的示例:
如下是 AutoDev 的一些 ActionGroup:
在编写 ShowIntentionsGroup 时,我们可以参考 AutoDev 的实现来构建对应的 Group:
多语言上下文架构
由于 Intellij 的平台策略,使得运行于 Java IDE(Intellij IDEA)与在其它 IDE 如 Python IDE(Pycharm)之间的差异性变得更大。我们需要提供基于多平台产品的兼容性,详细介绍可以参考:Plugin Compatibility with IntelliJ Platform Products
首先,将插件的架构进一步模块化,即针对于不同的语言,提供不同的模块。如下是 AutoDev 的模块化架构:
在 中,我们需要添加对应的 ,以及 ,如下是一个示例:
而在 中,我们需要添加对应的 ,以及 ,如下是一个示例:
随后,Intellij 会自动加载对应的模块,以实现多语言的支持。根据我们预期支持的不同语言,便需要对应的 ,诸如于:
最后,在不同的语言模块里,实现对应的功能即可。
上下文构建
为了简化这个过程,我们使用 Unit Eval 来展示如何构建两种类似的上下文。
静态代码分析
通过静态代码分析,我们可以得到当前的函数、当前的类、当前的文件等。再结合路径相似性,寻找最贴进的上下文。
上述的代码,我们可以通过代码的 Imports 信息作为相关代码的一部分。再通过代码的继承关系,来寻找相关的代码。最后,通过再路径相似性,来寻找最贴近的上下文。
相关代码分析
先寻找,再通过代码相似性,来寻找相关的代码。核心逻辑所示:
详细见:SimilarChunker
VSCode 插件
TODO
TreeSitter
TreeSitter 是一个用于生成高效的自定义语法分析器的框架,由 GitHub 开发。它使用 LR(1)解析器,这意味着它可以在 O(n)时间内解析任何语言,而不是 O(n²)时间。它还使用了一种称为“语法树的重用”的技术,该技术使其能够在不重新解析整个文件的情况下更新语法树。
由于 TreeSitter 已经提供了多语言的支持,你可以使用 Node.js、Rust 等语言来构建对应的插件。详细见:TreeSitter。
根据我们的意图不同,使用 TreeSitter 也有不同的方式:
解析 Symbol
在代码自然语言搜索引擎 Bloop 中,我们使用 TreeSitter 来解析 Symbol,以实现更好的搜索质量。
随后,根据不同的类型来决定如何显示:
Chunk 代码
如下是 Improving LlamaIndex’s Code Chunker by Cleaning Tree-Sitter CSTs 中的 TreeSitter 的使用方式:
度量体系设计
常用指标
代码接受率
AI 生成的代码被开发者接受的比例。
入库率
AI 生成的代码被开发者入库的比例。
开发者体验驱动
如微软和 GitHub 所构建的:DevEx: What Actually Drives Productivity: The developer-centric approach to measuring and improving productivity
评估数据集:HumanEval
模型选择与测试
在结合公开 API 的大语言模型之后,我们就可以构建基本的 IDE 功能。随后,应该进一步探索适合于内部的模型,以适合于组织内部的效果。
模型选择
现有的开源模型里采用 LLaMA 架构相对比较多,并且由于其模型的质量比较高,其生态也相对比较完善。因此,我们也采用 LLaMA 架构来构建,即:DeepSeek Coder。
OpenBayes 平台部署与测试
随后,我们需要部署模型,并提供一个对应的 API,这个 API 需要与我们的 IDE 接口保持一致。这里我们采用了 OpenBayes 平台来部署模型。详细见: 目录下的相关代码。
如下是适用于 OpenBayes 的代码,以在后台提供公网 API:
随后,在 IDE 插件中,我们就可以结合他们来测试功能。
大规模模型部署
结合模型量化技术,如 INT4,可以实现 6B 模型在消费级的显卡上进行本地部署。
(TODO)
模型微调
有监督微调(SFT)是指采用预先训练好的神经网络模型,并针对你自己的专门任务在少量的监督数据上对其进行重新训练的技术。
数据驱动的微调方法
结合 【SFT最佳实践 】中提供的权衡考虑:
这就意味着:
通常来说,我们测试是结合 IDE 的功能,以及代码补全的功能,因此,我们需要合并两个数据集。
数据集构建
根据不同的模型,其所需要的指令也是不同的。如下是一个基于 DeepSeek + DeepSpeed 的数据集示例:
下面是 LLaMA 模型的数据集示例:
数据集构建
我们构建 Unit Eval 项目,以生成更适合于 AutoDev 的数据集。
而为了提供 IDE 中的其他功能支持,我们结合了开源数据集,以及数据蒸馏的方式来构建数据集。
开源数据集
在 GitHub、HuggingFace 等平台上,有一些开源的数据集。
Magicoder: Source Code Is All You Need 中开源的两个数据集:
在 License 合适的情况下,我们可以直接使用这些数据集;在不合适的情况下,我们可以拿来做一些实验。
数据蒸馏
数据蒸馏。过去的定义是,即将大型真实数据集(训练集)作为输入,并输出一个小的合成蒸馏数据集。但是,我们要做的是直接用 OpenAI 这一类公开 API 的模型:
微调示例:OpenBayes + DeepSeek
在这里我们使用的是,以及 DeepSeek 官方提供的脚本来进行微调。
我在 OpenBayes 上传了的 DeepSeek 模型:OpenBayes deepseek-coder-6.7b-instruct,你可以在创建时直接使用这个模型。
数据集信息
由 Unit Eval + OSS Instruct 数据集构建而来:
而从结果来看,如何保持高质量的数据是最大的挑战。
测试视频:开源 AI 辅助编程方案:Unit Mesh 端到端打通 v0.0.1 版本
参数示例:
运行日志:
其它:
Unit Tools Workflow
Unit Eval 是一个针对于构建高质量代码微调的开源工具箱。其三个核心设计原则:
即要解决易于测试的数据集生成,以及易于评估的模型评估问题。
IDE 指令设计与演化
AutoDev 早期采用的是 OpenAI API,其模型能力较强,因此在指令设计上比较强大。而当我们需要微调里,我们需要更简单、易于区分的指令来构建。
模板指令
如下是在 AutoDev 中精简化后的 Prompt 示例:
其中包含了:
而这个模板指令,也是我们在 Unit Eval 中所采用的指令。
统一指令模板
为了实现统一的指令模板,我们引入了 Apache Velocity 模板引擎来实现,并通过 Chocolate Factory 实现底层的通用逻辑:
高质量数据集生成
年初(2023 年 4 月),我们做了一系列的代码微调探索, 在那篇 《AI 研发提效的正确姿势:开源 LLM + LoRA 》里,企业应该开始着力于:
只有微调是不够的,模型需要与工具紧密相结合。
质量流水线设计示例
Code Quality Workflow
基于 Thoughtworks 在软件工程的丰富经验,以及 Thoughtworks 的架构治理开源工具 ArchGuard 作为基础设施。在 UnitEval 中,我们也将代码质量的筛选构建成 pipeline 的方式:
而基于 ArchGuard 中所提供的丰富代码质量和架构质量分析能力,诸如 OpenAPI、 SCA(软件依赖分析)能力,我们也在思考未来是否也加入相关的设计。
实现高质量数据集生成
如下是 Unit Eval 0.3.0 的主要代码逻辑:
随后是根据不同的质量门禁,来进行不同的质量检查:
在过虑之后,我们就可以由不同语言的 Worker 来进行处理,诸如 JavaWorker、PythonWorker 等。
根据用户选择的上下文策略,我们就可以构建出不同的上下文,如:相关上下文、相似上下文等
在上下文策略中检查代码质量
SimilarChunksStrategyBuilder 主要逻辑如下
在规则检查里,我们可以通过不同的规则来检查不同的代码质量问题,如:代码坏味道、测试坏味道、API 设计味道等。