教學課程:使用語言模型 API 產生 AI 驅動的程式碼註解
在本教學課程中,您將學習如何建立一個 VS Code 擴充功能,以建構一個 AI 驅動的程式碼導師 (Code Tutor)。您將使用語言模型 (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" 命令
為了讓我們的程式碼導師註解功能運作,我們需要發送一些程式碼並要求它提供註解。我們將分三個步驟完成此操作
- 從使用者目前開啟的分頁中獲取帶有行號的程式碼。
- 將該程式碼與自訂提示 (Prompt) 一起發送到語言模型 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 檔案中的 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 稱這些為「裝飾」(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"
}
]
}
}
這會導致一個按鈕出現在編輯器標題列的導覽區(右側)。該 "icon" 來自 產品圖示參考。
用綠色箭頭重新啟動您的擴充功能,如果擴充功能尚未執行,請按下 F5。您現在應該會看到一個評論圖示,點擊它將會觸發 "Toggle Tutor Annotations" 命令。

後續步驟
在本教學課程中,您學習了如何使用語言模型 API 建立一個將 AI 整合到編輯器中的 VS Code 擴充功能。您使用了 VS Code 擴充功能 API 從目前分頁獲取程式碼,將其與自訂提示一起發送到模型,然後使用裝飾器將模型結果直接解析並顯示在編輯器中。
接下來,您可以擴充您的程式碼導師擴充功能,以包含聊天參與者 (chat participant),這將允許使用者透過 GitHub Copilot 聊天介面直接與您的擴充功能互動。您也可以探索 VS Code 中完整的 API 範圍,以發掘在編輯器中建構自訂 AI 體驗的新方法。
您可以在 vscode-extensions-sample 儲存庫中找到本教學課程的完整原始程式碼。