💻

開発者のための生成 AI ツール『Duet AI for Developers』を触ってみた

2023/12/20に公開

こんにちは、SRE ディビジョンの小堀内です。

今回は、Google Cloud の中でも目玉プロダクトの内の 1 つである、Duet AI for Developers を触ってみました。
その中で感じた、他生成 AI との違いや Duet AI for Developers の使い所を紹介していきます。

結論

Duet AI for Developers は、

  • Google Cloud を用いたアプリケーション開発に向いている
  • チャットアシスタンスのレスポンスが速い
  • コードアシスタンスの機能が強力
  • モデルトレーニングのためのデータとして、プロンプトやその応答を使用しない

他生成 AI ツールとの比較表

比較項目 Duet AI for Developers ChatGPT (GPT-4) GitHub Copilot (コード補完) コメント
回答精度 Duet AI は同じ文章を繰り返し生成することがある
プロンプトの与えやすさ - ChatGPT は曖昧な質問でもその意図を汲み取ってくれる
コード補完の扱いやすさ - Duet AI, GitHub Copilot 共に意図した内容を提案してくれることが多い
コスト $19/月 (詳細) $20/月 (詳細) $19/月 (詳細) -
総評 - コードアシスタンス(補完)は、他ツールと遜色なしで非常に強力
- 明確な質問をしないと意図した回答が得られない
- 実際の開発でも活用可能
- 質問/回答の精度はピカイチ
- Google Cloud にこだわらないのであれば ChatGPT (GPT-4) のみでも問題ないように感じる
- 他ツールのチャット機能と合わせて活用することより効率的な開発が可能 -

Duet AI for Developers がフィットするユーザー

  • Google Cloud を活用してアプリケーション開発を行うユーザー
    • Google Cloud 環境でのアプリケーション開発に関する質問やベストプラクティスの提供が可能である
  • 迅速なチャットアシスタンスが必要なユーザー
    • 応答速度が速く、開発中に迅速なフィードバックが求められる状況に適している
  • 強力なコードアシスタンス機能を活用したいユーザー
    • コード生成や補完機能が便利で使いやすい
  • 機密性が求められる開発に従事するユーザー
    • プロンプトやその応答をモデルトレーニングのデータとして使用しないため、セキュリティに関する懸念があるユーザーにも適している

本記事で説明すること/説明しないこと

  • 説明すること
    • Duet AI の概要
    • Duet AI for Developers の概要 および 検証結果
    • Duet AI for Developers を最大限に活用するコツ
    • 他生成 AI サービス(GitHub Copilot, ChatGPT)との違い
  • 説明しないこと
    • VS Code 以外の IDE (JetBrains, Cloud Workstations) での Duet AI for Developers の活用
    • その他の Duet AI の機能

そもそも Duet AI とは?

Duet AI は Google Cloud が提供する AI 支援ツールです。

duet-ai
Duet AI を Google Cloud コンソールから使用した際のイメージ

このツールを介して、Google Cloudに関する疑問やベストプラクティスに関するガイダンスを自然言語でのチャットを通じて受け取ることが可能です。
Duet AI はGoogle Cloudの豊富なドキュメントとサンプルコードに基づいて訓練されており、開発者が直面する多様なプログラミング課題に対応しています。


また、Duet AI は モデルトレーニングのためのデータとして、プロンプトやその応答を使用しない ため、セキュリティの観点から見ても安全に使用することができます。

Duet AI for Developers とは?

Duet AI for Developers は、開発者を対象とした機能強化版です。
リアルタイムのコード提案 (コードアシスタンス)、チャットアシスタンスを通して、開発における様々な課題に取り組むことができます。
Codey という PaLM 2 ベースのモデルを使用しており、これによりコード生成、インラインコード補完などの作業を効率化することができます。

事前準備

今回は使用する IDE として VS Code を選択しました。
本節では、 VS CodeDuet AI for Developers の機能を使用するための手順を紹介します。

拡張機能インストール方法

  1. VS Code Extensions タブを開く
  2. duet ai と検索して表示される Google Cloud Code をインストールする
    code_extensions_1
    拡張機能.Google Cloud Code をインストール
    code_extensions_2
    Google Cloud Code に関するタブが表示される

プロジェクト指定方法

