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

目次
CrewAIとそのマルチエージェント機能を理解するCrewAIのエージェント指向の主要機能LLMの可観測性を高めるW&B Weaveの紹介コスト・トークン使用量・エージェントの失敗を監視するチュートリアル:CrewAIでコードデバッガーエージェントを構築する手順1:bashのエイリアスでロギングシステムを作成する手順 2:「バグ入り」テスト用スクリプトを作成する手順 3:CrewAI と W&B Weave でエージェントを構築するまとめ
CrewAIとそのマルチエージェント機能を理解する
AIによる自動化が進化するにつれ、現実の多くの課題は単一の巨大なモデルだけでは十分に扱えないことが明らかになってきました。最も堅牢な成果は、複数の専門特化したAIエージェントが協調し、それぞれがワークフローの異なる部分に集中することで生まれることが多いのです。CrewAIは、このエージェント指向のアプローチを、開発者であれば誰でも手に取りやすく、実践的に活用できるように設計されています。 言語モデル。
本質的には、CrewAIは「複雑な課題は小さな要素に分割し、それぞれを専任のエージェントに担当させることで扱いやすくなる」という原則に基づいています。これは、人間のチームが役割を割り当て、責任を分担するやり方に通じます。あらゆる要件をひとつの巨大なプロンプトに押し込むのではなく、CrewAIではエージェントごとに明確な役割を定義し、担当タスクを整理し、より大きなワークフローの中で協調を調整できます。
各AIエージェントは個別に検査・テスト・改善できます。その結果、要件が変化してもシ��テム全体を保守しやすく、適応もしやすくなります。単一モデルのアプローチから構造化されたエージェントベースの体制へ切り替えることで、開発者はより信頼性が高く、透明性があり、拡張性に優れたAIソリューションを構築できます。
要するに、CrewAIは複数の専門特化したAIエージェントを編成・協調させるためのフレームワークです。各エージェントがそれぞれの専門性を持ち寄り、明確で扱いやすい形で連携しながら、複雑なタスクを解決します。
CrewAIのエージェント指向の主要機能
CrewAIは、マルチエージェントを効果的に管理するための実践的な機能群を提供します。 AIワークフロー。CrewAIの基盤となる考え方は、明確な役割・境界・割り当てツールを持つエージェントを定義できることにあります。各エージェントはワークフローの特定の一部に集中するよう構成され、システム全体の明確性と効率を高めます。
タスク管理も中核的な機能です。CrewAIでは、エージェントにタスクを割り当て、その依存関係を定義し、ワークフロー全体で情報の流れを制御できます。ワークフローは逐次実行にも並行実行にも設定でき、シンプルなケースから複雑なケースまで柔軟に対応します。こうした構造により、エージェント間でのデータ交換と協調がシームレスに行えます。
エージェントは多様な外部ツールやAPIにアクセスでき、組み込みの言語モデルの能力をはるかに超える情報の統合・処理が可能です。こうした拡張性により、エージェントは現実世界の変化する要件に柔軟に適応し、対応できます。CrewAIでは、エージェントとワークフローの設定方法として2通りをサポートしています。迅速な試行やデバッグの容易さを重視する場合は、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 アプリケーションのデバッグでは、エラーの性質を十分に把握するために、ウェブと 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 scriptpython /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" ]]; thenpython 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 arraydt = np.dtype([('x', 'f8'), ('y', 'i4')])arr = np.zeros(100, dtype=dt)# Fill with dataarr['x'] = np.random.random(100)arr['y'] = np.arange(100)# Create problematic buffer viewbuffer_data = arr.tobytes()[:-5] # Truncated buffer# This triggers a numpy buffer/memory bugcorrupted = np.frombuffer(buffer_data, dtype=np.complex128, count=-1)# Try to use the corrupted array - this often segfaultsresult = 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 と W&B Weave でエージェントを構築する
ここからは、CrewAI と W&B Weave を使ってデバッグ用エージェントを構築します。まず、手順 1 で用意した Python の stderr ログファイルを読み込みます。エージェントはこのログを取り込むと、エラー内容を解析し、関与するファイルや行番号を特定し、その問題に基づく検索クエリを生成します。生成した検索クエリは自動的にウェブと GitHub の両方で解決策を探すために使用されます。さらに、プロジェクト内から関連するコードスニペットを抽出して洞察を深め、最終的にリンクや推奨解決策を含む詳細なデバッグレポートを生成します。
このワークフローを実装するために、まず必要なライブラリとツールをすべてインポートし、ロギングとトラッキングのために W&B Weave を初期化します。ログの読み込みは最初に行い、すべてのエージェントがこの情報にアクセスできるようにします。続いて CrewAI で一連のエージェントを定義します。プロセスの各部分に専任の役割を割り当て、ログ解析用、検索クエリ作成用、コードリポジトリやウェブの検索用、コードスニペットのレビュー用、そしてレポート組み立て用のエージェントを用意します。各タスクの出力は次のタスクに渡され、ファイル解析、ウェブ検索、コードレビューの結果が統合されて、現実的で完結したデバッグセッションになります。コードは次のとおりです。
import osimport sysimport reimport requestsimport tempfileimport webbrowserimport htmlfrom pathlib import Pathfrom typing import Type, List, Optional, Dict, Any, Unionimport jsonfrom pydantic import BaseModel, Fieldfrom crewai import Agent, Task, Crew, Processfrom crewai.tools import BaseToolfrom crewai import BaseLLM, LLMimport weave; weave.init("crewai_debug_agent")from langchain_openai import ChatOpenAIimport osimport refrom openai import OpenAIfrom pydantic import BaseModel, Fieldfrom typing import Typeimport subprocessLOGFILE = 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 offLOG_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] = LogAnalysisInputdef _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_CONTENTerror_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] = SearchQueryInputdef _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] = CombinedSearchInputdef _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 httpxGITHUB_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 queryparams = {'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 texttry: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 herefrom openai import OpenAI # or however your actual OpenAI client is importedclient = 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 modeltools=[{"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] = FileAnalysisInputdef _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 providedcontent_to_analyze = log_content or LOG_CONTENTtry: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:continuetry: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] = FileSnippetInputdef _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] = ToolSuggestionInputdef _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] = ReportGenerationInputdef _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 Instanceslog_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.contentllm = 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 reportreport_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 コードのエラー解析とトラブルシューティングを自動化するために、特化したエージェント群とワークフローを構築します。主なエージェントは次のとおりです。:ログアナリスト、検索クエリ担当、統合リサーチ担当、コード解析担当、レポート生成担当。各役割の概要は次のとおりです。
- ログ解析担当エラーログを読み取り、主要な問題点と影響を受けたファイルを特定します。
- 検索クエリ担当この情報を基に、ウェブで解決策を探すのに有用な簡潔な検索クエリを作成します。
- 統合リサーチ担当これらのクエリを活用して、GitHub Issues とウェブ全体を同時に検索し、最も関連性の高いディスカッションスレッド、コード例、解答を収集します。
- コード解析担当影響を受けたファイルからコードスニペットを抽出し、問題が発生している箇所の特定を助けるとともに、具体的なデバッグ手順を提案します。
- 最後に、レポート生成担当すべての情報を集約し、解説・リンク・推奨修正を含む読みやすく明快なHTMLレポートを生成します。
各エージェントは順番に特定のタスクを実行します。各エージェントは専用のツールを用い、分析と要約に大規模言語モデルを活用します。あるエージェントの出力が次のタスクに引き継がれることで、フロー全体がエラーを捕捉・分析・調査・報告し、開発者にとってデバッグプロセスをより迅速かつ徹底したものにします。
Weave により、デバッグの全過程で各エージェントのあらゆるアクションと各モデルの応答を明示する、見やすく操作可能なトレースが得られます。エラーがどのように分析されたか、どんな検索クエリが生成されたか、そしてウェブや GitHub からどの結果が取得されたかを確認できます。こうした可視化により、エージェントの思考過程を追いやすくなり、あなた自身のデバッグワークフローの理解と改善にも役立ちます。
エージェントを作成した後、そのツールの使い方に関していくつか問題があることに気付きました。具体的には、一部のツールが私の作成したモデルを使ってLLM推論を呼び出しており、そのモデルはLangchainのChatOpenAIモデルのインスタンスでした。問題は、私が使用していたのが…でした。 .call そのクラスには存在しないはずの method を呼び出しており、しかも CrewAI はこのエラーを明確に表面化してくれませんでした。幸い、Weave を使っていたおかげで、Weave のトレースダッシュボードでこれらの呼び出しを分析し、エラーをはっきり確認できました。

この問題を解決するには、私の ChatOpenAI モデルインスタンスに .call メソッドを追加する必要がありました。LangChain の ChatOpenAI クラスは本来、標準で .invoke() メソッドですが、…を提供していません .call() 既定で method を備えています。しかし、私のツールやエージェント側は を前提としていたため、問題が発生していました。 .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 というサービス、OpenRouter 経由でホストされている Qwen 3 32B へのアクセス権を得ました Cerebras、超高速なLLM向けアクセラレータチップを開発している企業です。このモデルを利用するには、まずエージェントが OpenAI 互換の方法で Qwen 3 32B と対話できるようにするバックエンドサービスを実装する必要がありました。私は実質的にプロキシとして機能するシンプルな FastAPI アプリを作成しました。この API は OpenAI 形式のリクエストを受け取り、 /v1/chat/completions リクエストを受け取り、OpenRouter に転送します。特にバックエンドを適切に設定し、Cerebras でホストされている Qwen モデルにリクエストが確実にルーティングされるようにしています。
ローカルホスト経由でモデルをホストするためのコードは次のとおりです。
from fastapi import FastAPI, Requestfrom fastapi.responses import JSONResponseimport uvicornimport osfrom typing import List, Dict, Any, Optional, Unionimport requests# Your re-used settingsAPI_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 = modelself.temperature = temperatureself.api_key = api_keyself.site_url = site_urlself.site_name = site_nameself.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 onceapp = 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. heretry: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 メソッドは user と system のいずれのプロンプトも受け取り、ペイロードとしてまとめて 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 osimport sysimport reimport requestsimport tempfileimport webbrowserimport htmlfrom pathlib import Pathfrom typing import Type, List, Optional, Dict, Any, Unionimport jsonfrom pydantic import BaseModel, Fieldfrom crewai import Agent, Task, Crew, Processfrom crewai.tools import BaseToolfrom crewai import LLMimport weave; weave.init("crewai_debug_agent")from langchain_openai import ChatOpenAIimport osimport reimport asyncioimport httpxfrom openai import OpenAIfrom pydantic import BaseModel, Fieldfrom typing import Typeimport subprocessLOGFILE = 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 offLOG_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] = LogAnalysisInputdef _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_CONTENTerror_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] = SearchQueryInputdef _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] = CombinedSearchInputdef _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 paralleltasks = [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 queryparams = {'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 texttry: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 IOloop = 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] = FileAnalysisInputdef _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 providedcontent_to_analyze = log_content or LOG_CONTENTtry: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:continuetry: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] = FileSnippetInputdef _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] = ToolSuggestionInputdef _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] = ReportGenerationInputdef _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 Instanceslog_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 reportreport_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 および async 対応の 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] = CombinedSearchInputdef _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 paralleltasks = [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 queryparams = {'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 駆動のエージェントを開発しているなら、この「可観測性ファースト」のアプローチは大きな転換点になります。機能バグを早期に検出できるだけでなく、システムの進化に合わせてコスト、レイテンシ、リソース使用状況をきめ細かく監視できます。最終的には、開発の加速、より堅牢なエージェントの実現、そして開発者とユーザーの双方にとってより円滑な体験につながります。
Add a comment
Iterate on AI agents and models faster. Try Weights & Biases today.