DeepSeek R1をVSCode拡張機能としてローカル環境で動かす方法

# 概要
本記事では、VSCode で DeepSeek R1 を利用する拡張機能を作成し、ローカルで AI アシスタントとして動作させる方法を初心者向けに解説します。AIがサポートしてくれながら開発できるVSCode風のエディタのCursorがありますが、「有料で高くて使えない」という方も無料でAIアシスタントを導入してみてはいかがでしょうか。

# DeepSeekについて
AI技術の進歩に伴い、大規模言語モデルの研究が活発化しています。その中でも 中国のDeepSeek社が開発した大規模言語推論モデル `DeepSeek R1` は、高性能かつ低コストな推論モデルとして話題になっています。2025年1月27日に DeepSeek の影響によって AIの推論モデルの学習に必要とされるGPU製造を手掛ける最大手米国企業 Nvidia の株が暴落し、DeepSeekショックと呼ばれるほどその影響の大きさが改めて証明されました。
> 参考: [米史上最大の5890億ドル減－DeepSeekショック](https://www.bloomberg.com/news/articles/2025-01-27/asml-sinks-as-china-ai-startup-triggers-panic-in-tech-stocks)

DeepSeekは、従来の OpenAI や Google が手掛けている手法よりも圧倒的に低コストで大規模モデルの学習が可能であり、多くの開発者にとって魅力的な選択肢となっています。API利用料金もOpenAIと比べると非常に安価でOpenAI 4oと比べても性能に遜色がないと評価されています。
  
以下のグラフは、DeepSeek-R1と他のサービスのo1クラス推論モデルの100万トークンあたりの入力/出力API料金を比較したものです。

![deepseek_r1_price_compare_en.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/25320845-bc7b-9d66-4acb-e778a453b9e0.png)

DeepSeek-R1とo1の料金比較（画像引用: [DeepSeek API Docs](https://api-docs.deepseek.com/news/news250120) ）


DeepSeekは同社が提供しているWebサイト上で試すことができますが、そのまま使うと中国にデータが送られて学習に使用される懸念があります。DeepSeek をローカル環境で使用することで、入力データを中国に送信することなく利用できます。
また、DeepSeek をローカル環境で使用することで、OpenAI のような月額料金を支払うことなく、無料で AI の力を活用できるメリットがあります。DeepSeek の技術基盤は数年前から論文として発表されており、このタイミングで大体的に話題になったのは、トランプ政権の肝いりプロジェクトStargateに当ててきた可能性も…

# 実装方法
実装方法について説明します。仕組みとしては、ローカルでOllamaというツールを使ってモデルを実行し、コマンドラインでチャットする代わりに、エディタで直接チャットできる独自のカスタムVSCode拡張機能を作成して利用するという形です。

## 1. VSCode 拡張機能の作成
まずは、VSCode の拡張機能を作成するための環境を整えます。[VSCodeの公式サイト](https://code.visualstudio.com/api/get-started/your-first-extension)に拡張機能の開発用テンプレートが用意されているので、以下のコマンドを実行してプロジェクトを作成します。

```sh: bash
npx --package yo --package generator-code -- yo code
```

このコマンドを実行すると、拡張機能のテンプレートがセットアップされます。
yoのインストールが求められたら `y` を押してインストールします。
`New Extension (TypeScript)` を選び、拡張機能名を決めて、その他の項目はデフォルト設定で進めます。

![Screenshot 2025-01-29 at 14.47.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/126e4439-d203-9951-f31c-6468c46bcec3.png)


## 2. 拡張機能の基本構造
### 2-1. コマンドの登録
作成されたプロジェクト内の `src/extension.ts` を開くと、拡張機能のエントリーポイントとなる `activate` 関数が定義されています。この関数は拡張機能が起動した際に実行されます。
importで読み込んでいる `vscode` は、VSCodeのAPIにアクセスするための機能が含まれており、例えば、ウィンドウを開いたりエラーメッセージの表示をしたりすることができます。

新しいコマンドを登録するには `commands.registerCommand` を使用します。

```typescript:extension.ts
import * as vscode from 'vscode';

export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "deepseek-ext" is now active!');

  const disposable = vscode.commands.registerCommand('deepseek-ext.helloWorld', () => {
    vscode.window.showInformationMessage('Hello World from DeepSeek-ext!');
  });

  context.subscriptions.push(disposable);
}
```

activate関数内で定義したコマンドは、`package.json` 内の `contributes.commands` にコマンドを登録しておく必要があります。
開発用テンプレートを使ってプロジェクトを作成していたらすでに記入されていると思います。コマンド名はソースコードに書いた文字列と同じである必要があります。

```json:package.json
"contributes": {
  "commands": [
    {
      "command": "deepseek-ext.helloWorld",
      "title": "Hello World"
    }
  ]
}
```

### 2-2. 拡張機能の動作テスト
コンパイルをしてローカル環境にインストールすることで動作を確認できますが、もっと簡単な方法としてデバッガーを使います。

`F5` キーを押すか、ツールバーの`Run` -> `Start Debugging` を選択して、デバッガーを起動します。開発中の拡張機能がインストールされた状態のVSCodeのウィンドウが新しく立ち上がります。

新しく立ち上がったウィンドウで、Windwosの場合は `Ctrl + P`、Macの場合は `Command + Shift + P` を押してコマンドパレットを開き、先ほど作ったコマンド（本記事では `Hello World`）を入力すると、コマンドを実行できます。画面右下に`Hello World from DeepSeek-ext!`とポップアップが表示されれば動作確認はOKです。
![img_debug.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/d73dd8c2-7a03-85ba-75f7-3f3fde1c1952.png)


## 3. DeepSeek R1 をローカル環境で動作させる
DeepSeek R1 をローカルで動作させるには、**Ollama** というツールを使用します。Ollama  はDeepSeekに限らず、オープンソースのAIモデルをダウンロードでき、実行するためのツールを提供してくれています。Ollama をインストールし、DeepSeek R1 のモデルを取得しましょう。

### 3-1. Ollama のインストール

Ollamaのウェブサイトに移動して、Ollamaのインストーラーをダウンロードして、Ollamaをインストールします。
Ollama: https://ollama.com/download

npmパッケージが用意されているので、npmコマンドを使ってOllamaをインストールします。
- Ollama: https://ollama.com/download
```sh: bash
npm install ollama
```

### 3-2. DeepSeek R1 のインストール
Ollamaのウェブサイトから DeepSeek R1 の言語モデルをダウンロードします。

- Ollama DeepSeek R1: https://ollama.com/library/deepseek-r1

言語モデルはいくつかありますが、大きなモデルであればより推論の精度が高くなる反面、ファイルサイズが大きかったり推論に時間がかかったりします。まずは、精度が低いが応答が速いモデルでテストして、精度が足りなければ少しずつ上げていくのが良いでしょう。


インストールが完了したら、以下のコマンドで DeepSeek R1 のモデルをダウンロードします。
今回は、`latest`をつけてデフォルト設定の`7b`をインストールします。

```sh: bash
ollama pull deepseek-r1:latest
```
`1.5b`というモデルを使いたければ、`deepseek-r1:1.5b` とすればインストールできます。
```sh: bash
ollama pull deepseek-r1:1.5b
```

## 4. VSCode 拡張機能からDeepSeek R1 を呼び出す実装

VSCode から ローカル環境の`DeepSeek R1` モデル にアクセスするため、拡張機能内で `Ollama` を使用します。

### 4-1. 事前準備
`tsconfig.json`を開いて、`lib`にHTMLのwindowなどを利用するための`DOM`を追加してください。この記述がないと、Ollamaのモジュール利用時にエラーが出ていまします。
```typescript:tsconfig.json
{
  "compilerOptions": {
    // 中略
    "lib": [
      "ES2022",
+     "DOM" // for document, window, etc.
    ],
    // 中略
  }
}
```


`extiension.ts`に戻り、activate関数内にコマンドで実行する処理を追加していきます。その前に、拡張機能の実行コマンド名を`helloWorld`から`deepseekChat`に変更しておきます。
```typescript:extiension.ts
export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand('deepseek-ext.deepseekChat', () => {
    // ここにあとで処理を書く
  });
}
```
package.jsonのほうも同じく変更してください。
```typescript:package.json
"contributes": {
  "commands": [
    {
-      "command": "deepseek-ext.helloWorld",
-      "title": "Hello World"
+      "command": "deepseek-ext.deepseekChat",
+      "title": "DeepSeek Chat"
    }
  ]
}
```

### 4-2. ユーザーインターフェースの作成
次に、チャットボックスを作成して、DeepSeek R1 とのやり取りを可能にします。GUIをWebviewとしてカスタマイズしたいので、`createWebviewPanel`でパネルを作ります。

```typescript:extension.ts
export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand('deepseek-ext.deepseekChat', () => {
    const panel = vscode.window. createWebviewPanel(
      'deepSeekChat',
      'DeepSeek R1 Chat',
      vscode.ViewColumn.One,
      { enableScripts: true }
    );
    // 中略
  });
  context.subscriptions.push(disposable);
}
```

getWebviewContentという関数を作り、UI用のHTMLを書いて関数呼び出し元に返却するようにします。
返却するHTMLは バッククウォート(\`) を使って区切ります。バッククウォートで囲まれたHTMLは通常だと構文解析してくれないので、`es6-string-html`というVSCodeの拡張機能をインストールすると、見やすくて便利です。

```typescript:extension.ts
export function activate(context: vscode.ExtensionContext) {
  const disposable = vscode.commands.registerCommand('deepseek-ext.deepseekChat', () => {
    const panel = vscode.window. createWebviewPanel(
      'deepSeekChat',
      'DeepSeek R1 Chat',
      vscode.ViewColumn.One,
      { enableScripts: true }
    );
    panel.webview.html = getWebviewContent(); // 追加
    // 中略
  });
  context.subscriptions.push(disposable);
}

// 追加: WebViewのUIをHTMLで記述した内容を返す関数
const getWebviewContent = (): string => {
  return /*html*/ `<! DOCTYPE html>
  <html lang="en" > 
    <head>
      <meta charset="UTF-8" / >
      <style>
        body { font-family: sans-serif; margin: 1rem; }
        #prompt { width: 100%; box-sizing: border-box; border-radius: 0.25rem; padding: 0.5rem; margin-top: 0.5rem; }
        #askBtn { margin-top: 0.5rem; font-size: 0.75rem; padding: 0.2rem 0.5rem; border: 1px solid #aaa; border-radius: 0.25rem; background-color: #8EA8F9; }
        #askBtn: hover { background-color: #ABBEF8; }
        #response { border: 1px solid #aaa; border-radius: 0.25rem; margin-top: 2rem; padding: 0.5rem; min-height: 100px; }
      </style>
    </head>
    <body>
      <h2>DeepSeek Chat</h2>
      <textarea id="prompt" placeholder="Type something..."></textarea>
      <br/>
      <button id="askBtn">Ask</button>
      <div id="response"></div>
    </body>
  </html>`;
};
```

この状態で、一度デバッガーで確認すると下記のような画面が表示されます。
![img_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/bfc8545a-f4c3-71dc-595d-582d7be1e0c3.png)


### 4-3. GUIとVSCode拡張機能のメッセージの送受信
#### 4-3-1. ユーザーが入力したテキストをVSCode拡張機能の処理に送る
次に、Webviewのテキストボックスに入力した内容をVSCodeの拡張機能の処理に送ります。
getWebviewContent関数内の `<body>` タグの中に `<script>` タグを追記し、ボタン押下イベントの処理と拡張機能からメッセージを受信したときの処理を書きます。WebviewのUIで入力した内容をVSCodeの拡張機能の処理に送るには`vscode.postMessage`を使います。受信側でメッセージの種別がわかるように、`chat`という名前をつけます。

```html: extension.ts
<script>
const vscode = acquireVsCodeApi(); // VSCodeの拡張機能APIを使う準備
document.getElementById('askBtn').addEventListener('click', () => {
  const text = document.getElementById('prompt').value;
  vscode.postMessage({ command: 'chat', text }); // VSCodeの拡張機能にメッセージを送る
});

window.addEventListener ('message', event => {
  const { command, text } = event.data;
  if (command === 'chatResponse') {
    document.getElementById('response').innerText = text;
  }
});
</script>
```

#### 4-3-2. ユーザーが入力したテキストを`DeepSeek R1` に送って応答を得る
ユーザーからの入力は、`panel.webview.onDidReceiveMessage` で受け取れます。メッセージの名前が`chat`の場合に、ollamaでインストールした`DeepSeek R1` にユーザーが入力したテキストを送信し、応答を UI に表示する処理を追加します。拡張機能で処理した結果をWebviewのUIに送るには、`panel.webview.postMessage`を使います。受信側でメッセージの種別がわかるように、`chatResponse`という名前をつけます。

```typescript: extension.ts
const disposable = vscode.commands.registerCommand('deepseek-ext.deepseekChat', () => {
  // 中略
  panel.webview.html = getWebviewContent();

  panel.webview.onDidReceiveMessage(async (message: any) => {
    switch (message.command) {
      // 'chat'のコマンドを受け取った場合の処理
      case 'chat':
        const userPrompt = message.text;
        const chatRequest: ChatRequest & { stream: true } = {
          model: 'deepseek-r1:latest',
          messages: [{ role: 'user', content: userPrompt }],
          stream: true
        };
        let responseText = '';

        try {
          const streamResponse = await ollama.chat(chatRequest); // ollamaにチャットの内容リクエストとして投げる
          for await (const part of streamResponse) {
            responseText += part.message.content; // 応答テキストを順次追加していく
            panel.webview.postMessage({ command: 'chatResponse', text: responseText }); // postMessageで応答内容をWebviewに送る
          }
        }
        catch (error: any) {
          responseText = `Error: ${error.message}`; // エラーメッセージの場合はエラー内容を表示する
          panel.webview.postMessage({ command: 'chatResponse', text: responseText });
        break;
      }
    }
  });
  // 中略
}
```

#### 4-3-3. `DeepSeek R1` の応答を画面に表示する
応答結果を画面に表示するため、getWebviewContent関数内の `<script>` タグ内の処理にメッセージイベントを受信するための処理を追加します。受け取ったメッセージの名前が`chatResponse`の場合に、HTMLのDOMに結果を挿入します。

```html: extension.ts
<script>
const vscode = acquireVsCodeApi(); // VSCodeの拡張機能APIを使う準備
// 中略

window.addEventListener ('message', event => {
  const { command, text } = event.data;
  if (command === 'chatResponse') {
    document.getElementById('response').innerText = text;
  }
});
</script>
```
### 4-4. 動作確認
VSCodeのデバッガーを起動して、コマンドパレットを開き、'DeepSeek Chat'と入力して動作確認をします。
![img_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/05c1c64d-1ecf-5a83-e1be-e93c4973de71.png)



## 5. 拡張機能のビルドとインストール
開発中の拡張機能をビルドするには、`vsce` というモジュールを使います。npmでインストールできます。
```sh: bash
npm install vsce --save-dev
```

ビルドをする前に、テンプレート作成時に追加されている`README.md`を変更する必要があります。`README.md`の3行目あたりの文章の変更差分がないとビルド時にエラーが出るので、とりあえずビルドしたい方はこの行を削除します。

```md: README.md
This is the README for your extension "deepseek-ext". After writing up a brief description, we recommend including the following sections.
```

コンソールで、下記のコマンドを実行すると、ビルドができます。
```sh: bash
npx vsce package
```
無事にビルドが成功すると、プロジェクトのルートディレクトリに`.vsix`の拡張子のファイルができると思います。
```sh: bash
DONE  Packaged: /Path/to/project/dir/deepseek-ext-0.0.1.vsix (27 files, 43.64KB)
```

作成した拡張機能をインストールしたい場合は、VSCode の`Extensions`から三点リーダーのアイコンをクリックし、「Install from VSIX...」をクリックし、先ほど作成した.vsix のファイルを選択します。
![img_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/2271b63f-5cd8-3ed5-e37e-fbd67cfb7d5c.png)

コマンドパレットを開き、'DeepSeek Chat'と入力すると、先ほど作成したコマンドが利用できるようになっていますので、実行して動作確認をします。

![ret.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/34853/96f42357-1356-faf0-41ee-5b7ae5bea915.gif)

無事動作確認ができました。

## まとめ
VSCode拡張機能を用いてDeepSeek R1をローカルで利用する方法を紹介しました。拡張機能を開発することで、安全かつ低コストでAIアシスタントを活用できるようになります。ぜひ試してみてください。