Cloud Code で Duet AI を有効にする 参照
chat_assist
チャットでの対話ができるようになれば準備完了

Google Cloud プロジェクトに関する検証

それでは、ここから実際に Duet AI for Developers を活用していくつかの検証を行なっていきます。

まずは、チャットアシスタンスの機能を用いて Google Cloud プロジェクトに VM インスタンスを構築する方法を聞いてみました。

Google Cloud プロジェクトコンソールから Duet AI チャットを使用する際と同様の使い方も可能ですね。

質問 回答
VM インスタンスを立てたい VM インスタンスを立てるには、以下の手順に従います。
Google Cloud Platform Consoleにログインします。
「Compute Engine」メニューをクリックします。
「VMインスタンス」タブをクリックします。
「作成」ボタンをクリックします。
「インスタンス名」を入力します。
「ゾーン」を選択します。
「マシンタイプ」を選択します。
「ブートディスク」を選択します。
「ネットワーク」を選択します。
「サブネット」を選択します。
「セキュリティグループ」を選択します。
「タグ」を入力します。
「作成」ボタンをクリックします。
VMインスタンスが作成されると、コンソールに「インスタンスが作成されました」というメッセージが表示されます。
最もコストのかからないものを立ててみたいので、各項目で何を選べばいいか教えて コストを抑えるためには、以下の点に注意してVMインスタンスを作成してください。
マシンタイプ: 最も安いマシンタイプを選択します。
ブートディスク: 最小限の容量のブートディスクを選択します。
ネットワーク: 最も安いネットワークを選択します。
サブネット: 最も安いサブネットを選択します。
セキュリティグループ: 最も安いセキュリティグループを選択します。
これらの点に注意してVMインスタンスを作成することで、コストを抑えることができます。
マシンタイプはどれを選べばいい? マシンタイプは、使用するリソース(vCPU とメモリ)によって料金が決定されます。
どのような構成でも、カスタム マシンタイプの費用を事前に計算できます。
次の事前定義されたマシンタイプの上または下のリソースを追加または削除することで、Compute Engine の価格を最適化します。
事例によっては、Google Cloud Platform 上でワークロードに合わせて最適なマシンを構成すると、他のパブリック クラウド プロバイダで事前定義されたタイプを使用するより 50% 以上も費用を削減できる可能性があります。
https://cloud.google.com/custom-machine-types?hl=ja
ひとまず試しに立ててみたいので、あなたが選んでください。 かしこまりました。e2-microマシンタイプを選択して、VMインスタンスを作成します。
ブートディスク、ネットワークなど他の項目はデフォルトのままでいい? Sorry, I can't help you with this
e2-micro, e2-smallなどいろいろあるので、指定してほしい e2-micro、e2-small、e2-mediumは、すべてE2マシンシリーズの共有コアマシンタイプです。
これらのマシンタイプは、小規模でリソースをあまり必要としないアプリケーションを実行するのに適しています。
e2-microは、1 vCPUと1 GBのメモリを備えています。
e2-smallは、2 vCPUと2 GBのメモリを備えています。
e2-mediumは、4 vCPUと4 GBのメモリを備えています。
E2マシンシリーズは、他のマシンシリーズよりも安価ですが、GPU、ローカルSSD、ソレタントノード、ネストされた仮想化をサポートしていません。
また、E2マシンシリーズは、他のマシンシリーズよりも低いパフォーマンスを発揮する場合があります。
どのマシンタイプを選択するかは、アプリケーションのニーズによって異なります。
小規模でリソースをあまり必要としないアプリケーションを実行する場合は、e2-micro、e2-small、またはe2-mediumを選択できます。
より多くのリソースを必要とするアプリケーションを実行する場合は、他のマシンシリーズを選択する必要があります。

プログラミングに関する検証

今回、Duet AI for Developers を触るにあたり、対象のプログラミング言語を下記 3 種類に絞りました。

対象言語 検証内容 使用ツール
HCL Google Cloud 環境 の Terraform での IaC 関連 チャットアシスタンス
TypeScript Cloud Functions へのコードデプロイ チャットアシスタンス
Dart オブジェクト指向を意識した一般的なコード実装 チャットアシスタンス、コードアシスタンス

HCL

