効率化 生成AI

ズボラエンジニアのChatGPT活用事例と今後やりたいこと!

「AI勉強しないとエンジニアとして遅れをとるんやろな〜焦る〜〜〜〜」と思いつつ、必要に迫られないと勉強ができないエンジニアの皆様、こんにちは。

今回は、僕が日々の業務とか私的な調査でChatGPTをどんな感じで使っているかを紹介します。

やってきたこと

めんどい調査を効率化①:github APIを呼ぶ

こちらの記事で使ったもの。

特定のブランチに、特定に期間に入ったPullRequestをスプシにまとめてコメントつけたりしたいんやけど、それってgithubAPI使ったらいける?行けそうならちょっと叩き作って〜

みたいなことを聞いて、ズレたところにツッコミ入れながらGPTと一緒にスクリプトを作っていきました。
所要時間1hぐらい。

めんどい調査を効率化②:マニアックな文字列検索をするスクリプトを書いてもらう

opnecsvを使ってCSVを読み込む処理を書いていたときのこと。

マルチバイト文字で書かれたヘッダ文字を「正確に」一覧化して設計書に反映しないといけない時がありました。読み込む対象のクラスはこんな感じです。

import com.opencsv.bean.CsvBindByName;

public class UserCsvRecord {

    @CsvBindByName(column = "ID")
    private int id;

    @CsvBindByName(column = "氏名(漢字)")
    private String nameKanji;

    @CsvBindByName(column = "氏名(カナ)")
    private String nameKana;

    @CsvBindByName(column = "メールアドレス")
    private String email;
    
    ...
}

そもそも項目数が多かったのと、全角カッコとか半角カッコとか、コピペしているうちに要らん加工を施してしまいそうだったので、機械的にgrepするスクリプトを書いてもらうことにしました。

@CsvBindByName(column = の後に続くマルチバイト文字列をザーッとカンマ区切りで出すようなスクリプト書いてー

#!/bin/bash

# ファイル名指定(例: UserCsvRecord.java)
FILE="UserCsvRecord.java"

grep '@CsvBindByName(column *= *"' "$FILE" | \
  sed -E 's/.*@CsvBindByName\(column *= *"(.*?)"\).*/\1/' | \
  paste -sd, -

(※後から考えたらIntelliJの複数ファイル検索で正規表現使うのでもいけたな...というのに気づきました。ただこっちのGPT案だと自分が正規表現考える手間すら要らず雑に指示出せるので、それなりのメリットはあるはず。その時点での作業者の慣れ次第な気はします。)

私用で興味ある技術検証してみたいとき、新規導入するまでの環境構築サポートをしてもらう

  • 「そういえばKotlinFestでORMはJOOQおすすめって聞いたな」
  • →ワイJPAしかほとんど触ってないわ。JOOQってどんな書き味になるのか、知りたい(手で触って感触確かめたい)
  • →テキトーな要件作ってミニアプリ立ち上げたいけど、JOOQ以外にも書かないといけないコードあるな、めんどくせー
  • →以下のスクリプトを投げて考えてもらう

LocalでJooq使った簡単TODOアプリケーションを作りたい
MySQLベースで、Dockerで簡単に立ち上げられるやつ

サブクエリ使ったちょっと複雑なSQLも書き味見てみたい。

ちょっとサブクエリ使わんとかけないような面倒な要件を考えてみてほしい〜

色々GPT君に教えてもらった結果、こういう検証リポジトリを作ることができています。

https://github.com/kannna5296/jooq-todo-app

@Repository
class TodoRepository(
	private val dsl: DSLContext
) {
	// 今月最も時間がかかったタスクをサブクエリを使って取得する
	// サブクエリを使って、今月最も時間がかかったタスクを取得する
	fun fetchMostTimeConsumingTaskForThisMonth(): List<MaxDurationTodoDto> {
		val now = LocalDate.now()
		val currentMonthStart = now.withDayOfMonth(1).atStartOfDay()
		val currentMonthEnd = now.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59)

		val durationField = timestampDiff(
			DatePart.MINUTE,
			TODO_LOGS.START_TIME.cast(Timestamp::class.java),
			TODO_LOGS.END_TIME.cast(Timestamp::class.java)
		)

		val rankField = rowNumber().over()
			.partitionBy(TODOS.USER_ID)
			.orderBy(durationField.desc())

		val doneTaskByUser = dsl
			.select(
				TODOS.USER_ID,
				TODOS.ID.`as`("todo_id"),
				TODOS.TITLE,
				durationField.`as`("duration_in_minutes"),
				rankField.`as`("rank")
			)
			.from(TODOS)
			.join(TODO_LOGS).on(TODOS.ID.eq(TODO_LOGS.TODO_ID))
			.where(
				TODOS.DONE.isTrue
					.and(TODO_LOGS.START_TIME.between(currentMonthStart, currentMonthEnd))
			).asTable("doneTaskByUser")

        // 重複を排除し、1位のタスクのみを取得
		val result = dsl
			.select(
				USERS.ID.`as`("user_id"),
				USERS.NAME.`as`("user_name"),
				doneTaskByUser.field("todo_id"),
				doneTaskByUser.field("title"),
				doneTaskByUser.field("duration_in_minutes")
			)
			.from(doneTaskByUser)
			.join(USERS).on(doneTaskByUser.field("user_id", Int::class.java)?.eq(USERS.ID))
			.where(doneTaskByUser.field("rank", Int::class.java)?.eq(1))
			.fetch()
		// 結果をMaxDurationTodoDtoにマッピング
		return result.map {
			MaxDurationTodoDto(
				userId = it.get("user_id", Int::class.java),
				userName = it.get("user_name", String::class.java),
				todoId = it.get("todo_id", Int::class.java),
				title = it.get("title", String::class.java),
				duration = it.get("duration_in_minutes", Int::class.java)
			)
		}
	}
-- 雑に考えてもらったテーブル設計(MySQL)
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(100) NOT NULL
);

