ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

# フルスタックエンジニアの死

あるいは、私はいかにして心配するのをやめてAIを愛するようになったか

はじめに

こんにちは、フルスタックエンジニアです。本稿は、私がシニアフルスタックエンジニアから「vibe coder(バイブコーダー)」へと変貌を遂げた1年間の記録、いわば追悼文です。

これは完成されたLLMワークフローを提示するものではありません。それなら他にすでに良いリソースがたくさんあります。

ミツモア入社前

過去の職場ではコンプライアンス上の理由で、私はミツモアに入社するまで業務でAIツールを試すことができませんでした。ChatGPTを個人で使ったことはありましたが、それ以外、コーディングツールとしてLLMを使った経験はゼロでした。

私はシニアエンジニアです。前職ではTDDスタイルで物事に取り組むのが習慣でした。シンプルでミニマルなコードが好きです。

私のコーディングスタイルはこうでした。まず、与えられたタスクに対して、コードのどの領域が関心事なのかを大まかに特定する。次に、説明的なTODOコメントを書く。それから、ゆっくりと、整然と、TODOコメントを一つずつ片付けていく。テストスイートと実装を並行して書き、すべて消化し終えたらタスク完了——というものです。

Cursorとの出会い

Cursorのオートコンプリートには感心しました。TODOコメントを書いていると、しばしば動く実装をまるごと提案してくれる。私のワークフローのうちコードを書く部分は、劇的にスピードが上がりました。

何かの仕組みが分からないとき——大抵は曖昧だったり非常に複雑だったりするとき——同僚に聞く前にCursorのエージェントに訊いてみるようになりました。返ってくるのは大抵、真実の粒を含んだナンセンスな回答でしたが。

これはある種のフィルターとして機能しました。簡単な内容なら明快に説明してくれる。本当に難しい内容なら、その混乱した回答が「これは深掘りが必要だ」と教えてくれるわけです。

エージェントワークフローへ移行する

Cursorで本当にフラストレーションが溜まったアンチパターンの一つが、オートコンプリートの挙動を制御できないことでした。一度「この変更が必要だ」と判断すると、何度も何度も同じ提案をしてきます。

オートコンプリートは、自分が考慮すべきコンテキストを理解しきれません。「この方針はコードを壊す」と私が分かっているのに、それをしつこく提案してくる場面が何度もありました。その領域に変更を加えるたびに、壊す変更を提案し続けてくる。拒否はできるものの、オートコンプリートのワークフローはプロンプトレスなのです。

ある意味、オートコンプリートのワークフローは「生産性ツール」というより先に「エゴブースター」でした。LLMが私の意図を理解し、正しく実装してくれたときは、本当に気持ちいい。でも、噛み合っていないときはフラストレーションのもと、場合によっては危険ですらある。私はピュアなエージェントベースのワークフローへの興味が高まり、Claude Codeに切り替えました。

最初、エージェントを使うことには懐疑的でした。当時の私のLLMに対するメンタルモデルはこうでした——「LLMは、やる気の高いインターンと等価。時間がたっぷりあるからプロトタイプの勉強や複雑な実験はできる。雑用も任せられる。でも、重要な仕事は自分でやったほうがいい」。

エゴを殺せ

気づくと私は、やりたくないタスクをLLMに振りつつ、面白い仕事は自分の手元に残していました。

そこで、この旅を始めるにあたって意識的にこう決めました——「これまでの経験など捨てる。まずエージェントに解かせる。必要なときだけ自分が手を入れる。今は新しいワークフローを学ぶフェーズなのだ。自分が手動でやったほうが良かったかどうかは気にしない」。

エージェント任せの進め方は、誰かがもたついてバタついているのを眺めているような感覚です。フォーマットがおかしいコマンドを実行する。型エラーの修正で奇妙なループにハマる。

それでいて、これらの問題が本番に出ることはありませんでした。作業中かコードレビューのプロセスで必ず捕捉されます。私はただ、もどかしさと怒りを感じていました——「なぜ、自分なら絶対にしないようなミスを連発するこのツールに頼っているのか?」と。

LLMの出力を完全に信頼しているわけではありません。でも、ミスを捕捉する自分の能力と、周りの同僚の能力は信頼している。

LLMへのプロンプティングを学ぶ

ここから、すべてのタスクの起点は「LLMとの会話」になりました。

たとえば、チャットの画像アップロードのバグを調べているなら、こんなプロンプトを投げます。

