$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

Claude Codeが並列にSubAgentを起動した時に自宅ネットワークが死ぬ問題を解消した

表題のとおり、Claude Codeが4~5並列にSubAgentを起動した時に自宅ネットワークが死ぬ問題が起きていた。その状況になると次のような現象が起きていた。

  • pingなども含めて一切外部との通信ができなくなる
  • Claude Codeを一旦止めて数分待つと復活する

この現象について知人に相談しながら対応を進めたところ解消できたので、ログとしてブログに残しておく。ただしネットワークについて専門分野ではないため、かなり間違ったことを書くかもしれない。その場合は指摘してもらえると嬉しい。

自宅ネットワークの構成

  • ISP: enひかり + v6プラスオプション
  • ルーター: Aterm 2600HP4

再現させる

まず再現はかなり簡単に取れた。Claudeを起動し、「どういう内容でもいいので、10並列で何かの調査を行なってみて」と指示を出すと、Explore SubAgentが一斉に起動する。この状態でしばらく経つと、外部との通信が全くできなくなる。

Wiresharkでパケットを見る

ここからどう調査しようかと考えたが、まずAterm 2600HP4にはログを出す機能がないことが分かった。普通に困る。

そこでClaude Code以外を閉じてWiresharkを起動し、パケットを確認してみることにした。知人からExpert Infoを見ると良いと言われたので、それも確認する。

さて悲しいかな、知識がなく、これを見ても全くわからんとなってしまった。ひとまずChatGPTにWiresharkのパケットキャプチャとExpert Infoのスクショを渡して分析をお願いした。するとNAT周りが怪しいかも?という情報が出てきた。知人も同じ見解であった。

一方、Claude Codeを起動して並列にSubAgent起動したとしても、多くてもTCP接続のESTABLISHED状態は100程度増えるくらいであった。ルーターのNATテーブルは、ポート数のことを考えるとおそらく6万セッションほどは作れるはずだし、このくらいで上限に達する?という疑問が出てきた。

MAP-E方式のポート数制限が原因かも?という予測が立つ

すると、さらに知人から、v6プラス(MAP-E方式)でのIPv4通信の場合、ISP側でポート数が240ほどに制限されており、そっちで詰まっているのではないかという指摘があった。

確かにありそう。ただv6プラスではあるけど、この記事とは違ってWiresharkで見るとClaude CodeがQUICなどでUDP通信をしているわけではないのに詰まるんだよな〜。またClaude Code以外にもnettopでUDP通信を見てみたが、大量に接続が起きているようにも見えない。ただこれ以外に調べるポイントがなかったため、この方向で調査を進めることにした。

まずISP側でのv6プラスのポート数制限に引っかかっているということは、もし別のISP環境ではネットワークが死ぬことはないはずだ。幸い別の場所でIPv4をPPPoE接続している環境があったのと、スマホでテザリングして試すことができたので、検証してみた。

  • PPPoE環境で試した場合 => ネットワークは死なない
  • テザリングで試した場合 => ネットワークは死なない

PPPoE環境だけでなく、通信がかなり不安定なテザリング環境でも死なないことが分かったのは結構収穫だ。

ISP側ポート数制限に対して対策する

ここで対策として二つの手段が考えられそう(というのも知人に教えてもらえた)。

  • ポートセービングIPマスカレードという機能でポート数をいい感じに節約してくれる新しめなルーターを購入する
    • この記事によると、安物ルーターでも最近のやつは大体入ってるみたい
  • ISP側のv6プラス(MAP-E方式)の契約から、仕組み的にポート数が多めにもらえやすいDS-Lite形式の契約に変更する
    • 通常DS-Liteでも1000個程度のポート制限があるらしいが、enひかりだと超transixというDS-Lite方式の契約で、12800ポートほど確保できる

enひかりの超transixは、v6プラス198円/month => 超transix 399円/monthと、月額200円増やすだけで良い。ISP側のポート数を一気に増やせると検証もしやすそうだということで、ISP契約切り替えを試すことにした。

その結果、同じようにSubAgentが一斉起動したとしても、ネットワークが死ななくなった!!!ようやく解消してめでたい。

ただし原因に対しては疑問が残る...

ただ原因を完全に理解した状態になってないのは悲しい。自分の自宅環境だと契約切り替え前はかなり簡単に再現してしまっていて、

  • Claude Code以外のほとんど全ての通信をしているアプリケーションを落とし、Wireshark上でもあまりパケットが流れない状態にしておく
  • その状態でClaude Codeで5並列くらいでSubAgentを起動する

程度でもネットワークが死んでいた。前述したとおり、この時のTCPの同時接続数は100も行っておらず、240ポート数の制限にも達してない。またAnthropic APIへの接続はv6で行われていたので、そっちはポート数制限はそもそも関係ない。以下はその状況下でのnettopのスクショ。

そのため、何かもう一つの要因が重なって起きてたんだろうな〜と思う。たとえばこういうことが考えられそう。ただ全く検証できなかったので実際のところは分からない。

  • MAP-E方式は、ISP側はポート範囲を割り当てるだけで、NATテーブルの管理は自宅のルーター側に任されている。つまり、NATテーブルをどのように管理するかは、ルーター側の実装依存である
  • UDPだとタイムアウトまでNATセッションを維持してしまう問題は有名だが、ルーターによってはTCPも接続が切れた後もNATセッションをある程度維持してしまうのかもしれない
  • 結果として短期間に大量接続をしていると、TCPコネクションを切ってもしばらくはNATセッションを占有し、同時接続数が240に到達しなくても上限に引っかかってしまう

【2025/11/11 11:25】追記TCP/FINが来た後にNATテーブルに残るのあるあるかもという話も聞いた

23.9 NAT の IP アドレスマップの消去タイマの設定

まとめ

v6プラスでのIPv4通信のネットワーク環境の場合、Claude Codeが一定並列にSubAgentなどを起動しているとネットワークが死んでしまうということが分かった。これに対して、ISP側の契約変更をしてDS-Lite形式にすると解消できた。結構困っていたので解消できて良かった!

知人に教えてもらわないと絶対解消できなかったので助かりました🙏

そのほか調べたことメモ

