サイトアイコン 協栄情報ブログ

Bugターミネーター:AIアシストデバッグの効率的な方法

こんにちは、松山です。
これは「AI駆動開発実践」シリーズの第6篇です。

シリーズの目次

第1篇 AIプログラミングの新時代:Claude Codeとは?
第2篇 10分で始める:Claude Codeのインストールと設定完全ガイド
第3篇 基本操作:Claude Codeとの正しい対話の仕方
第4篇 AIにあなたのプロジェクトを理解させる:コンテキスト管理のテクニック
第5篇 コードリファクタリングの神器:AIに古いコードを最適化させる

深夜3時、本番環境でアラート発生。

ユーザーから機能異常の苦情が来て、ログはエラーだらけ。でも、見ても見てもさっぱりわからない…

こんな状況、すべての開発者が経験したことがあるはずです。

従来のデバッグ:

Claude Codeを使うと:

今日は、AIをあなたのデバッグアシスタントにする方法を教えます。

AIデバッグの強み

なぜAIはデバッグが得意?

1. 膨大な経験

2. 体系的な思考

3. 高速学習

エラーメッセージの解読

テクニック1:AIにエラーを説明させる

シーン: Pythonでエラーが出たが、意味がわからない

あなた:このエラーに遭遇しました。分析してください:

TypeError: unsupported operand type(s) for +: 'int' and 'NoneType'
  File "app.py", line 45, in calculate_total
    total = subtotal + shipping_cost
  File "services/pricing.py", line 120, in get_shipping_cost
    return base_rate * weight

Claude:エラー情報を分析します:

【エラータイプ】
TypeError:intとNoneを加算しようとした

【発生場所】
app.py 45行目:total = subtotal + shipping_cost

【根本原因】
shipping_costがNoneで、subtotalがint
Noneに対して数学演算はできません

【トレース元】
スタックから見ると、shipping_costはget_shipping_cost関数から来ています
この関数はpricing.py 120行目で base_rate * weight を返しています

【考えられる原因】
1. base_rateがNone
2. weightがNone
3. または、get_shipping_costがある分岐でreturnしていない

【確認をお勧め】
pricing.pyのget_shipping_cost関数を確認:
- すべての分岐でreturnしていますか?
- base_rateはどこから来ますか?Noneの可能性は?
- weightパラメータは検証されていますか?
あなた:pricing.pyを確認したら、確かにある分岐でreturnしていませんでした

Claude:問題が見つかりました!修正案:

def get_shipping_cost(weight, destination):
    if destination == 'local':
        return 5.0
    elif destination == 'domestic':
        base_rate = 10.0
        return base_rate * weight
    elif destination == 'international':
        base_rate = 25.0
        return base_rate * weight
    # ❌ 問題:destinationがどの分岐にも一致しない場合、returnがない

修正:
    else:
        # デフォルトはローカル配送料
        return 5.0
        # または例外をスロー:
        # raise ValueError(f"Unknown destination: {destination}")

テクニック2:複雑なスタックトレースを解読

シーン: 深い再帰エラー

あなた:プログラムがクラッシュしました。スタック情報が超長いです:
[100行のスタックトレースを貼り付け]

RecursionError: maximum recursion depth exceeded
[...100行のスタック...]

Claude:スタックトレースを分析中…

【問題の特定】
再帰深度制限超過(通常は1000層)

【主要な呼び出しチェーン】(重複を除く)
process_node() → find_children() → process_node() → ...

【ループパターン】
process_nodeとfind_childrenが相互に呼び出し、無限ループを形成

【問題の特定】
ファイルtree.py:
- 45行目:process_nodeがfind_childrenを呼び出し
- 60行目:find_childrenがprocess_nodeを呼び出し

【考えられる原因】
データ構造に循環参照:
ノードAの子ノードにノードBが含まれる
ノードBの子ノードにまたノードAが含まれる

【修正案】
1. 訪問記録を追加し、重複訪問を回避:
   visited = set()
   if node.id in visited:
       return
   visited.add(node.id)

2. または再帰深度を制限:
   def process_node(node, depth=0, max_depth=10):
       if depth > max_depth:
           raise ValueError("Tree too deep")

テクニック3:わかりにくいエラーメッセージを分析

シーン: サードパーティライブラリの謎のエラー

