Skip to content

错误处理:Runnable 如何抛出结构化错误?

在构建复杂的 LLM 应用时,错误处理是一个至关重要的方面。LangChain V3 通过结构化的错误处理机制,使得开发者能够更好地理解、调试和处理在执行链中可能出现的各种错误情况。本章将深入探讨 LangChain V3 中的错误处理设计和实践。

错误处理的重要性

在 LLM 应用中,错误处理比传统应用程序更加复杂,原因包括:

  1. 网络依赖 - 大多数 LLM 调用都依赖网络,容易出现超时、连接失败等问题
  2. 模型不确定性 - LLM 的输出具有不确定性,可能导致解析错误
  3. 第三方服务 - 通常依赖多个第三方服务,每个都可能出错
  4. 资源限制 - 可能受到速率限制、配额限制等约束

LangChain V3 的错误层次结构

LangChain V3 定义了一个清晰的错误层次结构,所有错误都继承自基础的 LangChainError

typescript
class LangChainError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'LangChainError';
  }
}

class BaseLLMError extends LangChainError {
  llmInput?: any;
  originalError?: Error;
  
  constructor(message: string, llmInput?: any, originalError?: Error) {
    super(message);
    this.llmInput = llmInput;
    this.originalError = originalError;
    this.name = 'BaseLLMError';
  }
}

class OutputParserError extends BaseLLMError {
  constructor(message: string, llmInput?: any, originalError?: Error) {
    super(message, llmInput, originalError);
    this.name = 'OutputParserError';
  }
}

class NetworkError extends BaseLLMError {
  statusCode?: number;
  
  constructor(message: string, statusCode?: number, originalError?: Error) {
    super(message, undefined, originalError);
    this.statusCode = statusCode;
    this.name = 'NetworkError';
  }
}

结构化错误信息

LangChain V3 中的错误不仅仅是简单的错误消息,它们携带了丰富的上下文信息:

llmInput

llmInput 属性包含了传递给 LLM 的原始输入,这对于调试非常有用:

typescript
try {
  const result = await llm.invoke(input);
} catch (error) {
  if (error instanceof BaseLLMError) {
    console.log('LLM 输入:', error.llmInput);
    console.log('原始错误:', error.originalError);
  }
}

originalError

originalError 属性保留了原始的错误信息,有助于追踪错误的根本原因:

typescript
class LLM {
  async invoke(input) {
    try {
      const response = await this.callLLM(input);
      return response;
    } catch (error) {
      // 将原始错误包装在结构化错误中
      throw new BaseLLMError(
        `LLM 调用失败: ${error.message}`,
        input,
        error
      );
    }
  }
}

特定错误类型的处理

LangChain V3 定义了多种特定的错误类型,每种类型都有其特定的处理方式:

OutputParserException

当输出解析失败时抛出:

typescript
class JsonOutputParser {
  async invoke(input) {
    try {
      return JSON.parse(input);
    } catch (error) {
      throw new OutputParserError(
        `JSON 解析失败: ${error.message}`,
        input,
        error
      );
    }
  }
}

TimeoutError

当操作超时时抛出:

typescript
class TimeoutError extends BaseLLMError {
  timeout: number;
  
  constructor(timeout: number) {
    super(`操作超时 (${timeout}ms)`);
    this.timeout = timeout;
    this.name = 'TimeoutError';
  }
}

RateLimitError

当遇到速率限制时抛出:

typescript
class RateLimitError extends NetworkError {
  retryAfter?: number;
  
  constructor(message: string, retryAfter?: number) {
    super(message, 429);
    this.retryAfter = retryAfter;
    this.name = 'RateLimitError';
  }
}

错误处理的最佳实践

1. 使用 instanceof 进行类型检查

typescript
try {
  const result = await chain.invoke(input);
} catch (error) {
  if (error instanceof OutputParserError) {
    // 处理解析错误
    console.log('输出解析失败:', error.message);
  } else if (error instanceof NetworkError) {
    // 处理网络错误
    console.log('网络错误:', error.message);
  } else if (error instanceof BaseLLMError) {
    // 处理其他 LLM 错误
    console.log('LLM 错误:', error.message);
  } else {
    // 处理其他未预期的错误
    console.log('未知错误:', error.message);
  }
}

2. 实现重试机制

typescript
async function withRetry(runnable, input, maxAttempts = 3) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await runnable.invoke(input);
    } catch (error) {
      // 对于某些错误类型不重试
      if (error instanceof OutputParserError) {
        throw error;
      }
      
      // 达到最大重试次数时抛出错误
      if (attempt === maxAttempts) {
        throw error;
      }
      
      // 指数退避
      await new Promise(resolve => 
        setTimeout(resolve, Math.pow(2, attempt) * 1000)
      );
    }
  }
}

3. 错误日志记录

typescript
class ErrorLoggingCallback {
  handleError(error, config) {
    // 记录结构化错误信息
    logger.error({
      error: {
        name: error.name,
        message: error.message,
        stack: error.stack,
        llmInput: error.llmInput,
        originalError: error.originalError
      },
      metadata: config.metadata
    });
  }
}

与 RunnableConfig 集成

错误处理与 紧密集成,可以通过配置来控制错误处理行为:

typescript
const config: RunnableConfig = {
  callbacks: [
    {
      handleChainError(error, runId) {
        // 自定义错误处理逻辑
        if (error instanceof RateLimitError) {
          // 特殊处理速率限制错误
          this.handleRateLimit(error);
        }
      }
    }
  ]
};

错误恢复策略

在某些情况下,可以实现错误恢复策略:

typescript
class FallbackLLM extends Runnable {
  constructor(private primaryLLM, private fallbackLLM) {
    super();
  }
  
  async invoke(input, config) {
    try {
      return await this.primaryLLM.invoke(input, config);
    } catch (error) {
      // 如果是配置错误或解析错误,不尝试回退
      if (error instanceof OutputParserError) {
        throw error;
      }
      
      // 对于网络错误或 LLM 错误,尝试回退
      console.warn('主 LLM 失败,切换到备用 LLM:', error.message);
      return await this.fallbackLLM.invoke(input, config);
    }
  }
}

总结

LangChain V3 通过结构化的错误处理机制,为开发者提供了强大的错误诊断和处理能力。通过继承自 BaseLLMError 的错误层次结构,携带 llmInputoriginalError 的丰富上下文信息,以及与 的紧密集成,开发者可以构建更加健壮和可维护的 LLM 应用。

在下一章中,我们将探讨如何利用 AbortSignal 实现请求中断,进一步提升应用的控制能力。