CodeExtractorクラス：LLM出力からのコード抽出と検証の完全ガイド

## はじめに

近年、ChatGPTやGPT-4などの大規模言語モデル（LLM）の登場により、プログラミングの世界は大きな変革を迎えています。LLMは驚異的な自然言語理解力とコード生成能力を持っていますが、その出力からコードを正確に抽出し、検証することは新たな課題となっています。そこで登場するのが、今回ご紹介する`CodeExtractor`クラスです。

この記事では、`CodeExtractor`クラスの機能を詳しく解説し、LLM出力の処理における重要性を強調しながら、初心者の方でも簡単に使えるよう丁寧に説明していきます。


<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">結構面倒なコードブロックの抽出！②<br>Diffの検証も強化！！！<br>これで抽出が原因でエラーが出ることはないはず！！ <a href="https://t.co/QS1UIITYuQ">https://t.co/QS1UIITYuQ</a> <a href="https://t.co/JnX60K3mD5">pic.twitter.com/JnX60K3mD5</a></p>&mdash; Maki@Sunwood AI Labs. (@hAru_mAki_ch) <a href="https://twitter.com/hAru_mAki_ch/status/1810984198410490017?ref_src=twsrc%5Etfw">July 10, 2024</a></blockquote> 

## CodeExtractorクラスとは

`CodeExtractor`クラスは、テキスト（特にLLMの出力）からコードを抽出し、その妥当性を検証するための強力なツールです。主に以下の機能を提供します：

1. コードブロックの検出
2. コードの抽出
3. 抽出されたコードの検証

このクラスは、LLMを活用するプログラマーや、テキスト解析、自然言語処理に携わる方々にとって、非常に有用なツールとなります。

## LLMとCodeExtractorの相性

LLMの出力は非常に柔軟で、自然言語とコードが混在していることが多々あります。`CodeExtractor`クラスは、このようなLLMの特性を考慮して設計されており、以下のような点でLLM出力の処理に特に適しています：

1. **混在テキストの処理**: LLMの出力には、説明文とコードが混在していることが多いですが、`CodeExtractor`はこれらを適切に分離できます。

2. **多言語対応**: LLMは様々なプログラミング言語のコードを生成しますが、`CodeExtractor`は複数の言語やフォーマットを識別・検証できます。

3. **柔軟な検出**: コードブロックのマークダウン記法（```）だけでなく、インデントやその他の特徴からもコードを検出します。

4. **エラー検出**: LLMが生成したコードに文法エラーがある場合、それを検出し報告します。

## 主要な機能

### 4.1 コードブロックの検出

コードブロックの検出は、`_check_code_block_presence`メソッドで行われます。

```python
def _check_code_block_presence(self, response):
    logger.debug("LLMを使用してコードブロックの存在を確認しています。")
    prompt = f"""
    次のテキストにコードブロック（```で囲まれた部分）が含まれていますか？ 'はい'か'いいえ'で答えてください:

    {response}

    回答は以下の形式で提供してください：
    はい
    または
    いいえ
    """
    llm_response = self.llm_service.get_response(prompt)
    result = llm_response.lower().strip() == 'はい'
    logger.debug(f"LLMの回答: コードブロックは{'存在します' if result else '存在しません'}")
    return result
```

このメソッドは、別のLLMを使用して、テキスト中にコードブロックが存在するかどうかを判断します。これにより、人間の介入なしに高度な判断が可能になります。

### 4.2 コードの抽出

コードの抽出は、`_extract_code`メソッドで行われます。

```python
def _extract_code(self, response):
    logger.debug("コードブロックの抽出を開始します。")
    code_block_pattern = r'```(?:(\w+)\n)?([\s\S]*?)```'
    match = re.search(code_block_pattern, response, re.DOTALL)
    
    if match:
        language = match.group(1).strip().lower() if match.group(1) else None
        code = match.group(2).strip()
        logger.debug(f"コードブロックを抽出しました。言語: {language if language else '不明'}")
        return self._validate_code(code, language)
    else:
        logger.warning("コードブロックのパターンが見つかりませんでした。全体を1つのコードとして扱います。")
        return self._validate_code(response.strip())