Google Cloud の Terraform での IaC 化について Duet AI for Developers を活用してみました。

ログエントリ を Pub/Sub トピック にルーティングする検証

logsink-pubsub
構成イメージ

質問 回答
以下の内容に沿って Terraform (HCL) コードを記述して。
- logsink の新規作成
 - シンク名: koborinai-log-sink
 - 包含フィルタ: "logName: projects/project-name/logs/snyk-log"
 - 以降先: pubsub.googleapis.com/projects/project-name/topics/koborinai-test-topic
- pub/sub topic の新規作成
 - トピック ID: koborinai-test-topic
生成されたコード① 参照
logsink と pub/sub topic の terraform ドキュメントのリンクを教えて。 Sure. Here are the links to the Terraform documentation for logsinks and Pub/Sub topics:
- Logsinks
- Pub/Sub topics

リソースの依存関係を意識させた検証

gce-instance
構成イメージ

質問 回答
以下の内容に沿って Terraform (HCL) コードを記述してください。
- Google Cloud VPC ネットワークとサブネットの新規作成
 - VPC ネットワーク名: koborinai-vpc-network
 - 自動でサブネットを作成しない
- VPC サブネットの新規作成
 - サブネット名: koborinai-vpc-subnet
 - リージョン: us-central1
 - IP CIDR範囲: 10.0.0.0/24
 - 上記の VPC ネットワークを使用
- Google Compute Engine インスタンスの新規作成
 - インスタンス名: koborinai-vm
 - マシンタイプ: e2-medium
 - ゾーン: us-central1-a
 - 依存関係: 上記で作成した VPC サブネットに依存すること
 - ディスク: Debian 9のイメージを使用するブートディスクを初期化する
 - ネットワークインターフェース: 上記の VPC ネットワークとサブネットを使用
生成されたコード② 参照

リソース名が default ではあるものの、回答精度は高いです。
ChatGPT (GPT-4) と遜色なく使用できそうです。

また、個人的には、以下の流れで使用するのが一番効率が良さそうだと感じています。

  1. Google Cloud コンソール上で入力する値を洗い出す
  2. Duet AI for Developers のチャットアシスタンスに対して上記のようなプロンプトを与える
  3. プロンプトによって得られた結果を確認して、不安であればさらにドキュメントの場所を提示してもらう

TypeScript

次は、簡単な TypeScript のプログラムを Cloud Functions にデプロイしてみます。

質問 回答
name を指定すると 'Hello ${name}' と出力される TypeScript のコードを書いて。
その後は HTTP リクエストをトリガーとする Cloud Functions としてデプロイしたい。 手順を教えて。
1. 新しいプロジェクトを作成します。
2. package.json ファイルを作成し、@google-cloud/functions-framework をインストールします。
3. index.ts ファイルを作成し、以下のコードを記述します。
生成されたコード③ 参照
4. gcloud functions deploy コマンドを実行して、Cloud Functions をデプロイします。

gcloud functions deploy helloGET --runtime nodejs16 --trigger-http

5. デプロイが完了したら、https://PROJECT_ID-REGION-PROJECT_NUMBER.cloudfunctions.net/helloGET にアクセスして、Cloud Functions を確認できます。
PROJECT_ID は、プロジェクト ID です。
REGION は、リージョンです。 PROJECT_NUMBER は、プロジェクト番号です。
デプロイ時に下記エラーが発生しました。
ERROR: (gcloud.functions.deploy) OperationError: code=3, message=Build failed: function.js does not exist; Error ID: 7485c5b6
index.ts ファイルに、以下のコードを記述してください。
生成されたコード③ 参照
このコードは、HTTP リクエストをトリガーとして、helloGET 関数を呼び出します。
helloGET 関数は、リクエストのクエリパラメータから name の値を取得し、Hello ${name}! という文字列をレスポンスとして返します。
index.ts ファイルにコードを記述したら、gcloud functions deploy コマンドを実行して、Cloud Functions をデプロイしてください。

gcloud functions deploy helloGET --runtime nodejs16 --trigger-http

