巨大なdammy log file を高速に生成するpython script

巨大なログファイルを集計・分析する際、いきなり本番データを使うのは少し怖い。そこで、本番と同じフォーマットのダミーデータを大量生成できるスクリプトを作成した。

今回は、その実装過程で遭遇した並列処理・メモリ管理・Jupyter Notebook 特有の問題と、その対処方法をメモとして残しておく。

生成結果

今回の設定では、3日分で約7.2億件のアクセスログを生成した。

  • レコード数:約7.2億件
  • ファイルサイズ:約32GB
  • 生成時間:約31分
  • 平均件数:約1,000万件/時間

実行環境は以下の通り。

  • Windows 11
  • Ryzen 7 5825U(8コア)
  • メモリ 16GB
  • NVMe SSD 512GB

手元の M5 MacBook Pro の方が速そうだが、今回は職場に置いてきていたため、自宅の Windows マシンで実施した。そろそろ自宅環境も強化したいところだが、最近のPC価格はなかなか厳しい。

Pythonでダミーアクセスログを生成する

最初は特に工夫せず実装していたが、7億件規模になると生成だけで3時間近くかかりそうだった。また、CPU・メモリ・ディスクI/Oを無駄に消費し、その間ほかの作業がしづらい。

そこで、

  • CPUはマルチプロセスで活用する
  • メモリ使用量を抑える
  • ディスクI/Oの無駄を減らす

という方針で改善を進めた。

要件

生成するログの仕様は以下の通り。

カラム

  • timestamp
  • method
  • path
  • status_code
  • response_time_ms

ログ特性

  • 12:00〜13:00をピークとするアクセス分布
  • 4xx / 5xx エラー率は約0.01%
  • ごく低確率で異常値(スパイク)を混在
  • 1時間あたり平均10万件〜1,000万件規模
  • 1日1ファイルのCSVを指定日数分出力

第1段階:素直な実装

最初は時間帯ごとにログを生成し、CSVへ追記していく単純な実装にした。

def generate_day_log(date, rng, output_dir):
    for hour in range(24):
        count = int(round(AVG_REQUESTS_PER_HOUR * hourly_weights[hour]))
        df = generate_hour_rows(day_start + timedelta(hours=hour), count, rng)
        df.to_csv(
            filename,
            mode="a" if hour > 0 else "w",
            header=(hour == 0),
            index=False
        )

行生成自体は NumPy / pandas によるベクトル化を利用しているため、Python の for ループで1件ずつ生成するよりは十分高速だった。

1時間あたり数万〜数十万件程度なら、この方式でも快適に動作する。

レスポンスタイムやステータスコードは、それぞれ異なる分布を持たせている。

# 正常レスポンス
vals = rng.lognormal(mean=2.9, sigma=0.6, size=n_success)

# 5xx
vals = rng.uniform(500, 5000, size=n_server)

# スパイク
vals = rng.uniform(lo, hi, size=n_spike)

また、スパイクはエラー率とは独立した確率で発生させるようにした。

そのため、

  • status=200 だが異常に遅いケース
  • status=500 かつ異常に遅いケース

の両方が混在する。


第2段階:ProcessPoolExecutorによる並列化

リクエスト数が増えると生成時間も比例して増える。

そこで8コアCPUを活用するため、時間帯単位で並列処理を導入した。

concurrent.futures.ProcessPoolExecutor

ここで注意した点は2つある。

1. 乱数の再現性

並列処理では各ワーカーが独立した乱数生成器を持つ必要がある。

単純に同じシードを使うと、実行順序によって結果が変わってしまうため、

seed = base_seed + day_index * 24 + hour

のように、日付と時間帯から決定的にシードを生成した。

これにより、

  • 並列度が変わっても
  • 実行順序が変わっても

常に同じログを再生成できる。

2. ファイル内の順序

各時間帯は並列実行されるため、完了順序はバラバラになる。

そのため一度結果を集め、

futures = {
    executor.submit(_hour_task, t): i
    for i, t in enumerate(tasks)
}

results = [None] * 24

for future in as_completed(futures):
    hour = futures[future]
    results[hour] = future.result()

day_df = pd.concat(results, ignore_index=True)

時間帯順に並べ直してからCSVへ出力した。

この時点では問題なく動作していた。


第3段階:Jupyter Notebook対応

実行環境を Jupyter Notebook に移したところ、別の問題に遭遇した。

