Skip to main content

CrewAI のマルチエージェントアプリケーションをデバッグする

CrewAI と W&B Weave で、AI エージェントの構築とデバッグをより高速に。マルチエージェントのワークフローのあらゆるステップを監視・分析し、最適化できます。この記事は AI による翻訳です。誤訳の可能性があればコメント欄でお知らせください。
Created on August 27|Last edited on September 3
マルチエージェント型 AI アプリケーションのデバッグ は大きな課題です。 エージェント指向のワークフロー より複雑で現実的なタスクの解決に用いられるようになるにつれ、開発者は各エージェントの動作を観測し、理解し、最適化することへの要求が高まっています。 CrewAI 特化したメンバーで構成されるチームを構築し、連携させるための高度なフレームワークを提供します。 AI エージェントさらにその上で W&B Weave 各エージェントの意思決定プロセスのあらゆるステップを監視・分析・リプレイできるようにすることで、これらのワークフローに切実に求められていた透明性をもたらします。
本記事では、CrewAI と Weave を組み合わせることで、エージェント指向システムを構築するだけでなく、継続的にデバッグし改良していくための手法をどのように実現できるかを紹介します。実践的な例として、エラーログを読み取り、根本原因を特定し、効果的な検索クエリを作成し、GitHub の Issue とウェブ全体を横断して解決策を調査し、影響を受けたコードファイルを精査し、最終的に人間が読みやすい HTML のデバッグレポートを生成する、ログ分析エージェントを取り上げます。
このプロセスは自動的に行われ、Weave によって各エージェントのあらゆるステップが完全に可視化されます。マルチエージェント型の LLM システムに不慣れな方でも、デバッグ手法の改善を目指す方でも、本ガイドを通じて、より速く、より自信を持って反復改善する方法を学べます。
待ちきれないタイプの方(あるいは「とにかくコードを触りたい!」という方)は、次のいずれかを実行できます。
Jump to the tutorial




目次



CrewAI とそのマルチエージェント機能を理解する

AI オートメーションが進化を続けるなか、現実の多くの課題は単一の巨大なモデルだけでは十分に対処しきれないことが明らかになってきました。最も堅牢な成果は、ワークフローの各部分に特化した複数の AI エージェントが協調し、それぞれが明確な役割に集中して取り組むときに生まれます。CrewAI は、このエージェント指向のアプローチを、開発者なら誰でも扱いやすく実践的なものにするために設計されています。 言語モデル
根本にある考え方は、複雑な課題でも小さな要素に分解し、それぞれを担当する専任のエージェントに責任を持たせれば、はるかに扱いやすくなるという原則です。これは、人間のチームが役割を割り当て、責任を分担するやり方と同じ発想です。巨大なプロンプトひとつに要件をすべて詰め込むのではなく、CrewAI では、各エージェントに固有の役割を定義し、遂行すべきタスクを明確にし、より大きなワークフローの中でそれらの協調を調整できます。
各 AI エージェントは個別に検査・テスト・改善できます。その結果、要件が変化してもシステム全体をより容易に保守し、適応させられます。単一モデルのアプローチから、構造化されたエージェント指向のセットアップへ移行することで、より信頼性が高く、透明性があり、拡張性に優れた AI ソリューションを構築できます。
要するに、CrewAI は複数の専門特化した AI エージェントを統合的にオーケストレーションするためのフレームワークです。各エージェントがそれぞれの専門性を持ち寄り、協調して作業することで、複雑なタスクを分かりやすく、扱いやすい形で解決できます。

CrewAI のエージェント指向の主な機能

CrewAI には、マルチエージェントを効果的に管理するための実践的な機能一式が備わっています。 AI ワークフロー。 基本となる考え方として、CrewAI では明確な役割、責務の範囲、割り当てられたツールを備えたエージェントを定義できます。各エージェントはワークフローの特定部分に専念するよう構成され、システム全体での明確さと効率性を確保します。
タスク管理も重要な中核機能です。CrewAI では、エージェントにタスクを割り当て、依存関係を指定し、ワークフロー内の情報の流れを制御できます。ワークフローは逐次実行と並列実行を切り替えられるため、シンプルなケースから複雑なケースまで柔軟に対応可能です。こうした構造により、エージェント間でのデータ交換と協調作業がスムーズに行えます。
エージェントは多様な外部ツールや API にアクセスでき、組み込みの言語モデルの能力を超える情報の統合と処理が可能です。こうした拡張性により、エージェントは実世界の変化する要件にも柔軟に対応できます。CrewAI では、エージェントとワークフローの構成方法として二つの手段を提供します。すべてを Python コードで直接定義すれば、素早い試行やデバッグが容易になります。もう一つは YAML の設定ファイルを用いる方法で、エージェント定義を頻繁に更新したり再利用したりする必要がある大規模プロジェクトの管理に特に有効です。
モジュール性、堅牢なタスク調整、柔軟な統合、そして選択可能な構成方法に重点を置くことで、CrewAI は透明性が高く、保守しやすく、スケール可能な AI エージェントシステムを構築するための堅固な基盤を提供します。

LLM の可観測性を高める W&B Weave の紹介

W&B Weave は、マルチエージェントシステムに対する強力な可観測性レイヤーを提供し、CrewAI と直接統合されることでシームレスなユーザー体験を実現します。プロジェクトに Weave を簡単にインポートして初期化するだけで、エージェントのあらゆる動作ステップが自動的に取得・記録されます。
後述するよう��、この 包括的な可観測性 各エージェントの処理ステップを可視化し、意思決定の経路を追跡し、エージェント間の情報の流れをリアルタイムで監視できます。Weave を使えば、エージェントが実行したアクションや詳細なログを確認し、ワークフローのどこで、なぜその判断が下されたのかを正確に特定できます。こうした可視性は、複雑なパイプラインのデバッグや問題の診断、そしてシステムが信頼できる結果を出すことを担保するうえで不可欠です。
Weave を CrewAI に統合することで、開発者はエージェントチームの内部動作を即座に把握できます。こうした明確な可視性により、反復開発は加速し、より自信を持って本番展開でき、継続的改善への道筋もスムーズになります。可観測性を課題として扱うのではなく、Weave はそれを強みに変えます。より賢く効果的なエージェントを構築できるだけでなく、ワークフローのあらゆる段階でエージェントの挙動を深く理解し、最適化できるようになります。

コスト、トークン使用量、エージェントの失敗の監視

W&B Weave は、開発者に詳細なツールを提供しマルチエージェントシステムにおける、コスト、トークン使用量、レイテンシ、エージェントの失敗といった重要な運用指標を監視するこれらの指標は見やすいダッシュボードに表示され、システム全体の健全性と各エージェント個別のパフォーマンスの両方を容易に追跡できます。
Weave を使えば、各エージェントが利用する各モデルについて、正確なコストとトークン使用量の内訳を確認できます。こうした透明性により、どのモデルやプロセスが費用を押し上げているかを特定でき、強力な大規模モデルを使うべき場面と、より低コストな選択で十分な場面を見極めた上で意思決定できます。エージェント単位・モデル単位のトークン使用量を継続的に監視することで、想定外のスパイクを防ぎ、品質とコストのバランスを取るようにワークフローを微調整できます。

レイテンシの追跡により、エージェント指向のパイプライン全体でボトルネックを検出し、応答時間を最適化できます。どのエージェントやモデルが処理を遅らせているかをすばやく特定できるため、デバッグやシステム調整を迅速に行えます。さらに、Weave はワークフローの各ステップでエージェントの失敗やエラーを追跡します。
エージェントで問題が発生したり、モデルへのリクエストが失敗した場合でも、Weave は詳細なコンテキストとともにイベントを記録します。これにより、ログを手作業で掘り起こすことなく、問題の特定・診断・修正をスムーズに行えます。これらの指標を監視することは、エージェントシステムのパフォーマンスを高め、運用コストを適切に管理するうえで不可欠です。W&B Weave を使えば、エージェント指向システムのリソース使用状況と信頼性のあらゆる側面を明確に把握できます。その結果、プロアクティブな最適化が可能になり、コストの予見性が高まり、AI ワークフローを自信を持ってスケールさせるための道のりが格段にスムーズになります。