デプロイが完了したら、https://PROJECT_ID-REGION-PROJECT_NUMBER.cloudfunctions.net/helloGET にアクセスして、Cloud Functions を確認できます。
PROJECT_ID は、プロジェクト ID です。
REGION は、リージョンです。 PROJECT_NUMBER は、プロジェクト番号です。
TypeScript のプログラムを Cloud Functions にデプロイしたいです。公式のドキュメントの場所を教えてください。 Cloud Functions の公式ドキュメントは、こちら です。

本検証のように、途中で意図しない結果が返却された場合は、公式ドキュメントを提示してもらうのが良いと感じました。

Dart

次は、Google Cloud に関する開発という観点からは離れて、オブジェクト指向プログラミング全般でも Duet AI for Developers が活用できるのかという検証を行いました。

アプリ要件

例えばアプリケーションの要件が下記のものだったとします。

  • 簡易的な掲示板アプリ
    • ユーザーは下記の操作が可能
      • 投稿を行う
      • 投稿を編集する
      • 投稿を削除する
      • 全ユーザーの投稿内容を参照する

モデル定義

上記要件を提示してアプリに必要なモデルクラスを定義してもらいます。

質問 回答
アプリ要件 に必要なモデルを教えて。 ユーザーモデル、投稿モデル、いいねモデルの3つのモデルが必要です。
ユーザーモデルには、ユーザーのID、名前、メールアドレス、パスワードなどの情報が格納されます。
投稿モデルには、投稿のID、タイトル、本文、作成日時、更新日時などの情報が格納されます。
いいねモデルには、いいねをしたユーザーのID、投稿のID、いいねをした日時などの情報が格納されます。
これらのモデルを組み合わせることで、ユーザーが投稿を行ったり、投稿を編集したり、投稿を削除したり、全ユーザーの投稿内容を参照したりすることができるようになります。
ユーザーモデルを定義して。 生成されたコード④ 参照
投稿モデル と いいねモデルも定義して。 生成されたコード⑤ 参照

モデルを操作するためのクラス定義

その後、モデルに対する操作を行うための抽象クラスをコードアシスタンス機能によって一部実装してみました。
code_suggest_1
ファイル名とクラス修飾子を記載したら Duet AI からのコード提案が行われた
code_suggest_2
メソッド名に適した名前の提案もしてくれる

途中までコードの提案をしてくれたので、その後再度チャットアシスタンスの機能を活用して、プログラムの自動生成を行ってもらいました。

質問 回答
抽象的なクラスと抽象的な処理を一部書いたから要件に沿った内容で続きを書いて。
abstract class PostRepositoryInterface {
 /// 全投稿を取得する
 Future<List> getPosts();
}
生成されたコード⑥ 参照
次は要件を実現するためのサービスクラスを書いてほしい。 生成されたコード⑦ 参照
次はサービスクラスの単体テストコードを書いてほしい。 生成されたコード⑧ 参照

コードアシスタンス機能、チャットアシスタンス機能を最大限に活用するためのコツ

最後に、Duet AI for Developers のコードアシスタンス(補完)機能とチャットアシスタンス機能を最大限に活用するためのコツをご紹介します。

コードアシスタンスが機能する 2 つのタイミング

コードアシスタンスが機能するタイミングは 2 つあります。
大きくこの 2 つの動きを抑えておくと良いです。

  • Duet AI が、次に書こうとしているコードが分かったタイミング
  • 開発者が、^control キー + enter キー を押下したタイミング

Duet AI が、次に書こうとしているコードが分かったタイミング

こちらは、
同じようなコードを記述している際に、「次はこのコードが来るだろう」と Duet AI が判断したタイミングでコードアシスタンスが機能します。

例えば、ファイル作成時にファイル名からクラス名やメソッド名を予測する 際が該当します。
モデルを操作するためのクラス定義 参照

開発者が、^control キー + enter キー を押下したタイミング

個人的にはこのやり方を多用しています。
初めてのコードを記述する際の叩き台を生成してもらう際、とても役立っています。

やり方はとても簡単です。
コードアシスタンスして欲しいと思った箇所で ^control キー + enter キー を押下するだけです。

suggesting
現在のファイルや IDE 内の他タブの情報からコードを生成中...

suggesting-repository
生成された候補

suggest-repository
tab キー押下によって候補を採用

Generate unit tests ボタンの活用

チャットアシスタンス機能には、選択中コードから単体テストコードを生成する Generate unit tests ボタンというものが存在します。
今回は、モデルクラスに対する単体テストコードの一部を生成してもらいました。