今回、調べる際に前提知識が無さすぎたので、一つずつ調べていくことをした。その時の調査のメモを最後に残す。

  • IPv6での通信の場合、クライアント側とサーバー側もIPが一意に決まるため、そもそもNATが不要。NATテーブルでの問題はIPv4通信の場合で起きる
  • PPPoEでのIPv4通信なら、IPv4アドレスが契約につき1つ割り当てられるのでほとんどのポート(60000個ほど?)が使える
  • MAP-EやDS-LiteなどIPv4 over IPv6の場合は、複数の契約でIPv4アドレスが共有されて、ポートで分割されている。そのため、ポート数を全て使えるわけではない
  • MAP-E方式の場合、ISP側は契約に対して静的にポート範囲を決めている。自宅のルーター側ではそのポート範囲を使って、自宅のルーター側でNATをすることになる。つまり自宅ルーター側のNAT実装が関係する
  • DS-Lite方式の場合、ルーターはIPv4パケットをカプセル化して送るだけになる。その後ISP側のAFTR(Address Family Transition Router)でNATされる。この時ISP側が動的に決められるようになるので、MAP-E方式と比較するとポート数を柔軟に使える。逆に言えばISP側のNAT実装は触れないので、そちらの制限にかかると手は出せない
  • ルーターにはポートセービングIPマスカレードという機能があり、これを使うと利用するポート数を節約してくれる。イメージとしては、ルーターが扱うポートごとに、送信元IP・送信元ポート・プロトコル・送信先IP・送信先ポートを保持しておけば、ルーター側送信ポートは同じものを使っていても、戻りパケットの送信先情報から送信元情報に逆変換できるよねってイメージ?送信先はIP(+ポート?)単位なので、かなり節約できるイメージは持つ。

参考記事

京アジャで「EMこそClaude Codeでコード調査しよう」という発表をしました

京都アジャイル勉強会LT大会 #118 - connpassに参加してきました。LT発表の内容の密度が高く非常に楽しめました。特にコンフリクトの話が面白かったので、ぜひ資料を公開してほしいと思いました。また懇親会でも深掘りした話が出来て良かったです。また忘年会があるらしいので行ってみようと思います。

僕はせっかくの勉強会ということで「EMこそClaude Codeでコード調査しよう」という発表をしました。資料はこちら。

speakerdeck.com

以下書き起こしです。

EMこそClaude Codeでコード調査しよう

開発チームを持つEMは、他チーム・他職種から質問を受けることが多い

回答する時どうする?

  • 詳しそうなエンジニアに聞く
  • 自分でコードを読んでみる
  • Claude Codeやcodexを活用して調査する

質問が来たら、EMこそClaude Codeでコード調査することをおすすめ。

大前提: EMに質問が来る時の傾向

  • 6~7割の精度での回答で良いことが多い
  • 素早く大量に回答することが多い

コーディングエージェントと相性が良い

  • EMへの質問傾向とコーディングエージェントのコード調査は相性がいい
  • Claude Codeによる調査の特性
    • 完璧な調査は難しいが
    • 6~7割くらいの精度の高速なコード調査ができる

例: ブログの予約投稿って、その時刻ぴったり投稿できるの?

たとえば質問が来た時の最近の僕の調査の流れはこんな感じ。

  • (1) 調査するためのプロンプトへ調整する
    • ブログの予約投稿はどのような仕組みで実装されている?予約投稿でどの程度遅れうる?
    • 最初の腕の見せどころ。うまく質問に答えるための情報を得られる良いプロンプトを作る
    • 2~3分程度である程度回答が返ってくる
  • (2) Claude Codeへ質問を投げておく
  • (3) 調査結果をもとに実コードを見て正当性チェック
    • 現段階ではコーディングエージェントは堂々と全く違うことを言うことがあるので、最低限正当性チェックをする
    • 自分でチェックが難しければ詳しい人へ。調査情報で的確に再質問できる
  • (4) 調査内容を組み合わせて質問者に回答

工夫次第でコード調査精度も段違いに

3つの工夫を紹介。

  1. Thinkingモードで調査
  2. ディレクトリ構造などのドキュメントを必ず用意
  3. ダブルチェックしやすいCLAUDE.mdを整備

Thinkingモードで調査

  • ONにするだけで明らかに精度が上がる

ディレクトリ構造などのドキュメントを必ず用意

  • 全体の構造を教えて探索精度を上げる

ダブルチェックしやすいCLAUDE.mdを整備

  • 自分でダブルチェックしたい
  • 素早くチェックできるようにプロセスや証拠を必ず提示させておく

まとめ

  • コーディングエージェントは6~7割精度の調査が得意
  • 6~7割の精度の大量回答をするEMこそClaude Codeでコード調査しよう
  • 工夫をして調査精度も上げよう

LLM回答精度検証でテストデータやテストケースケースをAIに作ってもらう

RAGでのデータ整形(改行・インデント)がLLMの回答精度に与える影響を検証したでは、ダミーのテストデータやテストケースを色々作っている。実はこのデータはAIと壁打ちしながら作ったので、やり方を共有する。

ダミーのテストデータを作る

まずslack-explorer-mcpのメッセージ検索のレスポンスを模したダミーデータを用意したかった。流れとしては、slack-explorer-mcpで適当にメッセージ検索してJSONをゲット => そのJSONをClaude Codeに与えてダミーデータに変えてもらう、ということをした。

下記の会話内容が壁打ち&データ生成の流れ。最初からはうまくいかないので、データを作りながら修正していった。

> Slack Explorer MCPの挙動を色々試すため、ダミーのMessageのJSONを用意したい。以下のJSONをフォーマット例として、スレッド内メッセージはthread_tsがあり、スレッド外メッセージはないとする。ではプロジェクト内の情報を
含めず公開できるテストケースとして、以下の例を参考に作ってみてほしい。
この時
- thread内とthread外のメッセージは50:50の100メッセージ作る。中はランダムで並んでいることとする

{{ここに実際のJSONを貼る}}

⏺ できた!✨ .claude/tmp/dummy_messages.json にダミーデータを作成したよ!

  作成内容:
  - 合計100メッセージ
  - スレッド内(thread_tsあり): 50個
  - スレッド外(thread_tsなし): 50個
  - ユーザーID: U + 10文字の英数字(例: U012ABC3DEF)
  - チャンネルID: C + 8文字の英数字(例: C01ABC2DE)
  - チャンネル名: general、dev、tech-talk、project-alpha、devops、cost-reduction、qa、security、infrastructure
  - テキスト: 技術的な会話っぽい一般的な内容
  - タイムスタンプ: ランダムに配置

  これでSlack Explorer MCPの動作確認に使えるはずだよ!🎉

