教程:使用语言模型API生成AI驱动的代码注释
在本教程中,您将学习如何创建一个VS Code扩展来构建一个AI驱动的代码导师。您将使用语言模型(LM)API生成改进代码的建议,并利用VS Code扩展API将其无缝集成到编辑器中,作为用户可以悬停以获取更多信息的内联注释。完成本教程后,您将了解如何在VS Code中实现自定义AI功能。
先决条件
完成本教程,您需要以下工具和账户:
搭建扩展框架
首先,使用Yeoman和VS Code扩展生成器来搭建一个准备好进行开发的TypeScript或JavaScript项目。
npx --package yo --package generator-code -- yo code
选择以下选项以完成新扩展向导...
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? Code Tutor
### Press <Enter> to choose default for all options below ###
# ? What's the identifier of your extension? code-tutor
# ? What's the description of your extension? LEAVE BLANK
# ? Initialize a git repository? Yes
# ? Bundle the source code with webpack? No
# ? Which package manager to use? npm
# ? Do you want to open the new folder with Visual Studio Code? Open with `code`
修改package.json文件以包含正确的命令
脚手架项目在package.json
文件中包含一个单一的“helloWorld”命令。当您的扩展安装后,此命令会显示在命令面板中。
"contributes": {
"commands": [
{
"command": "code-tutor.helloWorld",
"title": "Hello World"
}
]
}
由于我们正在构建一个代码导师扩展,该扩展将向代码行添加注释,我们需要一个命令来允许用户切换这些注释的开启和关闭。更新command
和title
属性:
"contributes": {
"commands": [
{
"command": "code-tutor.annotate",
"title": "Toggle Tutor Annotations"
}
]
}
虽然package.json
定义了扩展的命令和UI元素,但src/extension.ts
文件是你放置应执行这些命令的代码的地方。
打开src/extension.ts
文件并更改registerCommand
方法,使其与package.json
文件中的command
属性匹配。
const disposable = vscode.commands.registerCommand('code-tutor.annotate', () => {
通过按下 F5 来运行扩展。这将打开一个新的 VS Code 实例,并安装扩展。通过按下 ⇧⌘P (Windows, Linux Ctrl+Shift+P) 打开命令面板,并搜索 "tutor"。你应该会看到 "Tutor Annotations" 命令。
如果您选择“导师注释”命令,您将看到一个“Hello World”通知消息。
实现 "annotate" 命令
为了让我们的代码导师注释工作,我们需要发送一些代码并要求它提供注释。我们将通过三个步骤来完成这个任务:
- 从用户当前打开的标签页中获取带有行号的代码。
- 将该代码发送到语言模型API,并附带一个自定义提示,指示模型如何提供注释。
- 解析注释并在编辑器中显示它们。
步骤1:获取带有行号的代码
要从当前标签页获取代码,我们需要一个对用户打开的标签页的引用。我们可以通过将registerCommand
方法修改为registerTextEditorCommand
来实现这一点。这两个命令的区别在于后者为我们提供了一个对用户打开的标签页的引用,称为TextEditor
。
const disposable = vscode.commands.registerTextEditorCommand('code-tutor.annotate', async (textEditor: vscode.TextEditor) => {
现在我们可以使用textEditor
引用来获取“可见编辑器空间”中的所有代码。这是屏幕上可以看到的代码——它不包括在可见编辑器空间上方或下方的代码。
在extension.ts
文件底部的export function deactivate() { }
行上方直接添加以下方法。
function getVisibleCodeWithLineNumbers(textEditor: vscode.TextEditor) {
// get the position of the first and last visible lines
let currentLine = textEditor.visibleRanges[0].start.line;
const endLine = textEditor.visibleRanges[0].end.line;
let code = '';
// get the text from the line at the current position.
// The line number is 0-based, so we add 1 to it to make it 1-based.
while (currentLine < endLine) {
code += `${currentLine + 1}: ${textEditor.document.lineAt(currentLine).text} \n`;
// move to the next line position
currentLine++;
}
return code;
}
此代码使用TextEditor的visibleRanges
属性来获取编辑器中当前可见行的位置。然后,它从第一行的位置开始,移动到最后一行的位置,将每行代码及其行号添加到字符串中。最后,它返回包含所有带有行号的可查看代码的字符串。
现在我们可以从code-tutor.annotate
命令中调用这个方法。修改命令的实现,使其看起来像这样:
const disposable = vscode.commands.registerTextEditorCommand(
'code-tutor.annotate',
async (textEditor: vscode.TextEditor) => {
// Get the code with line numbers from the current editor
const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);
}
);
步骤2:发送代码和提示到语言模型API
下一步是调用GitHub Copilot语言模型,并将用户的代码连同创建注释的指令一起发送给它。
为此,我们首先需要指定要使用的聊天模型。我们在这里选择4o,因为它是一个快速且能力强的模型,适合我们正在构建的这种交互。
const disposable = vscode.commands.registerTextEditorCommand(
'code-tutor.annotate',
async (textEditor: vscode.TextEditor) => {
// Get the code with line numbers from the current editor
const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);
// select the 4o chat model
let [model] = await vscode.lm.selectChatModels({
vendor: 'copilot',
family: 'gpt-4o'
});
}
);
我们需要指令 - 或“提示” - 来告诉模型创建注释以及我们希望响应的格式。将以下代码直接添加到文件顶部,位于导入语句下方。
const ANNOTATION_PROMPT = `You are a code tutor who helps students learn how to write better code. Your job is to evaluate a block of code that the user gives you and then annotate any lines that could be improved with a brief suggestion and the reason why you are making that suggestion. Only make suggestions when you feel the severity is enough that it will impact the readability and maintainability of the code. Be friendly with your suggestions and remember that these are students so they need gentle guidance. Format each suggestion as a single JSON object. It is not necessary to wrap your response in triple backticks. Here is an example of what your response should look like:
{ "line": 1, "suggestion": "I think you should use a for loop instead of a while loop. A for loop is more concise and easier to read." }{ "line": 12, "suggestion": "I think you should use a for loop instead of a while loop. A for loop is more concise and easier to read." }
`;
这是一个特殊的提示,指导语言模型如何生成注释。它还包括了模型应如何格式化其响应的示例。这些示例(也称为“多示例”)使我们能够定义响应的格式,以便我们可以解析它并将其显示为注释。
我们将消息传递给模型在一个数组中。这个数组可以包含任意数量的消息。在我们的例子中,它包含了提示,后面跟着带有行号的用户代码。
const disposable = vscode.commands.registerTextEditorCommand(
'code-tutor.annotate',
async (textEditor: vscode.TextEditor) => {
// Get the code with line numbers from the current editor
const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);
// select the 4o chat model
let [model] = await vscode.lm.selectChatModels({
vendor: 'copilot',
family: 'gpt-4o'
});
// init the chat message
const messages = [
vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT),
vscode.LanguageModelChatMessage.User(codeWithLineNumbers)
];
}
);
要向模型发送消息,我们首先需要确保所选模型可用。这处理了扩展未准备好或用户未登录到GitHub Copilot的情况。然后我们将消息发送到模型。
const disposable = vscode.commands.registerTextEditorCommand(
'code-tutor.annotate',
async (textEditor: vscode.TextEditor) => {
// Get the code with line numbers from the current editor
const codeWithLineNumbers = getVisibleCodeWithLineNumbers(textEditor);
// select the 4o chat model
let [model] = await vscode.lm.selectChatModels({
vendor: 'copilot',
family: 'gpt-4o'
});
// init the chat message
const messages = [
vscode.LanguageModelChatMessage.User(ANNOTATION_PROMPT),
vscode.LanguageModelChatMessage.User(codeWithLineNumbers)
];
// make sure the model is available
if (model) {
// send the messages array to the model and get the response
let chatResponse = await model.sendRequest(
messages,
{},
new vscode.CancellationTokenSource().token
);
// handle chat response
await parseChatResponse(chatResponse, textEditor);
}
}
);
聊天响应以片段形式到达。这些片段通常包含单个单词,但有时只包含标点符号。为了在响应流中显示注释,我们希望等到有一个完整的注释后再显示它。由于我们指示模型返回其响应的方式,我们知道当我们看到一个闭合的}
时,我们就有了一个完整的注释。然后我们可以解析注释并在编辑器中显示它。
在extension.ts
文件中的getVisibleCodeWithLineNumbers
方法上方添加缺失的parseChatResponse
函数。
async function parseChatResponse(
chatResponse: vscode.LanguageModelChatResponse,
textEditor: vscode.TextEditor
) {
let accumulatedResponse = '';
for await (const fragment of chatResponse.text) {
accumulatedResponse += fragment;
// if the fragment is a }, we can try to parse the whole line
if (fragment.includes('}')) {
try {
const annotation = JSON.parse(accumulatedResponse);
applyDecoration(textEditor, annotation.line, annotation.suggestion);
// reset the accumulator for the next line
accumulatedResponse = '';
} catch (e) {
// do nothing
}
}
}
}
我们需要最后一个方法来实际显示注释。VS Code 将这些称为“装饰”。在 extension.ts
文件中的 parseChatResponse
方法上方添加以下方法。
function applyDecoration(editor: vscode.TextEditor, line: number, suggestion: string) {
const decorationType = vscode.window.createTextEditorDecorationType({
after: {
contentText: ` ${suggestion.substring(0, 25) + '...'}`,
color: 'grey'
}
});
// get the end of the line with the specified line number
const lineLength = editor.document.lineAt(line - 1).text.length;
const range = new vscode.Range(
new vscode.Position(line - 1, lineLength),
new vscode.Position(line - 1, lineLength)
);
const decoration = { range: range, hoverMessage: suggestion };
vscode.window.activeTextEditor?.setDecorations(decorationType, [decoration]);
}
此方法接收我们从模型中解析出的注释,并使用它来创建一个装饰。这是通过首先创建一个TextEditorDecorationType
来完成的,该类型指定了装饰的外观。在这种情况下,我们只是添加了一个灰色的注释,并将其截断为25个字符。当用户将鼠标悬停在消息上时,我们将显示完整的消息。
然后我们设置装饰应该出现的位置。我们需要它出现在注释中指定的行号上,并且在行的末尾。
最后,我们在活动的文本编辑器上设置了装饰,这导致注释出现在编辑器中。
如果您的扩展仍在运行,请通过从调试栏选择绿色箭头来重新启动它。如果您关闭了调试会话,请按F5来运行扩展。在新打开的VS Code窗口实例中打开一个代码文件。当您从命令面板中选择“切换导师注释”时,您应该会在编辑器中看到代码注释出现。
在编辑器标题栏添加一个按钮
你可以让你的命令从命令面板以外的地方被调用。在我们的例子中,我们可以在当前标签页的顶部添加一个按钮,让用户可以轻松切换注释。
为此,请按如下方式修改package.json
的“contributes”部分:
"contributes": {
"commands": [
{
"command": "code-tutor.annotate",
"title": "Toggle Tutor Annotations",
"icon": "$(comment)"
}
],
"menus": {
"editor/title": [
{
"command": "code-tutor.annotate",
"group": "navigation"
}
]
}
}
这会导致在编辑器标题栏的导航区域(右侧)出现一个按钮。"icon" 来自 产品图标参考。
如果扩展尚未运行,请使用绿色箭头重新启动扩展或按 F5。你现在应该会看到一个注释图标,该图标将触发“切换导师注释”命令。
下一步
在本教程中,您学习了如何创建一个VS Code扩展,该扩展通过语言模型API将AI集成到编辑器中。您使用了VS Code扩展API从当前标签页获取代码,使用自定义提示将其发送到模型,然后使用装饰器在编辑器中解析并显示模型结果。
接下来,您可以扩展您的Code Tutor扩展,以包括一个聊天参与者,这将允许用户通过GitHub Copilot聊天界面直接与您的扩展进行交互。您还可以探索VS Code中的全套API,以探索在编辑器中构建自定义AI体验的新方法。
你可以在vscode-extensions-sample 仓库中找到本教程的完整源代码。