emahiro/b.log

日々の勉強の記録とか育児の記録とか。

LLM と serena をセットで使い続けている

Overview

タイトルのとおりなんですが、Serena MCPはClaude Codeを救うのか? の記事に触発されて、ここ2ヶ月くらい LLM (Claud Code) と serena をセットで利用しています。

最初はトークン数を削減できるとか、Agentic Coding ならではのペインを解決できるものとした一過性の流行りかなと思って静観していたのですが、使ってみながら挙動を眺めてると LLM も人間と同じようなコードの読み方をしているんだなということを感じられて、アウトプットの精度も向上したので結果として使い続けています。

どういうところが人間とにているのか?

例えばある実装やコードの調査においてその依存先まで含めて調査をしたいときに LLM に「〇〇という関数の実装を依存先まで含めて詳細に調査し、説明してください」というプロンプトを入力したとします。
このとき、LLM は愚直に〇〇という関数とその依存先のコードをファイルを探して、依存先を grep して、というような流れをたどることが多いです。 さながら「人間が find して grep して」を繰り返すのと同じような振る舞いをしています。そしてやったことがあればわかりますが、この調べ方はめちゃくちゃハイコストです。現代でこんな感じで実装の調査をすることは稀ではないでしょうか?

基本的には Editor に備わっている依存先へのコードジャンプや Call Hierarchy を追っかけていって調査することが多いと思いいます。
serena を動かしてるときのコードの探索の仕方はさながらこれです。serena も LSP を使ったコードの解析をしてると明言しています。

つまり serena を使わずにコードを探索するというのはさながら今どきな Editor を使わずにメモ帳で開発してるようなもので、かなりハイカロリーなことを LLM に要求してることになるのとほぼ同じことなんだなと使っていて気づきを得ました。

人間ももはや Editor 同梱の LSP や独自の Index をつかったコードジャンプがない世界で実装を進めるなんてものは厳しいを通り越してもう無理なんじゃないかとすら思います。

LLM と LSP

serena はどういった立ち位置のプロジェクトなのかは自分はあまり詳しくないですが、LLM に LSP がついてコードを解釈できるようになった、というのはめちゃくちゃ効くな、と実感しました。
エントリでも言及されていますが、今後は LLM も IDE のようなコードを解析して index を貼っておく、という風に進化していきそうな気がします。というかコードを書くのであれば絶対必要なサポートだと実感します。

自分ですらもう Editor のサポートがない世界でプログラミングをするのは無理なので、LLM もまぁ多分ハイカロリーなんだろうと想像しています。そしてそれを少しでも減らせるなら使って損はないなと思います。

Agentic Coding はモデルを持っているところが先行してこういったプロトコルのサポートを表明、サポートしていくことで開発者体験も高攘夷していくと思うので、ぜひとも各社頑張ってほしいとも思いました。

serena を使い続けてる理由

ちなみに LSP を使うのであれば普段自分自身は Go をメインで使っていて、公式が gopls の MCP を公開していたりするので、そちらを整備すれば良かったりするんですが、serena は Go 以外にも対応してることと、Docker をつかうと local で簡単に起動できるという手軽さ + コンテナイメージは公開されているので必要なツールが増えたときに公式の対応待たずとも自分でイメージをカスタマイズすればいい、というポータビリティの高さが自分の使用感にマッチしているので利用を続けています。
モデルを提供してる公式が自前の Coding Agent に LSP を導入してくるまでは利用続けようかなと思っています。(なお明日には別のツールに移行してる可能性もあります)

protovalidate でリクエスト内部の時刻型を検証するときに考えたこと

Overview

protovalidate でリクエスト内に定義した時刻型の検証を書いたときに考えたことを備忘録としてメモっておきます。

1. そのまま prorovalidate を書く