CREATE TABLE todos (
  id INT PRIMARY KEY AUTO_INCREMENT,
  user_id INT NOT NULL,
  title VARCHAR(255) NOT NULL,
  done BOOLEAN NOT NULL DEFAULT FALSE,
  created_at DATETIME NOT NULL,
  FOREIGN KEY (user_id) REFERENCES users(id)
);

CREATE TABLE todo_logs (
  id INT PRIMARY KEY AUTO_INCREMENT,
  todo_id INT NOT NULL,
  start_time DATETIME NOT NULL,
  end_time DATETIME NOT NULL,
  FOREIGN KEY (todo_id) REFERENCES todos(id)
);

よくわからん技術用語について調べるときの初手

Dockerを立ち上げる環境としてlimaを触っていた時、「VZ」と「QEMU」という単語が何の設定値なのかよくわかりませんでした。

現場では「VZ使っとこう〜」という雰囲気があったのですが、それはそもそもなんでなの??というのが不思議でした。

そこでGPT先生にしれっと教えてもらいました。

VZとQEMUって何が違うん

結局回答としては、以下のような感じ。

  • どちらも仮想化技術
  • VZ(Virtualization.framework)
    • Appleが提供する仮想化フレームワークでmaOS専用
    • AppleSillicon機ならば立ち上げ速度が速い
  • QEMU
    • いろんな環境で動作する(Linux,Windows、macOS)
    • パフォーマンスはVZの方が早いことがある

生成AIはこういった「概要」を掴むときにもよく使います。

Googleに聞いた場合、「わかりやすく説明してくれているサイトにヒットしないと理解に時間がかかる」リスクことがあるので、概要掴むだけならこっちの方が楽かな、というのが僕の所感です。

(正確性は担保されないので、もちろん自分で検証してみたり公式サイト見に行ったり、は大事ですが。)

やりたいこと

「Vibe Coding」ってやつ

個人的 Vibe Coding のやりかた

要件作るところからコード作るところまで、AIにやってもらうコーディング手法をこういうらしいですね。

最近思いついた作りたいものとして「IntelliJでの操作ログを分析して、「より効率的な使い方」や「覚えるべきショートカット」を提案してくれるAIプラグイン」があります。(この思いつきもAIと対話してて気付いたものなんですが。。)

こういうのをVibeCodingでやってみたいなあと思っています。新しい技術のキャッチアップにもなると思うし...

「テスト書いたうえでリファクタして」というやつ

テストが微妙に足りてない、またはMockまみれになっててそもそもテストとしてアカン、という時に使いたい。

ちゃんとセーフティネットになるようなテストを書いてくれるのか、結局介護しないと良いテスト書いてくれないのか、楽しみ。

-効率化, 生成AI