홈으로 돌아가기

LangChain에서 CoT 모델의 추론 내용을 저장하는 방법

이 기사는 LangChain이 CoT 모델에서 추론 내용을 왜 무시하는지 설명하고 additional_kwargs에 이 블록을 저장하는 작동하는 패치를 제공합니다. polza.ai 또는 OpenAI-compatible API를 통한 StepFun과 같은 제공자를 사용하는 개발자에게 적합합니다.

LangChain에서 추론 내용이 손실되고 있나요? 여기 수정 방법이 있습니다
Advertisement 728x90

LangChain에서 CoT 모델의 추론 내용 저장: 패치 방법

LangChain을 통해 CoT 모델을 사용할 때 개발자들은 모델이 최종 답변 전에 생성하는 핵심 추론 블록인 추론 내용의 손실 문제를 겪습니다. 이는 버그가 아니라 아키텍처 결정의 결과입니다: ChatOpenAI 같은 클래스들은 공식 OpenAI API를 엄격히 준수하며, reasoning, reasoning_content, reasoning_details 같은 비표준 필드를 무시합니다. 결과적으로 응답 시간이 느려지고 사용자 경험이 나빠지며, 사용자가 AI의 중간 사고 과정을 보지 못합니다. 공식 업데이트를 기다리지 않고 코드 수준에서 이를 수정하는 방법을 소개합니다.

LangChain이 추론 내용을 무시하는 이유

LangChain은 다양한 LLM 제공자를 지원하는 범용 프레임워크로 자리 잡았습니다. 파편화를 피하기 위해 핵심 클래스(예: ChatOpenAI)들은 공식 OpenAI Chat Completion API 명세만 구현합니다. reasoning_content, reasoning 등의 필드는 DeepSeek, xAI, vLLM 등이 도입한 확장 기능입니다. 각 제공자마다 명칭과 형식이 달라 혼란이 발생합니다:

  • DeepSeek → reasoning_content
  • vLLM → reasoning_content에서 reasoning으로 변경
  • xAI / OpenRouter → reasoning_details 사용 가능

ChatOpenAI에서 모든 변형을 지원하려면 다음이 필요합니다:

Google AdInline article slot
  • 타사 제공자의 API 변경 사항 지속적 모니터링.
  • 필드 매핑 목록 유지 관리.
  • ChatOpenAI를 범용 어댑터로 전환—이는 본래 목적에 어긋납니다.

그래서 LangChain 개발자들은 ChatDeepSeek 같은 특화 클래스를 사용하라고 권장합니다. 하지만 polza.ai, StepFun 등 대부분 제공자들은 여전히 OpenAI 형식 API를 제공하며—여기서 문제가 지속됩니다.

base.py에 패치 적용 방법

해결책은 LangChain 소스 코드의 두 함수를 수정하는 것입니다: _convert_dict_to_message_convert_delta_to_message_chunk. 이들은 원시 LLM 응답을 프레임워크 내 Message 객체로 변환합니다. 추론 필드 추출을 추가하고 additional_kwargs에 저장하세요.

최소 패치:

Google AdInline article slot
def _convert_dict_to_message(_dict):
    ...
    content = _dict.get("content", "") or ""
    reasoning = _dict.get("reasoning", "") or ""  # added
    additional_kwargs: dict = {}
    if reasoning:  # added
        additional_kwargs['reasoning_content'] = reasoning  # added
    if function_call := _dict.get("function_call"):
        additional_kwargs["function_call"] = dict(function_call)
    tool_calls = []
    ...

스트리밍 모드도 마찬가지:

def _convert_delta_to_message_chunk(_dict):
    ...
    id_ = _dict.get("id")
    role = cast(str, _dict.get("role"))
    content = cast(str, _dict.get("content") or "")
    reasoning = cast(str, _dict.get("reasoning", "") or "")  # added
    additional_kwargs: dict = {}
    if reasoning:  # added
        additional_kwargs['reasoning_content'] = reasoning  # added
    ...

이 패치는:

  • 기존 아키텍처를 깨뜨리지 않습니다.
  • 공식 API 호환성을 유지합니다.
  • reasoning 필드를 반환하는 모든 제공자에 대한 reasoning_content 지원을 추가합니다.

스트리밍 출력 사용 예시

패치 적용 후 각 청크의 additional_kwargs에서 추론 내용에 접근할 수 있습니다. 출력 설정 방법:

Google AdInline article slot
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('Hello!'):
    if "reasoning_content" in chunk.additional_kwargs:
        print(chunk.additional_kwargs['reasoning_content'], end="", flush=True)
    else:
        print(chunk.content, end="", flush=True)

이제 사용자는 모델의 추론 과정을 실시간으로 볼 수 있습니다—이는 데이터 분석, 가설 비교, 논리 검증 같은 중간 단계가 중요한 CoT 앱에서 특히 가치 있습니다.

대안 접근법과 한계

패치는 효과적이지만 한계가 있습니다:

  • 로컬 수정: LangChain의 본인 사본에만 적용되며 패키지 업데이트 시 사라집니다.
  • 필드 이름 비호환: 제공자가 reasoning_details를 사용하면 코드를 조정해야 합니다.
  • 표준화 부족: 제공자 간 통합 합의가 없어 각 경우 수동 조정이 필요합니다.

깔끔한 대안:

  • 커스텀 MessageConverter—BaseMessageConverter를 상속한 클래스 생성 후 파싱 로직 재정의.
  • 미들웨어 레이어—LangChain에 도달하기 전에 제공자 HTTP 응답 가로채 필드 정규화.
  • LangChain Expression Language (LCEL)—모델 호출을 RunnableLambda로 감싸 체인에 전달 전에 추론 추출.

하지만 이러한 방법들은 더 복잡하며 프레임워크 내부를 깊이 이해해야 합니다. MVP나 빠른 수정에는 base.py 패치가 최적입니다.

주요 요점

  • 추론 내용 손실은 버그가 아니라 LangChain의 OpenAI API 엄격 준수 때문입니다.
  • 패치는 backward compatibility를 깨뜨리지 않고 additional_kwargs에 reasoning 필드 지원을 추가합니다.
  • 일반 호출과 스트리밍 호출 모두 작동합니다.
  • LangChain 업데이트마다 수동 재적용이 필요합니다.
  • CoT 앱에서 모델의 사고 과정을 사용자에게 투명하게 보여 사용자 경험을 향상시킵니다.

— Editorial Team

Advertisement 728x90

다음 읽기