string start_datetime = 3 [(buf.validate.field).string = {pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{2}:\\d{2}$"}];

そのままですね。こんな感じで都度検証したい時刻の書式(このサンプルは RFC 3339)を正規表現で書きます。

2. 外部の定義を利用する

google/protobuf/timestamp.proto を利用する

下記のブログを参考にしました。

blog.soushi.me

型定義は以下。

https://github.com/protocolbuffers/protobuf/blob/89c585602af7d28646ce92cd3abba07cfdad7fa6/src/google/protobuf/timestamp.proto#L133-L144:embed:lang=proto

これは Go だと google.golang.org/protobuf/types/known/timestamppb.Timestamp 型になって利用できます。

type Timestamp struct {

    // Represents seconds of UTC time since Unix epoch
    // 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
    // 9999-12-31T23:59:59Z inclusive.
    Seconds int64 `protobuf:"varint,1,opt,name=seconds,proto3" json:"seconds,omitempty"`
    // Non-negative fractions of a second at nanosecond resolution. Negative
    // second values with fractions must still have non-negative nanos values
    // that count forward in time. Must be from 0 to 999,999,999
    // inclusive.
    Nanos int32 `protobuf:"varint,2,opt,name=nanos,proto3" json:"nanos,omitempty"`
    // contains filtered or unexported fields
}

ref:https://pkg.go.dev/google.golang.org/protobuf@v1.36.7/types/known/timestamppb#Timestamp

アプリケーションの中で time.Time 型として扱いたいときは、 AsTime メソッドを利用します。

googleapis/googleapis/datetime.proto を利用する

自公の 外部定義としては buf.build/googleapis/googleapis 配下の datetime.proto も利用できます。

これは timestamppb.Timestamp と異なり Datetime 形式で時刻を定義、検証ができます。
実際の定義 は以下。

https://github.com/googleapis/googleapis/blob/master/google/type/datetime.proto#L52-L94:embed:lang=proto

GoDoc は以下。

pkg.go.dev

timestamppb 型に比べるとこちらの方が直感的で使いやすいんじゃないかなと思います。

その他

google/protobuf は外部の定義を確認したいときに確認する

以下に色んな型が定義されているので定期的に参照すると良さそう。

github.com

カスタムエラーメッセージを定義する

時刻型に限った話じゃないのですが、validation のエラーが発生したとき、そのエラーメッセージをそのまま返してしまうと、思わぬアプリケーション内部の情報をクライアントに伝えてしまうことになるので、 proto 内の validation の内部で定義して、それをクライアントに返すようにしたい場合、CEL 記法を使って validation を拡張できます。

  string start_datetime = 1 [(buf.validate.field) = {
    cel: {
      id: "start_datetime.format"
      message: "開始日時はRFC3339形式(例: 2024-03-20T19:00:00+09:00)で入力してください"
      expression: "this.matches('^\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}[+-]\\\\d{2}:\\\\d{2}$')"
    }
  }];

デフォルトでは protovalidation の結果が返却されますが、上記の場合 message に指定したメッセージが返されます。

Firebase の API をエミュレートしてローカルでテストしやすくする

Overview

※ Firebase Go SDK のお話です。

Firebase を始め、GCP の公式の Go のライブラリは interceptor 等を公式で提供していないことが多く、local でテストをするときに GCP のライブラリそのものをモックするのが一般的なテスト方法かと思いますが、そもそもモックを差し替えたりと行ったことはテスト時の記述量が多くなってしまうので自分はあまり好みません。
Go にはせっかく httptest というテストサーバーを立ち上げるテストライブラリが公式から提供されているので極力こちらを使って実際のサーバーの振る舞いをエミュレートしてテストを書くことが多いです。

今回は同じアプローチで Firease のテストを書いてみます。

テストサーバーの作り方

Firebase Go SDKFIREBASE_AUTH_EMULATOR_HOST という環境変数が実行時にセットされていると自動的に emulator を利用して、実際 Firebase で行われている検証をスキップできるので、この環境変数をセットした httptest サーバーに http のリクエストをルーティングして payload の検証のみを行うだけで Firebase の通信をエミュレートできます。
※ payload の検証は Firebase の Call する API ごとに異なるので Emulate したい API ごとに httptest サーバー内の振る舞いは変更する必要があります。

実際のコードは この辺 です。

https://github.com/firebase/firebase-admin-go/blob/26dec0b7589ef7641eefd6681981024079b8524c/auth/token_verifier.go#L172-L175:embed:lang=go:h150

この isEmulator は Firebase クライアントを初期化するときに差し込んでいます。

https://github.com/firebase/firebase-admin-go/blob/d515faf47673ae79005d4b0abceca74716a5ac92/auth/auth.go#L69-L80:embed:lang=go:h300

これを使って httptest サーバーを立てるコードは以下です。

func UseFakeFirebase(t *testing.T) {
    t.Helper()
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
        //....
    }))
    t.Cleanup(ts.Close)
    t.Setenv("FIREBASE_AUTH_EMULATOR_HOST", strings.TrimPrefix(ts.URL, "http://"))
    t.Setenv("GOOGLE_CLOUD_PROJECT", "test")
}

利用するときは UseFakeFirebase(t) でテストサーバーが立ち上げます。
環境変数を実行時にセットしてるのでテスト内における Firebase のリクエストは自動的に httptest で立ち上げたサーバーに routing されます。

今回は Firebase.VerifyIDTokenAPI を Emulate するサーバーを実装してみました。
LLM に聞いて必要な API (Firebase Go SDK 内で実際に叩いている Firebase の API) について調べさせたところ accounts:lookup という path を call してる事がわかったので、以下のようになります。

   ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if strings.Contains(r.URL.Path, "accounts:lookup") {
            response := map[string]any{
                "kind": "identitytoolkit#GetAccountInfoResponse",
                "users": []map[string]any{
                    {
                        "localId":       "test-user",
                        "email":         "test@example.com",
                        "emailVerified": true,
                        "displayName":   "Test User",
                        "disabled":      false,
                    },
                },
            }

            w.Header().Set("Content-Type", "application/json")
            _ = json.NewEncoder(w).Encode(response)
            return
        }

        w.WriteHeader(http.StatusOK)
    }))

また検証するには Firebase 用のダミーの IdToken (JWT形式) を作ってこれを渡して疑似検証プロセス(payload の中に必須では行っていて欲しい field 群のみ Validation 処理が入っている) を通す必要があるのでそのダミー Token も作成します。

func genIdToken() string {
    now := time.Now().Unix()

    // JWT Header
    header := map[string]any{
        "alg": "none",
        "typ": "JWT",
    }

    // JWT Payload with Firebase-specific claims
    payload := map[string]any{
        "iss":            "https://securetoken.google.com/test",
        "aud":            "test",
        "sub":            "test-sub",
        "user_id":        "test-user_id",
        "iat":            now,
        "exp":            now + 3600,
        "auth_time":      now,
        "email":          "hoge@example.com",
        "email_verified": true,
        "firebase": map[string]any{
            "identities": map[string]any{
                "email": []string{"hoge@example.com"},
            },
            "sign_in_provider": "custom",
        },
    }

    headerStr := mustEncode(header)
    payloadStr := mustEncode(payload)

    return headerStr + "." + payloadStr + "."
}

func mustEncode(v any) string {
    b, _ := json.Marshal(v)
    return base64.RawURLEncoding.EncodeToString(b)
}

これ実際に利用するときはこんな感じでテストサーバー起動後、トークンを作成して VerifyToken を Call する、という流れで VeridyToken API の処理がエミュレートされます。

testhelper.UseFakeFirebase(t)
// Firebase の初期化処理 (略)