> - tsとthread_tsは別のタイムスタンプ内容にしてほしい
- 10件ほど、改行を含むものも混ぜてほしい。また文字数ももう少しばらつきがあった方がいいね。文字数ばらつき、改行ばらつきで色々用意よろしく

⏺ 了解!tsとthread_tsを別にして、改行や文字数のバリエーションを増やすね!✨

⏺ できたよ!✨ ダミーデータを更新したよ〜

  主な改善点:

  1. tsとthread_tsを分離 🕒
    - スレッドの親メッセージ:ts == thread_ts
    - スレッドの返信メッセージ:ts ≠ thread_ts(親メッセージのタイムスタンプを参照)
    - 例:最初のメッセージ(親)のtsは1756096495.765749、2番目のメッセージ(返信)のtsは1756096501.234567だけど、両方ともthread_tsは1756096495.765749
  2. 改行を含むメッセージを12件追加 📝
    - 箇条書き形式(例:「調査の結果、以下のことが分かりました:\n• 現在のビュー...」)
    - コードブロック形式(例:セットアップ手順のコード)
    - 複数段落形式(例:スプリントの振り返り、容量計画)
  3. 文字数のバリエーション 📊
    - 超短文:「OK」「了解です!」(2-10文字)
    - 短文:「新しい機能のテストが完了しました」(20-40文字)
    - 中文:通常の1行メッセージ(40-80文字)
    - 長文:改行を含む複数行メッセージ(150-300文字以上)

  これでより実際のSlackメッセージに近いリアルなテストデータになったと思うよ!🎉

oneline json、CSVのフォーマットを作る

上記のテストデータはpretty jsonの形式で、これを使ってprompt-with-pretty-json.txtを作っていた。

これを変換しoneline jsonとCSVバージョンのpromptも作りたい。形式変換は一度きりのスクリプトをAIに用意させて実行した方が正確になるだろうということで、次のような会話をした。

会話内容

> @prompt-with-pretty-json.txt を1行のJSONへonelinerを使って変換できる?

⏺ あー、それってJSON部分だけをワンライナーにしたいってことだよね?🤔

  ファイルを見てみたけど、これってプロンプトテキストとJSONデータが混在してるから、やり方が2つあるかなって思うんだけど!

  1. JSON部分だけを1行にする方法:
  sed -n '12,1047p' prompt-with-pretty-json.txt | jq -c .

  2. ファイル全体をJSON文字列としてエスケープする方法:
  jq -Rs . prompt-with-pretty-json.txt

  どっちが欲しいかなー?👀

  もし最初の方法で、JSON部分だけをワンライナーにして、前後のテキストも保持したいなら、もうちょっと複雑なコマンドになるけど、そういうのが必要?

  それとも、このプロンプトファイル全体を何かに使うための変換なのかな?使い方を教えてくれたら、もっとピッタリな方法を提案できるかも!✨

> ごめん。JSON部分だけ1行にして、prompt-with-oneline-json.txtという別のファイルを作りたいんだよね

⏺ あー、わかった!それならこうすればいいんじゃない?✨

⏺ Bash((sed -n '1,11p' prompt-with-pretty-json.txt; sed -n '12,1047p' prompt-with-pretty-json.txt | jq -c .; sed -n '1048,$p' prompt-with-pretty-json.txt) > prompt-wit…)
  ⎿  (No content)

⏺ よし、できたっぽい!🎉

  確認してみるね!

⏺ Bash(wc -l prompt-with-oneline-json.txt)
  ⎿        17 prompt-with-oneline-json.txt

⏺ Bash(head -n 15 prompt-with-oneline-json.txt)
  ⎿  Your role is to construct the permalink URL for a single Slack message specified by the user from Slack message data. Use the information below to construct the permalink URL.

     # How to construct the permalink
     … +1047 lines (ctrl+o to expand)

⏺ Bash(tail -n 5 prompt-with-oneline-json.txt)
  ⎿  # Let's begin
     Please construct the Slack permalink URL according to the instructions below.

     {{input}}

⏺ 完璧だよ!✨ できたみたい!

  元のファイルは1053行だったけど、新しいファイルは17行になってるよ!JSON部分が全部1行にまとまったから、すごくコンパクトになったね😊

  確認した感じだと:
  - 最初の11行:プロンプトの説明部分(そのまま)
  - 12行目:JSON全体が1行に(workspace_url から pagination まで全部!)
  - 最後の数行:プロンプトの続き(そのまま)

  って感じになってて、ちゃんと変換できてるっぽい!🎉

  これで prompt-with-oneline-json.txt ファイルができたから、使いたかった用途で使えるんじゃない?👍

> いいね。続いて。CSVのフォーマットを使った形式 = prompt-with-csv.txtを作りたい。
csv化するのは、messages.matchesの部分だけでよく、workspace_url、messages.paginationは別形式でいい。まずどういう形式にすべきか考えてみて

