错误处理:Runnable 如何抛出结构化错误?
在构建复杂的 LLM 应用时,错误处理是一个至关重要的方面。LangChain V3 通过结构化的错误处理机制,使得开发者能够更好地理解、调试和处理在执行链中可能出现的各种错误情况。本章将深入探讨 LangChain V3 中的错误处理设计和实践。
错误处理的重要性
在 LLM 应用中,错误处理比传统应用程序更加复杂,原因包括:
- 网络依赖 - 大多数 LLM 调用都依赖网络,容易出现超时、连接失败等问题
- 模型不确定性 - LLM 的输出具有不确定性,可能导致解析错误
- 第三方服务 - 通常依赖多个第三方服务,每个都可能出错
- 资源限制 - 可能受到速率限制、配额限制等约束
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 的错误层次结构,携带 llmInput 和 originalError 的丰富上下文信息,以及与 的紧密集成,开发者可以构建更加健壮和可维护的 LLM 应用。
在下一章中,我们将探讨如何利用 AbortSignal 实现请求中断,进一步提升应用的控制能力。