チュートリアル:CrewAI でコードデバッガーエージェントを構築する

例として、このチュートリアルでは Python 向けに特化したコードデバッガーエージェントを開発します。Python アプリケーションのデバッグでは、エラーの性質を正確に把握するために、Web と GitHub の両方を検索する必要が生じることがよくあります。これは、Python のエラーメッセージである stderr(標準エラー出力)に、通常はトレースバックやその他の診断情報が含まれているものの、十分な文脈や即座に役立つ解決策が示されない場合があるためです。
stderr は、コードでエラーが発生した際に Python がエラーメッセージを出力するストリームです。これらのメッセージは、関係するファイルや行番号の特定に役立ちますが、開発者は依然としてエラーの意味や、他の人がどのように解決したかを調査する必要があります。
私たちの CrewAI ベースのエージェントは、Python を自動的に解析します。 stderr 出力を解析してエラーの根本原因を特定し、Stack Overflow、公式ドキュメント、GitHub リポジトリなどのサイト横断で関連情報を見つけるための的確なクエリを生成します。続いて、エージェントは調査結果を統合し、詳細なデバッグレポートを作成します。
CrewAI を使ってこの種のエージェントの構築と設計方法を示すとともに、本チュートリアルでは W&B Weave を用いて、リソース使用状況からエージェントの思考過程のトレースに至るまで、プロセスのあらゆるステップを観測する方法を実演します。こうしたアプローチにより、透明性を保ちながらデバッグのワークフローを効率化し、最適化もしやすくなります。

手順 1:bash のエイリアスでロギングシステムを作成する

Python のコードデバッガーエージェントを構築し始めるにあたり、まずは Python スクリプトからのエラーメッセージを一貫して取得する手段が必要です。そうすることで、何が問題だったのかをエージェントが分析しやすくなります。
この手順では、Bash 関数を作成します。作成した関数は、シェルのプロファイル(たとえば .bashrc または .zshrc)を通常の python コマンドの代わりに使います。仕組みは次のとおりです。
agentpython() {
logfile="/tmp/agentpython-stderr.log"
python "$@" 2> >(tee "$logfile" >&2)
if [[ -s "$logfile" ]]; then
# If logfile is NOT empty, run check script
python /Users/brettyoung/Desktop/dev25/tutorials/dbg_crw/debug_main.py "$logfile"
else
# If logfile is empty, clear it (truncate to zero length)
> "$logfile"
fi
}
これを追加するには、次のコマンドを実行してエイリアスを設定ファイルに追加します。なお、次の部分は置き換えてください。 full_path_to_your_script このコマンドを実行する前に、(手順 3 で作成する)Python スクリプトのフルパスをお使いのシステム上のパスに置き換えてください。
profile_file=$(test -f ~/.zshrc && echo ~/.zshrc || (test -f ~/.bashrc && echo ~/.bashrc)); echo 'agentpython() {
logfile="/tmp/agentpython-stderr.log"
python "$@" 2> >(tee "$logfile" >&2)
if [[ -s "$logfile" ]]; then
python full_path_to_your_script "$logfile"
else
> "$logfile"
fi
}' >> "$profile_file" && source "$profile_file" && echo "Added and sourced $profile_file"
仕組みは次のとおりです。
  • 実行すると agentpython myscript.pyでは、その関数があなたの Python スクリプトを実行します。
  • 通常であればターミナル(stderr〔標準エラー出力〕)に表示されるエラーやトレースバックは、/tmp/agentpython- にあるログファイルにも書き出されます。stderr.log
  • スクリプトがエラーなく実行されると、ログファイルはクリアされます。
  • エラーが発生した場合はログファイルの内容が保持され、その後このログが自動的にデバッグ用エージェントのスクリプト(debug_main.py)に渡され、同スクリプトがエラー出力を解析します。
このロギングシステムにより、Python 実行からのすべての stderr(標準エラー出力)が確実に収集され、即座に解析できる状態になります。これが、その後のデバッグワークフロー全体の土台を築きます。

手順 2:「バグあり」スクリプトをテスト用に作成する

Python のエラーを確実に取得するロギングシステムが用意できたので、次は意図的にエラーを発生させる Python スクリプトを作成します。これは、デバッグ用エージェントが解析するテスト用スクリプトになります。
以下は、NumPy を使用してバッファを意図的に破損させる例のスクリプトです。これにより、NumPy のバッファ関連エラーやセグメンテーションフォールトなど、分かりにくく深刻なエラーが発生する可能性があります。
import numpy as np

# Create a structured array
dt = np.dtype([('x', 'f8'), ('y', 'i4')])
arr = np.zeros(100, dtype=dt)

# Fill with data
arr['x'] = np.random.random(100)
arr['y'] = np.arange(100)

# Create problematic buffer view
buffer_data = arr.tobytes()[:-5] # Truncated buffer

# This triggers a numpy buffer/memory bug
corrupted = np.frombuffer(buffer_data, dtype=np.complex128, count=-1)

# Try to use the corrupted array - this often segfaults
result = np.fft.fft(corrupted) * np.ones(len(corrupted))
print(f"Result shape: {result.shape}")
このファイルを次の名前で保存してください bad_code.py (任意の名前で構いません)。新しいロギング用のコマンド(例:agentpython)で実行すると、 buggy_script.py)を実行すると、Bash のロギングシステムにエラー出力が保存されます。これにより、次の手順で CrewAI ベースのエージェントが実際の状況を解析できるようになります。必ずエラーを起こすテストスクリプトから始めることで、Python のデバッグ自動化をテストしやすくなり、改善もしやすくなります。

手順 3:CrewAI と Weave でエージェントを構築する

ここからは、CrewAI と Weave を使ってデバッグ用エージェントを構築します。手順は、手順 1 で用意した Python の stderr(標準エラー出力)ログファイルを読み込むところから始まります。エージェントがこのログを読み込むと、エラーを解析し、関与するファイルや行番号を特定し、判明した問題に基づいて検索クエリを生成します。生成された検索クエリは自動的に Web と GitHub の双方で解決策を探すために使われます。プロジェクト内から関連するコードスニペットも抽出して背景理解を深め、最終的にリンクと推奨解決策を含む詳細なデバッグレポートを作成します。
このワークフローを実装するために、まず必要なライブラリとツールを読み込み、ロギングとトラッキングのために Weave を初期化します。ログ内容は最初に読み込み、すべてのエージェントが参照できるようにします。次に CrewAI で一連のエージェントを定義します。各エージェントはプロセスの一部に特化しており、ログ解析、検索クエリ作成、コードリポジトリや Web の検索、コードスニペットのレビュー、レポート組み立てをそれぞれ担当します。各タスクの出力は次のタスクに引き継がれ、ファイル解析・Web 検索・コードレビューの結果が結合されて、実用的で完結したデバッグ作業につながります。以下がコードです。
import os
import sys
import re
import requests
import tempfile
import webbrowser
import html
from pathlib import Path
from typing import Type, List, Optional, Dict, Any, Union
import json

from pydantic import BaseModel, Field

from crewai import Agent, Task, Crew, Process
from crewai.tools import BaseTool
from crewai import BaseLLM, LLM
import weave; weave.init("crewai_debug_agent")

from langchain_openai import ChatOpenAI
import os
import re
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Type
import subprocess


LOGFILE = sys.argv[1] if len(sys.argv) > 1 else "/tmp/agentpython-stderr.log"
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')

