GLM-4-9B-Chat-1M在VSCode中的智能编程助手插件开发

想象一下,你正在写一段复杂的业务逻辑代码,突然卡在一个函数设计上,或者不确定某个库的用法。这时候,如果有个懂行的伙伴坐在旁边,看一眼你的代码,就能给出精准的建议,甚至帮你把代码补全,那该多省心。

现在,这个“伙伴”可以随时待命在你的VSCode里了。今天我们就来聊聊,如何把GLM-4-9B-Chat-1M这个拥有百万上下文能力的“代码专家”,变成一个真正能帮你干活的VSCode智能编程助手。

1. 为什么是GLM-4-9B-Chat-1M?

你可能听说过很多大模型,但GLM-4-9B-Chat-1M在编程助手这个场景下,有几个特别实在的优势。

首先,它支持100万token的上下文长度。这是什么概念?差不多相当于200万个中文字符,或者几十个完整的代码文件。这意味着你的插件可以一次性把整个项目的主要文件都“喂”给模型,让它对你的代码库有全局的理解。当你问“这个函数在哪儿被调用了?”或者“这个模块的接口是什么?”时,它不会只盯着当前这几行代码瞎猜。

其次,9B的参数规模,在保证不错推理能力的同时,对硬件的要求相对友好。用一张消费级的显卡(比如RTX 3090/4090)就能跑起来,这让个人开发者或者小团队自己部署成为可能,不用总依赖云端API,既省钱又不用担心代码隐私泄露。

最后,它原生支持代码执行和工具调用。这意味着你开发的插件不仅能聊天、补全代码,未来还能扩展出更高级的功能,比如让模型分析代码后直接运行单元测试,或者调用外部工具来格式化代码、检查语法。

说白了,选它就是为了做一个更懂你项目、更私有安全、潜力更大的编程助手。

2. 插件核心功能设计

一个光会聊天的模型放在编辑器里,用处不大。我们的目标是让它成为开发工作流的一部分。我建议先从下面几个最能直接提升效率的功能做起。

2.1 智能代码补全与生成

这不是简单的按Tab键补全一个变量名。而是基于你正在写的代码上下文和整个项目的语义,生成整块有意义的代码。

比如,你刚定义了一个User类,有nameemail属性。当你开始输入def get_的时候,插件能提示def get_user_info(self):,甚至直接把函数体return {"name": self.name, "email": self.email}都给你生成好。它理解你的意图是创建一个获取用户信息的方法。

实现上,我们需要监听编辑器的文件变化和光标位置,把当前文件的内容、以及相关依赖文件的部分内容作为上下文,发送给模型,请求它生成接下来的几行代码。

2.2 代码解释与文档生成

读别人(或者几个月前自己写的)代码时,最头疼的就是看不懂某段复杂逻辑是干嘛的。选中这段代码,右键点击“解释这段代码”,插件就能用大白话告诉你:“这段代码是在处理用户登录请求,先验证密码,然后检查账户是否被锁定,最后生成一个访问令牌。”

反过来,写完一个函数后,一键“生成文档字符串”,它就能根据函数名、参数和代码逻辑,自动写出规范的docstring,省去你不少机械劳动。

2.3 错误检测与重构建议

比静态检查工具更智能一点。它不仅能告诉你语法错误,还能从逻辑和设计模式层面给出建议。

例如,它可能提示你:“这个循环里每次都在重复计算同一个值,建议提到循环外面来提升性能。”或者“这个类的职责似乎过于复杂,考虑拆分成UserValidatorUserPersister两个类?”

2.4 自然语言对话与问答

这是最直接的人机交互方式。在侧边栏开一个聊天面板,你可以问:

  • “项目里处理支付失败重试的逻辑在哪里?”
  • “帮我写一个用requests库发送POST请求并处理异常的例子。”
  • config.yamldatabase.pool_size这个配置项是什么意思?”

模型可以结合被问及的代码文件内容来回答,做到“指哪打哪”。

3. 一步步搭建开发环境

理论说完了,我们动手把环境搭起来。这里假设你已经有基本的Python和Node.js开发环境。

3.1 模型服务端部署

首先,我们需要一个地方来运行GLM-4-9B-Chat-1M模型,并提供API给VSCode插件调用。为了简单高效,我推荐使用vLLM作为推理后端。

# 创建一个新的项目目录
mkdir glm4-vscode-assistant && cd glm4-vscode-assistant