```

このメソッドは正規表現を使用して、LLM出力からコードブロックを抽出します。コードブロックが見つからない場合は、テキスト全体をコードとして扱います。これにより、LLMが予期せぬ形式でコードを出力した場合でも対応できます。

### 4.3 コードの検証

抽出されたコードは、`_validate_code`メソッドで検証されます。

```python
def _validate_code(self, code, language=None):
    logger.debug(f"コードの検証を開始します。指定された言語: {language if language else '指定なし'}")
    if language == 'json' or (language is None and self._is_json(code)):
        return self._validate_json(code)
    
    if language == 'python' or (language is None and self._is_python(code)):
        return self._validate_python(code)
    
    if language == 'markdown' or (language is None and self._is_markdown(code)):
        return self._validate_markdown(code)
    
    if language == 'diff' or (language is None and self._is_diff(code)):
        is_valid, message, errors = self._validate_diff(code)
        if is_valid:
            return code
        else:
            logger.error(f"diffの検証エラー: {message}")
            for error in errors:
                logger.error(error)
            return code  # エラーがあってもコードを返す
    
    logger.debug("言語が特定できませんでした。コードをそのまま返します。")
    return code
```

このメソッドは、抽出されたコードの言語を判断し、適切な検証メソッドを呼び出します。LLMが生成したコードの品質を保証する上で重要な役割を果たします。


## サポートされる言語

`CodeExtractor`クラスは、LLMが頻繁に生成する以下の言語やフォーマットをサポートしています。各言語やフォーマットに対して、特定の検証メソッドが用意されています。

### 5.1 JSON

JSONの検証は以下のように行われます：

```python
def _is_json(self, code):
    logger.debug("JSONフォーマットかどうかを確認しています。")
    try:
        code = code.strip()
        if code.startswith('{') and code.endswith('}'):
            json.loads(code)
            logger.debug("JSONフォーマットであることを確認しました。")
            return True
    except json.JSONDecodeError:
        logger.debug("JSONフォーマットではありません。")
        pass
    return False

def _validate_json(self, code):
    logger.debug("JSONの検証を開始します。")
    try:
        json.loads(code)
        logger.debug("有効なJSONであることを確認しました。")
        return code
    except json.JSONDecodeError as e:
        logger.error(f"無効なJSONです。エラー: {str(e)}")
        return code
```

これらのメソッドは、コードがJSON形式であるかを確認し、有効なJSONであるかを検証します。LLMが生成したJSONデータの構造的な正確性を確保するのに役立ちます。

### 5.2 Python

Pythonコードの検証は以下のように行われます：

```python
def _is_python(self, code):
    logger.debug("Pythonコードかどうかを確認しています。")
    try:
        ast.parse(code)
        logger.debug("Pythonコードであることを確認しました。")
        return True
    except SyntaxError:
        logger.debug("Pythonコードではありません。")
        return False

def _validate_python(self, code):
    logger.debug("Pythonコードの検証を開始します。")
    try:
        ast.parse(code)
        logger.debug("有効なPythonコードであることを確認しました。")
        return code
    except SyntaxError as e:
        logger.error(f"無効なPythonコードです。エラー: {str(e)}")
        return code
```

これらのメソッドは、コードがPython言語で書かれているかを確認し、構文的に正しいかを検証します。LLMが生成したPythonコードの即時実行可能性を確保します。

### 5.3 Markdown

Markdownの検証は以下のように行われます：

```python
def _is_markdown(self, code):
    logger.debug("マークダウンフォーマットかどうかを確認しています。")
    # 基本的なマークダウン要素のパターン
    patterns = [
        r'^#{1,6}\s.+$',  # 見出し
        r'^\s*[-*+]\s.+$',  # リスト
        r'^\s*\d+\.\s.+$',  # 番号付きリスト
        r'^\s*>.+$',  # 引用
        r'`[^`\n]+`',  # インラインコード
        r'\[.+\]\(.+\)',  # リンク
        r'!\[.+\]\(.+\)',  # 画像
        r'\*\*.+\*\*',  # 太字
        r'\*.+\*',  # イタリック
        r'^---$',  # 水平線
    ]
    
    lines = code.split('\n')
    markdown_line_count = sum(1 for line in lines if any(re.match(pattern, line) for pattern in patterns))
    markdown_ratio = markdown_line_count / len(lines)
    
    is_markdown = markdown_ratio > 0.3  # 30%以上の行がマークダウン要素を含む場合、マークダウンとみなす
    logger.debug(f"マークダウンフォーマットで{'あります' if is_markdown else 'ありません'}。")
    return is_markdown

def _validate_markdown(self, code):
    logger.debug("マークダウンの検証を開始します。")
    if self._is_markdown(code):
        logger.debug("有効なマークダウンであることを確認しました。")
        return code
    else:
        logger.warning("マークダウンとして認識されましたが、構造が不明確です。")
        return code