# Read the log file BEFORE kicking off
LOG_CONTENT = ""
if os.path.exists(LOGFILE) and os.path.getsize(LOGFILE) > 0:
with open(LOGFILE, 'r') as f:
LOG_CONTENT = f.read()
print(f"\033[95m[LOG CONTENT LOADED] {len(LOG_CONTENT)} characters from {LOGFILE}\033[0m")
else:
LOG_CONTENT = "No log file found or file is empty"
print(f"\033[95m[LOG] No content found in {LOGFILE}\033[0m")

def verbose_print(msg):
print(f"\033[95m[LOG] {msg}\033[0m", flush=True)



# ----- Tool Input Schemas -----
class LogAnalysisInput(BaseModel):
log_content: str = Field(..., description="Log content to analyze (already loaded)")

class SearchQueryInput(BaseModel):
error_text: str = Field(..., description="Error text to generate search query from")

class CombinedSearchInput(BaseModel):
query: str = Field(..., description="Search query for both GitHub issues and web")
owner: str = Field(default="", description="GitHub repository owner")
repo: str = Field(default="", description="GitHub repository name")

class FileAnalysisInput(BaseModel):
log_content: str = Field(..., description="Log content to extract file information from")

class FileSnippetInput(BaseModel):
file_path: str = Field(..., description="Path to the file to get snippet from")
line: Optional[int] = Field(default=None, description="Line number to focus on")
n_lines: int = Field(default=20, description="Number of lines to return")

class ToolSuggestionInput(BaseModel):
error_message: str = Field(..., description="Error message to analyze")
code_snippet: str = Field(..., description="Code snippet related to the error")

class ReportGenerationInput(BaseModel):
log: str = Field(..., description="Error log content")
file_snippet: str = Field(default="", description="Relevant code snippet")
tools: str = Field(default="", description="Tool recommendations")
gh_results: str = Field(default="", description="GitHub search results")
web_results: str = Field(default="", description="Web search results")

# ----- Tools -----

class LogReaderTool(BaseTool):
name: str = Field(default="Log Reader")
description: str = Field(default="Provides access to the pre-loaded log content")
args_schema: Type[BaseModel] = LogAnalysisInput
def _run(self, log_content: str = None) -> str:
verbose_print(f"Using pre-loaded log content")
if not LOG_CONTENT or LOG_CONTENT == "No log file found or file is empty":
return "[LOG] Log file empty or not found. No action needed."
is_python_error = "Traceback" in LOG_CONTENT or "Exception" in LOG_CONTENT or "Error" in LOG_CONTENT
error_type = "Python Error" if is_python_error else "General Error"
return f"Error Type: {error_type}\n\nLog Content:\n{LOG_CONTENT}"

class SearchQueryGeneratorTool(BaseTool):
name: str = Field(default="Search Query Generator")
description: str = Field(default="Generates optimized search queries from error messages")
args_schema: Type[BaseModel] = SearchQueryInput
def _run(self, error_text: str) -> str:
verbose_print("Generating search query via LLM...")
try:
prompt = (
"Given this error or question, write a concise search query to help the person find a solution online. "
"Output only the query (no explanation):\n\n" + error_text
)
query = llm.call(prompt)
return f"Generated search query: {query.strip()}"
except Exception as e:
return f"Error generating search query: {str(e)}"




class CombinedSearchTool(BaseTool):
name: str = Field(default="Combined GitHub & Web Search")
description: str = Field(default="Searches both GitHub issues and the web in one call, returning both results.")
args_schema: Type[BaseModel] = CombinedSearchInput

def _run(self, query: str, owner: str = "", repo: str = "") -> dict:
github_results = self._github_search(query, owner, repo)
web_results = self._web_search(query)
return {
"github_issues": github_results,
"web_search": web_results
}

def _github_search(self, query: str, owner: str, repo: str):
import httpx
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
url = 'https://api.github.com/search/issues'
headers = {'Accept': 'application/vnd.github.v3+json'}
if GITHUB_TOKEN:
headers['Authorization'] = f'token {GITHUB_TOKEN}'
gh_query = f'repo:{owner}/{repo} is:issue {query}' if owner and repo else query
params = {'q': gh_query, 'per_page': 5}
try:
with httpx.Client(timeout=15) as client:
resp = client.get(url, headers=headers, params=params)
if resp.status_code == 200:
items = resp.json().get("items", [])
return [
{
"number": item.get("number"),
"title": item.get("title"),
"url": item.get("html_url"),
"body": (item.get("body") or "")[:500]
}
for item in items
]
else:
return [{"error": f"GitHub search failed: {resp.status_code} {resp.text}"}]
except Exception as e:
return [{"error": f"Error searching GitHub: {str(e)}"}]

def _extract_json(self, text):
m = re.search(r"```json\s*(.*?)\s*```", text, re.DOTALL)
if not m:
m = re.search(r"```(.*?)```", text, re.DOTALL)
block = m.group(1) if m else text
try:
j = json.loads(block)
return j if isinstance(j, list) else [j]
except Exception:
return []
def _web_search(self, query: str, n_results: int = 5):
# Your actual OpenAI-based tool call here
from openai import OpenAI # or however your actual OpenAI client is imported
client = OpenAI()
prompt = (
f"Show me {n_results} of the most important/useful web results for this search along with a summary of the problem and proposed solution: '{query}'. "
"Return as markdown JSON:\n"
"[{\"title\": ..., \"url\": ..., \"date_published\": ..., \"snippet\": ...}]"
)
response = client.responses.create(
model="gpt-4.1", # or "gpt-4.1", or your available web-enabled model
tools=[{"type": "web_search_preview"}],
input=prompt,
)
return self._extract_json(response.output_text)




class FileAnalysisTool(BaseTool):
name: str = Field(default="File Analysis")
description: str = Field(default="Extracts file paths and line numbers from error logs")
args_schema: Type[BaseModel] = FileAnalysisInput
def _run(self, log_content: str = None) -> str:
verbose_print("Invoking LLM to identify files from log...")
# Use the global LOG_CONTENT if log_content not provided
content_to_analyze = log_content or LOG_CONTENT
try:
prompt = (
"Given this error message or traceback, list all file paths (and, if available, line numbers) involved in the error. "
"Output one JSON per line, as:\n"
'{"file": "path/to/file.py", "line": 123}\n'
'If line is not found, use null.\n'
f"\nError:\n{content_to_analyze}"
)
output = llm.call(prompt)
results = []
for l in output.splitlines():
l = l.strip()
if not l:
continue
try:
results.append(eval(l, {"null": None}))
except Exception as exc:
verbose_print(f"[File Extraction Skipped Line]: {l!r} ({exc})")
return f"Files found in error: {results}"
except Exception as e:
return f"Error analyzing files: {str(e)}"

class FileSnippetTool(BaseTool):
name: str = Field(default="File Snippet Extractor")
description: str = Field(default="Extracts code snippets from files around specific lines")
args_schema: Type[BaseModel] = FileSnippetInput
def _run(self, file_path: str, line: Optional[int] = None, n_lines: int = 20) -> str:
if not os.path.exists(file_path):
return f"File not found: {file_path}"
try:
with open(file_path, "r") as f:
lines = f.readlines()
if line and 1 <= line <= len(lines):
s = max(0, line-6)
e = min(len(lines), line+5)
code = lines[s:e]
else:
code = lines[:n_lines]
return f"Code snippet from {file_path}:\n{''.join(code)}"
except Exception as e:
return f"Error reading file {file_path}: {str(e)}"