# 创建模型服务端目录
mkdir model_server && cd model_server

# 建议使用Python虚拟环境
python -m venv venv
source venv/bin/activate  # Linux/macOS
# venv\Scripts\activate  # Windows

# 安装核心依赖
pip install vllm transformers torch

接下来,创建一个启动脚本server.py

# server.py
from vllm import LLM, SamplingParams
from transformers import AutoTokenizer
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import uvicorn
from typing import List, Optional

# 定义API请求模型
class ChatMessage(BaseModel):
    role: str  # "user" or "assistant"
    content: str

class CompletionRequest(BaseModel):
    messages: List[ChatMessage]
    max_tokens: Optional[int] = 1024
    temperature: Optional[float] = 0.7

# 初始化模型和tokenizer
print("正在加载GLM-4-9B-Chat-1M模型,这可能需要几分钟...")
model_name = "THUDM/glm-4-9b-chat-1m"

# 根据你的GPU显存调整参数。40G显存(如A100)可以尝试以下配置。
# 如果显存较小,请减小max_model_len或使用量化模型。
llm = LLM(
    model=model_name,
    tensor_parallel_size=1,  # 单GPU
    max_model_len=131072,     # 初始可设为128K,后续根据需求调整
    trust_remote_code=True,
    enforce_eager=True,       # 兼容性选项
)

tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
print("模型加载完成!")

app = FastAPI(title="GLM-4 Code Assistant API")

@app.post("/v1/chat/completions")
async def create_chat_completion(request: CompletionRequest):
    try:
        # 将消息列表转换为模型所需的聊天模板格式
        prompt_text = tokenizer.apply_chat_template(
            [msg.dict() for msg in request.messages],
            tokenize=False,
            add_generation_prompt=True
        )

        # 设置生成参数
        sampling_params = SamplingParams(
            temperature=request.temperature,
            max_tokens=request.max_tokens,
            stop_token_ids=[151329, 151336, 151338]  # GLM模型特定的停止符
        )

        # 生成回复
        outputs = llm.generate([prompt_text], sampling_params)
        generated_text = outputs[0].outputs[0].text

        return {
            "choices": [{
                "message": {
                    "role": "assistant",
                    "content": generated_text.strip()
                }
            }]
        }
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

if __name__ == "__main__":
    # 启动服务,默认在本地8000端口
    uvicorn.run(app, host="0.0.0.0", port=8000)

运行这个服务:

python server.py

看到“模型加载完成!”的日志后,你的本地模型API服务就启动了。你可以用curl测试一下:

curl -X POST http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{"role": "user", "content": "用Python写一个快速排序函数。"}],
    "max_tokens": 300
  }'

3.2 VSCode插件项目初始化

模型服务跑起来了,现在我们来创建VSCode插件本体。打开一个新的终端窗口,回到项目根目录。

# 安装Yeoman和VSCode插件生成器
npm install -g yo generator-code

# 生成插件脚手架
yo code

在交互式命令行里,按照提示选择:

  • 项目类型New Extension (TypeScript)
  • 名称glm4-code-assistant
  • 标识符glm4-code-assistant
  • 描述AI-powered code assistant using GLM-4-9B-Chat-1M
  • 是否初始化Git仓库Yes
  • 包管理器npm

完成后,进入插件目录并安装一个用于HTTP请求的库:

cd glm4-code-assistant
npm install axios

4. 实现核心插件功能

我们聚焦实现两个最常用的功能:代码补全和侧边栏聊天。打开src/extension.ts,这是插件的入口文件。

4.1 配置模型连接

首先,在package.json里添加配置项,让用户能设置自己模型服务的地址:

// 在 package.json 的 contributes.configuration 部分添加
"configuration": {
  "title": "GLM-4 Assistant",
  "properties": {
    "glm4Assistant.modelEndpoint": {
      "type": "string",
      "default": "http://localhost:8000/v1/chat/completions",
      "description": "GLM-4模型API的服务地址"
    },
    "glm4Assistant.apiKey": {
      "type": "string",
      "default": "",
      "description": "API密钥(如需)"
    }
  }
}

4.2 实现智能代码补全

我们来创建一个代码补全提供器。在src目录下新建一个CompletionProvider.ts

// src/CompletionProvider.ts
import * as vscode from 'vscode';
import axios from 'axios';

export class GLM4CompletionProvider implements vscode.CompletionItemProvider {