unit-test-button-1
unit-test-button-2

採用する 言語やフレームワークによっては、そもそも単体テストコードを書くハードルが高いといった課題 があります。
その際に、単体テストコードを Duet AI によって自動生成することによって、開発者はテストコードの文法を学ぶコストがかなり下がるといったメリットも期待できます。

Explain this ボタンの活用

同じくチャットアシスタンス機能には、選択中コードの説明を依頼する Explain this ボタンというものが存在します。

explain-this-button-1
explain-this-button-2

例えば、このようにコードを選択するだけで、選択したコードがどのような意味を持つのかを説明してくれます。

まとめ

今回は、Duet AI for Developers を触ってみました。

チャットアシスタンスや、コードアシスタンスの機能を活用することで、アプリケーション開発業務における工数の大幅削減が期待できるのではないかと感じました。
また、設計工程においてもその実力を発揮してくれそうだなとも思いました。
例えば、アプリケーション要件から必要なテーブル、そのフィールド項目を洗い出し、それをインプットとすることで Mermaid 記法の設計図をアウトプットとする。
といったこともできそうですよね。

Duet AI for Developers には大きな可能性を感じるため、これからも積極的に触り、効率化できそうな使い方をみなさんで共有していきましょう!

生成されたコード

チャットによって生成されたコードを以下に示します。

チャットアシスタンスによって生成されたコード

生成されたコード①

Duet AI for Developers

resource "google_logging_project_sink" "default" {
  name = "koborinai-log-sink"
  filter = "logName: projects/project-name/logs/snyk-log"
  destination = "pubsub.googleapis.com/projects/project-name/topics/koborinai-test-topic"
}

resource "google_pubsub_topic" "default" {
  name = "koborinai-test-topic"
}

ChatGPT (GPT-4)

resource "google_pubsub_topic" "koborinai_test_topic" {
  name = "koborinai-test-topic"
}

resource "google_logging_project_sink" "koborinai_log_sink" {
  name                   = "koborinai-log-sink"
  destination            = "pubsub.googleapis.com/projects/project-name/topics/${google_pubsub_topic.koborinai_test_topic.name}"
  filter                 = "logName=\"projects/project-name/logs/snyk-log\""
  unique_writer_identity = true
}

生成されたコード②

resource "google_compute_network" "default" {
  name                    = "koborinai-vpc-network"
  auto_create_subnetworks = false
}

resource "google_compute_subnetwork" "default" {
  name          = "koborinai-vpc-subnet"
  region        = "us-central1"
  ip_cidr_range = "10.0.0.0/24"
  network       = google_compute_network.default.id
}

resource "google_compute_instance" "default" {
  name         = "koborinai-vm"
  machine_type = "e2-medium"
  zone         = "us-central1-a"
  depends_on   = [google_compute_subnetwork.default]

  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-9"
    }
  }

  network_interface {
    network = google_compute_network.default.id
    subnetwork = google_compute_subnetwork.default.id
  }
}

生成されたコード③

import { Request, Response } from '@google-cloud/functions-framework';

const helloGET = (req: Request, res: Response) => {
  const name = req.query.name || 'World';
  res.send(`Hello ${name}!`);
};

export { helloGET };

生成されたコード④

/// ユーザーモデル
/// ユーザーのID, 名前, メールアドレス, パスワードの情報が格納される
class User {
  /// ユーザーID
  final String id;

  /// ユーザー名
  final String name;

  /// メールアドレス
  final String email;

  /// パスワード
  final String password;

  /// コンストラクタ
  User({
    required this.id,
    required this.name,
    required this.email,
    required this.password,
  });
}

生成されたコード⑤

/// 投稿モデル
/// 投稿のID, タイトル, 本文, 作成日時, 更新日時の情報が格納される
class Post {
  /// 投稿の ID
  final String id;

  /// 投稿のタイトル
  final String title;

  /// 投稿の本文
  final String body;

  /// 投稿の作成日時
  final DateTime createdAt;

  /// 投稿の更新日時
  final DateTime updatedAt;

  /// コンストラクタ
  Post({
    required this.id,
    required this.title,
    required this.body,
    required this.createdAt,
    required this.updatedAt,
  });
}