class ToolSuggestionTool(BaseTool):
name: str = Field(default="Tool Suggestion")
description: str = Field(default="Suggests which debugging tools to use next based on error analysis")
args_schema: Type[BaseModel] = ToolSuggestionInput
def _run(self, error_message: str, code_snippet: str) -> str:
verbose_print("Requesting tool suggestions via LLM...")
prompt = (
"You are an AI debugging orchestrator. The following is a Python error message and a snippet of code "
"from a file involved in the error. Based on this, choose which tools should be used next, and explain why. "
"Possible tools: github_issue_search, web_search. "
"Always recommend github_issue_search as it's very helpful. "
"Provide your recommendation in a clear, structured format.\n"
"Error:\n" + error_message + "\n\nFile snippet:\n" + code_snippet
)
try:
return llm.call(prompt).strip()
except Exception as e:
return f"Error generating tool suggestions: {str(e)}"

class ReportGeneratorTool(BaseTool):
name: str = Field(default="HTML Report Generator")
description: str = Field(default="Generates HTML debug reports")
args_schema: Type[BaseModel] = ReportGenerationInput
def _run(self, log: str, file_snippet: str = "", tools: str = "", gh_results: str = "", web_results: str = "") -> str:
verbose_print("Writing HTML report ...")
out_path = os.path.join(tempfile.gettempdir(), 'dbg_report.html')
try:
with open(out_path, "w", encoding="utf-8") as f:
f.write("<html><head><meta charset='utf-8'><title>Debug Results</title></head><body>\n")
f.write("<h1 style='color:#444;'>Debugging Session Report</h1>\n")
f.write("<h2>Error Log</h2>")
f.write("<pre style='background:#f3f3f3;padding:8px;'>" + html.escape(log or "None") + "</pre>")
if file_snippet:
f.write("<h2>Relevant Source Snippet</h2><pre style='background:#fafaff;padding:8px;'>" + html.escape(file_snippet) + "</pre>")
if tools:
f.write("<h2>LLM Tool Recommendations</h2><pre style='background:#eef;'>" + html.escape(tools) + "</pre>")
if gh_results:
f.write("<h2>GitHub & Web Search Results</h2><pre>" + html.escape(gh_results) + "</pre>")
if web_results:
f.write("<h2>Web Search AI Answer</h2><pre>" + html.escape(web_results) + "</pre>")
f.write("</body></html>")
return f"HTML report generated and opened at: {out_path}"
except Exception as e:
return f"Error generating HTML report: {str(e)}"

# --- Tool Instances
log_reader_tool = LogReaderTool()
search_query_generator_tool = SearchQueryGeneratorTool()
combined_search_tool = CombinedSearchTool()
file_analysis_tool = FileAnalysisTool()
file_snippet_tool = FileSnippetTool()
tool_suggestion_tool = ToolSuggestionTool()
report_generator_tool = ReportGeneratorTool()


class CustomChatOpenAI(ChatOpenAI):
def call(self, prompt, system_message=None):
"""
Run inference on a prompt (string). Optionally provide a system message.
Args:
prompt (str): The user's message.
system_message (str, optional): The system context for the assistant.
Returns:
str: The model's response content.
"""
messages = []
if system_message:
messages.append(("system", system_message))
messages.append(("human", prompt))
result = self.invoke(messages)
return result.content


llm = CustomChatOpenAI(model_name="gpt-4o-mini", temperature=0.3)
fouro_llm = CustomChatOpenAI(model_name="gpt-4o-mini", temperature=0.3)



# --- Agents ---
log_analyst_agent = Agent(
role="Log Analysis Specialist",
goal="Analyze the pre-loaded log content to identify errors and extract relevant information. Start by reading log with the log_reader_tool, then move on to usign the file_alaysis_tool to read important info from the file(s) involved in the error",
backstory="Expert in parsing error logs and identifying the root causes of issues",
tools=[log_reader_tool, file_analysis_tool],
allow_delegation=False,
llm=fouro_llm
)

search_specialist_agent = Agent(
role="Search Query Specialist",
goal="Generate optimized search queries from error messages for effective problem resolution. The search must be less that 100 chars long!!!!!!!!!!",
backstory="Expert in crafting search queries that yield the most relevant debugging results. The search must be less that 100 chars long!!!!!!!!!!",
tools=[search_query_generator_tool],
allow_delegation=False,
llm=fouro_llm
)


combined_research_agent = Agent(
role="Combined Repository and Web Search Specialist",
goal="Search both GitHub issues and the web for relevant solutions to errors and problems. You must use the combined_search_tool no matter what!!!!!!! Try to summarize each specific github/web problem and solution to help the user solve their issue. Make sure to include the links from the original sources next to their corresponding summaries / code etc",
backstory="Expert in both GitHub open-source research and web documentation sleuthing for code solutions.",
tools=[combined_search_tool],
allow_delegation=False,
llm=fouro_llm
)

code_analyst_agent = Agent(
role="Code Analysis Specialist",
goal="Analyze code snippets and suggest debugging approaches",
backstory="Expert in code analysis and debugging strategy recommendation",
tools=[file_snippet_tool],
allow_delegation=False,
llm=fouro_llm
)

report_generator_agent = Agent(
role="Debug Report Generator",
goal="Compile all debugging information into comprehensive HTML reports. Make sure to include the links to sources when they are provides -- but DO NOT make up links if they are not given. Write an extensive report covering all possible solutions to the problem!!!",
backstory="Specialist in creating detailed, actionable debugging reports",
tools=[report_generator_tool],
allow_delegation=False,
llm=llm
)

# --- Tasks ---
log_analysis_task = Task(
description=f"Analyze the pre-loaded log content. The log content is already available: {LOG_CONTENT[:500]}... Extract error information and identify the type of error.",
expected_output="Detailed analysis of the log content including error type and content",
agent=log_analyst_agent,
output_file="log_analysis.md"
)

file_extraction_task = Task(
description="Extract file paths and line numbers from the analyzed log content. Use the pre-loaded log content to identify which files are involved in the error.",
expected_output="List of files and line numbers involved in the error",
agent=log_analyst_agent,
context=[log_analysis_task],
output_file="file_analysis.md"
)

search_query_task = Task(
description="Generate optimized search queries based on the error analysis for finding solutions online. The search must be less that 100 chars long!!!!!!!!!!",
expected_output="Optimized search queries for the identified errors. The search must be less that 100 chars long!!!!!!!!!!",
agent=search_specialist_agent,
context=[log_analysis_task],
output_file="search_queries.md"
)

combined_search_task = Task(
description="Use the search queries to search both GitHub issues and the wide web for solutions. Make sure to make a very robust report incorporating ALL sources. Dont just give desciptions of the issue- write a detailed summary showcasing code and exact explanations to issues in the report.",
expected_output="Relevant GitHub issues and web documentation/articles/answers.",
agent=combined_research_agent,
context=[search_query_task],
output_file="combined_results.md"
)

code_analysis_task = Task(
description="Extract and analyze code snippets from the implicated files. Suggest debugging tools and approaches.",
expected_output="Code snippets and debugging tool recommendations",
agent=code_analyst_agent,
context=[file_extraction_task],
output_file="code_analysis.md"
)

report_generation_task = Task(
description="Compile all debugging information into a comprehensive HTML report and open it in the browser. Make sure to make a very robust report incorporating ALL sources Make sure to include the links to sources when they are provides -- but DO NOT make up links if they are not given. -- ALL sourced information must be cited!!!!!! Write an extensive report covering all possible solutions to the problem!!!",
expected_output="Complete HTML debugging report",
agent=report_generator_agent,
context=[log_analysis_task, combined_search_task, code_analysis_task],
output_file="debug_report.html"
)

# --- Run Crew ---
crew = Crew(
agents=[
log_analyst_agent,
search_specialist_agent,
combined_research_agent,
code_analyst_agent,
report_generator_agent
],
tasks=[
log_analysis_task,
file_extraction_task,
search_query_task,
combined_search_task,
code_analysis_task,
report_generation_task
],
process=Process.sequential,
verbose=True
)

