1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMが存在しないライブラリを幻覚したので、本当に作ることにした

1
Last updated at Posted at 2026-06-23

まず見てほしい

このコードを実行する。

demo.py
from super_pdf_magic import convert_anything

result = convert_anything("report.docx")
print(result)

しかし super_pdf_magic というライブラリは 存在しない

普通ならこうなる。

ModuleNotFoundError: No module named 'super_pdf_magic'

ところが今回の実行結果はこうだ。

[Hallucination Engine] Module not found: super_pdf_magic
[Hallucination Engine] Asking LLM to generate implementation...
[Hallucination Engine] Module 'super_pdf_magic' created successfully.
Converted: report.docx -> report.pdf

存在しないライブラリが、その場で生成されて動いた。

この記事では、この仕組みを最初から最後まで実装する。


前提条件

この記事のコードを動かすには以下が必要です。

項目 バージョン 備考
Python 3.10以上 標準ライブラリのみ使用
Ollama インストール済み ローカルLLM実行環境
LLMモデル 任意 本記事では qwen3:8b を使用
Ollamaの準備
ollama pull qwen3:8b

外部ライブラリのインストールは 不要。Python標準ライブラリの urllib だけで動く。


ハルシネーションは本当に間違いなのか

LLMを使っているとよく遭遇するコード。

from image_understanding import describe_image
from semantic_table_join import join_tables
from smart_csv_cleaner import clean_dataset

そんなライブラリは存在しない。

一般的には「またハルシネーションだ」で終わる。

しかし私は別の見方をしている。

ハルシネーションは「未来の仕様書」ではないか?


発想の逆転

普通の開発はこう進む。

仕様 → 設計 → 実装 → コード

しかしLLMは 逆方向 に進む。

コード → 暗黙仕様 → 実装

つまり from image_understanding import describe_image を出力した時点で、LLMの内部には以下の仕様が存在している。

  • 画像を説明するライブラリ
  • describe_image() という関数
  • 文字列を返す

ならばその仕様を使ってライブラリを作ればよい。


importシステムをハックする

Pythonには sys.meta_path がある。ここに独自Finderを差し込む。

import sys
sys.meta_path.insert(0, MyLLMFinder())

すると import super_pdf_magic が失敗した瞬間に MyLLMFinder.find_spec() が呼ばれる。


アーキテクチャ

import xxx
   ↓
sys.meta_path の Finder が検知
   ↓
Ollama API へ問い合わせ
   ↓
Pythonコード生成
   ↓
exec() でモジュール化
   ↓
import 成功

完全版ソースコード

以下が 実際に動く全コード。1ファイルで完結する。

hallucination_engine.py
"""
hallucination_engine.py

存在しないPythonモジュールをLLMで動的生成するimport hook。

前提条件:
  - Ollama がローカルで起動していること
  - モデルが pull 済みであること(デフォルト: qwen3:8b)

使い方:
  import hallucination_engine  # これだけでhookが有効になる
  from super_pdf_magic import convert_anything  # 存在しないモジュールが生成される
"""

import sys
import types
import json
import importlib
import importlib.abc
import importlib.machinery
import re
from urllib.request import Request, urlopen
from urllib.error import URLError


# ===========================================================================
# 設定
# ===========================================================================

OLLAMA_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "qwen3:8b"

# 標準ライブラリ・pip済みパッケージは生成対象外
SKIP_PREFIXES = (
    "_", "pip", "setuptools", "pkg_resources",
    "importlib", "encodings", "codecs", "zipimport",
)

# コードブロック検出用のフェンス文字列
FENCE = "`" * 3


# ===========================================================================
# LLM呼び出し
# ===========================================================================

def ask_llm(module_name: str) -> str:
    """OllamaにモジュールのPythonコードを生成させる。"""

    prompt = f"""/no_think
あなたはPythonライブラリの実装者です。

以下のモジュールを実装してください。

モジュール名: {module_name}

ルール:
- Pythonコードのみ出力すること
- import文も含めること
- 外部ライブラリに依存しないこと(標準ライブラリのみ使用可)
- クラスや関数を、モジュール名から推測される用途に合わせて実装すること
- 説明文やMarkdownは一切不要。コードブロック記法も不要
- コードだけを出力すること
"""

    payload = json.dumps({
        "model": OLLAMA_MODEL,
        "prompt": prompt,
        "stream": False,
    }).encode("utf-8")

    req = Request(
        OLLAMA_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
    )

    try:
        with urlopen(req, timeout=120) as resp:
            data = json.loads(resp.read().decode("utf-8"))
            raw = data.get("response", "")
            return _extract_code(raw)
    except URLError as e:
        raise RuntimeError(
            f"Ollama に接続できません ({OLLAMA_URL})。"
            f"Ollama が起動しているか確認してください: {e}"
        )


def _extract_code(text: str) -> str:
    """LLM出力からPythonコード部分だけを抽出する。"""

    # コードフェンス付きブロック(python指定あり)を検出
    pattern_py = FENCE + r"python\s*\n(.*?)" + FENCE
    match = re.search(pattern_py, text, re.DOTALL)
    if match:
        return match.group(1).strip()

    # コードフェンス付きブロック(言語指定なし)を検出
    pattern_plain = FENCE + r"\s*\n(.*?)" + FENCE
    match = re.search(pattern_plain, text, re.DOTALL)
    if match:
        return match.group(1).strip()

    # フェンスがなければ全体をコードとして扱う
    return text.strip()