    private getConfig() {
        const config = vscode.workspace.getConfiguration('glm4Assistant');
        return {
            endpoint: config.get<string>('modelEndpoint', 'http://localhost:8000/v1/chat/completions'),
            apiKey: config.get<string>('apiKey', '')
        };
    }

    async provideCompletionItems(
        document: vscode.TextDocument,
        position: vscode.Position,
        token: vscode.CancellationToken,
        context: vscode.CompletionContext
    ): Promise<vscode.CompletionItem[]> {
        
        // 获取当前行的文本和光标前的部分
        const linePrefix = document.lineAt(position).text.substr(0, position.character);
        
        // 简单判断:如果用户正在输入,且不是在写注释或字符串,则触发补全
        if (linePrefix.trim().length === 0 || linePrefix.trim().startsWith('//') || linePrefix.trim().startsWith('#')) {
            return [];
        }

        // 获取当前文件的前面一部分内容作为上下文(比如前50行)
        const startLine = Math.max(0, position.line - 50);
        const contextRange = new vscode.Range(
            new vscode.Position(startLine, 0),
            position
        );
        const codeContext = document.getText(contextRange);

        const config = this.getConfig();
        
        // 构建给模型的提示
        const prompt = `你是一个专业的编程助手。请根据下面的代码上下文,预测并生成接下来最可能的一行或几行代码。只返回代码,不要解释。
        
代码上下文:
\`\`\`
${codeContext}
\`\`\`

当前光标位置在行末,用户刚输入了:“${linePrefix}”
请补全代码:`;

        try {
            const response = await axios.post(config.endpoint, {
                messages: [{ role: "user", content: prompt }],
                max_tokens: 100,
                temperature: 0.3  // 低温度,让输出更确定
            }, {
                headers: config.apiKey ? { 'Authorization': `Bearer ${config.apiKey}` } : {},
                timeout: 10000  // 10秒超时
            });

            const generatedCode = response.data.choices[0]?.message?.content?.trim();
            if (!generatedCode) {
                return [];
            }

            // 将模型返回的代码包装成补全项
            const completionItem = new vscode.CompletionItem(generatedCode, vscode.CompletionItemKind.Snippet);
            completionItem.detail = 'GLM-4 智能补全';
            completionItem.documentation = new vscode.MarkdownString(`由GLM-4模型生成的代码补全建议。`);
            
            // 设置插入文本,并确保插入后光标位置合理
            completionItem.insertText = new vscode.SnippetString(generatedCode);
            
            return [completionItem];

        } catch (error) {
            console.error('GLM-4补全请求失败:', error);
            // 失败时静默返回,不干扰用户
            return [];
        }
    }
}

4.3 实现侧边栏聊天视图

聊天面板能让用户自由提问。我们需要创建一个Webview。在src下创建ChatPanel.ts

// src/ChatPanel.ts
import * as vscode from 'vscode';
import axios from 'axios';

export class ChatPanel {
    public static currentPanel: ChatPanel | undefined;
    private readonly _panel: vscode.WebviewPanel;
    private _disposables: vscode.Disposable[] = [];

    private constructor(panel: vscode.WebviewPanel, extensionUri: vscode.Uri) {
        this._panel = panel;
        this._panel.webview.html = this._getHtmlForWebview();
        
        // 处理来自Webview的消息
        this._panel.webview.onDidReceiveMessage(
            async (message) => {
                switch (message.command) {
                    case 'sendMessage':
                        await this._handleUserMessage(message.text);
                        return;
                }
            },
            null,
            this._disposables
        );
    }

