LangChain の FakeLLM は、API を叩かずにアプリのロジックだけをテストするためのダミーLLMです。あらかじめ渡した文字列を順番に返すだけで、推論もトークン課金も発生しません。ところがクラスが6つあり、どれを選ぶかで stream() のチャンク分割もコールバックの発火も変わります。特に FakeStreamingListLLM は1文字ずつ流れているように見えて on_llm_new_token が一度も呼ばれません。この記事では langchain-core 1.5.4 での実行結果をもとに、6クラスの使い分け、Streaming の挙動差、コールバックが沈黙する原因と回避策までを整理します。
まとめ
結論から示します。
- LangChain の FakeLLM は
langchain_core.language_modelsにあるテスト用のモックです。軽量な実用LLMではなく、推論は一切しません。 pip install fakellmで入るのは LangChain とは無関係の別パッケージ(OpenAI/Anthropic 互換のモックHTTPサーバー)です。from fakellm import FakeLLMは ImportError になります。- 単純な文字列応答なら
FakeListLLM、文字単位の Streaming を見たいならFakeStreamingListLLM、コールバックまで検証したいならFakeListChatModelを選びます。 FakeStreamingListLLMはstream()自体を上書きしているためon_llm_new_tokenが発火せず、on_llm_endが最初のチャンクより先に来ます。ストリーミングUIのコールバック検証には使えません。- ツール呼び出しは
GenericFakeChatModelのinvoke()なら再現できます。stream()はcontentが空だとValueError、非空だと例外を出さずにtool_callsだけが消えます。
以降で、各クラスの実測値と、テストが「通っているのに何も検証していない」状態に陥る条件を順に見ていきます。
FakeLLMの正体|LangChainのテスト用モックと同名PyPIパッケージの取り違え
FakeLLM という語は2つの別物を指します。この取り違えが、動かないサンプルコードが出回る原因になっています。
langchain-core 1.5.4 が公開する6クラスと選び分け
LangChain 本体の Fake 群は langchain_core.language_models にまとまっています。1.5.4 の __all__ は19要素あり、そのうち Fake 系は次の6クラスです。
| クラス | 種別 | 渡すもの | stream()のチャンク | on_llm_new_token |
|---|---|---|---|---|
| FakeListLLM | LLM | responses(文字列リスト) | 応答まるごと1個 | 発火しない |
| FakeStreamingListLLM | LLM | responses(文字列リスト) | 1文字ずつ | 発火しない |
| FakeListChatModel | チャットモデル | responses(文字列リスト) | 1文字ずつ | 発火する |
| FakeMessagesListChatModel | チャットモデル | responses(BaseMessageリスト) | 応答まるごと1個 | 発火しない |
| GenericFakeChatModel | チャットモデル | messages(イテレータ) | 空白区切り | 発火する |
| ParrotFakeChatModel | チャットモデル | 不要(入力を反射) | 応答まるごと1個 | 発火しない |
実務でまず押さえるべきは FakeListLLM と FakeListChatModel の2つです。前者はチェーンの配線確認、後者はコールバックまで含めた検証に使います。残り4つは、文字単位の遅延再現(FakeStreamingListLLM)、ツール呼び出しやメッセージ属性の再現(GenericFakeChatModel)、入力の反射(ParrotFakeChatModel)といった目的が決まったときだけ出番があります。表の「応答まるごと1個」は _stream を実装していないクラスの既定動作で、stream() を呼んでも例外にはならず1チャンクだけ返ります。
なお fake_chat_models モジュールには FakeChatModel も実在しますが __all__ に載っていません。公開APIとして数えるのは上の6つです。
インポート元を間違えやすい点にも触れておきます。langchain_core.llms と langchain_core.chat_models はモジュールとして存在せず、いずれも ModuleNotFoundError です。旧世代の記事にある langchain_community.llms.fake は最新の langchain-community 0.4.2 にも同梱されていて今も動きますが、import すると DeprecationWarning: langchain-community is being sunset and is no longer actively maintained. が出ます。新規のコードは langchain_core.language_models から読んでください。LangChain 本体のパッケージ再編については LangChain v1.0とは|create_agent・ミドルウェア・langchain-classicを実装例で解説で整理しています。
pip install fakellm で入る別物|0.3.5のモックHTTPサーバー
PyPI には fakellm という名前のパッケージが実在します。ただし LangChain とは無関係です。2026年8月時点の最新は 0.3.5 で、説明は「A mock LLM server for testing. Drop-in replacement for OpenAI and Anthropic APIs.」。OpenAI/Anthropic の HTTP API を喋るモックサーバーを立て、YAML で応答を定義して使う CLI ツールです。
ここが厄介な点です。Python 3.10 以上の環境なら pip install fakellm は成功します。エラーが出ないので、そのまま次の行を書いてしまいます。
# langchain-core と fakellm を同じ venv に入れた状態で実行
from fakellm import FakeLLM
# ImportError: cannot import name 'FakeLLM' from 'fakellm' (.../site-packages/fakellm/__init__.py)
インストールは通るのにインポートで落ちるため、原因を「バージョンが古い」「環境が壊れた」と誤診しがちです。LangChain の FakeLLM を使いたいなら、追加インストールは要りません。langchain-core に最初から入っています。
pip install langchain-core
逆に、HTTP レイヤごとモックしたい場合はこの PyPI 版が使えます。手順は3つです。
pip install fakellm
fakellm init # fakellm.yaml を生成し、応答を定義する
fakellm serve # http://127.0.0.1:9999 で待ち受ける
あとはクライアント側の base_url をこのアドレスへ向けるだけです。LangChain の Fake クラスがフレームワーク内部で応答を差し替えるのに対し、こちらはネットワーク境界で差し替えます。SDK の認証・リトライ・タイムアウト処理まで通したいときはこちらが向きます。
FakeListLLMの基本動作|responsesの巡回とinvokeの戻り値
FakeListLLM は responses に渡した文字列を先頭から順に返します。リストの末尾まで到達すると内部カウンタ i が 0 に戻り、先頭から巡回します。
from langchain_core.language_models import FakeListLLM
llm = FakeListLLM(responses=["A", "B"])
print([llm.invoke("x") for _ in range(4)])
# ['A', 'B', 'A', 'B']
この巡回仕様はテスト設計に影響します。応答が尽きたら例外を出す実装なら「LLMを想定回数だけ呼んだか」を副作用で検証できますが、FakeListLLM は静かに先頭へ戻ります。
では呼び出し回数はどう数えるか。llm.i を見る方法は、len(responses) を超えない範囲でしか使えません。カウンタ自身も巡回するからです。実測では responses が2件のとき、invoke を6回呼んでも i は 1, 0, 1, 0, 1, 0 と往復するだけでした。呼び出し回数が要素数を超えうるなら、コールバックハンドラで on_llm_start を数えてください。
コンストラクタ引数 sleep にも注意点があります。FakeListLLM にも定義されていますが、ソースの docstring に「Ignored by FakeListLLM, but used by sub-classes.」と明記されているとおり、このクラスでは無視されます。遅延を入れるなら FakeStreamingListLLM か FakeListChatModel です。
Streaming時のチャンク分割|クラス別の粒度差
「Streaming に対応しているか」はクラスごとに答えが異なります。langchain-core 1.5.4 で同じ文字列を流したときの実測値が下記です。
同一入力で比較したチャンク数の実測
from langchain_core.language_models import (
FakeListLLM, FakeStreamingListLLM, FakeListChatModel,
)
print(list(FakeListLLM(responses=["こんにちは世界"]).stream("q")))
# ['こんにちは世界'] ... 1チャンク
print(list(FakeStreamingListLLM(responses=["こんにちは世界"]).stream("q")))
# ['こ', 'ん', 'に', 'ち', 'は', '世', '界'] ... 7チャンク
print([c.content for c in FakeListChatModel(responses=["こんにちは世界"]).stream("q")])
# ['こ', 'ん', 'に', 'ち', 'は', '世', '界'] ... 7チャンク
FakeListLLM にも stream() はあります。ただし専用の _stream を持たないため、BaseLLM.stream が type(self)._stream == BaseLLM._stream という比較で未実装を検知し、invoke() の結果をそのまま1チャンクだけ yield します。チャンク境界の処理(バッファリング、途中でのUI更新、途中キャンセル)を検証したいテストで FakeListLLM を使うと、境界が1つしか無いので何も検証できません。
GenericFakeChatModelの分割規則|空白を保持する分割
GenericFakeChatModel だけは分割規則が違います。ソースでは re.split(r"(\s)", content) を使っており、空白文字自体も独立したチャンクとして残ります。
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
g = GenericFakeChatModel(messages=iter([AIMessage(content="hello fake world")]))
print([c.content for c in g.stream("q")])
# ['hello', ' ', 'fake', ' ', 'world'] ... 5チャンク
英文なら実際のトークン単位に近い挙動になります。ただし日本語は空白を含まないため分割されず1チャンクのままです。日本語UIで逐次描画の見た目を確認したい場合、このクラスは適していません。文字単位で流すなら FakeListChatModel を選びます。
Callbacksが発火しない落とし穴|FakeStreamingListLLMでon_llm_new_tokenが0回
ここが FakeLLM で最も事故が起きる箇所です。FakeStreamingListLLM は1文字ずつチャンクを返すのに、コールバックハンドラの on_llm_new_token が一度も呼ばれません。同じハンドラを2つのクラスに付けて、イベントの発生順を記録した結果を並べます。
from langchain_core.language_models import FakeStreamingListLLM, FakeListChatModel
from langchain_core.callbacks import BaseCallbackHandler
EV = []
class H(BaseCallbackHandler):
def on_llm_start(self, *a, **k): EV.append("on_llm_start")
def on_llm_new_token(self, token, **k): EV.append(f"on_llm_new_token({token})")
def on_llm_end(self, *a, **k): EV.append("on_llm_end")
EV.clear()
for c in FakeStreamingListLLM(responses=["abc"]).stream("q", config={"callbacks": [H()]}):
EV.append(f"yield({c})")
print(EV)
# ['on_llm_start', 'on_llm_end', 'yield(a)', 'yield(b)', 'yield(c)']
EV.clear()
for c in FakeListChatModel(responses=["abc"]).stream("q", config={"callbacks": [H()]}):
EV.append(f"yield({c.content})")
print(EV)
# ['on_llm_start', 'on_llm_new_token(a)', 'yield(a)', 'on_llm_new_token(b)', 'yield(b)',
# 'on_llm_new_token(c)', 'yield(c)', 'on_llm_end']
違いは2点あります。FakeStreamingListLLM では on_llm_new_token が0回。そして on_llm_end が最初のチャンクより先に記録されています。実際のモデルなら最後に来るイベントが、先頭に来ています。
原因はソースを見ると明確です。langchain_core/language_models/fake.py の FakeStreamingListLLM は、_stream を実装するのではなく Runnable の stream() と astream() そのものを override しています。中身は self.invoke(input, config) を先に完走させ、返ってきた文字列を後から1文字ずつ yield するだけです。
def stream(self, input, config=None, *, stop=None, **kwargs):
result = self.invoke(input, config) # ここで完結し on_llm_end まで発火
for i_c, c in enumerate(result):
if self.sleep is not None:
time.sleep(self.sleep)
if self.error_on_chunk_number is not None and i_c == self.error_on_chunk_number:
raise FakeListLLMError
yield c
チャンクを刻んでいるループはコールバックマネージャの外側にあります。だからトークンイベントが流れません。一方 FakeListChatModel は _stream を実装しており、チャットモデルの基底クラスが各チャンクで run_manager.on_llm_new_token を呼ぶため、実モデルと同じ順序になります。
結論として、コールバック経由でトークンを受けるコード(WebSocket配信、ストリーミングUI、トークン数カウンタ、監査ログ)のテストに FakeStreamingListLLM を使うべきではありません。テストはグリーンになりますが、ハンドラは一度も呼ばれていないので実質ノーテストです。この用途では FakeListChatModel、または英語圏のトークン境界を再現したい場合のみ GenericFakeChatModel に置き換えてください。FakeStreamingListLLM が向くのは、コールバックを介さずジェネレータを直接消費する処理の検証と、sleep でチャンク間の遅延を再現したいケースに限られます。
ツール呼び出しテストの適用範囲|GenericFakeChatModelがtool_callsを落とす2条件
エージェントのテストでは、モデルにツール呼び出しを返させたい場面があります。GenericFakeChatModel は AIMessage をそのまま渡せるので一見使えそうですが、invoke() と stream() で対応範囲が分かれます。
from langchain_core.language_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
msg = AIMessage(
content="",
tool_calls=[{"name": "get_weather", "args": {"city": "Tokyo"}, "id": "call_1"}],
)
# invoke なら tool_calls はそのまま保持される
print(GenericFakeChatModel(messages=iter([msg])).invoke("q").tool_calls)
# [{'name': 'get_weather', 'args': {'city': 'Tokyo'}, 'id': 'call_1', 'type': 'tool_call'}]
# stream は例外
list(GenericFakeChatModel(messages=iter([msg])).stream("q"))
# ValueError: No generation chunks were returned
_stream の実装が見ているのは content と additional_kwargs の2つだけで、現行の tool_calls フィールドは対象外です。content が空文字だとチャンクが1つも生成されず、チャットモデルの基底クラスが「返すものが無い」と判断して ValueError を投げます。
より危険なのは content に何か入れた場合です。例外は出ません。そのかわり tool_calls が黙って消えます。
msg = AIMessage(
content="ok done",
tool_calls=[{"name": "get_weather", "args": {"city": "Tokyo"}, "id": "call_1"}],
)
for c in GenericFakeChatModel(messages=iter([msg])).stream("q"):
print(repr(c.content), c.tool_calls, c.tool_call_chunks)
# 'ok' [] []
# ' ' [] []
# 'done' [] []
チャンクは3つ流れ、どれも tool_calls は空リストです。テストは通ります。しかしツール呼び出しは一度も検証されていません。この記事の前半で扱ったコールバックの落とし穴と同じ構図が、ここでも起きます。
ストリーミングでツール呼び出しを再現するには、旧形式の additional_kwargs に function_call を入れます。引数文字列はカンマで分割されて流れます。
msg = AIMessage(
content="",
additional_kwargs={
"function_call": {"name": "get_weather", "arguments": '{"city":"Tokyo","unit":"c"}'}
},
)
for c in GenericFakeChatModel(messages=iter([msg])).stream("q"):
print(c.additional_kwargs)
# {'function_call': {'name': 'get_weather'}}
# {'function_call': {'arguments': '{"city":"Tokyo"'}}
# {'function_call': {'arguments': ','}}
# {'function_call': {'arguments': '"unit":"c"}'}}
# {}
もう1つの制約として、Fake 系チャットモデルは bind_tools() に対応していません。FakeListChatModel(responses=["ok"]).bind_tools([]) は NotImplementedError になります。ツールスキーマのバインドまで含めた経路を検証したいなら、Fake ではなく HTTP レベルのモック(前述の PyPI 版 fakellm のようなモックサーバー)か、実モデルを使った統合テストに切り替える判断が要ります。グラフ構造でのエージェント制御を検証したい場合は LangChainとLangGraphの違い|v1.0で逆転した関係と使い分けもあわせて確認してください。
pytestへの組み込み|エラー注入・遅延・Embeddingのモック
Fake を使う目的の多くは、正常系ではなく異常系の再現です。実モデルでは狙って起こせない状況を、パラメータで作れます。
error_on_chunk_numberによるチャンク途中での中断
error_on_chunk_number は指定したインデックスのチャンクで例外を送出します。このフィールドを持つのは FakeStreamingListLLM と FakeListChatModel の2クラスだけで、送出される例外はそれぞれ FakeListLLMError と FakeListChatModelError です。
import pytest
from langchain_core.language_models import FakeListChatModel
from langchain_core.language_models.fake_chat_models import FakeListChatModelError
def test_stream_interrupted_midway():
model = FakeListChatModel(responses=["abcdef"], error_on_chunk_number=3)
received = []
with pytest.raises(FakeListChatModelError):
for chunk in model.stream("q"):
received.append(chunk.content)
# 3チャンク受信後に中断される
assert received == ["a", "b", "c"]
sleepの適用範囲|クラスで違う遅延の入り方
sleep は待ち時間を挟みますが、どこに挟まるかがクラスで違います。FakeListChatModel は _call と _stream の両方に適用するため invoke() でも遅延します。FakeStreamingListLLM はチャンク間だけです。タイムアウト処理を試すなら前者、プログレス表示の描画間隔を見るなら後者を選びます。値は 0.01 秒程度に抑えないとテスト時間が伸びます。responses を複数用意しておけば、リトライ時に別の応答を返す挙動も巡回で再現できます。
FakeEmbeddingsとDeterministicFakeEmbedding|numpy必須の注意
LLM だけモックしても、RAG 構成では Embedding API の課金が残ります。langchain_core.embeddings の __all__ には FakeEmbeddings と DeterministicFakeEmbedding があり、前者は毎回ランダム、後者はテキストのハッシュから決定的なベクトルを返します。類似度の期待値をアサートするテストでは後者が必須です。
from langchain_core.embeddings import DeterministicFakeEmbedding
emb = DeterministicFakeEmbedding(size=4)
assert emb.embed_query("langchain") == emb.embed_query("langchain") # 何度呼んでも同じ
ここに1つ罠があります。langchain-core 1.5.4 の依存に numpy は含まれておらず、fake.py 側も numpy の import 失敗を握りつぶす書き方をしています。そのため numpy を入れていない環境では、import は通るのに embed_query() を呼んだ瞬間 NameError: name 'np' is not defined で落ちます。pip install numpy を明示してください。検索側のモックについては LangChain BM25Retrieverの使い方|インストール・日本語対応・ハイブリッド検索で扱っている BM25 のようにベクトル化不要の Retriever を使う手もあります。
なお、これらは Python 版に固有の実装です。LangChain.jsとは|JavaScript/TypeScriptでLLM・エージェントを作る導入と使い方で扱う JS 版はクラス構成が異なるため、同じ名前で読み替えないでください。
よくある質問
FakeLLMとは何ですか?
LangChain が提供するテスト用のダミーLLMです。langchain-core 1.5.4 では langchain_core.language_models にある FakeListLLM などのクラス群を指し、渡した文字列を順番に返すだけで推論は行いません。API キーもネットワーク接続も不要で、課金もレート制限も発生しないため、チェーンやエージェントの配線・分岐・例外処理を単体テストで確かめる用途に使います。実運用のモデルとして推論品質を出すものではない点が、軽量な実用LLMとの最大の違いです。
LangChainのFakeLLMはどこからインポートしますか?
from langchain_core.language_models import FakeListLLM です。langchain-core に同梱されているため追加インストールは不要です。langchain_core.llms や langchain_core.chat_models というモジュールは存在せず ModuleNotFoundError になります。langchain_community.llms.fake も 0.4.2 時点で動きますが、パッケージ自体が sunset 宣言済みで DeprecationWarning が出ます。pip install fakellm は LangChain と無関係の別パッケージなので実行しないでください。
FakeListLLMとFakeStreamingListLLMはどちらを使うべきですか?
応答文字列の内容だけを検証するなら FakeListLLM で十分です。stream() は応答まるごと1チャンクを返します。チャンク境界の処理を確かめたい場合は FakeStreamingListLLM が1文字ずつ返しますが、on_llm_new_token が発火しないという制約があります。コールバック経由でトークンを受けるコードをテストするなら、どちらでもなく FakeListChatModel を選んでください。
Streamingでcallbacksが呼ばれないのはなぜですか?
FakeStreamingListLLM を使っている場合、実装が Runnable の stream() 自体を override しており、内部で invoke() を完走させてから結果を1文字ずつ yield しているためです。チャンクを刻むループがコールバックマネージャの外にあるので on_llm_new_token が流れず、on_llm_end は最初のチャンクより先に発火します。_stream を実装している FakeListChatModel に置き換えると、実モデルと同じ順序でイベントが発生します。
FakeLLMでツール呼び出し(tool calling)はテストできますか?
部分的に可能です。GenericFakeChatModel に tool_calls を設定した AIMessage を渡せば invoke() では保持されます。ただし stream() は tool_calls を参照しないため、content が空だと ValueError: No generation chunks were returned、content が非空だと例外を出さずに tool_calls だけが空リストになります。ストリーミングで流したい場合は additional_kwargs の function_call 形式を使います。また Fake 系は bind_tools() 非対応(NotImplementedError)なので、バインド経路まで検証するなら別の手段が必要です。