testToken := genIdToken()
if err := localVerifyIdToken(ctx, testToken); err != nil {...}

ここまで書きましたが、どんな API Call がされていて dummy 処理としてはどういう物が必要なのか?ということは実は LLM に聞くと大体回答してくれます。
ちょっと前までは実際に API の仕様調べてたんですけど、こういうテストのための事前準備と行った実装は LLM でだいぶ進めやすくなったなと思います。

Go で protoc のカスタムプラグインを書く

Overview

protoc のプラグインを自分で書いてみたのでその備忘録です。

なぜ protoc プラグインを自前で用意したくなったのか

ちょうど業務で RBAC 制御の実装を考えていたときに、過去何度もあたってきた「操作に対する Role、Permission をアタッチするマッピング」の管理方法をいくつか検討していて、これを proto で管理する拡張を作ることを考えました。

ただし、protoc にそういったカスタムフォーマットを parse する仕様はないので、自前で用意した、という経緯です。

カスタムプラグインの作成

今回作成したカスタム protoc プラグインでやってることは以下です。

  • protoc のプラグインとしての Go ツールを自前で用意。
  • 操作に対する permission を定義をするカスタム書式を proto に定義。
  • buf generate 実行時にカスタムプラグインコマンドを実行し、カスタム書式を解析 -> 操作:Permission のマップを作成。

ざっくりとしたディレクトリ構成は以下です。

.
├── cmd/
│   └── customPluginCmd/
│       ├── main.go
│       └── parser.go
├── proto/
│   ├── option.proto
│   └── operation.proto
├── gen/
│   └── rbac_map.go
├── buf.yaml
└── buf.gen.yaml

option.proto に今回の対象となる拡張設定を定義してそれを operation.proto で import して利用します。

option.proto は以下のように設定します。

syntax="proto3";

package proto;

import "google/protobuf/descriptor.proto";

extend google.protobuf.MethodOptions {
  Authz authz = 50001;
}

message Authz {
  repeated string permissions = 1;
}

"google/protobuf/descriptor.proto" は proto の service や message にカスタムメタデータを組み込むライブラリで、今回は MethodOptions を利用してメソッドにメタデータを付与してるので、 rpc $Method を拡張しています。MessageOptions では message (リクエストパラメータなど)にメタデータを付与できます。
またメタデータを付与するときはデフォルトの番号と競合しない拡張番号を付与する必要があります(ものすごい大きい数にとかにするとまぁコンフリクトは発生しないと思います)

このカスタムオプションを利用したい proto の定義で import すれば proto 自体はメタデータを読み込めるようになります。

syntax="proto3";

package proto;

import "proto/option.proto";

