Исправление обработки блока reasoning content в CoT-моделях для LangChain

от автора

В процессе работы с фреймворком LangChain была обнаружена существенная проблема в чат-классах (ChatOpenAI, ChatDeepSeek и др.) при интеграции с различными провайдерами и агрегаторами LLM. Ни один из них не сохраняет содержимое блока рассуждений (reasoning content) в финальном ответе, что увеличивает время ожидания ответа пользователем и негативно сказывается на UX ИИ-приложений, использующих CoT-модели.

В данной статье я расскажу, как можно решить эту проблему на примере модели stepfun/step-3.5-flash и провайдера polza.ai.

Почему все так? Путаница в стандартах

При разработке ИИ-ассистента для инвестирования я столкнулся с этой проблемой. Первоначально я пытался решить её с помощью официальной библиотеки OpenAI, однако такой подход не давал возможности в полной мере использовать фреймворки LangChain и LangGraph в моём MVP. Я погрузился в эту проблему глубже, и вот что узнал:

В одном из баг репортов разработчики прокомментировали это так:

ChatOpenAI не будет добавляться поддержка полей ответа reasoning_contentreasoningreasoning_details, или других полей, специфичных для конкретного поставщика услуг.

ChatOpenAIбудет ориентирован на официальную спецификацию API завершения чата OpenAI . reasoning_contentне является частью этой спецификации — это нестандартное расширение, разработанное DeepSeek и принятое другими поставщиками (OpenRouter, vLLM, xAI и т. д.), которые используют формат завершения чата. Каждый поставщик также реализует его немного по-разному ( reasoning_contentvs reasoningvs reasoning_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 ChatOpenAImodel = 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.

Патч лёгкий, не ломает архитектуру. Это изменение, которое уже сейчас может помочь разработчикам ИИ-приложений.

Если вы обошли проблему иначе, знаете более чистый способ или столкнулись с подводными камнями — кидайте в комментарии. Буду рад обсудить и дополнить статью!

ссылка на оригинал статьи https://habr.com/ru/articles/1025460/