if __name__ == "__main__":
print(f"\033[95m[STARTING] Log content loaded: {len(LOG_CONTENT)} chars\033[0m")
result = crew.kickoff()
print("\n\nDebug Analysis Complete:\n")
print(result)
# Try to open the generated report
report_path = './debug_report.html'
if os.path.exists(report_path):
verbose_print(f"Opening final report: {report_path}")
if sys.platform.startswith("darwin"):
subprocess.Popen(['open', report_path])
elif sys.platform.startswith("linux"):
subprocess.Popen(['xdg-open', report_path])
elif sys.platform.startswith("win"):
os.startfile(report_path)

このクルーでは、Python コードのエラー解析とトラブルシューティングを自動化するために、特化したエージェント群とワークフローを構築します。主なエージェントは次のとおりです。ログアナリスト、検索クエリスペシャリスト、統合リサーチャー、コードアナリスト、レポートジェネレーター。各役割の概要は次のとおりです。
  • ログアナリストエラーログを読み取り、主要な問題と影響を受けたファイルを特定します。
  • 検索クエリスペシャリストこの情報を基に、Webで解決策を探すのに有用な簡潔な検索クエリを作成します。
  • 統合リサーチャーこれらのクエリを活用して、GitHub の Issue と広範なウェブソースを同時に検索し、最も関連性の高い議論スレッド、コードサンプル、解答を収集します。
  • コードアナリスト問題の発生箇所を特定するために関連ファイルからコードスニペットを抽出し、具体的なデバッグ手順を提案します。
  • 最後に、レポートジェネレーターすべての情報を集約し、解説やリンク、修正案を含む読みやすい明確な HTML レポートを生成します。
これらのエージェントは、順番にそれぞれ特定のタスクを実行します。各エージェントは専用のツールを用い、分析と要約に大規模言語モデルを活用します。あるエージェントの出力が次のタスクへ受け渡されることで、フロー全体がエラーを捕捉・分析・調査・報告まで行い、開発者にとってデバッグ工程をより迅速かつ徹底したものにします。
Weave のおかげで、デバッグの全過程で各エージェントのあらゆるアクションと各モデルの応答を示す、明確でインタラクティブなトレースを確認できます。エラーがどのように分析されたか、どんな検索クエリが生成されたか、そして Web や GitHub からどのような結果が取得されたかを振り返ることができます。こうした透明性により、エージェントの推論過程を追いやすくなり、あなた自身のデバッグワークフローの理解と改善にも役立ちます。
エージェントを作成した後、そのツールの使い方に関していくつか問題があることに気付きました。具体的には、いくつかのツールが私の用意したモデルを用いて LLM 推論を呼び出しており、そのモデルは LangChain の ChatOpenAI モデルのインスタンスでした。問題は、私が使用していたのが… .call メソッド(このクラスには実際には存在しませんでした)を呼び出していたのですが、CrewAI はこのエラーを明確に表面化してくれませんでした。幸いにも Weave を使っていたため、Weave のトレースダッシュボードでこれらの呼び出しを分析し、エラーをはっきり確認できました。

この問題を解決するには、私の ChatOpenAI モデルインスタンスに .call メソッドを追加する必要がありました。LangChain の ChatOpenAI クラスは標準で .invoke() メソッドですが、…を提供していません .call() メソッドをデフォルトで備えています。これにより、 メソッドを想定していた私のツールやエージェントで問題が発生していました。 .call() インターフェースです。私はこれを解決するために ChatOpenAI をサブクラス化し、必要なメッセージ整形を行ってリクエストを委譲するシンプルな .call メソッドを追加する方法を選びました。 .invoke()。これを行うためのコードは次のとおりです。
class CustomChatOpenAI(ChatOpenAI):
def call(self, prompt, system_message=None):
"""
Run inference on a prompt (string). Optionally provide a system message.
Args:
prompt (str): The user's message.
system_message (str, optional): The system context for the assistant.
Returns:
str: The model's response content.
"""
messages = []
if system_message:
messages.append(("system", system_message))
messages.append(("human", prompt))
result = self.invoke(messages)
return result.content
さらに分析とテストを進めた結果、特に目立った大きな問題はエージェントのレイテンシが高いことでした。エージェントの実行は毎回およそ2分かかり、エラーを手動で調査する場合と比べて極めて遅い状態でした。Weave の中では、エージェント全体のレイテンシに加えて、各呼び出しごとのレイテンシも明確に確認できました。
この高いレイテンシのため、より高速な LLM を探すことにしました。そこで活用したのが OpenRouter というサービス、Qwen 3 32B へのアクセス権を得ました。ホスト先は Cerebras、超高速な LLM アクセラレータチップを開発している企業です。このモデルを活用するには、まず私のエージェントが OpenAI 互換の方法で Qwen 3 32B とやり取りできるようにするバックエンドサービスを実装する必要がありました。そこで、実質的にプロキシとして機能するシンプルな FastAPI アプリを作成しました。この API は OpenAI 形式のリクエストを受け取り、 /v1/chat/completions リクエストを受け取って OpenRouter に転送し、バックエンドを適切に設定することで、リクエストが必ず Cerebras 上でホストされている Qwen モデルにルーティングされるよう保証します。
ローカルホスト経由でモデルをホストするためのコードは次のとおりです。
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import uvicorn
import os
from typing import List, Dict, Any, Optional, Union
import requests

# Your re-used settings
API_KEY = os.getenv("OPENROUTER_API_KEY") or "your_api_key"
SITE_URL = "https://your-site-url.com"
SITE_NAME = "Your Site Name"
MODEL = "qwen/qwen3-32b"

# Your OpenRouterCerebrasLLM class (truncated for brevity; copy your full code here)
class OpenRouterCerebrasLLM:
def __init__(
self,
model: str,
api_key: str,
site_url: str,
site_name: str,
temperature: Optional[float] = None,
):
self.model = model
self.temperature = temperature
self.api_key = api_key
self.site_url = site_url
self.site_name = site_name
self.endpoint = "https://openrouter.ai/api/v1/chat/completions"
def call(
self,
messages: Union[str, List[Dict[str, str]]],
tools: Optional[List[dict]] = None,
callbacks: Optional[List[Any]] = None,
available_functions: Optional[Dict[str, Any]] = None,
) -> Union[str, Any]:
if isinstance(messages, str):
messages = [{"role": "user", "content": messages}]
payload = {
"model": self.model,
"messages": messages,
"temperature": self.temperature,
"provider": {
"order": ["cerebras"],
"allow_fallbacks": False
}
}
headers = {
"Authorization": f"Bearer {self.api_key}",
"HTTP-Referer": self.site_url,
"X-Title": self.site_name,
"Content-Type": "application/json"
}
response = requests.post(
self.endpoint,
headers=headers,
json=payload,
timeout=30
)
response.raise_for_status()
result = response.json()
return result

# Initialize the FastAPI app and the LLM once
app = FastAPI()
llm = OpenRouterCerebrasLLM(
model=MODEL,
api_key=API_KEY,
site_url=SITE_URL,
site_name=SITE_NAME,
temperature=0.7
)

@app.post("/v1/chat/completions")
async def chat_completions(request: Request):
body = await request.json()
messages = body.get("messages")
# you can also handle tools, temperature, etc. here
try:
raw_response = llm.call(messages)
# This returns the full OpenRouter response. If you want to narrow down to only the OpenAI-compatible fields,
# you could filter here, but for maximum compatibility just return as-is.
return JSONResponse(raw_response)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)