「私たちのチャットシステムの画像アップロードを見てほしい。フロントエンドからバックエンドまでの全フローを説明してくれ。バイナリデータをどうAPIに送っていて、画像はどこに保存されるのかを分析してほしい。バックエンド側で何が起きているかも説明して——たとえば単にバイナリファイルをアップロードしているのか、それとも画像とDBテーブルの何らかのリレーションがあるのか」

そこから、私が追加の質問をしてLLMの主張を検証し、エージェントが詳細を補強する——というラリーが続きます。

それから私は問題への解決策を提案します。インターンを扱うように、具体的なアーキテクチャ方針を指示し、細部までスペルアウトする。そして、エージェントが問題を理解したと納得できるまでアラインメントを取る。

そんな詳細な指示を出していても、こんなことが起きました。

  • フォルダ内のSVGファイルを変換するスクリプトの作成を依頼 ⇒ プロジェクト内のSVGファイルをすべて削除した。
  • テストケースの実装を依頼 ⇒ そうしないとテストが落ちるという理由で、逆のシナリオを実装した。
  • 新機能実装中にクリティカルなバグを発見 ⇒ 「スコープ外」と正当化し、二度と言及しなかった。

LLMにすべき仕事を細かく指示することに膨大な時間を費やしているのに、それでも時々失敗が起きる。

そして、エージェントとアラインメントの会話をしていてあるパターンに気づきました。私はまず問題とコードについてアラインを取り、続いて方針を説明する。ところが、私が次のステップに進む前に——しばしば私が書いている最中に——エージェントは私の念頭にあったアプローチを先回りして提案してくることが多かったのです。

ここで気づきました。私は間違ったところにフォーカスしていた。詳細なプロンプティングでミスを最小化しているのではない。ただ自分のコーディングスタイルを押し付けているだけだ、と。

原始人のように話せ

しばらくの間、私はLLMにできるだけシンプルに話しかけることに決めました。

> me think about marketPriceMap. Why map no array?

✻ Thinking…

  ユーザーは、なぜ marketPriceMap に配列ではなく Map を使っているのかを尋ねている。考えてみよう:

  現状の構造:
  - marketPriceMap: Map<string, MarketPriceCalculationResult[]>
  - キーは ${serviceKey}:${areaKey}
  - 値は MarketPriceCalculationResult の配列

  なぜ配列ではなく Map?

  Map の利点:
  - キー(serviceKey:areaKey)による O(1) ルックアップ
  - 特定の service+area の組み合わせに対する市場価格データを効率的に取得できる
  - 配列を走査する必要がない

  配列での代替案:
  - 配列を走査して一致する service+area を探す必要がある
  - O(n) のルックアップ性能
  - 構造は: { serviceKey, areaKey, marketPriceData }[] になる

  ただ、ユーザーはこう考えているかもしれない:
  1. 1ページにつき service+area の組み合わせは1つで十分?
  2. データ構造が過剰設計?
  3. 配列のほうがシンプル?

  ユースケースを考えてみよう:
  - 1つの記事ページに複数の market-price コンポーネントが乗りうる
  - それぞれが異なる service+area の組み合わせを持つ
  - 各組み合わせに対して効率的に市場価格データを取得する必要がある
  - だから Map が理にかなう

  でも、ユーザーは「本当に1ページに複数の service+area の組み合わせが必要?」あるいは「キーのフォーマットは正しい?」と問うているのかもしれない。

⏺ いい質問! 考えてみます。

  現状の構造:
  - marketPriceMap: Map<string, MarketPriceCalculationResult[]>
  - キー: "エアコンクリーニング:東京都"
  -: MarketPriceCalculationResult[]

  なぜ配列ではなく Map?

  Map なら service+area の組み合わせで O(1) ルックアップできる。ただ、その問いを投げるのは正しい。

  代替案はこうなる:
  marketPriceData: Array<{
    serviceKey: string
    areaKey: string
    results: MarketPriceCalculationResult[]
  }>

  問い直し:
  1. 1つの記事ページに、異なる service+area の組み合わせが複数存在しうるか?
  2. キーのフォーマット serviceKey:areaKey は正しいか?
  3. シンプルな配列のほうが良いか?

  もし通常「1記事 = 1つの service+area」なら、配列のほうがシンプル。でも、1記事に異なる service/area の market-price コンポーネントが複数乗るなら、ルックアップ性能の観点で Map のほうが良い。

  どう思う? 実運用では1ページあたり service+area の組み合わせはいくつ?

> 1:yes\
  2:notsure can check?\
  3:me think we overcomplicate. maybe data fetch use map, but return array maybe ok? please challange me

