タイトル通りの小ネタ。
gpt-oss-20bやgpt-oss-120bを使ってLangChain(またはLangGraph)によるAIエージェント的なものを作ろうと思ったら、LLM呼び出しは langchain_openai.ChatOpenAIクラスを使うのが一般的だろう。またAIエージェントが思わぬ理由で動かなかったりしたときのトレース等のために reasoning の出力も取得したいことも多いだろう。
しかし実は ChatOpenAI.invoke は reasoning を返してくれない。まさかそんなはずは、と思いたいが、以下の通り返り値に入ってない。'output_tokens': 207 と出力テキストより明らかに長いので、Reasoningが行われているはずなのだが。
from langchain_openai import ChatOpenAI model = ChatOpenAI( model="openai/gpt-oss-20b", base_url="http://localhost:8000/v1", max_tokens=2048, ) print(model.invoke([{"role":"user", "content":"日本で1番高い山の高さは?"}]))
content='日本で最も高い山は**富士山**です。標高は **3,776\u202fメートル**(約3,777\u202fメートル)です。' additional_kwargs={'refusal': None} response_metadata={'token_usage': {'completion_tokens': 207, 'prompt_tokens': 80, 'total_tokens': 287, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_provider': 'openai', 'model_name': 'openai/gpt-oss-20b', 'system_fingerprint': None, 'id': 'chatcmpl-ad38ace62dcf07c9', 'finish_reason': 'stop', 'logprobs': None} id='lc_run--019b699a-8045-73b3-ac8f-5cd355e3274c-0' usage_metadata={'input_tokens': 80, 'output_tokens': 207, 'total_tokens': 287, 'input_token_details': {}, 'output_token_details': {}}
これは、次の issue(2025/3) のやり取りを見る限り仕様のようである。曰く、OpenAI API が Reasoning を返さないことに合わせているらしい。
この issue では langchain-deepseek や langchain-qwq を使う解決方法が提案されているが、どちらもクラウドAPI用でローカルサーバには向けられない。OllamaサーバならChatOllamaがあるが、vLLM で推論させたかった。なんで ChatVLLM は無いんだ……。
LangChainのモデルクラスではなく、OpenAIのPythonライブラリや、LiteLLMを使えば普通に reasoning は取れるのだが、ChatOpenAI の with_structured_output を使って強制的にフォーマットに従わせた出力を得たいときとかはそういうわけにもいかない。また LangFuse とかでAIエージェントグラフのトレース取っているときも ChatOpenAI を使えた方が都合いいだろう。
これは最悪ソースをいじるしかないか……と思いながら ChatOpenAI のソースを眺めていると以下の記述を発見。
Added in langchain-openai 0.3.26: Updated AIMessage format
langchain-openai >= 0.3.26 allows users to opt-in to an updated AIMessage format when using the Responses API. Setting ChatOpenAI(..., output_version="responses/v1") will format output from reasoning summaries, built-in tool invocations, and other response items into the message's content field, rather than additional_kwargs. We recommend this format for new applications.
OpenAI Responses API が reasoning のサマリーを返すようになったのに合わせて、バージョン 0.3.26 (2025/6) から output_version="responses/v1" を付けるとそれが得られるようになったらしい。gpt-oss の reasoning もこれで取れるんじゃね?
from langchain_openai import ChatOpenAI model = ChatOpenAI( model="openai/gpt-oss-20b", base_url="http://localhost:8000/v1", max_tokens=2048, output_version="responses/v1", # 追加 ) print(model.invoke([{"role":"user", "content": "日本で1番高い山の高さは?"}]))
content=[{'id': 'rs_9d628885bbe6945e', 'summary': , 'type': 'reasoning', 'content': [{'text': 'The user: "日本で1番高い山の高さは?" Which is Japanese: "What is the height of the tallest mountain in Japan?" The answer: Mount Fuji (富士山) height 3,776 meters (12,389 feet). Some sources say 3,776m. The question might want "日本で一番高い山" which is always Mount Fuji. So the answer would be: 3,776 m (or 3,776.4 m). Could mention that Mt. Fuji is the highest peak in Japan, 3,776 m. Optionally give foot conversion. It\'s okay. Just respond concisely.', 'type': 'reasoning_text'}]}, {'type': 'text', 'text': '日本で一番高い山は富士山(フジサン)です。高さは **3,776\u202fメートル**(約12,389\u202fフィート)です。', 'annotations': , 'id': 'msg_8b121036c46ff7d4'}] additional_kwargs={} response_metadata={'id': 'resp_83482a4ad14bbdf2', 'created_at': 1767003124.0, 'model': 'openai/gpt-oss-20b', 'object': 'response', 'service_tier': 'auto', 'status': 'completed', 'model_provider': 'openai', 'model_name': 'openai/gpt-oss-20b'} id='resp_83482a4ad14bbdf2' usage_metadata={'input_tokens': 76, 'output_tokens': 188, 'total_tokens': 264, 'input_token_details': {'cache_read': 64}, 'output_token_details': {'reasoning': 138}}
Reasoning 取れた!
with_structured_output したいときは include_raw=True を付けると raw にこのフォーマットで入っている。
検索しても全然ヒットしないので、意外とこの仕様は知られてないっぽい?