В процессе работы с фреймворком 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.
Патч лёгкий, не ломает архитектуру. Это изменение, которое уже сейчас может помочь разработчикам ИИ-приложений.
Если вы обошли проблему иначе, знаете более чистый способ или столкнулись с подводными камнями - кидайте в комментарии. Буду рад обсудить и дополнить статью!
kardanShurup
В решении используется поле
reasoningиз словаряdict. Однако вы сами цитируете комментарий разработчиков о том, что разные провайдеры называют это поле по-разному (reasoningcontent,reasoning,reasoning_details). Гарантирует ли предложенный патч работоспособность сvLLM(который перешел наreasoning) или сxAI, или же код жестко завязан на спецификуstepfun/step-3.5-flashиpolza.ai?RadAI Автор
Спасибо за вопрос!
В данной статье решение рассматривалось в рамках работы с провайдером polza.ai. Конечно можно сделать и более общее решение, чтобы подходило для всех.
Koster-Valdaw
Немного не по теме, но вызывает доверие, что вы написали минимум абзацев - строго по делу.
Провайдеры типа polza.ai (или конкретно polza.ai) - каким либо образом влияют на результаты нативных моделей, надстраиваются доп. системными промптами со стороны провайдера?
Конечно, вряд ли мы знаем начинку - но хотя бы на примере вашей практики.
RadAI Автор
Спасибо за вопрос!
Я думаю, что вряд ли провайдер (по крайней мере polza.ai) будет надстраивать скрытые системные промпты, но такое, в целом, возможно.