if __name__ == "__main__":
print(f"Serving local OpenAI-compatible LLM proxy on http://localhost:8001/v1/chat/completions")
print(f"Forwarding all requests to: {MODEL}, via OpenRouter w/ your secret/settings")
uvicorn.run(app, host="0.0.0.0", port=8001)
この構成の中核は、 OpenRouterCerebrasLLM クラスで、OpenRouter のエンドポイントに送るリクエスト構築の詳細をすべてラップします。認証ヘッダー、モデル名、温度、プロバイダーの順序を正しく挿入し、すべてのリクエストが目的の Qwen インスタンスに確実に到達するようにします。このクラスの call メソッドは、ユーザーまたはシステムのプロンプトを受け取り、それらをペイロードとして OpenRouter の API に送信して、JSON レスポンスを返します。
私の FastAPI ハンドラーでは、受信した POST リクエストを受け取り、messages のリストを抽出して、そのまま Qwen モデルに渡します。返ってくるペイロードは OpenAI のレスポンス形式を踏襲しているため、そのままクライアントへ返送できます。クライアントから OpenRouter までの経路で問題が発生した場合は、整形済みのエラーメッセージを JSON で返します。
ローカルで API サーバーを稼働させた状態で、エージェントを接続するように構成できました http://localhost:8001/v1/chat/completions まるで OpenAI の API に話しかけるのと同じ感覚で使えます。実際、この構成により、エージェントや OpenAI 互換のライブラリは、エンドポイント URL 以外のコード変更をほとんど必要とせずに Qwen 3 32B をシームレスに利用できるようになりました。
これで新しいモデルを使ってエージェントスクリプトを再実装できました。今回のコードでは、OpenAI モデルへの呼び出しの大半を Cerebras 上の自分のモデルへの呼び出しに置き換えています。
import os
import sys
import re
import requests
import tempfile
import webbrowser
import html
from pathlib import Path
from typing import Type, List, Optional, Dict, Any, Union
import json

from pydantic import BaseModel, Field

from crewai import Agent, Task, Crew, Process
from crewai.tools import BaseTool
from crewai import LLM
import weave; weave.init("crewai_debug_agent")

from langchain_openai import ChatOpenAI
import os
import re
import asyncio
import httpx
from openai import OpenAI
from pydantic import BaseModel, Field
from typing import Type
import subprocess


LOGFILE = sys.argv[1] if len(sys.argv) > 1 else "/tmp/agentpython-stderr.log"
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')

# Read the log file BEFORE kicking off
LOG_CONTENT = ""
if os.path.exists(LOGFILE) and os.path.getsize(LOGFILE) > 0:
with open(LOGFILE, 'r') as f:
LOG_CONTENT = f.read()
print(f"\033[95m[LOG CONTENT LOADED] {len(LOG_CONTENT)} characters from {LOGFILE}\033[0m")
else:
LOG_CONTENT = "No log file found or file is empty"
print(f"\033[95m[LOG] No content found in {LOGFILE}\033[0m")

def verbose_print(msg):
print(f"\033[95m[LOG] {msg}\033[0m", flush=True)

# ----- LLM (local or OpenRouter, as per your local config) -----
cerebras_llm = LLM(
model="openrouter/meta-llama/llama-4-scout",
base_url="http://localhost:8001/v1",
api_key="put_this_in_your_api_script"
)

# ----- Tool Input Schemas -----
class LogAnalysisInput(BaseModel):
log_content: str = Field(..., description="Log content to analyze (already loaded)")

class SearchQueryInput(BaseModel):
error_text: str = Field(..., description="Error text to generate search query from")

class CombinedSearchInput(BaseModel):
query: str = Field(..., description="Search query for both GitHub issues and web")
owner: str = Field(default="", description="GitHub repository owner")
repo: str = Field(default="", description="GitHub repository name")

class FileAnalysisInput(BaseModel):
log_content: str = Field(..., description="Log content to extract file information from")

class FileSnippetInput(BaseModel):
file_path: str = Field(..., description="Path to the file to get snippet from")
line: Optional[int] = Field(default=None, description="Line number to focus on")
n_lines: int = Field(default=20, description="Number of lines to return")

class ToolSuggestionInput(BaseModel):
error_message: str = Field(..., description="Error message to analyze")
code_snippet: str = Field(..., description="Code snippet related to the error")

class ReportGenerationInput(BaseModel):
log: str = Field(..., description="Error log content")
file_snippet: str = Field(default="", description="Relevant code snippet")
tools: str = Field(default="", description="Tool recommendations")
gh_results: str = Field(default="", description="GitHub search results")
web_results: str = Field(default="", description="Web search results")

# ----- Tools -----

class LogReaderTool(BaseTool):
name: str = Field(default="Log Reader")
description: str = Field(default="Provides access to the pre-loaded log content")
args_schema: Type[BaseModel] = LogAnalysisInput
def _run(self, log_content: str = None) -> str:
verbose_print(f"Using pre-loaded log content")
if not LOG_CONTENT or LOG_CONTENT == "No log file found or file is empty":
return "[LOG] Log file empty or not found. No action needed."
is_python_error = "Traceback" in LOG_CONTENT or "Exception" in LOG_CONTENT or "Error" in LOG_CONTENT
error_type = "Python Error" if is_python_error else "General Error"
return f"Error Type: {error_type}\n\nLog Content:\n{LOG_CONTENT}"

class SearchQueryGeneratorTool(BaseTool):
name: str = Field(default="Search Query Generator")
description: str = Field(default="Generates optimized search queries from error messages")
args_schema: Type[BaseModel] = SearchQueryInput
def _run(self, error_text: str) -> str:
verbose_print("Generating search query via LLM...")
try:
prompt = (
"Given this error or question, write a concise search query to help the person find a solution online. "
"Output only the query (no explanation):\n\n" + error_text
)
query = cerebras_llm.call(prompt)
return f"Generated search query: {query.strip()}"
except Exception as e:
return f"Error generating search query: {str(e)}"


class CombinedSearchTool(BaseTool):
name: str = Field(default="Combined GitHub & Web Search")
description: str = Field(default="Searches both GitHub issues and the web in one call, returning both results.")
args_schema: Type[BaseModel] = CombinedSearchInput

def _run(self, query: str, owner: str = "", repo: str = "") -> dict:
return asyncio.run(self._async_combined(query, owner, repo))

async def _async_combined(self, query: str, owner: str = "", repo: str = "") -> dict:
# Launch both searches in parallel
tasks = [
self._github_search(query, owner, repo),
self._web_search(query)
]
github_results, web_results = await asyncio.gather(*tasks)
return {
"github_issues": github_results,
"web_search": web_results
}

async def _github_search(self, query: str, owner: str, repo: str):
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
url = 'https://api.github.com/search/issues'
headers = {'Accept': 'application/vnd.github.v3+json'}
if GITHUB_TOKEN:
headers['Authorization'] = f'token {GITHUB_TOKEN}'
gh_query = f'repo:{owner}/{repo} is:issue {query}' if owner and repo else query
params = {'q': gh_query, 'per_page': 5}
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url, headers=headers, params=params)
if resp.status_code == 200:
items = resp.json().get("items", [])
return [
{
"number": item.get("number"),
"title": item.get("title"),
"url": item.get("html_url"),
"body": (item.get("body") or "")[:500]
}
for item in items
]
else:
return [{"error": f"GitHub search failed: {resp.status_code} {resp.text}"}]
except Exception as e:
return [{"error": f"Error searching GitHub: {str(e)}"}]

# ---- WEB SEARCH (from your preferred implementation, no markdown parsing) ----
def _extract_json(self, text):
m = re.search(r"```json\s*(.*?)\s*```", text, re.DOTALL)
if not m:
m = re.search(r"```(.*?)```", text, re.DOTALL)
block = m.group(1) if m else text
try:
j = json.loads(block)
return j if isinstance(j, list) else [j]
except Exception:
return []
async def _web_search(self, query: str, n_results: int = 5):
client = OpenAI()
prompt = (
f"Show me {n_results} of the most important/useful web results for this search along with a summary of the problem and proposed solution: '{query}'. "
"Return as markdown JSON:\n"
"[{\"title\": ..., \"url\": ..., \"date_published\": ..., \"snippet\": ...}]"
)
# Run in threadpool for IO
loop = asyncio.get_running_loop()
def blocking_openai():
response = client.responses.create(
model="gpt-4.1",
tools=[{"type": "web_search_preview"}],
input=prompt,
)
return self._extract_json(response.output_text)
return await loop.run_in_executor(None, blocking_openai)