```

これらのメソッドは、テキストがMarkdown形式で書かれているかを確認します。Markdownの特徴的な要素（見出し、リスト、リンクなど）のパターンを使用して判断します。LLMが生成したドキュメントやコメントの構造を保証するのに役立ちます。

### 5.4 Diff

Diffフォーマットの検証は以下のように行われます：

```python
def _is_diff(self, code: str) -> bool:
    logger.debug("diffフォーマットかどうかを確認しています。")
    lines = code.split('\n')
    
    # diffの特徴的なパターン
    diff_patterns = [
        r'^diff --git a/.+ b/.+$',
        r'^index [0-9a-f]+\.\.[0-9a-f]+( [0-9]+)?$',
        r'^--- (a/)?[^ \t\n\r\f\v]+.*$',
        r'^\+\+\+ (b/)?[^ \t\n\r\f\v]+.*$',
        r'^@@ -\d+,\d+ \+\d+,\d+ @@.*$',
    ]
    
    # 各行がdiffパターンに合致するか、または変更行（+, -, スペース）で始まるかをチェック
    is_diff_line = lambda line: any(re.match(pattern, line) for pattern in diff_patterns) or line.startswith(('+', '-', ' '))
    
    diff_line_count = sum(1 for line in lines if is_diff_line(line))
    diff_ratio = diff_line_count / len(lines)
    
    is_diff = diff_ratio > 0.5  # 50%以上の行がdiff要素を含む場合、diffとみなす
    logger.debug(f"diffフォーマットで{'あります' if is_diff else 'ありません'}。")
    return is_diff

def _validate_diff(self, code: str) -> Tuple[bool, str, List[str]]:
    logger.debug("diffの検証を開始します。")
    if not self._is_diff(code):
        logger.error("無効なdiffです。")
        return False, "無効なdiffフォーマットです。", []

    lines = code.split('\n')
    errors = []
    
    # ハンクヘッダーのパターン
    hunk_header_pattern = re.compile(r'^@@ -(\d+),(\d+) \+(\d+),(\d+) @@')
    
    current_hunk = None
    for i, line in enumerate(lines, 1):
        if line.startswith('diff ') or line.startswith('index ') or line.startswith('--- ') or line.startswith('+++ '):
            continue
        elif hunk_header_pattern.match(line):
            current_hunk = hunk_header_pattern.match(line)
        elif current_hunk:
            if not line.startswith(('+', '-', ' ')):
                errors.append(f"行 {i}: 無効な変更行です。'+', '-', または ' ' で始まる必要があります。")
        else:
            errors.append(f"行 {i}: ハンクヘッダーの前に無効な行があります。")

    if errors:
        logger.error("diffの検証中にエラーが見つかりました。")
        return False, "diffの形式に問題があります。", errors
    
    logger.debug("有効なdiffであることを確認しました。")
    return True, "有効なdiffです。", []
```

これらのメソッドは、テキストがdiffフォーマットで書かれているかを確認し、その構造が正しいかを検証します。LLMがコードの変更を提案する際に生成したdiffの正確性を確保するのに役立ちます。

## LLM出力での活用例

`CodeExtractor`クラスは、LLMの出力を処理する様々なシナリオで活用できます：

1. **コード生成タスク**: LLMにコード生成を指示した際、その出力から正確にコードを抽出し、即座に実行可能な状態にします。

2. **コードレビュー補助**: LLMによるコードレビューコメントから、提案された修正コードを抽出し、その妥当性を検証します。

3. **ドキュメント生成**: LLMが生成した技術ドキュメントから、コード例を抽出し、その正確性を確認します。

4. **対話型プログラミング支援**: LLMとの対話でコーディングを行う際、各ステップで生成されたコードを自動的に抽出・検証します。

## エラーハンドリングとログ記録

`CodeExtractor`クラスは、詳細なログ記録機能を備えています。これにより、LLMとの対話プロセスや、コード抽出・検証の各段階を追跡することが可能です。エラーが発生した場合も、その原因を特定しやすくなります。

## まとめ：LLMとCodeExtractorの未来

`CodeExtractor`クラスは、LLMの出力を効率的に処理し、高品質なコードを抽出・検証するための強力なツールです。LLMとプログラミングの融合が進む現代において、このようなツールの重要性はますます高まっていくでしょう。

今後、LLMの能力がさらに向上し、より複雑なコードや多様な言語を生成するようになっても、`CodeExtractor`のような専門的なツールが、その出力を適切に処理し、人間のプログラマーとLLMの架け橋となることが期待されます。

`CodeExtractor`を活用することで、LLMとのコラボレーションがより円滑になり、プログラミングの生産性と品質が飛躍的に向上することでしょう。

<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>