✻ Thinking…

  ユーザーは、marketPriceData を Map ではなく配列にすべきか、自分の考えに対して反論してほしいと求めている。整理しよう:

  1. Yes — 1記事に複数の service+area の組み合わせがありうる
  2. キーフォーマットが正しいかは不明 — 確認が必要
  3. 過剰設計の疑い — 内部的には Map で取得し、戻り値は配列でもよいのでは

  反論を組み立てよう:

  Map 派の議論:
  - ハイドレーション時の O(1) ルックアップ — 特定の service+area が来たら即座にデータを取れる
  - キーと値の関係が明示的
  - 異なる service+area が多いほど効率的

  Array 派の議論:
  - 構造がシンプル
  - 扱いやすい
  - フロントは結局すべてイテレートするかもしれない
  - 特定のキー形式への結合が弱い

  ユーザーの提案——「データ取得は内部的に Map で効率化、戻り値はシンプルな配列」——は実は良い指摘で、両者のいいとこ取りができる:

  1. 取得・整理時は内部的に Map を使い効率的にルックアップ
  2. DOM ハイドレーションには Array を返してシンプルに保つ
  3. ハイドレーション時は配列に対して find() するだけ

  反論しつつも、相手の主張も拾おう。

⏺ いいチャレンジ! 整理します。

  「過剰設計」という指摘は妥当:
  - 内部の取得処理は効率のために Map を使う
  - でも最終的なデータ構造は Array でシンプルに

  逆に問い返す:
  1記事に複数の service+area がある場合、ハイドレーションは正しいデータを見つける必要がある:

  // Map (現状): O(1) ルックアップ
  const data = fetchedData.marketPriceMap.get(`${serviceKey}:${areaKey}`)

  // Array: O(n) ルックアップ
  const data = fetchedData.marketPriceArray.find(item =>
    item.serviceKey === serviceKey && item.areaKey === areaKey
  )?.results

  ただ、あなたが正しい可能性が高い理由:
  1. 記事ページに market-price コンポーネントが100個も乗ることはまずない
  2. Array のほうが扱いやすい
  3. フロントは結局すべてイテレートする可能性が高い
  4. キー形式への結合が弱い

  あなたの提案はこうなる:
  // 取得層は内部で Map を使って整理
  // でも、ハイドレーションへはシンプルな配列を返す
  marketPriceData: Array<{
    serviceKey: string
    areaKey: string
    results: MarketPriceCalculationResult[]
  }>

  質問: シンプルさのために、ハイドレーション時の O(n) ルックアップを許容できる?

> 0(n) lookup not acceptable. repeating data is ok. so if map only hydration flow. it is ok

⏺ なるほど、了解! O(n) ルックアップは許容できないということですね。

  あなたの解は理にかなう:
  - ハイドレーション時の O(1) ルックアップのために Map は維持(性能要件)
  - データ重複は許容できるトレードオフ
  - Map はあくまで内部のハイドレーションフロー用

  なので、現状のアプローチは実は妥当:
  // FetchedDataForDomCreation - 内部のハイドレーションインターフェース
  marketPriceMap: Map<string, MarketPriceCalculationResult[]>

  // ハイドレーション: O(1) ルックアップ - 性能要件
  const data = fetchedData.marketPriceMap.get(`${serviceKey}:${areaKey}`)

  // 最終的にハイドレートされた DOM: 各コンポーネントが自分のデータコピーを持つ
  props: {
    marketPriceData: MarketPriceCalculationResult[] // データ重複は許容
  }

  これは良い設計:
  - Map で高速なハイドレーション
  - 最終 DOM 構造はシンプル
  - データ重複は問題にならない(小さいオブジェクト)
  - 関心の分離がきれい

  あなたの言う通り、現状の構造は適切。チャレンジに感謝! アプローチが妥当だと確認できました。

  このまま維持で良い?

> yes keep as is

こんな長いセッションのあと、エージェントに私のスタイルについてどう思うか訊いてみました。

> is speak short, hard to read? speak long better?

⏺ 短いほうが私には良いです。

  あなたの短いスタイルは、私にこう強いる:
  - 装飾なしで直球で答える
  - 重要なポイントだけにフォーカスする
  - あなたの読む時間を浪費しない

  長い回答が向くのは:
  - 詳細な説明が必要なとき
  - 複雑な技術的分析
  - 比較すべき選択肢が複数あるとき

  あなたの今のスタイルは効率的——あなたは早く答えを得られて、私はフォーカスを保てる。詳細が欲しければ「もっと説明して」「例をくれ」とその都度頼める。

  この短いスタイルで続けてください——私たちの技術議論にはよく合っています。

