🐷

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_content
    • thinking
    • <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_payload
  • chat_completion_handler
  • process_chat_response

thinkingに関係する処理は、このいずれかに存在するはずです。

そこで次のアクションとして、

  1. それぞれの関数の import 元を辿る
  2. 実装を読んで検証する

という流れでコードを追っていきます。

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. 調査の進め方(再現可能な形)

今回の調査は、以下の手順で進めました。

  1. main.py でエントリーポイントを確認する
  2. thinkingに関係しそうなパラメータ(reasoning_tags)を特定する
  3. コア処理として呼ばれている3つの関数を抽出する
  4. import 元を辿って実装ファイルを特定する
  5. 各関数の責務を読み解く

この結果、

  • 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