あなた:Celeryタスクが失敗、エラーメッセージの意味がわかりません:

kombu.exceptions.OperationalError: [Errno 111] Connection refused
  at /usr/lib/python3.9/site-packages/kombu/connection.py line 458

Claude:これはCelery/RabbitMQ関連のエラーです。

【エラー解読】
OperationalError:操作エラー
Errno 111:Connection refused - 接続拒否

【意味】
Celery workerがメッセージキュー(RabbitMQ/Redis)に接続できない

【よくある原因】
1. メッセージキューサービスが起動していない
2. 設定されているアドレス/ポートが間違っている
3. ファイアウォールが接続をブロック
4. ネットワーク問題

【調査手順】
1. RabbitMQ/Redisが動作しているか確認:
   sudo systemctl status rabbitmq-server
   # または
   redis-cli ping

2. Celery設定を確認:
   # settings.py
   CELERY_BROKER_URL = 'redis://localhost:6379/0'
   # アドレス、ポートが正しいか確認

3. 接続をテスト:
   telnet localhost 6379
   # または
   nc -zv localhost 6379

4. ファイアウォールルールを確認

【クイック修正】
開発環境の場合、最も可能性が高いのはメッセージキューの起動を忘れたこと:
sudo systemctl start rabbitmq-server
# または
redis-server

ログ分析テクニック

テクニック4:大量のログから問題を見つける

シーン: 5000行のログから問題を探す

あなた:ユーザーが機能異常を報告、ログファイルが大きい(5000行)。
logs/app.logを分析して、
関連するエラー情報を見つけてください

Claude:ログファイルを分析中…

【主要エラー】(時系列順)

1. 14:32:15 ERROR - Database connection timeout
   → データベース接続タイムアウト

2. 14:32:20 WARNING - Retry attempt 1/3
   → リトライ開始

3. 14:32:25 WARNING - Retry attempt 2/3
   → 2回目のリトライ

4. 14:32:30 ERROR - All retry attempts failed
   → リトライすべて失敗

5. 14:32:31 ERROR - User operation failed: create_order
   → ユーザー操作失敗:注文作成

【タイムライン分析】
14:32:15 - データベース問題が発生開始
14:32:30 - リトライ失敗
14:32:31 - ユーザーに見えるエラー発生

【根本原因】
データベース接続が不安定で、注文作成が失敗

【関連ログ】
1分前(14:31:00-14:32:00):
- データベースクエリ時間が50msから徐々に5000msに上昇
- 大量のスロークエリ警告

【提案】
1. データベース状態を確認:スロークエリ、接続数、ロック待機
2. データベースサーバーの負荷を確認
3. 問題のあるクエリの最適化または接続プールサイズの増加を検討

テクニック5:複数のログソースを関連付け

シーン: 分散システム、複数サービスのログ

あなた:ユーザーリクエストが失敗、3つのサービスが関係:
- APIサービスログ:api.log
- 注文サービスログ:order.log
- 決済サービスログ:payment.log

リクエストID:req-12345
この3つのログを関連付けて分析し、
失敗原因を見つけてください

Claude:リクエストreq-12345を関連付けて分析中…

【完全なリクエストチェーン】

1. APIサービス(api.log):
   14:30:00 INFO [req-12345] 注文作成リクエスト受信
   14:30:01 INFO [req-12345] 注文サービス呼び出し → order-service

2. 注文サービス(order.log):
   14:30:02 INFO [req-12345] 注文作成、注文番号:ORD-9876
   14:30:03 INFO [req-12345] 決済サービス呼び出し → payment-service

3. 決済サービス(payment.log):
   14:30:04 ERROR [req-12345] 決済ゲートウェイタイムアウト
   14:30:09 ERROR [req-12345] 決済失敗:GATEWAY_TIMEOUT
   14:30:09 INFO [req-12345] 注文サービスに失敗を返す ←

4. 注文サービス(order.log):
   14:30:10 ERROR [req-12345] 決済失敗、注文をロールバック
   14:30:10 INFO [req-12345] 注文キャンセル:ORD-9876
   14:30:10 INFO [req-12345] APIサービスに失敗を返す ←

5. APIサービス(api.log):
   14:30:11 ERROR [req-12345] 注文作成失敗
   14:30:11 INFO [req-12345] クライアントにエラーを返す

