教程:使用语言模型 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" 命令。当安装您的扩展后,此命令会显示在命令面板(Command Palette)中。
"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" 命令。

如果您选择 "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'
});
}
);
我们需要指令(即“提示词”)来告诉模型创建注释以及我们想要的响应格式。将以下代码添加到文件中 imports 下方。
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." }
`;
这是一个特殊的提示词,指导语言模型如何生成注释。它还包括模型应如何格式化其响应的示例。这些示例(也称为“少样本提示”或 multi-shot)使我们能够定义响应格式,以便我们进行解析并将其显示为注释。
我们以数组形式向模型传递消息。该数组可以包含任意数量的消息。在我们的例子中,它包含提示词,随后是带有行号的用户代码。
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 文件中,将缺失的 parseChatResponse 函数添加到 getVisibleCodeWithLineNumbers 方法上方。
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 将这些称为“装饰器”(decorations)。将以下方法添加到 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 窗口实例中打开一个代码文件。当您从命令面板中选择 "Toggle Tutor Annotations" 时,您应该会看到代码注释出现在编辑器中。

在编辑器标题栏中添加按钮
您可以使您的命令不仅能从命令面板调用。在我们的例子中,我们可以将一个按钮添加到当前选项卡的顶部,允许用户轻松切换注释。
为此,请按如下方式修改 package.json 的 "contributes" 部分:
"contributes": {
"commands": [
{
"command": "code-tutor.annotate",
"title": "Toggle Tutor Annotations",
"icon": "$(comment)"
}
],
"menus": {
"editor/title": [
{
"command": "code-tutor.annotate",
"group": "navigation"
}
]
}
}
这将导致一个按钮出现在编辑器标题栏的导航区域(右侧)。图标来自产品图标参考 (Product Icon Reference)。
使用绿色箭头重新启动扩展,如果扩展尚未运行,请按 F5。现在您应该会看到一个评论图标,它将触发 "Toggle Tutor Annotations" 命令。

后续步骤
在本教程中,您学习了如何创建一个通过语言模型 API 将 AI 集成到编辑器中的 VS Code 扩展。您使用了 VS Code 扩展 API 从当前选项卡获取代码,将其与自定义提示词一起发送给模型,然后使用装饰器解析并直接在编辑器中显示模型结果。
接下来,您可以扩展您的代码导师扩展,包含一个聊天参与者,这将允许用户通过 GitHub Copilot 聊天界面直接与您的扩展交互。您还可以浏览 VS Code 中的完整 API 系列,探索在编辑器中构建自定义 AI 体验的新方法。
您可以在 vscode-extensions-sample 仓库中找到本教程的完整源代码。