Chat Conversation

참여자: User, QueryPie

Message 1 from User

datadog 의 trace 에서 최근 발생한 에러 하나 찾아서, https://github.com/chequer-io/duplo 코드에서 문제되는 부분 찾아주세요.

Message 2 from QueryPie

Datadog의 trace에서 최근 에러를 찾고, 해당 에러와 관련된 코드 부분을 GitHub 저장소에서 찾아드리겠습니다.

먼저 Datadog에서 최근 trace 정보를 확인해보겠습니다. 첫 번째 trace 목록을 확인했습니다. 모든 trace들이 "status": "ok"로 표시되어 에러가 없는 것 같습니다. 에러가 있는 trace를 찾기 위해 에러가 포함된 trace를 검색해보겠습니다. Datadog 로그에서 명확한 에러를 발견했습니다. duplo-chat 서비스에서 QpError (Abort) 에러가 발생했습니다. 이제 해당 GitHub 리포지토리에서 관련 코드를 살펴보겠습니다. Datadog에서 최근 발생한 에러를 분석한 결과, duplo-chat 서비스에서 "Abort" 에러가 발생한 것을 확인했습니다. 이는 LangChain의 LangGraph 실행 중에 발생한 에러로, 사용자가 채팅을 중단했을 때 발생하는 정상적인 중단 처리와 관련이 있습니다.

에러 분석

에러 정보:

  • 서비스: duplo-chat
  • 에러 메시지: "QpError: Abort"
  • 발생 위치: ChatInternalService의 chatInternal 메서드
  • 에러 스택: LangChain의 PregelRunner._executeTasksWithRetry에서 발생한 "Error: Abort"

문제되는 코드 부분

에러가 발생하는 핵심 코드는 chat/packages/chat/src/modules/chat/internal/chat.internal.service.ts 파일의 다음 부분입니다:

private async *chatInternal(
  context: ChatContext,
  messages: BaseMessage[],
  graphConfig: GraphConfig,
): AsyncGenerator<StreamEvent> {
  let error: QpError | null = null;

  try {
    const stream = this.langchainGraphService.runStream(
      messages,
      graphConfig,
    );

    for await (const streamEvent of stream) {
      // ... stream event 처리
    }
  } catch (e: unknown) {
    error = makeQpError(e);  // 여기서 "Abort" 에러가 QpError로 래핑됨
    context.setError(error);
    this.logger.error('[chatInternal] Error', error);
  }
  
  // ... 에러 처리
}

현재 에러 처리의 문제점

  1. 중복 처리: chat 메서드에서는 AbortError를 정상적으로 처리하지만, chatInternal에서는 같은 Abort 에러를 다시 에러로 래핑하여 처리합니다.

  2. 불필요한 에러 로깅: 사용자 중단은 정상적인 동작이므로 에러로 로깅할 필요가 없습니다.

해결 방법

chat.internal.service.tschatInternal 메서드에서 AbortError를 별도로 처리해야 합니다:

private async *chatInternal(
  context: ChatContext,
  messages: BaseMessage[],
  graphConfig: GraphConfig,
): AsyncGenerator<StreamEvent> {
  let error: QpError | null = null;

  try {
    const stream = this.langchainGraphService.runStream(
      messages,
      graphConfig,
    );

    for await (const streamEvent of stream) {
      // ... stream event 처리
    }
  } catch (e: unknown) {
    // AbortError는 정상적인 중단이므로 에러로 처리하지 않음
    if (e instanceof Error && e.name === 'AbortError') {
      return; // 정상 종료
    }
    
    error = makeQpError(e);
    context.setError(error);
    this.logger.error('[chatInternal] Error', error);
  }
  
  // ... 에러 처리
}

이렇게 수정하면 사용자가 채팅을 중단했을 때 불필요한 에러 로깅과 처리를 방지할 수 있습니다. 현재는 상위 레벨의 chat 메서드에서만 AbortError를 처리하고 있지만, 하위 레벨에서도 동일한 처리가 필요한 상황입니다.

Message 3 from User

코드에서 해당 에러가 발생하기까지의 시퀀스 다이어그램을 간단히 그려줘

Message 4 from QueryPie

에러 발생까지의 시퀀스를 분석해서 다이어그램으로 그려드리겠습니다.

에러 발생 시퀀스 분석

이 다이어그램은 사용자가 채팅을 중단했을 때 "QpError: Abort" 에러가 발생하는 전체 흐름을 보여줍니다.

핵심 문제점

  1. 이중 에러 래핑: LangGraph에서 발생한 Error("Abort")LangchainGraphInternalService.makeQpError()에서 한 번 래핑되고, 다시 ChatInternalService.chatInternal()에서 또 다른 QpError로 래핑됩니다.

  2. 에러 타입 변경: 원래 AbortError였던 것이 QpError로 변환되어, 상위 레벨의 AbortError 처리 로직(e.name !== 'AbortError')을 우회하게 됩니다.

  3. 불필요한 로깅: 정상적인 사용자 중단 동작임에도 불구하고 에러로 로깅되어 Datadog에 에러로 기록됩니다.

해결 방안

Option 1: chatInternal에서 AbortError 직접 처리

catch (e: unknown) {
  if (e instanceof Error && e.name === 'AbortError') {
    return; // 정상 종료
  }
  // QpError인 경우 원본 에러 확인
  if (e instanceof QpError && e.cause instanceof Error && e.cause.name === 'AbortError') {
    return; // 정상 종료
  }
  
  error = makeQpError(e);
  // ...
}

Option 2: LangchainGraphInternalService에서 AbortError 전파

// LangchainGraphInternalService에서
catch (e: unknown) {
  if (e instanceof Error && e.name === 'AbortError') {
    throw e; // QpError로 래핑하지 않고 원본 유지
  }
  
  const error = this.makeQpError(e);
  throw error;
}

이렇게 수정하면 사용자 중단 시 불필요한 에러 로깅을 방지하고 정상적인 중단 처리가 가능합니다.