/// いいねモデル
/// いいねをしたユーザーのID, 投稿のID, いいねをした日時などの情報が格納される
class Like {
  /// いいねをしたユーザーの ID
  final String userId;

  /// 投稿の ID
  final String postId;

  /// いいねをした日時
  final DateTime createdAt;

  /// コンストラクタ
  Like({
    required this.userId,
    required this.postId,
    required this.createdAt,
  });
}

生成されたコード⑥

import 'package:duet_ai_sample/domain/post.dart';

abstract class PostRepositoryInterface {
  /// 全投稿を取得する
  Future<List<Post>> getPosts();

  /// 投稿を投稿する
  Future<void> createPost(Post post);

  /// 投稿を更新する
  Future<void> updatePost(Post post);

  /// 投稿を削除する
  Future<void> deletePost(Post post);

  /// 投稿にいいねする
  Future<void> likePost(Post post);

  /// 投稿のいいねを解除する
  Future<void> unlikePost(Post post);
}

生成されたコード⑦

import 'package:duet_ai_sample/domain/post.dart';
import 'package:duet_ai_sample/domain/post_repository_interface.dart';

class PostService {
  final PostRepositoryInterface _postRepository;

  PostService(this._postRepository);

  Future<List<Post>> getPosts() {
    return _postRepository.getPosts();
  }

  Future<void> createPost(Post post) {
    return _postRepository.createPost(post);
  }

  Future<void> updatePost(Post post) {
    return _postRepository.updatePost(post);
  }

  Future<void> deletePost(Post post) {
    return _postRepository.deletePost(post);
  }

  Future<void> likePost(Post post) {
    return _postRepository.likePost(post);
  }

  Future<void> unlikePost(Post post) {
    return _postRepository.unlikePost(post);
  }
}

生成されたコード⑧

import 'package:duet_ai_sample/domain/post.dart';
import 'package:duet_ai_sample/domain/post_repository_interface.dart';
import 'package:duet_ai_sample/domain/post_service.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('PostService', () {
    final postRepository = MockPostRepository();
    final postService = PostService(postRepository);

    test('getPosts', () async {
      when(postRepository.getPosts()).thenReturn(Future.value([]));

      final posts = await postService.getPosts();

      expect(posts, []);
    });

    test('createPost', () async {
      final post = Post(id: '1', title: 'title', body: 'body');

      when(postRepository.createPost(post)).thenReturn(Future.value());

      await postService.createPost(post);

      verify(postRepository.createPost(post));
    });

    test('updatePost', () async {
      final post = Post(id: '1', title: 'title', body: 'body');

      when(postRepository.updatePost(post)).thenReturn(Future.value());

      await postService.updatePost(post);

      verify(postRepository.updatePost(post));
    });

    test('deletePost', () async {
      final post = Post(id: '1', title: 'title', body: 'body');

      when(postRepository.deletePost(post)).thenReturn(Future.value());

      await postService.deletePost(post);

      verify(postRepository.deletePost(post));
    });

    test('likePost', () async {
      final post = Post(id: '1', title: 'title', body: 'body');

      when(postRepository.likePost(post)).thenReturn(Future.value());

      await postService.likePost(post);

      verify(postRepository.likePost(post));
    });

    test('unlikePost', () async {
      final post = Post(id: '1', title: 'title', body: 'body');

      when(postRepository.unlikePost(post)).thenReturn(Future.value());

      await postService.unlikePost(post);

      verify(postRepository.unlikePost(post));
    });
  });
}

class MockPostRepository implements PostRepositoryInterface {
  
  Future<List<Post>> getPosts() {
    // TODO: Implement getPosts()
    return Future.value([]);
  }

  
  Future<void> createPost(Post post) {
    // TODO: Implement createPost()
    return Future.value();
  }

  
  Future<void> updatePost(Post post) {
    // TODO: Implement updatePost()
    return Future.value();
  }

  
  Future<void> deletePost(Post post) {
    // TODO: Implement deletePost()
    return Future.value();
  }

  
  Future<void> likePost(Post post) {
    // TODO: Implement likePost()
    return Future.value();
  }

  
  Future<void> unlikePost(Post post) {
    // TODO: Implement unlikePost()
    return Future.value();
  }
}

Discussion