⏺ あー、なるほどね!CSVにするのって、matches部分だけで、workspace_urlとpaginationは別形式で残したいってことだよね🤔

  いくつか案を考えてみたから、メリット・デメリットと一緒に見てみて!✨

  案1: コメント形式 + CSV

  # workspace_url: https://example.slack.com
  # pagination.total_count: 100
  # pagination.page: 1
  # pagination.page_count: 1
  # pagination.per_page: 100
  # pagination.first: 1
  # pagination.last: 1

  user,text,ts,channel_id,channel_name,thread_ts
  U012ABC3DEF,"BigQuery materialized viewの...",1756096495.765749,C01ABC2DE,general,
  U023DEF4GHI,"了解です!",1756096501.234567,C01ABC2DE,general,1756096495.765749
  ...

  メリット:
  - シンプルで読みやすい
  - パースしやすい(コメント行をスキップすればいい)

  デメリット:
  - 階層構造が見えにくい

  案2: YAML frontmatter風

  ---
  workspace_url: https://example.slack.com
  pagination:
    total_count: 100
    page: 1
    page_count: 1
    per_page: 100
    first: 1
    last: 1
  ---

  user,text,ts,channel_id,channel_name,thread_ts
  U012ABC3DEF,"BigQuery materialized viewの...",1756096495.765749,C01ABC2DE,general,
  ...

  メリット:
  - 構造が見やすい
  - YAML形式なので、パースしやすい

  デメリット:
  - ちょっと複雑かも

  案3: JSONのまま残す(ハイブリッド)

  {"workspace_url":"https://example.slack.com","pagination":{"total_count":100,"page":1,"page_count":1,"per_page":100,"first":1,"last":1}}

  user,text,ts,channel_id,channel_name,thread_ts
  U012ABC3DEF,"BigQuery materialized viewの...",1756096495.765749,C01ABC2DE,general,
  ...

  メリット:
  - パースが簡単(1行目だけJSON.parseすればいい)
  - データの整合性が保たれる

  デメリット:
  - 見た目がちょっと統一感ない

  案4: 全部key=value形式

  workspace_url=https://example.slack.com
  pagination_total_count=100
  pagination_page=1
  pagination_page_count=1
  pagination_per_page=100
  pagination_first=1
  pagination_last=1

  user,text,ts,channel_id,channel_name,thread_ts
  U012ABC3DEF,"BigQuery materialized viewの...",1756096495.765749,C01ABC2DE,general,
  ...

  メリット:
  - シンプル
  - grep しやすい

  デメリット:
  - 階層構造が完全に失われる

  ---
  個人的には、案2のYAML frontmatter風か案3のJSONハイブリッドがいいんじゃないかなって思うんだけど!

  プロンプトとして使うなら、LLMが理解しやすい形式がいいから、案2の方がいいかも?🤔

  どう思う?他に「こういう形式がいい!」ってのがあったら教えて!

> 案4でいいんじゃないかなと思う。ただ
- 構造化は.を活用したらいいと思う。pagination.pageとかchannel.idとかだね
- CSVが、メッセージの一覧であることを補足した方が良さそう

⏺ あー、いいね!それすごくわかりやすい!✨

  じゃあこんな感じかな?

  [プロンプトの説明部分...]

  # Workspace and pagination info
  workspace_url=https://example.slack.com
  pagination.total_count=100
  pagination.page=1
  pagination.page_count=1
  pagination.per_page=100
  pagination.first=1
  pagination.last=1

  # Messages
  user,text,ts,channel.id,channel.name,thread_ts
  U012ABC3DEF,"BigQuery materialized viewの...",1756096495.765749,C01ABC2DE,general,
  U023DEF4GHI,"了解です!",1756096501.234567,C01ABC2DE,general,1756096495.765749
  ...

  [プロンプトの続き...]

  ポイントは:
  - pagination.page みたいに . で階層表現 ✅
  - CSVの前に # Messages コメントでメッセージ一覧って明示 ✅
  - CSVのヘッダーも channel.id, channel.name で統一感を出す ✅

  こんな感じでどう?これで実装してみていい?😊

> プロンプト的には階層化し、
# Slack message information
## Workspace and pagination info
...
## Messages
...
とした方が良さそうだね。

ではこのようになるようにプログラムを活用してtxtファイルを作れる?

⏺ あー、いいね!階層的にした方が断然わかりやすいよね!✨

  じゃあPythonスクリプトでサクッと作っちゃうよ!

⏺ Write(.claude/tmp/convert-to-csv.py)
  ⎿  Wrote 68 lines to .claude/tmp/convert-to-csv.py
     #!/usr/bin/env python3
     import json
     import csv
     import io
     # ファイルを読み込む
     with open('prompt-with-pretty-json.txt', 'r') as f:
         lines = f.readlines()
     # プロンプトの前半部分(1-11行目)
     … +58 lines (ctrl+o to expand)

⏺ よし、スクリプト作ったから実行してみるね!

⏺ Bash(python3 .claude/tmp/convert-to-csv.py)
  ⎿  ✅ prompt-with-csv.txt を作成しました!

⏺ やったー!✨ できたっぽい!確認してみるね!

⏺ Bash(head -n 30 prompt-with-csv.txt)
  ⎿  Your role is to construct the permalink URL for a single Slack message specified by the user from Slack message data. Use the information below to construct the permalink URL.

     # How to construct the permalink
     … +27 lines (ctrl+o to expand)

⏺ Bash(tail -n 10 prompt-with-csv.txt)
  ⎿  U078STU9VWX,品質メトリクスのダッシュボードを作成しました,1758681000.789012,C07STU8VW,qa,
     U089VWX0YZA,ペネトレーションテストの結果を報告します,1755861000.890123,C08VWX9YZ,security,1755860900.424242
     U090YZA1BCD,マルチリージョン構成の検討を始めました,1755854000.901234,C09YZA0BC,infrastructure,
     … +7 lines (ctrl+o to expand)

⏺ Bash(wc -l prompt-with-csv.txt)
  ⎿       196 prompt-with-csv.txt

⏺ あれ、ちょっと待って!😅 "# Slack message information" が2回出ちゃってるね!

  元のファイルの構造をもう一回確認してみるね!