    private _getHtmlForWebview(): string {
        return `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>GLM-4 编程助手</title>
            <style>
                body { padding: 10px; font-family: var(--vscode-font-family); }
                #chatContainer { display: flex; flex-direction: column; height: 90vh; }
                #messages { flex-grow: 1; overflow-y: auto; margin-bottom: 10px; }
                .message { margin-bottom: 15px; padding: 10px; border-radius: 5px; }
                .user { background-color: var(--vscode-input-background); align-self: flex-end; }
                .assistant { background-color: var(--vscode-editor-background); align-self: flex-start; }
                #inputArea { display: flex; }
                #messageInput { flex-grow: 1; margin-right: 10px; }
            </style>
        </head>
        <body>
            <div id="chatContainer">
                <div id="messages"></div>
                <div id="inputArea">
                    <input type="text" id="messageInput" placeholder="输入你的问题...">
                    <button id="sendButton">发送</button>
                </div>
            </div>
            <script>
                const vscode = acquireVsCodeApi();
                const messagesDiv = document.getElementById('messages');
                const messageInput = document.getElementById('messageInput');
                const sendButton = document.getElementById('sendButton');

                function addMessage(text, isUser) {
                    const messageDiv = document.createElement('div');
                    messageDiv.className = 'message ' + (isUser ? 'user' : 'assistant');
                    messageDiv.textContent = (isUser ? '你: ' : '助手: ') + text;
                    messagesDiv.appendChild(messageDiv);
                    messagesDiv.scrollTop = messagesDiv.scrollHeight;
                }

                sendButton.addEventListener('click', () => {
                    const text = messageInput.value.trim();
                    if (text) {
                        addMessage(text, true);
                        vscode.postMessage({ command: 'sendMessage', text: text });
                        messageInput.value = '';
                    }
                });

                messageInput.addEventListener('keypress', (e) => {
                    if (e.key === 'Enter') {
                        sendButton.click();
                    }
                });

                // 监听来自扩展的消息(用于显示助手回复)
                window.addEventListener('message', event => {
                    const message = event.data;
                    if (message.command === 'receiveMessage') {
                        addMessage(message.text, false);
                    }
                });
            </script>
        </body>
        </html>`;
    }