# ===========================================================================
# Import Hook
# ===========================================================================

class HallucinationFinder(importlib.abc.MetaPathFinder):
    """存在しないモジュールを検知するFinder。"""

    def find_spec(self, name, path, target=None):

        # サブモジュール(ドット付き)はスキップ
        if "." in name:
            return None

        # 標準系・ツール系はスキップ
        if name.startswith(SKIP_PREFIXES):
            return None

        # 既にインポート済みならスキップ
        if name in sys.modules:
            return None

        # 他のFinderで見つかるか確認(自分自身を除外して検索)
        original_meta_path = [
            finder for finder in sys.meta_path
            if not isinstance(finder, HallucinationFinder)
        ]
        for finder in original_meta_path:
            spec = finder.find_spec(name, path, target)
            if spec is not None:
                return None  # 正規モジュールが存在する → 何もしない

        # どこにも見つからない → LLMで生成する
        print(f"[Hallucination Engine] Module not found: {name}")

        return importlib.machinery.ModuleSpec(
            name,
            HallucinationLoader(),
        )


class HallucinationLoader(importlib.abc.Loader):
    """LLMが生成したコードでモジュールを構築するLoader。"""

    def create_module(self, spec):
        return types.ModuleType(spec.name)

    def exec_module(self, module):
        name = module.__name__

        print("[Hallucination Engine] Asking LLM to generate implementation...")

        code = ask_llm(name)

        separator = "=" * 40
        print(f"[Hallucination Engine] Generated code:\n{separator}")
        print(code)
        print(separator)

        try:
            exec(code, module.__dict__)
        except Exception as e:
            raise ImportError(
                f"LLMが生成したコードの実行に失敗しました "
                f"(module={name}): {e}"
            )

        # sys.modulesに登録
        sys.modules[name] = module

        print(f"[Hallucination Engine] Module '{name}' created successfully.")


# ===========================================================================
# Hook登録(import時に自動で有効化)
# ===========================================================================

# 二重登録を防止
if not any(isinstance(f, HallucinationFinder) for f in sys.meta_path):
    sys.meta_path.insert(0, HallucinationFinder())
    print("[Hallucination Engine] Import hook activated.")

動かしてみる

準備

# Ollamaが起動していることを確認
ollama list

実行

demo.py
import hallucination_engine  # hookが有効になる

from super_pdf_magic import convert_anything

result = convert_anything("report.docx")
print(result)
python demo.py

実行結果の例

[Hallucination Engine] Import hook activated.
[Hallucination Engine] Module not found: super_pdf_magic
[Hallucination Engine] Asking LLM to generate implementation...
[Hallucination Engine] Generated code:
========================================
import os

def convert_anything(input_path, output_path=None):
    base, ext = os.path.splitext(input_path)
    if output_path is None:
        output_path = base + ".pdf"
    return f"Converted: {input_path} -> {output_path}"
========================================
[Hallucination Engine] Module 'super_pdf_magic' created successfully.
Converted: report.docx -> report.pdf

LLMがモジュール名から用途を推測し、それらしい実装を生成した。


別のモジュールでも試す

demo2.py
import hallucination_engine

from smart_csv_cleaner import clean_dataset

result = clean_dataset([
    {"name": "Alice", "age": "30", "score": ""},
    {"name": "",      "age": "25", "score": "88"},
    {"name": "Bob",   "age": "xx", "score": "92"},
])

print(result)

LLMは smart_csv_cleaner という名前から「CSVデータをクリーニングする」と推測し、空行除去やバリデーションを含むコードを生成する。

もちろん 毎回生成されるコードは異なる。これはバグではなく、この仕組みの本質だ。


ここで起きていること

従来の考え方。

ハルシネーション = 間違い

今回の考え方。

ハルシネーション = 未実装機能

違いは エラーとして扱うか、仕様として扱うか だけだ。


Agent時代のソフトウェア

今後のAgentは「ツールを使う」から 「必要なツールを生成する」 へ変わる可能性がある。

つまり

従来: ハルシネーション → 失敗
今後: ハルシネーション → 仕様抽出 → 自動実装 → 成功

注意点

この仕組みは 実験的なもの であり、本番環境での使用は推奨しません。

  • exec() のセキュリティリスク — LLMが生成したコードを無検証で実行している。悪意あるコードが生成される可能性がある
  • 再現性がない — 同じモジュール名でも毎回異なるコードが生成される
  • 依存関係の連鎖 — 生成コードが外部ライブラリを import すると連鎖エラーになる可能性がある
  • 品質の保証がない — 生成コードが正しく動作する保証はない

本番で使うなら、生成コードのサンドボックス実行、キャッシュ、テスト自動生成などが必要になる。


まとめ

この記事で伝えたいのは

ハルシネーションを消すことだけが正解ではない

ということだ。

LLMが出力した架空APIは、見方を変えれば 未来のライブラリ設計書 かもしれない。

存在しないライブラリをimportした瞬間に生成し、そのまま動かす。
そんな世界が、Pythonなら意外と簡単に作れてしまう。

そして実際に動いた瞬間、

「ハルシネーションを真実にする」

という感覚が少しだけ理解できる。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?