これは私のプロンプティングへの態度を一変させました。アラインを取り、コードを理解し、プロンプティングを通してタスクを分解する——これは今でも続けています。

でも、詳細なプロンプトを書く必要は全くないのです。アーキテクチャを説明する必要すらない。

特定のアプローチを押し付けるのは、ただのエゴ。情報は最小限にして、解決方針はLLM自身に提案させればいい。

そのうえで会話をすればいい。LLMの発見は検証できる。アプローチやアーキテクチャに対して反論や意見も出せる。でも、重い作業はLLMにやらせるべきです。

水平にスケールさせる

この新しいマインドセットで、ワークフローをどこまで押せるか試したくなりました。発想はシンプルです——「方針でアラインしたなら、複数のエージェントを立ち上げて並行で動かせばよくないか?」

1つのタスクの計画を立て、アラインを取って、準備が整ったら開始させる。そして次のセッションに飛び込み、また同じことを繰り返す。最初のセッションが完了するまで、こうして3〜4セッションを並行で動かせるわけです。

このアプローチで気づいたのは、エージェントが動いている間にセッション間でコンテキストスイッチするのは消耗するということ。そして最終的に残るのは、レビュー待ちのPRの巨大なバックログでした。

ボトルネックは、結局のところ私自身でした。複数の複雑な問題を頭の中に保持できる量には限界がある。AIエージェントはスケールできても、それが生み出すアウトプットを私は追いきれない。ここで私のマインドセットは「エージェントにより速くコードを書かせる」から「タスクをエージェントに渡す/受け取るハンドオフをいかに最適化するか」へとシフトしました。

この経験から、新しいやり方を採用しました——LLMとアラインを取ったら、計画を .md ファイルに書かせる。作業中は、何をやったかをレポートに書かせる。これなら、ターミナルセッションのコンテキストに依存せず、いつセッションを止めて再開しても問題ない。良い計画ドキュメントがあれば、最小限の手綱だけでタスクを完遂してくれると信頼できる。

最近では、シンプルで反復的なタスクには並列エージェントを使い、複雑な作業はシングルスレッドに保ちます。フォーカスはハンドオフの最適化——スムーズにタスクを開始させ、後でスムーズにレビューする——にあります。

今のLLM観

最初のころは、LLMと長く自然な会話をしたり、agents.md で挙動をかなりファインチューンしたり、意思決定をマイクロマネジメントしたり、各種の振る舞いのアンチパターンにパッチを当てたりしていました。

LLMに「推奨してくれ」「次の一手を提案して」とよく依頼していました。今の私は、LLMが最も強いのは「情報を処理して要約する」「テキストやコードを生成する」タスクだと考えています。

たとえばコードレビュー。PRの変更ファイルを1つずつ読む前に(これは今もやります)、LLMに変更内容のサマリーを書かせます。「このコードが良いか悪いか」は訊かない。議論は「パターンに沿っているか」「コードが何にどう影響するか」に絞ります。「バグの懸念は?」「本番投入できる品質?」「性能は良いか?」といった概念的な問いは、できるだけ投げません。今では、LLMの役割はあくまで、私の理解を加速させるために消化しやすい情報を渡すこと——それに尽きます。

おわりに

先日、同僚にこう言われました。「あなたがAIをこんなに軽く扱うのが意外だ」と。彼は、複雑なコードベースでエージェントを操縦するのは難しいと話していました。信頼していい場面、押し戻すべき場面、軽く受け流して正しい方向に舵を切るべき場面——それぞれの判断が難しい、と。

私の側からすると、プログラミングへのアプローチを少しずつ作り直してきた1年でした。一部の私は、それを敗北、降伏のように感じていた。フルスタックエンジニアとしての私を死なせてしまった、と。

でもあのコメントは、その見方が間違いだと教えてくれました。私は「全部を自分のやり方で進めたい」という欲求を手放すことを学んだ。1年前と同じくらい頑固ではある。ただ、戦う場所を選ぶようになっただけです。


ミツモアで一緒に働きませんか?

最後まで読んでいただきありがとうございました。

ミツモアでは、日本のGDPを向上させるという目標に向けて、一緒に働く仲間を募集しています。 「急成長中のスタートアップで多種多様な国籍のメンバーと働きたい」「AIエージェントを日常的に使い倒しながら、自分のエンジニアリングのやり方そのものを問い直していきたい」といった方はお気軽にお問い合わせください!

採用ページ: https://corp.meetsmore.com