OpenWebUIの『Thinking...』は本当に推論しているのか?
1. はじめに
OpenWebUIには「Thinking...」という思考表示があります。
一見すると、
「OpenWebUIが内部で推論している」ように見えます。
どういう仕組みなのだろうと思って調べました。
結論から言うと、以下の通りです。
- thinkingはLLMが出力しています
- OpenWebUIはそれを検出・整形・表示しているだけです
本記事では、この仕組みをコードベースで追いながら整理します。
2. 全体像
OpenWebUIのthinkingは、以下の流れで実現されています。
[process_chat_payload]
↓ (thinking出せと指示)
[chat_completion_handler]
↓ (実際に<think>付きで出力)
[process_chat_response]
↓ (タグで分割)
[UI]
↓
thinking表示
3. thinkingはどこで生まれるのか
3.1. 生成:process_chat_payload(プロンプト層)
- "think step by step" などの指示をプロンプトに注入します
-
<think>を出力するようにモデルを誘導します
➡️ ここで初めて「thinkingが出力される可能性」が生まれます
3.2. 通過:chat_completion_handler(中継層)
- モデルの出力をそのまま返します
- thinkingの内容には関与しません
➡️ この層は完全にパススルーです
3.3. 解釈:process_chat_response(UI変換層)
-
<think>を検出します - reasoningブロックとして分離します
-
<details>に変換してUI用に整形します
➡️ thinkingが「可視化される場所」はここです
4. thinkingはどうやって表示されるのか
4.1. process_chat_response
open_webui.utils.middleware から import されています。
4.1.1. thinking のブロック化(コア部分)
if reasoning_content:
if (
not content_blocks
or content_blocks[-1]["type"] != "reasoning"
):
reasoning_block = {
"type": "reasoning",
"start_tag": "<think>",
"end_tag": "</think>",
"attributes": {
"type": "reasoning_content"
},
"content": "",
"started_at": time.time(),
}
content_blocks.append(reasoning_block)
ここで行っていることは以下の通りです。
- thinkingを "reasoning" という専用ブロックとして分離しています
- 開始時間を記録しています(後でdurationを算出するため)
4.1.2. ストリーミング中に思考を蓄積
reasoning_block["content"] += reasoning_content
LLMのストリーミング出力を、そのままthinkingとして蓄積しています。
4.1.3. thinking の終了検知
reasoning_block = content_blocks[-1]
reasoning_block["ended_at"] = time.time()
reasoning_block["duration"] = int(
reasoning_block["ended_at"]
- reasoning_block["started_at"]
)
thinkingの終了時に、処理時間(duration)を計測しています。
4.1.4. UI表示への変換
content = f'{content}<details type="reasoning" done="true" duration="{reasoning_duration}">\n<summary>Thought for {reasoning_duration} seconds</summary>\n{reasoning_display_content}\n</details>\n'
ここで、thinkingはUI表示用のHTMLに変換されます。
-
<details>タグで折りたたみ表示にします - 「Thinking...」に相当する表示を付与します
- 実行時間(秒数)も表示します
4.1.5. タグベースのthinking検出
tag_content_handler(
"reasoning",
reasoning_tags,
content,
content_blocks,
)
ここでは、タグベースでthinkingを検出しています。
-
<think> ... </think>を検出します - reasoningブロックとして変換します
モデルがタグを出力するタイプにも対応しています
5. まとめ
最終的な構造は以下の通りです。
1. LLM側
- thinkingを出力します
reasoning_contentthinking<think>...</think>
2. OpenWebUI側
- ストリームを監視します
- thinkingを検出します
-
content_blocksに分離します - durationを測定します
-
<details>に変換します
3. フロント
- 折りたたみ表示を行います
- 「Thinking...」「Thought for X seconds」を表示します
6. 補足:thinkingタグはどこから来るのか
ここまでで、thinkingが
- LLMが出力し
- OpenWebUIが検出・整形している
という流れは整理できました。
では、その前提となる <think> タグはどこから来るのでしょうか。
この疑問を起点に、実際のコードを追っていきます。
6.1. 入口は backend/open_webui/main.py
まずエントリーポイントである main.py を確認すると、
thinkingに関係しそうなパラメータとして reasoning_tags が登場します。
reasoning_tags = form_data.get("params", {}).get("reasoning_tags")
if model_info_params.get("reasoning_tags") is not None:
reasoning_tags = model_info_params.get("reasoning_tags")
ここから分かることは以下の通りです。
-
reasoning_tagsは UI / API から受け取ることができます - モデルごとの設定で上書きすることも可能です
つまり、<think>...</think> のようなタグは
外部から与えられる設定値であることが分かります。
6.2. まだ“ただの文字列”
この reasoning_tags は、そのまま metadata に格納されます。
"params": {
"stream_delta_chunk_size": stream_delta_chunk_size,
"reasoning_tags": reasoning_tags,
}
中身は例えば以下のようになります。
{
"reasoning_tags": {
"start": "<think>",
"end": "</think>"
}
}
この時点ではまだ、
ただの文字列設定に過ぎません。
特別な処理が行われているわけではなく、
後続の処理にそのまま渡されていきます。
6.3. コア処理の分岐点を見つける
次に、main.py の処理本体を見ると、以下の3つの関数が呼ばれています。
form_data, metadata, events = await process_chat_payload(
request, form_data, user, metadata, model
)
response = await chat_completion_handler(request, form_data, user)
return await process_chat_response(
request, response, form_data, user, metadata, model, events, tasks
)
ここが重要な分岐点になります。
6.4. 「この3つを追えば全体が分かる」と仮説を立てる
この時点で、以下の3つの関数に注目します。
process_chat_payloadchat_completion_handlerprocess_chat_response
thinkingに関係する処理は、このいずれかに存在するはずです。
そこで次のアクションとして、
- それぞれの関数の
import元を辿る - 実装を読んで検証する
という流れでコードを追っていきます。
6.5. import元を辿る
それぞれの関数は以下から import されています。
-
process_chat_payload
→open_webui.utils.middleware -
chat_completion_handler
→open_webui.utils.chat -
process_chat_response
→open_webui.utils.middleware
つまり、
実際のロジックは utils 配下に集約されていることが分かります。
6.6. 調査の進め方(再現可能な形)
今回の調査は、以下の手順で進めました。
-
main.pyでエントリーポイントを確認する - thinkingに関係しそうなパラメータ(
reasoning_tags)を特定する - コア処理として呼ばれている3つの関数を抽出する
-
import元を辿って実装ファイルを特定する - 各関数の責務を読み解く
この結果、
- thinkingは「生成されていない」
- thinkingは「解釈されている」
という構造にたどり着きました。
7. 補足:3つの関数の役割まとめ
main.py から抽出した以下の3つの関数:
- process_chat_payload
- chat_completion_handler
- process_chat_response
これらの役割を整理すると、以下の通りです。
| 層 | 役割 |
|---|---|
| process_chat_payload | thinkingを出させる(プロンプト調整) |
| chat_completion_handler | そのまま通す(中継) |
| process_chat_response | thinkingとして解釈・可視化 |
重要なのは以下の2点です。
- thinkingはこの中で「生成されていない」
- thinkingは「解釈されている」
つまり、
thinkingの正体は「生成ロジック」ではなく
「解釈ロジック」にあると言えます。
Discussion