ProcessPoolExecutor は、ワーカー関数を pickle 化して子プロセスへ送る必要がある。

しかし Notebook のセル内で定義した関数は、子プロセスから import できず失敗することがある。

特に Windows や macOS の spawn 方式では発生しやすい。

対処方法はシンプルだった。

ロジック本体を独立したモジュールへ切り出す。

# Notebook
import log_generator_lib as lg

lg.AVG_REQUESTS_PER_HOUR = 10_000
lg.main()

これにより、Notebook上でも安定して並列実行できるようになった。


第4段階:「リソースを使っているのに遅い」問題

AVG_REQUESTS_PER_HOUR を1,000万に引き上げると、CPU・メモリ・ディスクI/Oはフル稼働しているのに、思ったほど速度が出ない。

これは、

「リソース使用率が高い」

「効率的に動いている」

とは限らない典型例だった。

実際にはメモリ不足によりページングやスワップが発生していた可能性が高い。

原因

問題は以下の2点だった。

1. ワーカーが巨大なDataFrameを返していた

各ワーカーは最大1,800万行を一括生成し、

future.result()

で親プロセスへ返していた。

2. メインプロセスが全結果を保持していた

さらに24時間分のDataFrameを保持し、

pd.concat()

で結合していた。

つまり、

  • DataFrame生成
  • pickle化
  • プロセス間転送
  • concat

を巨大データで繰り返していたことになる。

16GBメモリではかなり厳しい。


実証:OOMで落ちる

仮説を検証するため、

df = generate_hour_rows(
    datetime(2026, 6, 1, 12),
    18_000_000,
    rng
)

を実行してみた。

空きメモリ約3.6GBの環境では、

Killed (exit code 137)

で強制終了。

OOM Killer による停止だった。

設計そのものに無理があったと言える。


第5段階:チャンク処理への変更

そこで設計を見直した。

1. チャンク単位で生成

1時間分を一括生成せず、

CHUNK_SIZE = 500_000

単位で生成して即座に書き出す。

while remaining > 0:
    n = min(CHUNK_SIZE, remaining)

    df = generate_hour_rows(
        hour_start,
        n,
        rng
    )

    df.to_csv(...)

2. ワーカーが直接ファイルへ出力

各ワーカーはDataFrameを返さない。

代わりに担当ファイルへ直接書き込む。

メインプロセスが受け取るのは件数だけ。

これにより、

  • pickle
  • プロセス間通信
  • 巨大オブジェクト転送

を完全に排除できた。

3. 一時ファイルを結合

最後に各時間帯のCSVを結合する。

with open(final_filename, "wb") as out_f:
    ...

2つ目以降はヘッダー行だけスキップする。

この方式では完全な時刻順は保証されないが、時間帯ブロック順は維持される。

今回はその点を許容した。


検証結果

同じ1,800万行を新方式で実行した結果は以下の通り。

  • ピークメモリ使用量:約354MB
  • 実行時間:約52秒

旧方式は同条件でOOMにより停止していたため、大幅な改善となった。

メモリ使用量は1桁以上削減できている。


教訓

今回得られた教訓は次の通り。

  • リソース使用率が高いことと効率的な処理は別問題
  • メモリ不足によるスワップはCPU・ディスクI/O使用率だけでは見抜きにくい
  • ProcessPoolExecutor ではプロセス間通信コストも考慮する必要がある
  • Jupyter Notebook での並列処理はモジュール分離が安全
  • 大規模バッチ処理ではチャンク処理が非常に有効

特に、

「巨大オブジェクトを作ってから処理する」

のではなく、

「小さく作ってすぐ捨てる」

という発想は、多くのデータ処理で応用できると思う。


まとめ

今回は「ダミーログを生成するだけ」の小さなスクリプトだったが、改善を重ねていく中で、

  • 並列処理
  • メモリ管理
  • プロセス間通信
  • Jupyter Notebook固有の制約

といった、実務でも頻繁に遭遇するテーマを一通り経験できた。

特に印象的だったのは、

「CPUもディスクもフル稼働しているのに遅い」

という現象の正体が、CPU性能ではなくメモリ設計だったことだ。

大量データを扱う処理では、アルゴリズムだけでなくデータの持ち方や受け渡し方も性能に大きく影響する。

今回のスクリプトは、そのことを改めて実感する良い題材になった。

Googleログインの許可制限について

開発中のWebアプリで、Googleログインの許可制限で、ちょっとややこしいことがあったのでメモ。