class FileAnalysisTool(BaseTool):
name: str = Field(default="File Analysis")
description: str = Field(default="Extracts file paths and line numbers from error logs")
args_schema: Type[BaseModel] = FileAnalysisInput
def _run(self, log_content: str = None) -> str:
verbose_print("Invoking LLM to identify files from log...")
# Use the global LOG_CONTENT if log_content not provided
content_to_analyze = log_content or LOG_CONTENT
try:
prompt = (
"Given this error message or traceback, list all file paths (and, if available, line numbers) involved in the error. "
"Output one JSON per line, as:\n"
'{"file": "path/to/file.py", "line": 123}\n'
'If line is not found, use null.\n'
f"\nError:\n{content_to_analyze}"
)
output = cerebras_llm.call(prompt)
results = []
for l in output.splitlines():
l = l.strip()
if not l:
continue
try:
results.append(eval(l, {"null": None}))
except Exception as exc:
verbose_print(f"[File Extraction Skipped Line]: {l!r} ({exc})")
return f"Files found in error: {results}"
except Exception as e:
return f"Error analyzing files: {str(e)}"

class FileSnippetTool(BaseTool):
name: str = Field(default="File Snippet Extractor")
description: str = Field(default="Extracts code snippets from files around specific lines")
args_schema: Type[BaseModel] = FileSnippetInput
def _run(self, file_path: str, line: Optional[int] = None, n_lines: int = 20) -> str:
if not os.path.exists(file_path):
return f"File not found: {file_path}"
try:
with open(file_path, "r") as f:
lines = f.readlines()
if line and 1 <= line <= len(lines):
s = max(0, line-6)
e = min(len(lines), line+5)
code = lines[s:e]
else:
code = lines[:n_lines]
return f"Code snippet from {file_path}:\n{''.join(code)}"
except Exception as e:
return f"Error reading file {file_path}: {str(e)}"

class ToolSuggestionTool(BaseTool):
name: str = Field(default="Tool Suggestion")
description: str = Field(default="Suggests which debugging tools to use next based on error analysis")
args_schema: Type[BaseModel] = ToolSuggestionInput
def _run(self, error_message: str, code_snippet: str) -> str:
verbose_print("Requesting tool suggestions via LLM...")
prompt = (
"You are an AI debugging orchestrator. The following is a Python error message and a snippet of code "
"from a file involved in the error. Based on this, choose which tools should be used next, and explain why. "
"Possible tools: github_issue_search, web_search, static_analysis. "
"Always recommend github_issue_search as it's very helpful. "
"Provide your recommendation in a clear, structured format.\n"
"Error:\n" + error_message + "\n\nFile snippet:\n" + code_snippet
)
try:
return cerebras_llm.call(prompt).strip()
except Exception as e:
return f"Error generating tool suggestions: {str(e)}"

class ReportGeneratorTool(BaseTool):
name: str = Field(default="HTML Report Generator")
description: str = Field(default="Generates HTML debug reports")
args_schema: Type[BaseModel] = ReportGenerationInput
def _run(self, log: str, file_snippet: str = "", tools: str = "", gh_results: str = "", web_results: str = "") -> str:
verbose_print("Writing HTML report ...")
out_path = os.path.join(tempfile.gettempdir(), 'dbg_report.html')
try:
with open(out_path, "w", encoding="utf-8") as f:
f.write("<html><head><meta charset='utf-8'><title>Debug Results</title></head><body>\n")
f.write("<h1 style='color:#444;'>Debugging Session Report</h1>\n")
f.write("<h2>Error Log</h2>")
f.write("<pre style='background:#f3f3f3;padding:8px;'>" + html.escape(log or "None") + "</pre>")
if file_snippet:
f.write("<h2>Relevant Source Snippet</h2><pre style='background:#fafaff;padding:8px;'>" + html.escape(file_snippet) + "</pre>")
if tools:
f.write("<h2>LLM Tool Recommendations</h2><pre style='background:#eef;'>" + html.escape(tools) + "</pre>")
if gh_results:
f.write("<h2>GitHub & Web Search Results</h2><pre>" + html.escape(gh_results) + "</pre>")
if web_results:
f.write("<h2>Web Search AI Answer</h2><pre>" + html.escape(web_results) + "</pre>")
f.write("</body></html>")
return f"HTML report generated and opened at: {out_path}"
except Exception as e:
return f"Error generating HTML report: {str(e)}"

# --- Tool Instances
log_reader_tool = LogReaderTool()
search_query_generator_tool = SearchQueryGeneratorTool()
combined_search_tool = CombinedSearchTool()
file_analysis_tool = FileAnalysisTool()
file_snippet_tool = FileSnippetTool()
tool_suggestion_tool = ToolSuggestionTool()
report_generator_tool = ReportGeneratorTool()

# --- Agents ---
log_analyst_agent = Agent(
role="Log Analysis Specialist",
goal="Analyze the pre-loaded log content to identify errors and extract relevant information",
backstory="Expert in parsing error logs and identifying the root causes of issues",
tools=[log_reader_tool, file_analysis_tool],
allow_delegation=False,
llm=cerebras_llm
)

search_specialist_agent = Agent(
role="Search Query Specialist",
goal="Generate optimized search queries from error messages for effective problem resolution. The search must be less that 100 chars long!!!!!!!!!!",
backstory="Expert in crafting search queries that yield the most relevant debugging results. The search must be less that 100 chars long!!!!!!!!!!",
tools=[search_query_generator_tool],
allow_delegation=False,
llm=cerebras_llm
)

combined_research_agent = Agent(
role="Combined Repository and Web Search Specialist",
goal="Search both GitHub issues and the web for relevant solutions to errors and problems. You must use the combined_search_tool no matter what!!!!!!! Try to summarize each specific github/web problem and solution to help the user solve their issue. Make sure to include the links from the original sources next to their corresponding summaries / code etc",
backstory="Expert in both GitHub open-source research and web documentation sleuthing for code solutions.",
tools=[combined_search_tool],
allow_delegation=False,
llm=ChatOpenAI(model_name="gpt-4.1", temperature=0.0)
)

code_analyst_agent = Agent(
role="Code Analysis Specialist",
goal="Analyze code snippets and suggest debugging approaches",
backstory="Expert in code analysis and debugging strategy recommendation",
tools=[file_snippet_tool],
allow_delegation=False,
llm=cerebras_llm
)

report_generator_agent = Agent(
role="Debug Report Generator",
goal="Compile all debugging information into comprehensive HTML reports. Make sure to include the links to sources when they are provides -- but DO NOT make up links if they are not given. Write an extensive report covering all possible solutions to the problem!!!",
backstory="Specialist in creating detailed, actionable debugging reports",
tools=[report_generator_tool],
allow_delegation=False,
llm=cerebras_llm
)

# --- Tasks ---
log_analysis_task = Task(
description=f"Analyze the pre-loaded log content. The log content is already available: {LOG_CONTENT[:500]}... Extract error information and identify the type of error.",
expected_output="Detailed analysis of the log content including error type and content",
agent=log_analyst_agent,
output_file="log_analysis.md"
)

file_extraction_task = Task(
description="Extract file paths and line numbers from the analyzed log content. Use the pre-loaded log content to identify which files are involved in the error.",
expected_output="List of files and line numbers involved in the error",
agent=log_analyst_agent,
context=[log_analysis_task],
output_file="file_analysis.md"
)