【問題特定】
決済サービスのサードパーティゲートウェイタイムアウト(14:30:04)

【影響チェーン】
決済タイムアウト → 注文ロールバック → リクエスト失敗

【提案】
1. 決済ゲートウェイの状態を確認
2. 決済タイムアウトのリトライメカニズムを検討
3. ユーザー体験を最適化:タイムアウト時は「失敗」ではなく「処理中」と表示

段階的な問題調査

テクニック6:二分探索法で特定

シーン: どこでエラーが発生したか不明

あなた:機能が昨日は正常で、今日は動きません。
コードに20個のコミットがあり、どの変更が原因かわかりません

Claude:二分探索法で特定します:

1. まず中間のコミット(10番目)をテスト:
   git checkout commit-10
   テストを実行…

あなた:10番目のコミットは正常です

Claude:問題はcommit-11からcommit-20の間です

2. 15番目のコミットをテスト:
   git checkout commit-15
   テストを実行…

あなた:15番目のコミットですでに問題があります

Claude:問題はcommit-11からcommit-15の間です

3. 13番目のコミットをテスト:
   git checkout commit-13

あなた:13番目は正常です

Claude:問題はcommit-14またはcommit-15です

4. commit-14を確認:
   git show commit-14

問題発見:commit-14で価格計算ロジックを修正し、
ゼロ除算エラーを導入

【問題のあるコミットを特定】
commit-14:"Update pricing logic"
具体的なファイル:services/pricing.py line 67
問題コード:price = base / quantity
quantityが0の時クラッシュ

テクニック7:診断コードを追加

シーン: 中間状態を知る必要がある

あなた:関数が間違った結果を返していますが、
どのステップの計算が間違っているかわかりません

Claude:診断ログを追加します:

def calculate_final_price(items, discount, tax_rate):
+   print(f"[DEBUG] Input: items={items}, discount={discount}, tax_rate={tax_rate}")

    subtotal = sum(item.price for item in items)
+   print(f"[DEBUG] Subtotal: {subtotal}")

    discounted = subtotal * (1 - discount)
+   print(f"[DEBUG] After discount: {discounted}")

    final = discounted * (1 + tax_rate)
+   print(f"[DEBUG] Final price: {final}")

    return final

一度実行して、出力を確認:
[DEBUG] Input: items=[...], discount=0.1, tax_rate=0.08
[DEBUG] Subtotal: 100.0
[DEBUG] After discount: 90.0
[DEBUG] Final price: 97.2  ← 正しい

でも、結果が間違っていると言いましたか?実際の出力を貼り付けてください

あなた:[出力を貼り付け]
[DEBUG] Final price: -97.2

Claude:問題発見!priceが負の数です。
subtotal計算を確認:

あぁ、見つけました:
subtotal = sum(item.price for item in items)
あるitem.priceが負の数です!

理由:返品商品のpriceが負の数に設定されている
修正:正の価格の商品のみを集計
subtotal = sum(item.price for item in items if item.price > 0)

実例:複雑な本番環境バグの解決

問題の説明

現象:

エラーメッセージ:

IntegrityError: duplicate key value violates unique constraint

デバッグプロセス

Step 1:エラー分析

あなた:本番環境でたまにこのエラーが出ます:
IntegrityError: duplicate key value violates unique constraint "orders_order_number_key"
DETAIL: Key (order_number)=(ORD-12345) already exists

問題:
1. 本番環境でのみ発生
2. ローカルでは再現できない
3. 発生確率は約5%

Claude:エラー情報を分析します:

【エラータイプ】
データベースユニーク制約違反

【具体的な問題】
注文番号(order_number)の重複

【考えられる原因】
1. 注文番号生成アルゴリズムに問題(十分にユニークでない)
2. 並行リクエストによる(競合状態)
3. リトライメカニズムによる重複作成

【なぜ本番環境だけ?】
本番環境の特徴:
- 高並行性(ローカルにはない)
- 複数インスタンス(ローカルは単一インスタンス)
→ ヒント:並行性問題の可能性が高い

【調査推奨】
注文番号生成コードを確認、
特に並行安全性の問題に注意

Step 2:コード確認

あなた:注文番号生成コード:

def generate_order_number():
    today = datetime.now().strftime('%Y%m%d')
    count = Order.objects.filter(
        created_at__date=datetime.now().date()
    ).count()
    return f"ORD-{today}-{count+1:04d}"