起きたこと

Webアプリにゲストモードを実装。実機確認中に、「ログインできるのが 正規user@gmail.com だけのはずが、他のGmailアカウントでもloginとできてしまう状態に気づいた。 Google Cloud Consoleの「テストユーザー」に登録していないアカウントでもログインができてしまう、という現象が発生。

原因

Google Cloud ConsoleのOAuth同意画面が「テスト」モードの場合、通常は登録した「テストユーザー」しかログインできない。しかし、これには例外があった。

アプリが「Sign in with Google」(emailprofileopenidの基本スコープのみ)を使ってユーザー認証している場合、テストユーザー制限は実質適用されない。これはGoogle側の仕様。

Firebase AuthenticationのGoogleAuthProviderによるログインは、まさにこの「基本スコープのみのSign in with Google」に該当する。そのため、OAuth同意画面が「テスト」状態であっても、テストユーザーリストに入れていない一般のGoogleアカウントでもログイン自体は通ってしまう

つまり「テストユーザーを追加すれば制限できる」という前提が、そもそも誤り。

対応方針

Firebase/Google Cloud側の設定だけでは制限できないと分かったため、アプリ側(コード)で許可リストを実装する方針に。安全性を高めるため、2段構えにしました。

  1. コード側(useAuth.ts:ログイン直後・ページ再読み込みでのセッション復元時の両方で、ログインしたメールアドレスが許可リストに入っているか判定し、入っていなければ即座にsignOut()してログイン画面に戻す。
  2. Firestoreセキュリティルール側request.auth.token.emailが許可リストに入っている場合のみ読み書きを許可するよう変更。コード側のチェックをすり抜けて直接Firestoreにアクセスしようとした場合でも、サーバー側でブロックされるようにする。

許可リストは .env.localVITE_ALLOWED_EMAILS(カンマ区切り)で管理し、Firestoreルール側にも同じメールアドレスを記載する(2箇所を両方更新する必要がある点に注意)。

結果、うまくいった。

教訓

  • 「テストユーザーに登録すればログインを制限できる」は、Googleログイン(Sign in with Google/基本スコープのみ)の場合は成立しない。 OAuth同意画面のテスト/本番ステータスや、テストユーザーリストは、ログインアクセス制限の手段として過信しないこと。
  • 本当にユーザーを制限したい場合は、アプリ側(コードまたはFirestoreルール)で明示的に許可リストを持つ必要がある。
  • Firestoreルール側の制限だけでも「データの保護」自体は実現できるが、「ログインのブロック」をしたい場合は別途コード側の対応が必要(両者は別の防御層であり、片方だけでは目的が異なる)
  • 許可リストをメールアドレスで管理する場合、.env.local(コード側)とFirestoreルール(Firebase Console側)の2箇所を同期して更新する運用が必要になる。片方だけ更新すると「ログインはできるのにデータが保存できない」のような不整合が起きるので注意

数億件のlog(csv file)を集計する処理にduckDBを使ったら爆速

数億件のlog(csv file)を集計して出力する処理に、duckDBを使ったら爆速だったのでmemo。

以下のようなlogが日別であり、

timestamp,method,path,status_code,response_time_ms 
2026-06-01 00:00:04,POST,/api/v1/cart/items,200,10 
2026-06-01 00:00:05,GET,/api/v1/products,200,13 
2026-06-01 00:00:05,GET,/api/v1/cart,200,17 
2026-06-01 00:00:05,GET,/api/v1/products/{id},200,11 
2026-06-01 00:00:08,GET,/api/v1/orders,200,25 
2026-06-01 00:00:11,GET,/api/v1/products/{id},200,23 
2026-06-01 00:00:11,GET,/api/v1/products/{id},200,9 
2026-06-01 00:00:14,POST,/api/v1/cart/items,200,37 
2026-06-01 00:00:15,GET,/api/v1/products,200,19

1 file(1日分)で 11GBくらいで3日分(をつまり33GBくらい)を

  • 時間帯別のlog数
  • 時間帯別のerror数、error種類の比率
  • 時間帯別の平均response_time
  • p95

を集計するとする。

普通にpythonでcsvを一行ず読んでcountして・・・なんて書くと、数億回の forループになり、すごく遅いはず。

それが、duckDBを使うと33GB分のlog処理が、65秒で終わった。 約520MB/秒 の処理量。

1/10 sizeのdataで試行しても、秒単位の処理量はあまりかわらなかった。 memoryは流石に使い切っているので、ssdからCSV読み込みがボトルネック。

MacBook PRO(m5,24GB)というマシンの性能もあるけど、

  • 列指向 (Columnar) 的な実行エンジン
  • ベクトル化実行
  • 自動的にマルチコア処理

などがduckDBの早い理由。

少し調整したら、さらに早くなった。

  1. logs/*.csv を読み込む
  2. 初回だけ Parquet 化する
  3. Parquet を使って集計する
  4. 結果表示

結果は31.69 sec

csv loadが遅かったのは明白。

import duckdb
import time
from pathlib import Path

start = time.time()

LOG_FILES = "logs/*.csv"
PARQUET_FILE = "logs.parquet"

con = duckdb.connect()

# DuckDBの使用スレッド数
con.execute("SET threads TO 10")

# -----------------------------
# CSV → Parquet変換(初回のみ)
# -----------------------------
if not Path(PARQUET_FILE).exists():
    print("Creating parquet file...")

    con.execute(f"""
    COPY (
        SELECT *
        FROM read_csv_auto('{LOG_FILES}')
    )
    TO '{PARQUET_FILE}'
    (FORMAT PARQUET);
    """)

    print("Parquet file created.")

# -----------------------------
# 時間帯別件数・平均応答時間・P95
# -----------------------------
hourly_stats = con.execute(f"""
SELECT
    date_trunc('hour', timestamp) AS hour,
    COUNT(*) AS log_count,
    ROUND(AVG(response_time_ms), 2) AS avg_response_time_ms,
    ROUND(
        quantile_cont(response_time_ms, 0.95),
        2
    ) AS p95_response_time_ms
FROM '{PARQUET_FILE}'
GROUP BY 1
ORDER BY 1
""").df()

# -----------------------------
# 時間帯別エラー件数
# -----------------------------
hourly_error_count = con.execute(f"""
SELECT
    date_trunc('hour', timestamp) AS hour,
    COUNT(*) AS error_count
FROM '{PARQUET_FILE}'
WHERE status_code >= 400
GROUP BY 1
ORDER BY 1
""").df()

# -----------------------------
# 時間帯別エラー比率
# -----------------------------
hourly_error_ratio = con.execute(f"""
WITH hourly_logs AS (
    SELECT
        date_trunc('hour', timestamp) AS hour,
        COUNT(*) AS total_logs
    FROM '{PARQUET_FILE}'
    GROUP BY 1
),
hourly_errors AS (
    SELECT
        date_trunc('hour', timestamp) AS hour,
        COUNT(*) AS error_logs
    FROM '{PARQUET_FILE}'
    WHERE status_code >= 400
    GROUP BY 1
)
SELECT
    l.hour,
    l.total_logs,
    COALESCE(e.error_logs, 0) AS error_logs,
    ROUND(
        COALESCE(e.error_logs, 0) * 100.0 / l.total_logs,
        4
    ) AS error_ratio_percent
FROM hourly_logs l
LEFT JOIN hourly_errors e
    ON l.hour = e.hour
ORDER BY l.hour
""").df()

# -----------------------------
# 時間帯別エラー種別比率
# -----------------------------
hourly_error_type_ratio = con.execute(f"""
WITH error_summary AS (
    SELECT
        date_trunc('hour', timestamp) AS hour,
        status_code,
        COUNT(*) AS error_count
    FROM '{PARQUET_FILE}'
    WHERE status_code >= 400
    GROUP BY 1, 2
),
hourly_total AS (
    SELECT
        hour,
        SUM(error_count) AS total_error_count
    FROM error_summary
    GROUP BY 1
)
SELECT
    e.hour,
    e.status_code,
    e.error_count,
    ROUND(
        e.error_count * 100.0 / t.total_error_count,
        2
    ) AS error_ratio_percent
FROM error_summary e
JOIN hourly_total t
    ON e.hour = t.hour
ORDER BY e.hour, e.status_code
""").df()

# -----------------------------
# 表示
# -----------------------------
print("\n=== Hourly Stats ===")
print(hourly_stats.head(72))

print("\n=== Hourly Error Count ===")
print(hourly_error_count.head(72))

print("\n=== Hourly Error Ratio ===")
print(hourly_error_ratio.head(72))

print("\n=== Hourly Error Type Ratio ===")
print(hourly_error_type_ratio.head(72))

print(f"\nelapsed: {time.time() - start:.2f} sec")

CPU も 700% 超えしていたので、しっかりマルチコアが走っている。

一昔前なら、そこそこのsysytemを組まないと厳しい規模だけど、 今は単一の MacBook Pro + DuckDB + Parquetで、1TBくらいまではいけそう。

その先は、Sparkやクラスタを考える領域だけど、さらに強いマシン(Threadripper + 256GB mem + NVMe 4TB*4 とか)なら、10TB級もいけそうなのが怖い。。。

10TB超のlog解析systemをできるだけ安く構築するには、こんな構成か。100万でいける。 2TB SSDの中で十分扱える size に log を小分けにして処理が前提。

  • MacBook PRO の現行上位機で50万くらい。M5 PRO(18 core)、mem 64GB、 2TB 構成。
  • 解析対象の 10TB超の log data を取り込むのに、16TB SSDマシン(4T×4 , Raid 0)で40-50万くらい。

data のやりとりが、network越しだとキツそう。 macとstroage機は、USB4 / Thunderbolt接続で出来るだけ早く。

追記

winマシン(8core/16GB/NVMe SSD 512GB)でもためしたら、倍くらいの時間がかかった。 やはりM5 MacBoodPRO は 強いわ。

claudeのgithub連携機能でスマホからcode編集

claudeのgithub連携機能でスマホからcode編集

claude.aiでprojectの機能に、github連携機能がありました。 いつからあるのかは不明。ベータ版らしい。

project>ファイル で、github のボタンを押すと、 連携したいリポジトリが登録できます。

登録後、sandboxにcloneしてきて、ブラウザから指示を出すと、codeの作成、変更、testなどを行ってくれます。

ただし、githubに comit/push まではできないので、パッチを作成するところまで。pushはclaude codeでやるか手動で。

つまり、電車の中とかで、スマホからでも的確に指示が出せれば、code編集を伴う作業ができます。

今までは電車の中だとアイデア出しの壁打ちくらいでしたが、一気に活用範囲が広がります。

どんどん便利になるなー。

最近みつけたclaude関連の新機能

ブラウザ自体をclaudeから操作できるchrome拡張機能

公式ではないですが、ブラウザ自体をclaudeから操作できるchrome拡張機能がありました。 試してみたけど、いまいちうまく機能しなかったですが。。。 https://www.lifehacker.jp/article/2606let-claude-control-browser-automated-tasks-used-take-hours-manual-work/

slack連携する Claude Tag

https://www.itmedia.co.jp/news/articles/2606/24/news115.htm これは公式なので良さそう。

人間相手だと聞きづらいことも、claude相手なら、遠慮なく聞けますよね。 検索すればわかることも、つい面倒でAIに聞くことが多いので、 組織内だけの決め事(PR作法とか)とかを調べるのに良さそうです(RAGそのものだね)。

あとは重要taskの見逃し/対応漏れ防止とか。

普段とは違うアラートの検知とか。大量のアラートに埋もれて、やばいアラートが見逃されがちだし。

LLM周辺領域

こうしてどんどんLLMが便利になると、簡単なtaskまでAIに任せてしまい、トークンを無駄に消費してお金かかるので、AIに任せるまでもないtaskを実行するためのSaasなりアプリなりはもっと流行る気がします。

AIが介入するにしても、トークンを抑えるためにギチギチに固めたprompt作成とか、事前資料まで揃えてくれるSaasとか。

LLMの登場でSaasの死とか言ってるけど、人間がボトルネックな部分は、むしろSaas依存が高まっているのが実感。

SlackとかLINEとか、backlogとかJIRAとか、Githubとか各種google系サービスは、もはやインフラって感じで、過去の積み上げもあって、容易には 他に Switch されそうにないし。

asus T90chi

asus T90chi という8インチのwindows 10タブレットが押し入れからでてきたので

久しぶりに通電してみたら、普通に起動して使えた。。。

OSもサポート切れだし、intel(R) Atom(TM) CPU  Z3775 2GB mem という

いまとなってはスマホ以下のspecだけど、フルスペックのwindowsなので、

ブラウザはもちろん、ターミナルも使えて、便利。

 

小さい鞄にもはいるし、電源もスマホ用の充電器でいける。

 

昔はこれもって旅行先からサーバにつないで、

ちょっとした作業しながら移動していた思い出。

 

再起にはこのサイズのマシンが出ていなくても寂しい。

小さいガジェット好きな人って一定数いるとおもうんだけどなぁ。。。

 

macも12インチモデルはでないし。

 

フリーランス向け経費・売上管理アプリに税金試算機能、CSV出力を追加

開発中のフリーランス向け経費・売上管理アプリに、税金試算機能とCSV出力機能を追加した。

 

社会保険料、各種控除は設定画面で手動入力。

概算だけど、だいたいあっている。

経費は6月分だけ入れてる。

売り上げは見込みとして、年内分を入れた。

 

 

証票画像アップロードは見送り。google driveに保管してある証票画像の共有URLだけ登録するようにした。google driveに置いていたほうが、スキャン機能もあって、いろいろ便利で、しかも無料だしね。

CSV出力は、経費と売上データをcsvでエクスポートして、他のToolに渡せるように。

予定していた機能はすべて完成。

あとは、PWA最終調整だけ。

 

tadano-akira/kaikei-app

AI導入後のシステム開発-クライアント側に起きていること

AIを活用した開発が進むにつれて、開発スピードそのものは明らかに上がっている。

ただし、その恩恵を最もダイレクトに受けているように見えて、実は一番混乱しているのがクライアント側かもしれない。

できるのが早すぎる・・・という違和感

以前は、要件を伝えてから成果物が出てくるまでに一定の時間があった。

その間に、

  • 社内調整
  • 予算確認
  • 仕様の微修正
  • 優先順位の再整理

などが自然に進んでいた。

しかしAI前提の開発では、実装が非常に速い。

その結果として起きるのは、

「まだ社内で決まっていないのに、もうできてしまう」

という現象だ。

意思決定が追いつかない

クライアント側の最大の変化はこれだと思う。

開発速度に比べて、

  • 社内稟議
  • 業務部門の合意
  • 責任者の承認
  • 現場の確認

が圧倒的に遅い。

そのため、

  • 仕様が固まる前に実装が進む
  • 一度できたものを止める判断が必要になる
  • 変更の影響範囲がすぐ広がる

といった状態が発生する。

結果として、

「開発は速いのに、決まるのが遅い」

という逆転現象が起きる。

UATがボトルネック化する

特に顕著なのがUAT(受け入れテスト)だ。

開発側はすでに完成しているのに、

  • 業務担当の時間が取れない
  • 確認観点が整理されていない
  • 承認者が複数いて意思決定が割れる

などの理由で、検証フェーズで止まる。

その結果、

「できているのにリリースできない」

という状態が増える。

何を頼んだのか分からなくなる問題

AIによって出力の量と速度が増えると、クライアント側でももう一つ問題が出てくる。

それは、

  • 依頼した仕様がどれだったか曖昧になる
  • 途中変更の履歴が増える
  • どれが正式な合意だったか分からなくなる

という“仕様の記憶コスト”の増加だ。

従来は時間があったため、自然と整理されていたものが、速度によって追いつかなくなる。

開発がブラックボックス化する逆転現象

AIを使った開発では、

  • 設計
  • 実装
  • テスト
  • 修正

のサイクルが高速で回る。

その結果、クライアントから見ると、

「気づいたらできている」

という状態が増える。

これは一見良いことに見えるが、同時に、

  • どの判断でそうなったのか分からない
  • どこで仕様が変わったのか追えない

というブラックボックス感も生む。

クライアント側の負荷はむしろ増えている

直感的には「AIで楽になる」と思われがちだが、実際には逆の現象も起きている。

  • 判断の回数が増える
  • 確認の頻度が増える
  • 承認スピードが求められる
  • 仕様の精度要求が上がる

つまり、

作る側が速くなった分、決める側が重くなる

という構造になっている。

本質的な変化

この変化を一言で言うと、

「開発の遅さは消えたが、意思決定の遅さだけが残った」

という状態に近い。

AIは実装を高速化するが、意思決定そのものは高速化しない。

そのギャップが、クライアント側の負荷として現れている。

おわりに

AIによって開発は確実に速くなった。

しかしクライアント側から見ると、それは必ずしも“楽になる”ことと同義ではない。

むしろ、

  • 決めることが増え
  • 判断のスピードが求められ
  • 仕様の精度が問われる

という形で、別の種類の負荷が発生している。

結果として見えてくるのは、

開発の問題が解決したのではなく、意思決定の問題が前面に出てきただけ

という構造である。