CrewAI のマルチエージェントアプリケーションをデバッグする
CrewAI と W&B Weave で、AI エージェントの構築とデバッグをより迅速に。マルチエージェントワークフローのあらゆるステップを監視・分析し、最適化できます。この記事は機械翻訳版です。誤訳の可能性があればコメント欄でお知らせください。
Created on August 26|Last edited on August 26
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 と Weave を使ってエージェントを構築するまとめ
CrewAI とマルチエージェント機能の概要
AI オートメーションが進化するにつれ、現実の課題の多くは単一の巨大なモデルだけでは効果的に扱えないことが明らかになってきました。最も堅牢な成果は、複数の特化した AI エージェントがワークフローの異なる部分にそれぞれ集中して協調することで生まれることがよくあります。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(“standard error” の略)が、通常は traceback やその他の診断情報を含むものの、十分な文脈や即座に使える解決策を提供しない場合があるためです。
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 と Weave を使ってエージェントを構築する
ここからは、CrewAI と Weave を使ってデバッグ用エージェントを構築します。まずはステップ1で用意した Python の stderr ログファイルを読み込みます。エージェントがこのログを取り込むと、エラーを解析し、関係するファイルや行番号を特定したうえで、判明した問題に基づく検索クエリを生成します。生成した検索クエリは自動的にウェブと GitHub での解決策の探索に使われます。さらに、プロジェクト内から関連するコードスニペットを抽出して深い理解を助け、最終的にはリンク付きの推奨解決策を含む詳細なデバッグレポートを生成します。
このワークフローを実装するにあたり、まず必要なライブラリとツールをすべてインポートし、ログ記録とトラッキングのために 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() メソッドにのみ標準対応しており、その結果、 メソッドを前提としていた私のツールやエージェントで不具合が発生していました。 .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, 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 メソッドはユーザーまたはシステムのプロンプトを受け取り、それらをペイロードとして 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 および非同期対応の 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