service Service {
  rpc Method (...) ... {
    option (proto.authz) = {
        permissions: [...]
    };
  }

このとき options の引数に渡す proto への path ですが (proto は path を A.B.C....のように表現します)、root ディレクトリからの options.proto までの path 定義 + メタデータの field 名になります。
今回のケースでは path が proto/option.protoメタデータの field 名が option.proto 内の Authz (authz) なので、option に渡す proto の定義までの path は proto.authz と表現されます。メタデータが定義されている proto までの path でこの option に渡す引数は変化します。

この状態で buf generate できれば proto の定義自体は通っています。あとは plugin の本体である Go のコマンドを buf generate 時に実行するように buf.gen.yaml に hook する処理を書きます。
今回のケースでは以下のようになります。

version: v2
clean: true
managed:
  enabled: true
  disable:
    - file_option: go_package_prefix
plugins:
  - local: [go, run, ./cmd/customPluginCmd]
    out: server/gen
    opt:
      - paths=source_relative
    strategy: all

これで buf generate したときに customPluginCmd が実行されます。

Plugin の実装自体は今回は割愛しますが、Go のコマンドが実行されるだけなので、proto から operation と permission を取り出して mapping する処理を書けば終わりです。自分はこの部分は LLM に書いてもらいました。

まとめ

  • 無いものは作る。
  • buf は hook 処理を書くのも楽。
  • hook 処理を登録するのも直感的。

ということで簡単に protoc のプラグインを書いて処理を拡張することができました。

protoc のプラグインを go tool で管理・実行する

Overview

buf.gen.yaml で local の protoc コマンドを hook して実行するときに go tool 経由で実行すると個人の環境差異にとらわれずに実行できて便利、という話です。

Motivation

proto からコードを生成するときに、今では buf generate を利用するのが一般的な選択肢ですが、 buf.gen.yaml で protoc のプラグイン(grpc や openapi) を利用するときに、各々の local 環境にこれからのプラグインがインストールされていて、かつバージョンが揃っていないと期待する生成結果を得られないことがあります。

事前に使うライブラリの種類やバージョンを指定して個々人の端末にインストールを行う make を作ることもできますが、結局実行し忘れたりで buf generate が正常に動作しないこともあります。

go tool で解決する

これを解決するために Go1.24 から導入された go tool を利用します。

詳細は以下ですがこれは Go プロジェクトにおける Module の管理の範囲を各種ツールにも広げた機能で、すでに一般的に使われるようになっているかと思います。

future-architect.github.io

結論から言ってしまうと、protoc に関連するツール郡(ほぼ Go 製)を tool としてプロジェクトの依存管理対象に設定し、 buf.gen.yaml の local での実行コマンドを go tool google.golang.org/protobuf/cmd/protoc-gen-go のように設定します。

例えば protoc において grpc, grpc-gateway, openapi を利用するときの go.modbuf.gen.yaml の plugin 設定は以下のようになります。

# go.mod
tool (
    github.com/bufbuild/buf/cmd/buf
    github.com/google/gnostic/cmd/protoc-gen-openapi
    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
    google.golang.org/grpc/cmd/protoc-gen-go-grpc
    google.golang.org/protobuf/cmd/protoc-gen-go
)
# buf.gen.yaml
version: v2
clean: true
managed:
  enabled: true
  disable:
    - file_option: go_package_prefix
plugins:
  - local: [go, tool, google.golang.org/protobuf/cmd/protoc-gen-go]
    out: server/gen
    opt:
      - paths=import
  - local: [go, tool, google.golang.org/grpc/cmd/protoc-gen-go-grpc]
    out: server/gen
    opt:
      - paths=import
  - local: [go, tool, github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway]
    out: server/gen
    opt:
      - paths=import
  - local: [go, tool, github.com/google/gnostic/cmd/protoc-gen-openapi]
    out: openapi
    opt:
      - output_mode=source_relative

これで buf generate すると go tool .... 経由で各種プラグインのコマンドが実行されるので、仮にプラグインのコマンドがインストールされ知なくても go tool 経由でプロジェクト内でインストールされ go tool 経由で実行されます。さらにバージョンも go.mod で管理されるのでバージョン違いによる、出力の違い、ということにも頭を悩ませることはありません。

まとめ

Go のプロジェクト限定にはなりますが、go tool で protoc のプラグインを管理するのは有用だなと思いました。

GitHub のレビュー画面で他の人のコメントを見えないようにする

ソースコードレビューをしてて

  • 他の人のコメントに引きづられて、コードをちゃんと自分事として見れない。
  • シンプルに他の人のコメントが自分がレビューするときに邪魔

と思ったことはありませんか?

自分は過去何度もあります。

他の人のコメントが見えなければいいのにな〜とずっと思ってましたが、 GitHub に個の機能が備わっていることを今日始めて知りました。

PR のレビュー画面の歯車マークを押すと以下のような画面が出てきますが、

この Minimize comments というを enable すると他の人コメントが見えなくなり(アイコン表示になりコメントされてることはわかる)、コードレビューの画面がめちゃくちゃスッキリします。

いつからこれができるようになってるのかわかりませんが、自分は長年 GitHub を使っていて今日始めて知りました。

メチャクチャ便利で快適です!

おしまい

Claude Code でもの作りが"本当に"変わるかもと思った

Cluade Code を体感して、"驚いてしまった" ことは前回のエントリに書いたのですが、このツールを触ってみたことでもの作り、特にソフトウェア開発における不可逆な変化を感じたので雑文として書き残しておきます。

ema-hiro.hatenablog.com

なお、明日には全く違うことを言ってる可能性もあります。

自分が Claude Code (後発の Gemini CLI 含む)が良かったのは CLI ツールだからなのですが、CLI ツールだと何故いいのか?というと動作環境を問わず、ターミナル上で動くことにほかならないと思います。

※ ちなみに自分は Claude Code が先駆けかと思っていたのですが、大手だと ChatGPT の Codex CLIAmazon Q に CLI ツールがあった んですね。

Cursor や GitHub Copilot を始めとした AI Agent は結局その Editor の UI を介してしか触れることができませんが、CLI ツールであればツールを問いません。Vim 使いにも emacs 使いにもその他の Editor 使いにも等しく LLM のインターフェースが提供されます。まずこの時点でツールとして有用だなと思いました。この時点でシンプルに体験が良いです。なぜなら上記のような Editor 同梱型の Coding Agent を利用しても、結局実行時にターミナルにコピペしたり結果をまた別のコマンドに渡したりといったひと手間が必要で、チャットUIだけではスムーズに行きませんでした。

Claude Code の体験の良さ、スッと自分の開発シーケンスに入ってくるこの感覚をいまいちうまく言語化できないんだけど、もともと Github Copilot にしろ Cursor にしろ、チャットUIでプロンプトのやり取りをするというあのフォーマットは過渡期の産物でもっと開発体験に直接作用するゲームチェンジャーが出てくるかもなと直感的に思ってたから、自然と受容できてる、というのはありそうな気がしている。

emahiro (@emahiro.bsky.social) 2025-06-19T11:51:57.228Z
bsky.app

CLI ツールであればパイプで繋いで入出力を繋いで様々なワークフローをワンライナーで構築できます。

ここで思い出したのは UNIX哲学 です。

特にここの3つが今回の CLI ツールとはマッチすると感じます。

  • ソフトウェアを梃子(てこ)として活用せよ (Use software leverage to your advantage.)
  • 梃子と移植性を高めるためにシェルスクリプトを使え (Use shell scripts to increase leverage and portability.)
  • 凝ったユーザーインターフェースは避けよ (Avoid captive user interfaces.)

CLI で提供されてるので既存のワークフローへの移植が用意で、かつそこで LLM の恩恵を受けることができるのでレバレッジが効きます。
これを最初から考えていたわけではないですが、触ってみて UNIX 哲学に則って作られたツールだなと感じ、だからこそ前述の通り、特に違和感なくスッと自分の開発体験に入ってきたんだと思います。

ここまでは CLI ツールの良さの話であり、本題のもの作りははどう変わるか?というところについては現時点での考えは以下です。

  • 人間がコードを書いてプロダクトを作ってた時代は "本当に" 終わる。
  • 機械にコードを書かせるためのドキュメントを用意することが大事。

「人間がコードを書いてプロダクトを作ってた時代は "本当に" 終わる *1」という点についてですが、今までもこれは言われていて自分も人間がコードを書く未来というのはもう無くなることが約束されているんだろうな?と思ってはいたのですが、実業務の中でそう入っても Cursor にしろ Copilot にしろ、これらが全て自分たちのワークフローを置き換えるのか?という点についてはまだ先が長そうだ、と直感的に考えていました。というのも、これは Editor 同梱という点が自分の中ではネックで、個人 PC で Editor を開いてそこでプロンプトをやり取りして成果物をレビューして、みたいな状態は目指すべき未来ではないと考えていたからです。

CLI ツールはこれを完全に覆して、ターミナル上で動作する以上別に場所を問わずに開発が続けられます。また前述の通り作業の成果についてはパイプで繋いで、別のプロセスに入出力を渡すことでインターフェースの制約すら存在しなくなります(もともと CLI ツールというのはそういうものですが)

これはもの作りのプロセス根本を変えると思っていて、「動作環境を問わない隔離された箱の中の自律的なプロセス」が動き続けることは、Editor がないと走れない今までの Coding Agent とは一線を画します。
Agent 前提のもの作りにおいて、もう人間が何かを操作する(プロンプトを考えたりする)ことすらなくなります。Agent が出た頃に言われていた「何を作るのか?を定義し、その定義を食わせ、出力をチェックする」という時代が本当に来ます。少なくとももう2,3 年のうちに一般的なソフトウェア開発の世界ではこの作り方が一般的になるでしょうし、この時代において大事なのは Agent の動く指針となる「何を作るのか?どう作るのか?が書かれたドキュメント」になります。Agent が走る道路を作るのが人間の仕事になるのではないかと思います。

体感するまでは、まだ先だろうと思っていた未来がすぐ手の届くところに来たというこの感覚、正直めちゃくちゃワクワクしました。
真面目に GitHub Copilot が登場したときや、初めて Google AppEngine に触れたときに高揚感に近いものを感じます。*2

自分がソフトウェア開発を生業にする中で、ここをこうしたら良くなるのにな〜と思っていたことがほぼ全て AI でやっていけそうだ、という直感があり、これを考えるのが楽しみで仕方ありません。

まだまだツール自体は進化すると思いますが、少なくとも1つのあり得る未来として CLI ツールが出てきてくれたことは本当に良かったと思います。
あと Claude Code の場合 Sonnet, Opus に最適化されているので、使い心地も群を抜いてよいです。Gemini CLIOSS として開発されていますが、OSS にせずに Gemini に最適化したチューニングとかしてくれると Claude Code と並んで良いツールになるんじゃないかなと思います。

思ったことをまとめただけなので本当に脈絡のない駄文になってしまいました。

繰り返しですが今考えていることというだけで明日には手のひら返ししてる可能性もあるのであしからずmm

おしまい

*1:もちろん LLM が入れない世界に置いてはまだまだ人間がコードを書く領域として残り続けると思いますが、それはごく一部の領域に限られ、大体のソフトウェア開発の領域においては人間がコードを書くことはなくなるんじゃないか?と思います。

*2:この2つは自分のエンジニアキャリアの中でも大きな転換点でした。

Claude Code に入門して衝撃を受けた

Overview

巷で大流行の Claude Code ですが、6/5 のアップデートで MAX プランに加入せずとも Pro プランである程度(Opus は使えないけど)使えるようになったので試してみて、そして衝撃を受けました。

今まで Copilot だ! Cursor だ!モデルは何だ!などとあちこち浮気しながら色々使っていましたが、もう Claude Code だけでいいと思うに至りました。

Claude Code の良さ

そもそもなんですが、自分は現在よく使われている Agent については過渡期のインターフェースだと思っていて、便利だから使ってるけど使っていての違和感というものを感じていました。

Claude Code の体験の良さ、スッと自分の開発シーケンスに入ってくるこの感覚をいまいちうまく言語化できないんだけど、もともと Github Copilot にしろ Cursor にしろ、チャットUIでプロンプトのやり取りをするというあのフォーマットは過渡期の産物でもっと開発体験に直接作用するゲームチェンジャーが出てくるかもなと直感的に思ってたから、自然と受容できてる、というのはありそうな気がしている。

emahiro (@emahiro.bsky.social) 2025-06-19T11:51:57.228Z
bsky.app

正直チャットでいちいち指示するのもめんどくさいし、プロンプトの結果として出てきたコマンド等をいちいちターミナルにコピペ作業するのも手間だと思っていました。Claude Code はターミナルにインテグレーションされたツールなので、そもそも結果のコマンド等を自動で実行してくれて出力を得ることができます。この手間がないだけでもだいぶマシです。

またこれは結構感覚的な表現ですがこの感覚はすごくわかるな〜と思いました。

なるほどなー。コード書いてもらうときのCursor/Copilotの饒舌さがきもかったんだけど、Claudeはエンジニア lingoをしゃべる感じだな。圧倒的に読みやすい。

Daisuke Maki (@lestrrat.bsky.social) 2025-06-19T13:20:47.592Z
bsky.app

自分と同じ土俵に経ってる、とも言うべき感覚に近いです。

極めて感覚的話をしてますが、道具である以上この「使い勝手の良さ」というのは本当に重要だなとも思います。

どういうことをしてみたのか?

とりあえず個人プロジェクトに全て CLAUDE.md を作成し、プロジェクトの概要等を全部 Claude で管理できるようにしました。そのうえで自分の持っているポートフォリオサイトである emahiro.dev で以下のことをやりました。

  1. Firebase -> Cloudflare への移行
  2. monorepo 化 (& pnpm の導入)
  3. OGP 画像生成への CloudFlare Workers の導入
  4. Firebase の依存コードの削除

どれもやろうやろうと思っていて全然時間がなくて手を付けられていなかった内容ですが、Claude Code ぶん回して2日程度で全て完了してしまいました。その間ほとんどコードレビューや差分のチェック(トークンが漏れてないか?などの最低限のレビューはした)をせずとも、動くコードとインフラの設定が出来上がってしまい、本当に衝撃を受けました(驚き屋は好きじゃないですが、驚かざるを得ませんでした)

2日後に自分のポートフォリオサイトを構成してるコードの9割は全て Claude に書いてもらったものに置き換わってしまったので、もはや「コーディングそのものは人間の仕事ではなくなった」というのは結構真実に近い話だと思います。

まとめ

Github Copilot が出た以来の衝撃を受けた Claude Code について、その衝撃の大きさにとにかく筆を進めてしました。

今までの Agent とは一線を画すぶっ壊れブキ的なレベルのものであり、AIに淘汰されると入ってもすぐにはソフトウェアエンジニアの仕事もなくならないだろう、、、と高を括っていた前提が瓦解し、自分の中でもそもそものものづくりの仕方の根底が変わるな〜という直感が大きくなってきました。
ものづくりの仕方についてはまた別のエントリで考えてることをつらつらまとめようと思います。

追記

ただこのエントリを書いてる最中に Google が Gemini CLI を出してきて、rate limit はあるものの Gemini 2.5 pro を無料でかなりの量分回せる環境を Google 様が提供してくれたので Claude 解約しました笑

github.com

追記2 (2025/06/26)

やっぱり Claude 再契約しました(1ヶ月分無駄にしましたw)

Gemini CLI の Rate Limit が思ったより早く来てしまい、Gemini 2.5 の場合Pro -> Flash で許容できないアプトプットの劣化を引き起こすので、Google 側の提供環境が落ち着いたり、Pro プランや Ultra プランとの連携で上限の枠が広がるなどの対応が出てくるまでは個人ユースであれば Claude Code でいこうと思います。掌返し大事。

apple/container を触ってみた

Overview

www.apple.com

WWDC 2025 で発表された apple/container を触ってみました。

github.com

How to use

以下にすべて書いてますのでここに書いてある通りにして実際にコンテナを立ち上げることができました。

再起動後に container system start をしないといけなかったりするのはちょっとめんどいですが。

感想

まず Mac を使うにあたって Docker なしでコンテナを立ち上げることができる、というのは嬉しいポイントでした。
Docker がエンタープライズ向けに有料化したり、動きが色々ある中でそもそもコンテナを立ち上げるのに何かをインストールする、みたいなことをしたくないな、と思うようになっていたので、Mac で特に何も意識せずに謹製ツールでコンテナの操作をできるというのは開発者としていい点だと感じました。

ただ、まだ出来立てホヤホヤのツールでもあるので、docker compose 互換じゃなくて手元の docker compose で立ち上げてるコンテナ群で作ってる local 環境をすぐ移行とかはできないですね。

issue も上がってますし、かなりホットな話題なので早晩対応されそうです。

github.com

すぐに乗り換えるとかはないですが、普段使ってる orbstack とかもいらなくなりそう。必要なツールは少ないほうがいいのでいいアップデートでした。

35歳になった

昨日 5/26 日で 35 歳になった。
そして世間で言うアラフォーというやつについに足を突っ込んだ。

今月第二子が生まれていて、プライベートはものすごくてんやわんやしているので、自分がまた1つ歳を重ねたという実感は実はあまりない。
35歳になったらキャリアの棚卸しとか、これからやりたいこととかちゃんと考えるか〜とこの5年間は考えていたけど、そんな余裕は完全になくなった。

気力と体力をほぼ全て育児に持っていかれた30代前半で、これは多分40になるまで続くと思うし、終わることはないと思う。

とりあえず健康でいれればなんでもいいや。

自分なりの Obsidian "with" Cursor (LLM) を始めてみた

Overview

Obisidian と Cursor を組み合わせてインプットのワークフローを構築するのが最近流行っていて、気になっていたので自分なりにこのインプットワークフローを組んでみて、その感想について記載します。

背景

実を言うと自分はあまりこういった効率UP系のコンテンツを残すことは少ないんですが、昨今 Obsidian in Cursor の話をあまりによく目にして、実際に触らず食わず嫌いも良くないので自分なりのメモワークフローというものを組みながら、何ができてなんでこんなに流行っているのか?ということを考えてみました。

なお、以下の記事は目を通して参考にしてみました。

note.com

note.com

結論

先に結論だけ書いておきます。現時点で自分のワークフローは以下になりました。

  • Daily Notes に日々の記録を書き、LLM でブログ形式でまとめてもらう。
  • Thino を使って生煮えの思考を Daily Note に記録して LLM でブログ形式でまとめてもらう。
  • Web Clipper で気になるページをクリップし、LLM に要約してもらう。気になるものは Thino 経由で保存する。

このワークフローを支えているのは、今のところ以下の plugin (core 2 つ、community 2 つ)です。

まだまだ発展途上なので使いながらブラッシュアップしていこうと思います。

そもそもなぜこんなに Obsidian と Cursor が流行っているのか

※ これは単なる自分の考察です。

Obsidian と Cursor を連携したインプットの効率化ハックがこれほどにまで流行るのは以下の理由があるのではないかなと思います。

  1. Web Clipper で ローカル に欲しい情報を md 形式で保存できる。
  2. ローカルに保存してあるので、Editor に同梱されている Agent (LLM) で要約やコンテンツの作成が簡単にできること。
  3. Obsidian 内部で知識を "うまく" 繋げて個人 wiki を作ることが用意であること

この中だと特に1の ローカルストレージに markdown 形式で置けること が非常に大きいのかなと思っていて、というのも今までも Web Clipper のようなツールは Notion なり Google Docs なりで提供されていて、自分なりの後で読むリストを作ることはできていました。

但し、Notion や Google Docs といったコラボレーション SaaS を利用してる以上そのコンテンツやデータに対して直で LLM を噛ませるということができません。ツール側で提供してもらう(Gemini インテーグレーションなど)か、コンテンツをコピペして LLM に噛ませる、などひと手間が必要でした。また

これがローカルストレージ(自分の PC 上)にコンテンツを配置するのであれば、そのデータの持ち主は自分自身になりますし、ローカルストレージ上のデータであれば同じくローカルにあるエディタクライアント(VSCode や Cursor) に同梱されている Agent に簡単に食わせることができます。
今回の場合は Obsidian Valut を VSCode や Cursor で開けば、あとは Editor 上で保存したファイルを開いて Cursor Agent なり Copilot なりに問い合わせをすれば、今までより簡単にコンテンツの要約や考察、二次コンテンツの生成まで可能になります。

結局今まであれこれ作業していたことをワンストップでできてしまう便利さが先行して流行っているのかな、という仮説を立てました。

なお、3 については Obsidian のベターなメモ記法 (Zettelkastenなど?) がありますが、これが「いい!」と思ってる人は今のはやりの前から Obsidian を利用していたと思うので今回は割愛します。ちなみに少しずつメモって有機的なつながりを作っていく、というのは Thino で少し実践してるので自分もこのメモ記法には価値を感じています。

まぁ、ちょっとこういった流行りには冷笑的なポジションを取りがちなんですが、使ってみると思いの外便利だったので流行りには乗っていこうと思いました。

P.S

エントリのタイトルに "with" を使っているのは、これ別に Cursor じゃなくても成り立つな?と思ったので "With LLM" の意味を持たせるために Obsidian in ~ ではなく Obsidian with ~ にしました。

2児の父になった

GW 中の 5/2 に2人目が生まれ、2児の父になりました。

2年ぶりの新生児の育児ですが、1度経験してるにも関わらずやったことは全て忘れていて、かつ上の子はちょうどイヤイヤ期真っ最中なのもあってとにかくバタバタでカオスな毎日が続いています。

というわけで、明日から 6 月末まで育休で 1.5 ヶ月ほどお休みします。
ただ、娘を保育園に送っていった後の日中は割と暇なので、またリモートで雑談する時間とか作ろうかなと思っています。暇つぶしに付き合ってあげようという優しい方はぜひ連絡ください笑。

育児がんばるぞい。

Cursor から Copilot に戻っている話

Overview

タイトルの通りなのですが、AI Coding ツールとして Cursor を利用してきましたが最近 Copilot の利用に戻っています。

なぜ戻ったか?

最大の理由は VSCode との親和性と Copilot がものすごい速度で Cursor (や Windsurf) などの AI エージェント内蔵エディタの機能をキャッチアップして機能差が減ってきていることです。

特にプロンプト入力周りの補助機能や MCP と連携したツールを実行するときの UX が Copilot の方が今は充実しています。

また最大のボトルネックだった Agent Mode 時の UX がちょうど GPT4.1 が登場したこともあり、精度はさておき大量の入力にも対応できるようになって Coding Agent の体験も改善したのも追い風になりました。

github.blog

実際コードを書くだけであればもう GPT4.1 だけ使っていればいいかな、という感じがあります。状況によっては Gemini 2.5 pro や o3 を利用することもありますが、自分はほとんど GPT 4.1 を利用しています。

実はこのエントリを書くまで Copilot に戻った最大の理由は VSCode のみが GitHub拡張機能をサポートしており、Cursor からは無理で、VSCodeからしGitHub の permlink を発行できないと思っていたのですが、Cursor にもこの GitHub の拡張をインストールできたので、同じことを Cursor でもできました笑. ただ後述もしますが、この拡張機能が Cursor でサポートされ続けるかは若干あやしい気がしています。

Cursor への不満と今後への不安

Claude との親和性が極めて高く、様々なモデルへの対応も即時に対応される Cursor の良さももちろんありますし、昨今 Cursor を使った様々な業務フローの改善 Tips 等も共有されていて、 Cursor は極めて汎用性の高いツールだなと思っていますが、自分は殆どコーディングにしか使っていないので今時点で Cursor でも Copilot でもどちらでも良い、というタイプになります。

また、Cursor のプレミアムリクエストを使っても度々 o3 や Gemini 2.5 pro といったプレミアムモデルへのリクエストが失敗することがあり、ユーザー数が爆発的に増えた結果諸々の使い勝手が若干悪くなってるなと感じてて、Cursor そのものへペインが少なからず溜まっていたこともあります。(Auto モードという選択肢もありますが、手動でモデルを指定する回数を減らしたいという意図とは裏腹に、プレミアムモデルへのリクエスト失敗が多いことから、モデルの利用キャパシティが逼迫気味なのかな?と邪推したりもしています)

最後に、これは本当にただの当てずっぽうの予想なのですが、VSCode の持ち主である Microsoft 側からの締め出しリスクということが懸念されます。

実際に C/C++ の拡張がブロックされてる事例もありますし(これはそもそものライセンスの問題らしいですが)

github.com

Cursor も Windsurf も OSS としての VSCode へのフリーライドに依存している点は正直気になっています。プロダクトの安定提供という観点で見るとどうしても大元を握っている企業が有利で、この場合Microsoftのさじ加減一つで状況が変わってしまう、みたいな点でどうしても信頼しきれない自分がいます。まぁMicrosoft側に立つとOSSで提供しているとはいえ、投資してきたコストにそのままフリー・ライドしている状況をどこまで許しておくのか、とも思いますし、Cursor に慣れきったところで VSCode が提供している拡張機能の利用ができなくなるといったことが起きないとも限らないので、ある程度機能差がなくなった現時点で Copilot に帰ってきた、という感じです。

戻ったところで使用感は自分の利用用途においてはそこまで変わらないんですが、ただやはり Copilot の Agent Mode の遅さは気になります。Copilot が遅いというよりは Cursor が速すぎるのでは?とも思っていますが笑

歴史的にもいろんなツールが生まれつつも GitHub が提供してるツールに収斂してしまった(CircleCI とか TravisCI とか結局 GitHub Actions になってしまった)流れもあり、今回も GitHub を持ってるMicrosoftのツールが最終的にはエコシステムを飲み込んでしまうのではないかな?と思っており、ある程度 AI Coding (Vibe Coding) というものに慣れたところで Copilot (純正 VSCode) 回帰してますが、まぁ色々あってまた他のツールに手を出す可能性もあります。ただ VSCode 自体もAgent Mode の速度は課題に感じているようで、今後 VSCode 側で この Agent Mode の速度の改善に取り組む ことが予定されていてCursor に使用感が近づく可能性もあります。 こういったところは使用感含めて試していきたいと思います。

FastMCP を触ってみる

サマリ

Mastra に続いて簡単に MCP サーバーを立てることができる FastMCP を触ってみました。

github.com

こちらも TypeScript で記述可能でした。

実装

書いたコードは以下に置いています。

github.com

今回は Gemini の API Key を使った Gemini wrapper を作りました。正直これだけのコードで MCP 作れちゃうのはだいぶ楽でした。

これを Claude の Desktop アプリで動かしています。

ドキュメント通りに実装したらほぼハマるところはありませんでした。

一点だけあったのは Mastra もそうだったのですが、MCP サーバーとして Local で node のランタイムを動かして動作させる場合、package は module として登録しないといけない というところです。

pacakge.json 内部では type: "module" で登録することで Claude や VSCode などの MCP クライアントから MCP サーバーとして認識させることができました。

感想

Mastra はエージェントを作るための重厚なフレームワークという印象があり、対象的に FastMCP はよりプレーンな薄いフレームワークといった感じです。
良い例えか微妙ですが Mastra は Rails で FastMCP は grape 、というようなウェブアプリのフレームワークの役割の違いを感じました。

Mastra に入門する

サマリ

Mastra とは

最近話題なので特に詳しくは書きませんが、自分は以下の記事を参考にしてました。

zenn.dev

自分は以下のスライドを見て初めて知りました。

speakerdeck.com

入門してみる

Scaffold する

npx create-mastra@latest すると scaffold なアプリが作成されます。
ドキュメントも丁寧ですし、作られたアプリ(お天気アプリ)の実装を見ると何をどこに書けばいいのか?ということはすぐに分かります。

面白なと思ったのは create mastra-app する過程で MCP の設定シーケンスがあることです。現時点では Cursor と Windsurf の設定のみがシーケンスにありますが、 npx コマンドを設定するだけで VSCode の設定もできます。

MCP 経由で Mastra の Example や Docs を参照でき、やりたいことをプロンプトで伝えるだけで、ドキュメントをベースに実装を提案してくれます。これはすべての言語、フレームワークで備わってほしい標準仕様になってほしいです。ドキュメントを読み漁るあの時間が限りなく短縮されますし、開発者体験として最高でした。

余談ですが、自分が普段開発に使ってる Go にも標準で入る話があるらしく、これはすごく嬉しい話。

github.com

なお自分は Scaffold したアプリを動かすのに Gemini を利用しています。Gemini だと簡単に API Key を発行できて管理も(自分的には)慣れた UI でできるのでこういうときにサクッと使えるのは GCP の良さだと感じます。

各種機能を使う

Mastra は API が生えていて、インターフェースは Swagger で提供されています。自分はまだ触っていないところなのでこの辺は触っていきたいと思います。API を提供してくれるから Scaffold なアプリの UI ではなく独自の Agent や、Slack のようなアプリとも連携できるわけなんですね。

また機能ではないですが Zod を使ったスキーマ定義をしてる部分も興味深かったのでこの辺も深堀りポイントかなと感じています。

MCP と連携する

MCP がサポートされているサービスに対して今回作成した Agent を連携してみました。
とりあえず自分はいちばん簡単な MCP との対話をできるような連携をしてみましたが、Stream という会話を継続させる中で MCP を使うインターフェースもあるみたいで、こちらも試してみる予定です。

手順は Using MCP in Your Code に書いてる通りの流れで試しました。サクッとできてすぐに動かせます。
MCP は認証込みのものを利用すると Client が Connection を確立できずに落ちるケースがあるので自分は mcp/time のような認証のいらない MCP で試しました。

使ってみての感想

ML や昨今の LLM のバックグラウンドに詳しくなくても実装できる

これは価値としては大きいと感じました。
自分も LLM、さらには Agent と呼ばれる流行りの技術に対してはあまり追っかけておらず素人なのですが、まずは Scaffold で作られるアプリケーションを眺めるだけでも、どういう実装なのか?とかどういう作りで行くべきなのか?といった流れが俯瞰できました。

ただ、作ってみて思ったのは LLM で非構造データを扱うことはできても、Agent が依存してるツール群(MCP やその他の実装、 APIなど)に渡す情報はある程度構造化されてる必要があって、入力値の検証や構造化といった前処理が大事になりそうだなという感じも見受けられました。

Typescript で統一できる

Typescript のエコシステムに乗っかっているので、プロジェクト全体の技術統一の観点からも良さそうだなと感じました。昨今技術スタックを Typescript に統一していく流れを感じていますが、そこにも乗っているように思います。

総じて体験は良い

と感じます。まだまだ触り始めで、自分で欲しいもののユースケースがわからないのですが、MCP が公開されてるサービズもあるのでいくつか組み合わせて Agent と呼ばれるものを作ってみようと思います。

またまた余談ですが、この提供されてる API をうまく組み合わせて(マッシュアップして)サービスを作る等には過去に通った道でもあり、時代や世界観が変わってもやってることはあまり変化ないなと感じました(笑)