Claude:問題が見つかりました!これは典型的な競合状態です:

【問題シナリオ】
2つの並行リクエストが同時に実行:

時刻T1:
- リクエストA:count = 10
- リクエストB:count = 10(同じく10!)

時刻T2:
- リクエストA:ORD-20250109-0011を生成
- リクエストB:ORD-20250109-0011を生成(重複!)

時刻T3:
- リクエストA:データベースに書き込み ✓
- リクエストB:データベースに書き込み ❌ ユニーク制約違反

【なぜ本番環境だけ?】
本番環境は並行性が高く、2つのリクエストが同時に到着する確率が高い

【解決策】

方法1:データベースシーケンスを使用
CREATE SEQUENCE order_number_seq;

def generate_order_number():
    today = datetime.now().strftime('%Y%m%d')
    seq = connection.execute("SELECT nextval('order_number_seq')").scalar()
    return f"ORD-{today}-{seq:04d}"

方法2:UUIDを使用
import uuid

def generate_order_number():
    return f"ORD-{uuid.uuid4().hex[:12].upper()}"

方法3:データベースレベルの自動増分+プレフィックス
# モデル
order_number = models.CharField(unique=True, blank=True)

def save(self, *args, **kwargs):
    if not self.order_number:
        super().save(*args, **kwargs)  # まず保存してIDを取得
        today = self.created_at.strftime('%Y%m%d')
        self.order_number = f"ORD-{today}-{self.id:06d}"
        super().save(update_fields=['order_number'])

推奨:方法2(UUID)、最もシンプルで完全に衝突なし

Step 3:修正の検証

あなた:UUID方式を採用、デプロイ後もたまにエラーが出ますが、
エラーメッセージが変わりました:
IntegrityError: duplicate key value violates unique constraint "orders_pkey"

Claude:これは新しい問題:主キー(ID)の衝突

原因:複数のアプリケーションインスタンスが異なるID生成器を使用

【問題】
自動増分IDを使用している場合、複数インスタンスが同じIDを生成する可能性

【確認】
データベースのID列の定義は?
- 自動増分(SERIAL):衝突しないはず
- 手動指定:衝突の可能性

モデル定義を確認:

あなた:モデルはカスタムID生成を使用:
id = models.IntegerField(primary_key=True)

def save(self):
    if not self.id:
        max_id = Order.objects.aggregate(Max('id'))['id__max'] or 0
        self.id = max_id + 1
    super().save()

Claude:また競合状態です!

2つのインスタンスが同時に:
- max_idをクエリ:100
- 新しいIDを計算:101
- 書き込み:衝突!

【修正】
データベース自動増分を使用:

id = models.AutoField(primary_key=True)

カスタムsaveロジックを削除

こうすればデータベースがID一意性を保証

Step 4:完全解決

修正後:
- UUID注文番号を使用
- データベース自動増分IDを使用
- 冪等性チェックを追加(リトライの重複を防止)

デプロイして3日間観察:
✓ 0エラー
✓ 問題解決!

デバッグテクニックまとめ

クイック診断チェックリスト

AIデバッグ対話テンプレート

【問題の説明】
現象:XXX機能が動作しない
再現:YYYの手順

【エラー情報】
[完全なエラースタック]

【環境情報】
- システム:XXX
- 言語/フレームワークバージョン:YYY
- データベース:ZZZ

【試したこと】
- 試行1:無効
- 試行2:無効

【関連コード】
[主要コードを貼り付け]

考えられる原因を分析してください

次回予告

次の記事『ゼロから始める:Claude Codeで完全なアプリケーションを構築』では、以下を教えます:

デバッグでバグを解決し、開発で価値を創造。次回はゼロから完全なアプリケーションを作ります!


💬 インタラクションタイム

あなたが遭遇した最も難しいバグは何でしたか?

小さな課題: 未解決のバグを1つ見つけて、Claude Codeで分析してみてください!

このブログをフォローして、デバッグマスターになりましょう! 🚀


このシリーズについて

これは「AI駆動開発実践」シリーズの第6篇です。今日はAIデバッグを学びました。次回はゼロから完全なアプリケーションを構築します!

新しいプロジェクトを作る準備はできましたか?次回もお楽しみに! 👋

モバイルバージョンを終了