まず見てほしい
このコードを実行する。
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 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
存在しない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
実行
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がモジュール名から用途を推測し、それらしい実装を生成した。
別のモジュールでも試す
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なら意外と簡単に作れてしまう。
そして実際に動いた瞬間、
「ハルシネーションを真実にする」
という感覚が少しだけ理解できる。