search_query_task = Task(
description="Generate optimized search queries based on the error analysis for finding solutions online. The search must be less that 100 chars long!!!!!!!!!!",
expected_output="Optimized search queries for the identified errors. The search must be less that 100 chars long!!!!!!!!!!",
agent=search_specialist_agent,
context=[log_analysis_task],
output_file="search_queries.md"
)

combined_search_task = Task(
description="Use the search queries to search both GitHub issues and the wide web for solutions. Make sure to make a very robust report incorporating ALL sources. Dont just give desciptions of the issue- write a detailed summary showcasing code and exact explanations to issues in the report.",
expected_output="Relevant GitHub issues and web documentation/articles/answers.",
agent=combined_research_agent,
context=[search_query_task],
output_file="combined_results.md"
)

code_analysis_task = Task(
description="Extract and analyze code snippets from the implicated files. Suggest debugging tools and approaches.",
expected_output="Code snippets and debugging tool recommendations",
agent=code_analyst_agent,
context=[file_extraction_task],
output_file="code_analysis.md"
)

report_generation_task = Task(
description="Compile all debugging information into a comprehensive HTML report and open it in the browser. Make sure to make a very robust report incorporating ALL sources Make sure to include the links to sources when they are provides -- but DO NOT make up links if they are not given. -- ALL sourced information must be cited!!!!!! Write an extensive report covering all possible solutions to the problem!!!",
expected_output="Complete HTML debugging report",
agent=report_generator_agent,
context=[log_analysis_task, combined_search_task, code_analysis_task],
output_file="debug_report.html"
)

# --- Run Crew ---
crew = Crew(
agents=[
log_analyst_agent,
search_specialist_agent,
combined_research_agent,
code_analyst_agent,
report_generator_agent
],
tasks=[
log_analysis_task,
file_extraction_task,
search_query_task,
combined_search_task,
code_analysis_task,
report_generation_task
],
process=Process.sequential,
verbose=True
)

if __name__ == "__main__":
print(f"\033[95m[STARTING] Log content loaded: {len(LOG_CONTENT)} chars\033[0m")
result = crew.kickoff()
print("\n\nDebug Analysis Complete:\n")
print(result)
# Try to open the generated report
report_path = './debug_report.html'
if os.path.exists(report_path):
verbose_print(f"Opening final report: {report_path}")
if sys.platform.startswith("darwin"):
subprocess.Popen(['open', report_path])
elif sys.platform.startswith("linux"):
subprocess.Popen(['xdg-open', report_path])
elif sys.platform.startswith("win"):
os.startfile(report_path)

上記のエージェントを使用するには、Cerebras のモデル API をローカルで起動しておく必要があります。
💡
この新しいモデルに切り替えると、レイテンシの改善がすぐに体感できました。私の中で確認できたのは、 Weave のトレース エージェント全体の実行時間は劇的に短縮され、総レイテンシは1分未満になりました。LLM 呼び出しを要する各ステップの完了が大幅に速くなり、エージェントのワークフロー全体がはるかに応答性が高くインタラクティブになりました。
Weave 上でエージェントをさらに分析した結果、レイテンシを削減できる別の余地が見つかりました。ツール内部で GitHub の Issue 検索とウェブ検索を逐次実行していることに気づいたのです。特にウェブ検索で LLM やサードパーティ API を呼び出す場合、どちらの処理も数秒かかることがあります。つまり、各処理は互いに独立しているにもかかわらず、単一のツールステップ全体としての所要時間がかなり長くなり得る、ということです。
検索ツールを最適化する前の、Weave 内のスクリーンショットです。

これに対処するため、私はコードをリファクタリングし、 CombinedSearchTool 両方の検索が並行実行されるよう、Python の asyncio および非同期対応の HTTP クライアント。GitHub 検索とウェブ検索を同時に開始し、結果をまとめて await するようにしたところ、逐次実行版と比べてツールのエンドツーエンドのレイテンシをほぼ半減できました。実装はごく簡単で、次の更新を加えるだけでした。
class CombinedSearchTool(BaseTool):
name: str = Field(default="Combined GitHub & Web Search")
description: str = Field(default="Searches both GitHub issues and the web in one call, returning both results.")
args_schema: Type[BaseModel] = CombinedSearchInput

def _run(self, query: str, owner: str = "", repo: str = "") -> dict:
return asyncio.run(self._async_combined(query, owner, repo))

async def _async_combined(self, query: str, owner: str = "", repo: str = "") -> dict:
# Launch both searches in parallel
tasks = [
self._github_search(query, owner, repo),
self._web_search(query)
]
github_results, web_results = await asyncio.gather(*tasks)
return {
"github_issues": github_results,
"web_search": web_results
}

async def _github_search(self, query: str, owner: str, repo: str):
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
url = 'https://api.github.com/search/issues'
headers = {'Accept': 'application/vnd.github.v3+json'}
if GITHUB_TOKEN:
headers['Authorization'] = f'token {GITHUB_TOKEN}'
gh_query = f'repo:{owner}/{repo} is:issue {query}' if owner and repo else query
params = {'q': gh_query, 'per_page': 5}
try:
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.get(url, headers=headers, params=params)
if resp.status_code == 200:
items = resp.json().get("items", [])
return [
{
"number": item.get("number"),
"title": item.get("title"),
"url": item.get("html_url"),
"body": (item.get("body") or "")[:500]
}
for item in items
]
else:
return [{"error": f"GitHub search failed: {resp.status_code} {resp.text}"}]
except Exception as e:
return [{"error": f"Error searching GitHub: {str(e)}"}]

この変更を加えると、Weave のトレースにもすぐに性能改善が反映されました。ツール内の複合ステップのレイテンシが大幅に下がり、エージェント全体の応答性も目に見えて向上しました。これは、特にインタラクティブまたはリアルタイムのワークロードを扱う場合に、エージェントのツール実装で相互に独立した外部 API 呼び出しを非同期並列化することの価値を強く示しています。
ここでは、実行の Weave サマリーから、さらに15秒の実行時間を短縮できたことが確認できます。


まとめ

CrewAI のようなフレームワークで複雑なマルチエージェントのワークフローを構築すると、実世界の自動化や問題解決で強力な能力を発揮できます。しかし、構成要素が増えるほど、デバッグや性能チューニング、エラー処理の難易度も高まります。そこで重要になるのが、W&B Weave のような可観測性レイヤーを統合することです。
このプロジェクト全体を通じて、Weave は単なるロギングツール以上の存在であることがはっきりしました。隠れたバグの修正、より高速な言語モデルへの切り替え、独立したツール呼び出しの並列化など、私が加えたあらゆる変更に対して、Weave は明確で実行可能なフィードバックを提供してくれました。詳細なトレースとダッシュボードにより原因究明が容易になり、エージェントを継続的に最適化できました。最初は不透明で遅いブラックボックスのように感じられたものが、すぐに透明でインタラクティブなワークフローへと変わり、あらゆるミスや非効率がリアルタイムに可視化されるようになったのです。
CrewAI と Weave を組み合わせたことで、素早い反復、安心して試せる実験、そしてエージェントのパイプラインの信頼性と速度の定量的な向上を実現できました。高度な LLM 駆動のエージェントを開発しているなら、可観測性を最優先に据えるこのアプローチは大きな転換点になります。機能バグを早期に検出できるだけでなく、システムの進化に伴うコスト、レイテンシ、リソース使用状況をきめ細かく監視できます。最終的には、開発の高速化、より堅牢なエージェント、そして開発者とユーザー双方にとってより���らかな体験につながります。


これは AI による翻訳記事です。訳語や表現に誤りがありましたら、コメント欄でお知らせください。元のレポートはこちらをご覧ください。 元のレポートを見る
Iterate on AI agents and models faster. Try Weights & Biases today.