⏺ Bash(sed -n '10,15p' prompt-with-pretty-json.txt)
  ⎿  # Slack message information
     {
         "workspace_url": "https://example.slack.com",
     … +2 lines (ctrl+o to expand)

⏺ あー、なるほど!元のファイルの11行目に "# Slack message information" があるから、それが重複しちゃったんだね😅

  スクリプトを修正するよ!

⏺ Update(.claude/tmp/convert-to-csv.py)
  ⎿  Updated .claude/tmp/convert-to-csv.py with 2 additions and 2 removals
        7    with open('prompt-with-pretty-json.txt', 'r') as f:
        8        lines = f.readlines()
        9
       10 -  # プロンプトの前半部分(1-11行目)
       11 -  prompt_header = ''.join(lines[0:11])
       10 +  # プロンプトの前半部分(1-10行目、11行目は "# Slack message information" なので除外)
       11 +  prompt_header = ''.join(lines[0:10])
       12
       13    # JSON部分(12-1047行目)
       14    json_text = ''.join(lines[11:1047])

⏺ よし、修正したから再実行するね!

⏺ Bash(python3 .claude/tmp/convert-to-csv.py)
  ⎿  ✅ prompt-with-csv.txt を作成しました!

⏺ Bash(head -n 30 prompt-with-csv.txt)
  ⎿  Your role is to construct the permalink URL for a single Slack message specified by the user from Slack message data. Use the information below to construct the permalink URL.

     # How to construct the permalink
     … +27 lines (ctrl+o to expand)

⏺ 完璧!✨ いい感じだね!最後の部分も確認するよ!

⏺ Bash(tail -n 10 prompt-with-csv.txt)
  ⎿  U078STU9VWX,品質メトリクスのダッシュボードを作成しました,1758681000.789012,C07STU8VW,qa,
     U089VWX0YZA,ペネトレーションテストの結果を報告します,1755861000.890123,C08VWX9YZ,security,1755860900.424242
     U090YZA1BCD,マルチリージョン構成の検討を始めました,1755854000.901234,C09YZA0BC,infrastructure,
     … +7 lines (ctrl+o to expand)

⏺ Bash(wc -l prompt-with-csv.txt)
  ⎿       195 prompt-with-csv.txt

⏺ 完璧だね!✨ うまくできたよ!

  prompt-with-csv.txt ができて、こんな構造になってるよ:

  [プロンプトの説明部分]

  # Slack message information
  ## Workspace and pagination info
  workspace_url=https://example.slack.com
  pagination.total_count=100
  ...

  ## Messages
  user,text,ts,channel.id,channel.name,thread_ts
  U012ABC3DEF,"...",1756096495.765749,C01ABC2DE,general,
  ...

  [プロンプトの続き]

  ポイント:
  - 階層構造が # と ## で明確になってる ✅
  - メタデータは pagination.page みたいに . で階層化 ✅
  - メッセージは読みやすいCSV形式 ✅
  - 元の1053行から195行にコンパクト化 ✅

  これでプロンプトとして使いやすくなったんじゃないかな!😊

テストケースを一緒に作る

さらにテストケースも一緒に作っていく。このときはAI に自分の回答を疑わせる /criticalthink コマンドを作ってみたが非常に役立った。最初は全く同じような内容のテストケースを作っていたが、一度criticalthinkを使って内省させることでバリエーションのある内容を作ることができた。

この時の具体的な会話の様子はこちら。

会話内容

> @prompt-with-pretty-json.txt からpermalink抽出をするようなテストケースを10件ほど作り、 @promptfooconfig.yaml に書き込みたい。

- いろんなタイプのメッセージ指定
- スレッド内メッセージとスレッド外メッセージ両方満遍なく
- 文字数のバリエーションを満遍なく

と考えた時、まずはどんなテストケースを作ればいいか考えてみて

⏺ おっけー!テストケース考えてみるね!💪

  prompt-with-pretty-json.txtのデータ見てみると、100件のメッセージがあって、スレッド返信(thread_tsあり)とスレッド外(thread_tsなし)が混ざってるね。

  こんな感じのテストケースどうかなー?✨

  番号指定のバリエーション

  1. 「最初のメッセージのURLが知りたい」
    - 1番目、通常メッセージ、長文(BigQueryの詳細な調査結果)
  2. 「2番目のメッセージのURL教えて」
    - 2番目、スレッド返信、超短文(「了解です!」)
  3. 「3番目のメッセージのURLが知りたい」(既存)
    - 3番目、通常メッセージ、中文
  4. 「5番目のメッセージ」
    - 5番目、スレッド返信、短文(「確認してみます」)

  位置指定のバリエーション

  5. 「最後のメッセージのURL」
    - 100番目、通常メッセージ、中文
  6. 「50番目のメッセージのpermalink」
    - 中間位置、確認が必要だけど多分通常メッセージ

  文字数バリエーション

  7. 「7番目のメッセージ」
    - 長文メッセージ(デプロイメントプロセスの詳細、改行・箇条書きあり)
  8. 「9番目のメッセージ」
    - 超短文(「OK」)、スレッド返信

  表現のバリエーション

  9. 「10個目のメッセージのURLください」
    - 「番目」じゃなくて「個目」表現
  10. 「20番目のメッセージのSlack URL教えて」
    - 「permalink」じゃなくて「URL」表現

  こんな感じでどう?🤔

  ポイント:
  - スレッド返信は2, 4, 8番目で3件
  - 通常メッセージは7件
  - 短文・中文・長文を満遍なく
  - 「番目」「個目」「URL」「permalink」など表現のバリエーションも入れた

  実際にpermalinkを構築する前に、この方針でOKか確認したいんだけど、どうかなー?それとも他のパターンも欲しい?😊

> /critical-think is running…

⏺ 1. 核心テーゼと信頼度スコア(初期)

  - 1-1. 核心テーゼ: 「番号指定・位置指定・表現のバリエーションを網羅し、スレッド返信と通常メッセージを3:7の比率で、短文・中文・長文を満遍なく含む10件のテストケースを作成する」
  - 1-2. 初期信頼度: 7/10(テストケースの方針は妥当だと思ったけど、実際のデータ検証が不十分)

  2. 基礎分析:仮定と文脈

  - 2-1. 高影響仮定:
    a. データの正確性仮定: prompt-with-pretty-json.txtのメッセージが1番目から順番に並んでいると仮定したが、実際にはmatches配列内の順序が何を基準にしているか未検証
    b. スレッド比率仮定: 「満遍なく」という要件に対してスレッド返信3件・通常メッセージ7件が適切と判断したが、根拠が不明確
    c. permalink構築仮定: 提案したメッセージ番号のpermalinkが実際に正しく構築できるか未検証
  - 2-2. 文脈の整合性:
    - ユーザーは「いろんなタイプのメッセージ指定」と言っているのに、番号指定に偏りすぎている
    - 「文字数のバリエーション」という要件があるのに、実際の文字数を数えていない
    - 既存のテストケース(3番目)の形式を踏襲すべきだったが、表現バリエーションを優先しすぎた

  3. 論理的整合性分析

  - 3-1. 前提の特定:
    - 「promptfooのテストケースは多様な入力パターンをカバーすべき」
    - 「matches配列のインデックスがそのままメッセージ番号になる」
    - 「10件のテストケースで十分な網羅性が得られる」
  - 3-2. 推論の連鎖:
    - 前提→バリエーション設計→具体的な番号選択という流れに論理的飛躍がある
    - 「50番目のメッセージ」を提案したが、「確認が必要だけど多分通常メッセージ」という曖昧な表現で、実際に確認していない
    - 「満遍なく」の定義が主観的で、なぜその配分が最適か説明できていない
  - 3-3. 潜在的な誤謬:
    - 性急な一般化: 既存テストケース1件から全体の方針を決めている
    - 確証バイアス: 「番号指定がメイン」と決めつけて、他のメッセージ指定方法(内容ベース、ユーザーベース等)を検討していない

  4. AI 特有の落とし穴分析

  - 4-1. 問題回避: 不合格 - ユーザーが求めているのは「どんなテストケースを作ればいいか」だが、実際のデータを検証せずに提案している。根本的な問題は「適切なテストケース選定には実データの分析が必要」という点
  - 4-2. 「ハッピーパス」バイアス: 不合格 - エッジケースを全く考慮していない。例えば:
    - 存在しない番号を指定された場合
    - 曖昧な指定(「最近のメッセージ」等)
    - 範囲指定(「1-5番目のメッセージ」)
    - チャンネル名やユーザー名での指定
  - 4-3. 過剰設計: 合格 - シンプルな10件の提案で適切
  - 4-4. 事実の正確性と幻覚: 不合格 - 実際にデータを確認せずに「50番目は多分通常メッセージ」と推測している。各番号のメッセージ内容、文字数、thread_tsの有無を検証していない

  5. リスクと緩和策分析

  - 5-1. 見落とされたリスク:
    a. データ構造の誤解: matches配列のインデックスとユーザーの「番目」が一致しない可能性(0-indexedか1-indexedか)
    b. permalink構築の失敗: 提案した番号のメッセージが実際に正しいpermalinkを生成できるか未検証のため、assert値が間違っている可能性
    c. テストの意図との乖離: promptfooの目的が「モデルの性能評価」なら、もっと難易度の高いケース(曖昧な指定、複雑な条件)を含めるべき
  - 5-2. 代替シナリオ:
    - 内容ベースの指定: 「BigQueryについて話しているメッセージ」「CVEについてのメッセージ」
    - ユーザーベースの指定: 「U012ABC3DEFさんの最初のメッセージ」
    - 時間ベースの指定: 「2025年10月のメッセージ」
    - チャンネルベースの指定: 「securityチャンネルの2番目のメッセージ」
    - エラーケース: 「1000番目のメッセージ」(存在しない)

  6. 統合と改訂推奨

  - 6-1. 欠陥の要約:
    - 実際のデータを読み込んで検証していない
    - 番号指定に偏りすぎて、他の指定方法を検討していない
    - エッジケースやエラーケースを全く考慮していない
    - 「満遍なく」の基準が主観的で根拠がない
    - permalink構築の正確性を検証していない
  - 6-2. 改訂信頼度スコア: 3/10(データ検証なしで提案したのは重大な欠陥)
  - 6-3. 実行可能な次のステップ:
    a. 即座に実データを読み込む: prompt-with-pretty-json.txtの1, 2, 3, 5, 7, 9, 10, 20, 50, 100番目のメッセージを実際に確認し、thread_tsの有無、文字数、正しいpermalinkを検証する
    b. テストケースの目的を明確化: promptfooで何を評価したいのか(基本的な抽出能力?曖昧な指定への対応?エラーハンドリング?)をユーザーに確認する
    c. バリエーションの再設計: 番号指定以外の指定方法(内容・ユーザー・チャンネル・時間ベース)とエッジケースを含めた包括的なテストケースセットを提案する

  ---
  結論: 前回の回答は表面的で、実データの検証を怠り、テストケースの多様性も不足していた。ユーザーに実装させる前に、必ず実データを確認して正確なpermalinkとassert値を提供すべきだったね💦

> > c. テストの意図との乖離: promptfooの目的が「モデルの性能評価」なら、もっと難易度の高いケース(曖昧な指定、複雑な条件)を含めるべき

これが大事だと思うな。
  - 5-2. 代替シナリオ:
    - 内容ベースの指定: 「BigQueryについて話しているメッセージ」「CVEについてのメッセージ」
    - ユーザーベースの指定: 「U012ABC3DEFさんの最初のメッセージ」
    - 時間ベースの指定: 「2025年10月のメッセージ」
    - チャンネルベースの指定: 「securityチャンネルの2番目のメッセージ」
    - エラーケース: 「1000番目のメッセージ」(存在しない)
も考慮して、テストケースを考え直して。
今回はエラーケースはいらないので気にしなくていいよ

⏺ おっけー!じゃあもっと実践的で難易度の高いテストケースを考え直すね!💪

  まず実際のデータを確認しながら、いろんな指定方法をカバーするテストケースを作っていくよー!

⏺ Read(prompt-with-pretty-json.txt)
  ⎿  Read 100 lines

⏺ データが確認できたよ!じゃあ、いろんな難易度と指定方法を網羅したテストケースを考えてみるね!✨

  まず、主要なパターンを整理してみた:

  テストケース案(10件)

  1. シンプルな番号指定(既存、難易度:低)

  input: "3番目のメッセージのURLが知りたい"
  expected: https://example.slack.com/archives/C02DEF3GH/p1757647893504629
  - index 2, 通常メッセージ、devチャンネル

  2. 内容ベース:特定技術名(難易度:中)

  input: "BigQueryについて話しているメッセージのURL"
  expected: https://example.slack.com/archives/C01ABC2DE/p1756096495765749
  - index 0, 通常メッセージ、長文、generalチャンネル

  3. 内容ベース:CVE番号含む(難易度:中-高)

  input: "CVEについて報告しているメッセージ"
  expected: https://example.slack.com/archives/C08VWX9YZ/p1758677875911179
  - index 19, 通常メッセージ、長文、securityチャンネル

  4. ユーザー名ベース(難易度:中-高)

  input: "U089VWX0YZAさんが最初に投稿したメッセージのURL"
  expected: https://example.slack.com/archives/C08VWX9YZ/p1757324832945909
  - index 9, 通常メッセージ、securityチャンネル

  5. チャンネル名ベース(難易度:中)

  input: "securityチャンネルの最初のメッセージ"
  expected: https://example.slack.com/archives/C08VWX9YZ/p1757324832945909
  - index 9, 通常メッセージ

  6. チャンネル名+順序(難易度:中)

  input: "devチャンネルの2番目のメッセージのURL"
  expected: https://example.slack.com/archives/C02DEF3GH/p1759403650647349
  - index 13, 通常メッセージ、長文(バグ修正のPR)

  7. 複合条件:チャンネル+内容(難易度:高)

  input: "securityチャンネルでCVEについて話しているメッセージ"
  expected: https://example.slack.com/archives/C08VWX9YZ/p1758677875911179
  - index 19, 通常メッセージ

  8. 複合条件:ユーザー+チャンネル+順序(難易度:最高)

  input: "U023DEF4GHIさんがdevチャンネルに投稿した最初のメッセージ"
  expected: https://example.slack.com/archives/C02DEF3GH/p1757647893504629
  - index 2, 通常メッセージ

  9. スレッド返信(難易度:中)

  input: "2番目のメッセージのURL"
  expected: https://example.slack.com/archives/C01ABC2DE/p1756096501234567?thread_ts=1756096495.765749&channel=C01ABC2DE&message_ts=1756096501.234567
  - index 1, スレッド返信、超短文(「了解です!」)

  10. 内容ベース:特定の技術フレーズ(難易度:中)

  input: "Blue-Greenデプロイメントについて検討しているメッセージ"
  expected: https://example.slack.com/archives/C05MNO6PQ/p1755237886747309
  - index 6, 通常メッセージ、長文、devopsチャンネル

  バリエーションの内訳

  - 番号指定: 2件(#1, #9)
  - 内容ベース: 3件(#2, #3, #10)
  - ユーザーベース: 1件(#4)
  - チャンネルベース: 2件(#5, #6)
  - 複合条件: 2件(#7, #8)
  - 通常メッセージ: 9件
  - スレッド返信: 1件
  - 短文: 1件
  - 中文: 4件
  - 長文: 5件

  どう?この方針で行ってみていい?それとも他に追加したいパターンある?😊

> めちゃくちゃいろんなパターンがあっていいね。ただスレッド返信系を3件ほどにしてほしい

⏺ おっけー!スレッド返信を3件に増やすね!💪

  じゃあ、テストケースを再構成してみるよー!

  改訂版テストケース案(10件)

  1. シンプルな番号指定・通常メッセージ(難易度:低)

  input: "3番目のメッセージのURLが知りたい"
  expected: https://example.slack.com/archives/C02DEF3GH/p1757647893504629
  - index 2, 通常メッセージ、中文、devチャンネル

  2. 内容ベース:特定技術名(難易度:中)

  input: "BigQueryについて話しているメッセージのURL"
  expected: https://example.slack.com/archives/C01ABC2DE/p1756096495765749
  - index 0, 通常メッセージ、長文、generalチャンネル

  3. 内容ベース:CVE番号含む(難易度:中-高)

  input: "CVEについて報告しているメッセージ"
  expected: https://example.slack.com/archives/C08VWX9YZ/p1758677875911179
  - index 19, 通常メッセージ、長文、securityチャンネル

  4. スレッド返信:シンプルな番号指定(難易度:低)

  input: "2番目のメッセージのURL"
  expected: https://example.slack.com/archives/C01ABC2DE/p1756096501234567?thread_ts=1756096495.765749&channel=C01ABC2DE&message_ts=1756096501.234567
  - index 1, スレッド返信、超短文(「了解です!」)、generalチャンネル

  5. スレッド返信:内容ベース(難易度:中)

  input: "「確認してみます」と返信しているメッセージ"
  expected: https://example.slack.com/archives/C03GHI4JK/p1759124091523456?thread_ts=1759124084.416529&channel=C03GHI4JK&message_ts=1759124091.523456
  - index 4, スレッド返信、短文、tech-talkチャンネル

  6. スレッド返信:チャンネル+内容ベース(難易度:中-高)

  input: "infrastructureチャンネルで「いいですね」と返信しているメッセージ"
  expected: https://example.slack.com/archives/C09YZA0BC/p1755685842123456?thread_ts=1755685836.962989&channel=C09YZA0BC&message_ts=1755685842.123456
  - index 11, スレッド返信、短文、infrastructureチャンネル

  7. チャンネル名ベース(難易度:中)

  input: "securityチャンネルの最初のメッセージ"
  expected: https://example.slack.com/archives/C08VWX9YZ/p1757324832945909
  - index 9, 通常メッセージ、中文、securityチャンネル

  8. チャンネル名+順序(難易度:中)

  input: "devチャンネルの2番目のメッセージのURL"
  expected: https://example.slack.com/archives/C02DEF3GH/p1759403650647349
  - index 13, 通常メッセージ、長文(バグ修正のPR)、devチャンネル

  9. 複合条件:ユーザー+チャンネル+順序(難易度:最高)

  input: "U023DEF4GHIさんがdevチャンネルに投稿した最初のメッセージ"
  expected: https://example.slack.com/archives/C02DEF3GH/p1757647893504629
  - index 2, 通常メッセージ、中文、devチャンネル

  10. 内容ベース:特定の技術フレーズ(難易度:中)

  input: "Blue-Greenデプロイメントについて検討しているメッセージ"
  expected: https://example.slack.com/archives/C05MNO6PQ/p1755237886747309
  - index 6, 通常メッセージ、長文、devopsチャンネル

  改訂版バリエーションの内訳

  - 番号指定: 2件(#1通常, #4スレッド)
  - 内容ベース: 4件(#2, #3通常, #5, #6スレッド)
  - チャンネルベース: 2件(#7, #8)
  - 複合条件: 1件(#9)
  - 技術フレーズ: 1件(#10)
  - 通常メッセージ: 7件
  - スレッド返信: 3件 ✨
  - 短文: 3件(うちスレッド返信2件)
  - 中文: 4件
  - 長文: 3件

  これでどうかな?スレッド返信も難易度のバリエーションをつけてみたよ!😊

> 良さそうだね。ではこの内容を使って、extract-slack-url-eval/promptfooconfig.yaml のテストケースを拡充してほしい。

⏺ おっけー!promptfooconfig.yaml にテストケース追加するね!✨

  まず現在のファイル確認してから、10件のテストケースを追加するよー!

⏺ Read(promptfooconfig.yaml)
  ⎿  Read 22 lines

⏺ おっけー、既存のフォーマット確認できたよ!

  ちょっと待って、既存の YAML のフォーマットが少し気になるんだけど...😅

  現在の設定だと assert が vars の中に入ってるけど、promptfoo の正しいフォーマットだと assert は vars と同じインデントレベルにあるべきなんだよね。

  こんな感じ:
  tests:
    - vars:
        input: "..."
      assert:
        - type: equals
          value: '...'

  このフォーマットで修正して、10件のテストケースを追加してもいい?それとも既存のフォーマットのまま進める?🤔

> ほんまや。それで進めてほしい。また各テストケースには、どんなテストケースかコメントで説明もしておいてほしい。内容ベース:特定の技術フレーズ(難易度:中)みたいなやつ

⏺ おっけー!正しいフォーマットで、コメント付きでテストケースを書き直すね!💪

⏺ Update(promptfooconfig.yaml)
  ⎿  Updated promptfooconfig.yaml with 67 additions and 3 removals
       14
       15
       16    tests:
       17 +    # 1. シンプルな番号指定・通常メッセージ(難易度:低)
       18      - vars:
       19          input: "3番目のメッセージのURLが知りたい"
       20 -        assert:
       21 -          - type: equals
       22 -            value: 'https://example.slack.com/archives/C02DEF3GH/p1757647893504629'
       20 +      assert:
       21 +        - type: equals
       22 +          value: 'https://example.slack.com/archives/C02DEF3GH/p1757647893504629'
       23 +
       24 +    # 2. 内容ベース:特定技術名(難易度:中)
       25 +    - vars:
       26 +        input: "BigQueryについて話しているメッセージのURL"
       27 +      assert:
       28 +        - type: equals
       29 +          value: 'https://example.slack.com/archives/C01ABC2DE/p1756096495765749'
       30 +
       31 +    # 3. 内容ベース:CVE番号含む(難易度:中-高)
       32 +    - vars:
       33 +        input: "CVEについて報告しているメッセージ"
       34 +      assert:
       35 +        - type: equals
       36 +          value: 'https://example.slack.com/archives/C08VWX9YZ/p1758677875911179'
       37 +
       38 +    # 4. スレッド返信:シンプルな番号指定(難易度:低)
       39 +    - vars:
       40 +        input: "2番目のメッセージのURL"
       41 +      assert:
       42 +        - type: equals
       43 +          value: 'https://example.slack.com/archives/C01ABC2DE/p1756096501234567?thread_ts=1756096495.765749&channel=C01ABC2DE&message_ts=1756096501.234567'
       44 +
       45 +    # 5. スレッド返信:内容ベース(難易度:中)
       46 +    - vars:
       47 +        input: "「確認してみます」と返信しているメッセージ"
       48 +      assert:
       49 +        - type: equals
       50 +          value: 'https://example.slack.com/archives/C03GHI4JK/p1759124091523456?thread_ts=1759124084.416529&channel=C03GHI4JK&message_ts=1759124091.523456'
       51 +
       52 +    # 6. スレッド返信:チャンネル+内容ベース(難易度:中-高)
       53 +    - vars:
       54 +        input: "infrastructureチャンネルで「いいですね」と返信しているメッセージ"
       55 +      assert:
       56 +        - type: equals
       57 +          value: 'https://example.slack.com/archives/C09YZA0BC/p1755685842123456?thread_ts=1755685836.962989&channel=C09YZA0BC&message_ts=1755685842.123456'
       58 +
       59 +    # 7. チャンネル名ベース(難易度:中)
       60 +    - vars:
       61 +        input: "securityチャンネルの最初のメッセージ"
       62 +      assert:
       63 +        - type: equals
       64 +          value: 'https://example.slack.com/archives/C08VWX9YZ/p1757324832945909'
       65 +
       66 +    # 8. チャンネル名+順序(難易度:中)
       67 +    - vars:
       68 +        input: "devチャンネルの2番目のメッセージのURL"
       69 +      assert:
       70 +        - type: equals
       71 +          value: 'https://example.slack.com/archives/C02DEF3GH/p1759403650647349'
       72 +
       73 +    # 9. 複合条件:ユーザー+チャンネル+順序(難易度:最高)
       74 +    - vars:
       75 +        input: "U023DEF4GHIさんがdevチャンネルに投稿した最初のメッセージ"
       76 +      assert:
       77 +        - type: equals
       78 +          value: 'https://example.slack.com/archives/C02DEF3GH/p1757647893504629'
       79 +
       80 +    # 10. 内容ベース:特定の技術フレーズ(難易度:中)
       81 +    - vars:
       82 +        input: "Blue-Greenデプロイメントについて検討しているメッセージ"
       83 +      assert:
       84 +        - type: equals
       85 +          value: 'https://example.slack.com/archives/C05MNO6PQ/p1755237886747309'

⏺ 完成したよ!✨

  promptfooconfig.yaml に10件のテストケースを追加できたよ!

  追加したテストケースの内訳:
  1. ✅ シンプルな番号指定・通常メッセージ(難易度:低)
  2. ✅ 内容ベース:特定技術名(難易度:中)
  3. ✅ 内容ベース:CVE番号含む(難易度:中-高)
  4. ✅ スレッド返信:シンプルな番号指定(難易度:低)
  5. ✅ スレッド返信:内容ベース(難易度:中)
  6. ✅ スレッド返信:チャンネル+内容ベース(難易度:中-高)
  7. ✅ チャンネル名ベース(難易度:中)
  8. ✅ チャンネル名+順序(難易度:中)
  9. ✅ 複合条件:ユーザー+チャンネル+順序(難易度:最高)
  10. ✅ 内容ベース:特定の技術フレーズ(難易度:中)

  主な変更点:
  - YAMLフォーマットを修正(assertをvarsと同じインデントレベルに)
  - 各テストケースに難易度と種類のコメントを追加
  - スレッド返信のpermalinkにはクエリパラメータ付き
  - 通常メッセージ7件、スレッド返信3件のバランス

  これでいろんなパターンのテストができるようになったよ!😊

まとめ

今回は、前回の記事の検証時にテストデータやテストケースをどのように作っていったかを共有した。AIによってテストデータも素早く作れて助かる。