    private async _handleUserMessage(userMessage: string) {
        const config = vscode.workspace.getConfiguration('glm4Assistant');
        const endpoint = config.get<string>('modelEndpoint', 'http://localhost:8000/v1/chat/completions');
        const apiKey = config.get<string>('apiKey', '');

        try {
            // 可以在这里获取当前活跃的编辑器内容,作为上下文附加到提示中
            let codeContext = '';
            const editor = vscode.window.activeTextEditor;
            if (editor) {
                const document = editor.document;
                // 获取前100行代码作为参考
                const lineCount = Math.min(100, document.lineCount);
                const range = new vscode.Range(
                    new vscode.Position(0, 0),
                    new vscode.Position(lineCount, 0)
                );
                codeContext = document.getText(range);
            }

            const prompt = `你是一个集成在VSCode中的编程助手。用户的问题是:${userMessage}
${codeContext ? `\n当前打开的文件内容(部分)供参考:\n\`\`\`\n${codeContext}\n\`\`\`` : ''}
请用友好、专业的语气回答,专注于提供编程相关的帮助。`;

            const response = await axios.post(endpoint, {
                messages: [{ role: "user", content: prompt }],
                max_tokens: 500,
                temperature: 0.7
            }, {
                headers: apiKey ? { 'Authorization': `Bearer ${apiKey}` } : {},
                timeout: 15000
            });

            const assistantReply = response.data.choices[0]?.message?.content;
            if (assistantReply) {
                this._panel.webview.postMessage({ command: 'receiveMessage', text: assistantReply });
            }
        } catch (error) {
            console.error('聊天请求失败:', error);
            this._panel.webview.postMessage({ 
                command: 'receiveMessage', 
                text: '抱歉,请求模型服务时出错了。请检查模型服务是否运行,以及网络连接。' 
            });
        }
    }

    public static createOrShow(extensionUri: vscode.Uri) {
        const column = vscode.window.activeTextEditor
            ? vscode.window.activeTextEditor.viewColumn
            : undefined;

        if (ChatPanel.currentPanel) {
            ChatPanel.currentPanel._panel.reveal(column);
            return;
        }

        const panel = vscode.window.createWebviewPanel(
            'glm4Chat',
            'GLM-4 编程助手',
            column || vscode.ViewColumn.Two,
            {
                enableScripts: true,
                retainContextWhenHidden: true,
            }
        );

        ChatPanel.currentPanel = new ChatPanel(panel, extensionUri);
    }

    public dispose() {
        ChatPanel.currentPanel = undefined;
        this._panel.dispose();
        while (this._disposables.length) {
            const x = this._disposables.pop();
            if (x) { x.dispose(); }
        }
    }
}

4.4 整合功能到主扩展

最后,修改src/extension.ts,把上面两个功能注册进去:

// src/extension.ts
import * as vscode from 'vscode';
import { GLM4CompletionProvider } from './CompletionProvider';
import { ChatPanel } from './ChatPanel';

export function activate(context: vscode.ExtensionContext) {
    console.log('GLM-4 Code Assistant 插件已激活');

    // 注册代码补全提供器
    const completionProvider = new GLM4CompletionProvider();
    const languages = ['python', 'javascript', 'typescript', 'java', 'go', 'cpp'];
    languages.forEach(lang => {
        const selector = { scheme: 'file', language: lang };
        const provider = vscode.languages.registerCompletionItemProvider(selector, completionProvider);
        context.subscriptions.push(provider);
    });

    // 注册打开聊天面板的命令
    const chatCommand = vscode.commands.registerCommand('glm4-assistant.openChat', () => {
        ChatPanel.createOrShow(context.extensionUri);
    });
    context.subscriptions.push(chatCommand);

    // 注册代码解释命令(示例)
    const explainCommand = vscode.commands.registerCommand('glm4-assistant.explainCode', async () => {
        const editor = vscode.window.activeTextEditor;
        if (!editor) {
            vscode.window.showWarningMessage('请先打开一个代码文件。');
            return;
        }

        const selection = editor.selection;
        const selectedText = editor.document.getText(selection);
        if (!selectedText.trim()) {
            vscode.window.showWarningMessage('请先选择一段代码。');
            return;
        }

        // 这里可以调用模型服务来解释代码,逻辑与聊天类似
        vscode.window.showInformationMessage(`已选中代码,解释功能开发中...`);
    });
    context.subscriptions.push(explainCommand);
}

export function deactivate() {}

别忘了更新package.json,注册我们创建的命令和视图:

// 在 package.json 的 contributes 部分添加
"contributes": {
  "commands": [
    {
      "command": "glm4-assistant.openChat",
      "title": "GLM-4: 打开聊天助手"
    },
    {
      "command": "glm4-assistant.explainCode",
      "title": "GLM-4: 解释选中代码"
    }
  ],
  "menus": {
    "editor/context": [
      {
        "command": "glm4-assistant.explainCode",
        "when": "editorHasSelection",
        "group": "glm4"
      }
    ]
  }
}

5. 调试、打包与优化建议

现在,一个基础可用的插件就成型了。按F5键,会打开一个扩展开发宿主窗口,你可以在里面测试插件的功能。

调试技巧

  • 在模型服务端,注意观察GPU显存使用情况。如果处理长上下文时内存不足(OOM),可以回到server.py里,调低max_model_len参数,或者考虑使用量化版本的模型(如GGUF格式)。
  • VSCode插件的输出控制台(Output -> Extension Host)是查看插件日志的好地方。

性能优化方向

  1. 上下文管理:百万上下文是优势,但全量发送每次请求都又慢又耗资源。需要设计智能的上下文窗口,只选取与当前光标位置最相关的代码片段(如当前文件、导入的文件、同目录下的文件)发送给模型。
  2. 缓存机制:对常见的、通用的代码补全请求结果进行缓存,避免重复查询模型。
  3. 流式响应:对于代码生成或长回答,可以实现流式输出,让用户边等边看,体验更好。
  4. 本地模型量化:如果觉得原版9B模型对硬件要求还是高,可以探索使用4bit或8bit量化版本的模型,能在几乎不损失太多精度的情况下,显著降低显存消耗和提升推理速度。

打包发布: 开发测试满意后,可以使用vsce工具打包成.vsix文件,分享给团队成员或发布到市场。

npm install -g @vscode/vsce
vsce package

6. 总结

走完这一趟,你会发现,把一个强大的大模型变成编辑器里趁手的工具,并没有想象中那么遥不可及。核心思路就是搭好一个本地的模型服务,然后通过VSCode插件这个桥梁,把编辑器的上下文(代码)和用户意图(操作、提问)传递给模型,再把模型的智慧带回到编辑器中。

我们今天实现的代码补全和聊天面板,只是一个起点。GLM-4-9B-Chat-1M的长上下文和工具调用能力,留出了巨大的想象空间。比如,未来可以做一个“一键代码审查”功能,把整个Pull Request的改动喂给模型,让它生成审查意见;或者做一个“自动化测试生成”功能,让它根据你的业务逻辑代码,自动编写对应的单元测试用例。

开发这类插件的乐趣就在于,你不仅是在做一个工具,更是在为自己和同行们塑造一种全新的、与代码对话的交互方式。从一行行补全开始,慢慢让它成长为项目里一个真正懂行的“副驾驶”。如果你已经部署好了模型服务,不妨就从今天写的这个简单插件开始试起,感受一下AI辅助编程最直接的温度。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