В процессе работы с фреймворком LangChain была обнаружена существенная проблема в чат-классах (ChatOpenAI, ChatDeepSeek и др.) при интеграции с различными провайдерами и агрегаторами LLM. Ни один из них не сохраняет содержимое блока рассуждений (reasoning content) в финальном ответе, что увеличивает время ожидания ответа пользователем и негативно сказывается на UX ИИ-приложений, использующих CoT-модели.
В данной статье я расскажу, как можно решить эту проблему на примере модели stepfun/step-3.5-flash и провайдера polza.ai.
Почему все так? Путаница в стандартах
При разработке ИИ-ассистента для инвестирования я столкнулся с этой проблемой. Первоначально я пытался решить её с помощью официальной библиотеки OpenAI, однако такой подход не давал возможности в полной мере использовать фреймворки LangChain и LangGraph в моём MVP. Я погрузился в эту проблему глубже, и вот что узнал:
В одном из баг репортов разработчики прокомментировали это так:
ChatOpenAIне будет добавляться поддержка полей ответаreasoning_content,reasoning,reasoning_details, или других полей, специфичных для конкретного поставщика услуг.
ChatOpenAIбудет ориентирован на официальную спецификацию API завершения чата OpenAI .reasoning_contentне является частью этой спецификации — это нестандартное расширение, разработанное DeepSeek и принятое другими поставщиками (OpenRouter, vLLM, xAI и т. д.), которые используют формат завершения чата. Каждый поставщик также реализует его немного по-разному (reasoning_contentvsreasoningvsreasoning_details), и эти поля продолжают развиваться (например, vLLM перешел сreasoning_contentнаreasoning).Добавление поддержки этих полей
ChatOpenAIозначало бы следующее:
Поддержание постоянно растущего набора сопоставлений полей, специфичных для каждого поставщика услуг.
Мы неявно обязуемся отслеживать изменения у поставщиков услуг, которые мы не контролируем.
Создание,
ChatOpenAIпо сути, универсального адаптера, что не является его предназначением....
Также в своем ответе они предлагают использовать другие чаты (ChatDeepSeek или ChatOpenRouter), НО большинство провайдеров используют стандарт OpenAI Chat Completion API. Получается замкнутый круг: одни утверждают, что в официальной документации такого поля нет, другие используют его.
Как все исправить?
Для решения данной проблемы я полез в исходный код langchain_openai/chat_models/base.py. Тут нужно найти функции convert_dict_to_message и convert_delta_to_message_chunkи добавить следующее:
def _convert_dict_to_message (...):
...
content = _dict.get("content", "") or ""
reasoning = _dict.get("reasoning", "") or "" # add
additional_kwargs: dict = {}
if reasoning: # add
additional_kwargs['reasoning_content'] = reasoning # add
if function_call := _dict.get("function_call"):
additional_kwargs["function_call"] = dict(function_call)
tool_calls = []
...
def _convert_delta_to_message_chunk (...):
...
id_ = _dict.get("id")
role = cast(str, _dict.get("role"))
content = cast(str, _dict.get("content") or "")
reasoning = cast(str, _dict.get("reasoning", "") or "") # add
additional_kwargs: dict = {}
if reasoning: # add
additional_kwargs['reasoning_content'] = reasoning # addБлагодаря такому способу, блок рассуждений будет сохраняться в additional_kwargs как в обычном, так и в потоковом вызове.
Пример:
from langchain_openai import ChatOpenAI
model = ChatOpenAI(
model="stepfun/step-3.5-flash",
openai_api_base="https://api.polza.ai/v1",
openai_api_key="your_api_key",
)
for chunk in model.stream('Привет!'):
if "reasoning_content" in chunk.additional_kwargs:
print(chunk.additional_kwargs['reasoning_content'], end="", flush=True)
else:
print(chunk.content, end="", flush=True)Вывод
В этой статье мы разобрали проблему потери блока рассуждений (reasoning content) в CoT-моделях при использовании фреймворка LangChain. Официальное исправление данной проблемы выйдет ещё не скоро, но ждать не обязательно. Пара изменений в base.py и мы забираем reasoning_content в additional_kwargs.
Патч лёгкий, не ломает архитектуру. Это изменение, которое уже сейчас может помочь разработчикам ИИ-приложений.
Если вы обошли проблему иначе, знаете более чистый способ или столкнулись с подводными камнями - кидайте в комментарии. Буду рад обсудить и дополнить статью!


























