【理想の開発環境を作ろう! その2】 ~影響調査・設計・実装エージェント編~
はじめまして
ふっきーです。
理想の開発環境を目指して、LangGraphを使ってコーディングエージェントを構築していきたいと思います。ざっくり以下のような流れにしようと思っています。
- 開発プロジェクトの分析
- 影響範囲の調査・変更対象ファイルのリストアップ
- 設計&設計文書の作成
- 設計文書のレビュー(人間)
- 実装
第二回の今回は、一気に影響範囲の調査、設計、実装まで実現してしまいます。
第一回はこちら
フローの概要
フロー図はこんな感じです
影響範囲の調査
影響範囲の調査では、前段で作成したプロジェクト全体の情報から、ユーザーがリクエストした修正内容に関係がありそうなファイルをリストアップします。
プロジェクト情報は抽象的な情報になっているため、リストアップされたファイルを対象に再調査します。この時、ファイルの中身を見ながら、そのファイルが影響範囲内にあるかを調査します。
この段階で、ファイルごとの作業になるので、処理の高速化のため並列化します。
上記の流れで、修正が必要な範囲を絞っていきます。
設計
影響ありと判断されたファイルだけを対象として、
ユーザーのリクエストに沿うためには、どのような修正が必要かをファイルごとに判断し、文書化します。
実装
設計文書をもとに、各ファイルの実装作業を実施します。
上記のような流れで、実装作業を進めていきます。
活躍してくれるエージェントくんたち
グラフやノードの実装は前回の記事と変わらないので、前回の記事を参照してください。
今回は、各ノードで活躍するエージェントくんたちの実装のみ紹介します。
影響ありそうなファイルリストアップするよちゃん
実装ポイントとしては、project_analysis.md
には存在しない対象ファイルの相対パスを作ることでした。
ロジックで作ってもいい気はしましたが、出力されるファイルパスが最初から相対パスになっていたり、適当なパスで埋められたりと、統一感なかったので、パスの生成も含めてLLMに任せました。
class ListupAffectedFiles(BaseLLM):
name: str = "listup_affected_files"
desc: str = "Analyzes project structure and identifies files potentially affected by changes"
class AnalysisOutput(BaseModel):
affected_files: List[str] = Field(description="影響を受ける可能性のあるファイルパスのリスト")
def __init__(self, llm):
system_prompt = """
あなたはプロジェクト分析の専門家です。
project_analysis.mdの内容と変更依頼に基づいて、少しでも影響を受ける可能性のあるファイルを特定することが役割です。
実際に影響があるかどうかの判定は、後工程で精査するので、あなたの役割は確実に関係のないファイルを間引くことです。
以下の点に注意して分析を行ってください:
- 直接的な変更が必要なファイルだけでなく、依存関係から影響を受ける可能性のあるファイルも含めてください
- project_analysis.mdに記載されている依存関係を注意深く確認してください
- ファイルパスは必ず完全パスで記載してください
- 出力する必要があるのは、ファイルパスのみです。
- 出力するファイルパスは、プロジェクトパスを含むパスで記載してください
"""
super().__init__(llm, system_prompt, output_structure=self.AnalysisOutput)
def analyze_impact(self, project_path: str, project_analysis: str, request: str) -> List[str]:
query = f"""
以下のプロジェクト分析と変更依頼に基づいて、影響を受ける可能性のあるファイルを特定してください。
=== プロジェクト分析 ===
{project_analysis}
=== 変更依頼 ===
{request}
=== プロジェクトパス ===
{project_path}
各ファイルについて、影響を受ける可能性がある理由を具体的に説明してください。
依存関係、インポート関係、機能の関連性などを考慮してください。
"""
return self._invoke(query).affected_files
ファイルの中見て修正必要か考えてくれるくん
名前のとおりです。
ファイルの中身を見るのに、Langchain標準のReadFileTool
を使用しています。
from langchain_community.tools.file_management import ReadFileTool
class InvestigationAgent(BaseAgent):
name: str = "investigation"
desc: str = "Analyzes codebase and identifies required changes"
def __init__(self, llm):
system_prompt = """
あなたはコード分析の専門家です。
ファイルの内容とユーザーリクエストに基づいて、修正が必要かどうかを判定してください
以下のツールが利用可能です:
- read_file: ファイルの内容を読み取ります
分析結果は以下の形式で出力してください:
変更内容:
- [変更点1]
- [変更点2]
注意:
- ユーザーリクエストを慎重に分析し、必要な変更箇所を正確に特定してください
- コードの構造を維持しつつ、最小限の変更で目的を達成する方法を提案してください
- 変更内容はブロック単位でもれなく列挙してください
"""
tools = [
ReadFileTool(),
]
super().__init__(llm, system_prompt, tools)
def analyze(self, file_path: str, user_request: str) -> str:
query = f"""
以下の情報に基づいて、必要な変更箇所を分析してください:
調査対象のファイルパス:
{file_path}
ユーザーリクエスト:
{user_request}
1. まずユーザーリクエストを詳細に分析し、必要な変更の種類を特定してください
2. 具体的な変更内容を特定してください
指定された形式で出力してください。
"""
return self._invoke(query)
ファイルの修正内容考えてくれるくん
修正必須と判断されたコードに対して、ユーザーのリクエストを満たすためにどう実装したらいいかを検討してくれます。
最初は、前段の**"ファイルの中見て修正必要か考えてくれるくん"**とひとまとめにしてもいいかなと思いました。
が、修正が不必要なファイルもまとめて詳細設計させると、不必要な修正が混ざってしまいそうだったので、2段に分けました。
class DesignSpec(BaseModel):
file_path: str = Field(description="変更対象のファイルパス")
changes: List[str] = Field(description="変更内容のリスト")
class DesignSpecs(BaseModel):
specs: List[DesignSpec] = Field(description="設計書のリスト")
class DesignAgent(BaseLLM):
name: str = "design"
desc: str = "Creates detailed implementation plans"
def __init__(self, llm):
system_prompt = """
あなたは設計の専門家です。
各ファイルでの影響範囲に基づいて、具体的な実装作業を検討することが役割です。
# 注意点
- 入力された影響範囲は、ファイル単体での調査結果です。
"""
super().__init__(llm, system_prompt, output_structure=DesignSpecs)
def design(self, analysis_results: dict):
query = f"""
以下の分析結果に基づいて、設計を行ってください:
{analysis_results}
"""
design_specs: DesignSpecs = self._invoke(query)
return design_specs.specs
ファイル編集するよちゃん
編集してくれます。
ファイル編集用のツールは自前で実装しています。
LangChainで自前でツールを実装する場合は、ツール化したい関数にdocstring書いて、toolデコレーターをつければよいだけなので、非常に楽です。
個人的な感覚ですが、ファイルを編集させる際に差分だけを扱うようにすると、ファイル全体を差分のみで上書きしたり、変な編集が混ざったりと破壊的な編集が多いように感じました。
なので、このツールではファイル全体を上書きするようにしています。
from langchain.tools import tool
class ImplementationAgent(BaseAgent):
name: str = "implementation"
desc: str = "Generates and saves code based on design"
def __init__(self, llm):
system_prompt = """
あなたは実装の専門家です。
設計仕様に基づいてコードを生成し、保存することが役割です。
以下のツールが利用可能です:
- update: ファイルの内容を更新します
Args:
- file_path: 編集するファイルのパス
- content: 更新後のファイル内容全体
実装後は以下の情報を出力してください:
- 変更内容の要約
注意:
- ファイルを編集する際は必ず `update` ツールを使用してください
- ファイルパスは完全パスで指定してください
- 既存のコードの構造やスタイルを維持してください
"""
tools = [update]
super().__init__(llm, system_prompt, tools)
def implement(self, flle_path: str, design_spec: List[str], code: str) -> str:
design_spec_str = '\n'.join(design_spec)
return self._invoke(f"#対象ファイルパス\n{flle_path}\n\n#ソースコード\n{code}\n\n#設計書\n{design_spec_str}")
@tool
def update(file_path: str, content: str) -> str:
"""
指定したファイルの内容を更新します。
Args:
file_path (str): 編集するファイルのパス
content (str): 更新後のファイル内容全体
Returns:
str: 更新後のファイル内容
"""
with open(file_path, 'w', encoding='utf-8') as file:
file.write(content)
return content
以上が今回のフローで活躍してくれているエージェントくんたちです。
動作結果
”LLMへの入力と出力をログファイルに記録して”とお願いして修正されたコードの例を一部紹介します。
LLMの基底クラス内の呼び出しメソッド内で、入出力をログとして残すような修正になっています。
ここだけ見ると、いくつか不満はありますが、依頼した内容に沿った修正なのかなと思います。
問題は、別ファイルのログの埋め込み方と統一感が全くないことです。並列化して設計・実装しているので当然ですね。これは想定通りです。今回はなかったですが、同じような処理が複数ファイルにまたいで実装されることもありえたかなと思っています。
次回、並列作成された設計文書を一度ひとまとめにして、設計方針の見直し・修正をするステップとそれを人間に確認させるステップを実現したいと思います。
これが終われば、プロジェクト全体で統一感のある修正が実現可能になるのではないのかなと思います。結果が楽しみですね!
以上、最後まで読んでいただきありがとうございました!!
Discussion