Movatterモバイル変換


[0]ホーム

URL:


$shibayu36->blog;

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

京アジャで「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によってテストデータも素早く作れて助かる。

RAGでのデータ整形(改行・インデント)がLLMの回答精度に与える影響を検証した

slack-explorer-mcpでは、該当メッセージのpermalink URLをレスポンスで返さずに、利用側のAI Agentで組み立ててもらっている。なぜなら、permalinkをメッセージごとに返してしまうとトークン消費量が非常に多くなってしまうからだ。permalinkは他で返しているデータで再構築できるため省略し、利用側で組み立ててもらうことでトークン節約をしている。しかし、この方法だとうまくいったりうまくいかなかったりという現象が起きていた。

slack-explorer-mcpは現状はonelineのJSON形式で巨大なレスポンスを返している。これを人間にも読みやすい改行・インデントありのJSON形式にすることでLLMにとっても精度が上がるのかを知りたくなった。

そこで今回は、データの整形方法(改行・インデントの有無)がLLMの回答精度に与える影響を検証した。検証のテストケース量やデータ量は少ない状態(リストデータ数が100個、テストケースが16個程度)なので参考程度にしてほしい。

実験した場所:shibayu36/playground/extract-slack-url-eval

結論

検証の結果、次の3つのことが分かった。

  • 改行・インデントで読みやすい形に整形しても特に精度は上がらない
  • LLMモデルが強くなればなるほど整形による精度の差がなくなっていく
  • RAGにおいてはトークン効率が最も良いフォーマットを選んでおけば良さそう

ちなみに最近データフォーマットによる回答精度に差があるが、LLMモデルが強くなると差がなくなるという議論を見かけた。こちらは改行やインデント有無ではなく、MarkdownやCSV、JSONLなどデータフォーマットの差での検証をしているが、似たような結論になっていそうだ。

検証方法

promptfooというツールを使って検証した。

まずテストのためのRAGデータとして、slack-explorer-mcpが返すメッセージ検索のレスポンスを模したダミーデータを用意した。そのデータに対して次の3つの形式を用意した。

  1. oneline JSON: 改行やインデントなしのJSON形式
  2. pretty JSON: 改行・インデントありで整形したJSON形式
  3. CSV: カンマ区切りのCSV形式。改行を含みトークン効率の良い形式だとどうなるか確認するため

実際のダミーデータ例はこんな感じ。詳しくはmessages-pretty.jsonを参照。

{    "workspace_url": "https://example.slack.com",    "messages":{        "matches":[{                "user": "U012ABC3DEF",                "text": "BigQuery materialized viewの重複排除についての詳細な調査結果を共有します\n\n調査の結果、以下のことが分かりました:",                "ts": "1756096495.765749",                "channel":{                    "id": "C01ABC2DE",                    "name": "general"}},{                "user": "U023DEF4GHI",                "text": "了解です!",                "ts": "1756096501.234567",                "channel":{                    "id": "C01ABC2DE",                    "name": "general"},                "thread_ts": "1756096495.765749"},            ...

プロンプトは次のように、Slackのpermalinkを生成するための情報を含めて、permalink URLのみ出力するようにした。

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 permalinkResponse includes workspace_url, channel.id, and ts (timestamp) which can be used to construct Slack permalinks:- Regular message (no thread_ts field): {workspace_url}/archives/{channel.id}/p{ts without dot}- Thread reply (has thread_ts field): Same URL with ?thread_ts={thread_ts}&cid={channel.id}# Output formatOutput only the permalink URL.# Slack message information{{RAG data}}# Let's beginPlease construct the Slack permalink URL according to the instructions below.{{input}}

テストケースは16個用意。例えばinputに入るテストケースはこのようなものがある。全テストケースはこちらを参照。

  • 3番目のメッセージのURLが知りたい
  • BigQueryについて話しているメッセージのURL

さらに検証モデルとしては、モデル性能差による違いも見るため、最新モデルと、少し古いモデルを選定した。

  • gpt-5-2025-08-07(最新)
  • gpt-4.1-mini-2025-04-14(少し古い小型モデル)
  • claude-sonnet-4-5-20250929(最新)
  • claude-3-7-sonnet-20250219(少し古い)

結果

16テストケースにおける正答率は次のとおりだった。

モデル oneline JSON pretty JSON CSV
gpt-5100% (16/16)100% (16/16)100% (16/16)
gpt-4.1-mini62.50% (10/16)43.75% (7/16)68.75% (11/16)
claude-sonnet-4-562.50% (10/16)68.75% (11/16)75% (12/16)
claude-3-7-sonnet56.25% (9/16)56.25% (9/16)43.75% (7/16)

gpt-5は全フォーマットで驚異の正答率100%で、フォーマットによる差が全くなかった。他のモデルでは、それぞれ精度が高いフォーマットはバラバラ。

この結果から、改行・インデントを入れると精度が上がるということはなく、さらにモデルが強くなればフォーマットによる差がなくなっていくことが確認できた。

まとめ

今回の検証から、RAGでLLMにリストデータを渡すとき、改行・インデントで読みやすい形に整形しても特に精度は上がらない事が分かった。またLLMモデルが強くなれば、フォーマットによる差はなくなっていく。現在のgpt-5でも正答率100%だったため、今後はさらに差がなくなっていくだろう。

この結果から、逆説的にRAGにおいてはトークン効率が最も良いフォーマットを選んでおけば良いと言える。今回のRAGデータでは

  • oneline JSON: 6,479トークン
  • pretty JSON: 9,144トークン
  • CSV: 4,784トークン

となっており、CSV形式が最もトークン効率が良いため、slack-explorer-mcpでもCSV形式を採用しても良さそうだ。

関連リンク

検索
おすすめの本
おすすめの本もっと見る

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp