llm-scraper错误处理最佳实践:确保数据提取稳定性的关键技巧

llm-scraper错误处理最佳实践:确保数据提取稳定性的关键技巧

【免费下载链接】llm-scraper Turn any webpage into structured data using LLMs 【免费下载链接】llm-scraper 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-scraper

引言:网页数据提取的稳定性挑战

在当今数据驱动的世界,从网页中提取结构化数据已成为许多应用程序和业务流程的关键环节。llm-scraper作为一个创新的开源项目,利用大型语言模型(LLM)的强大能力,将任何网页转换为结构化数据。然而,网页结构的多样性、网络不稳定性、LLM输出的不确定性等因素,都可能导致数据提取过程中出现各种错误。本文将深入探讨llm-scraper的错误处理最佳实践,帮助开发者构建更稳定、可靠的数据提取系统。

读完本文,你将能够:

  • 识别llm-scraper中常见的错误类型及其根本原因
  • 掌握针对不同错误场景的处理策略和实现方法
  • 了解如何使用重试机制、超时控制和降级策略提高系统稳定性
  • 学会设计全面的错误监控和日志系统
  • 应用最佳实践优化你的数据提取流程

一、llm-scraper架构与错误来源分析

1.1 llm-scraper核心组件

llm-scraper的核心架构基于三个主要组件:

mermaid

  • LLMScraper类:提供了主要的API接口,如run()stream()generate()方法,协调整个数据提取流程。
  • Preprocessor:负责网页内容的预处理,支持多种格式转换(HTML、Markdown、文本、图片等)。
  • ModelGenerator:与LLM交互,根据预处理后的内容和指定的schema生成结构化数据。

1.2 错误来源分类

在llm-scraper的数据提取流程中,错误可能来自以下几个关键环节:

mermaid

  1. 网络与页面加载错误:包括网络连接问题、页面加载超时、HTTP错误状态码等。
  2. 内容预处理错误:如格式转换失败、自定义预处理函数错误等。
  3. LLM处理与响应错误:包括模型调用失败、响应超时、输出格式不符合预期等。
  4. 数据验证错误:当LLM返回的数据不符合指定的schema时发生。

二、网络与页面加载错误处理

2.1 常见网络错误及处理策略

网络错误是网页数据提取中最常见的问题之一。以下是几种常见的网络错误及其处理策略:

错误类型可能原因处理策略严重程度
连接超时网络拥堵、目标服务器响应慢增加超时时间、实现指数退避重试
连接拒绝目标服务器防火墙限制、IP被封禁更换IP、使用代理池、降低请求频率
DNS解析失败域名错误、DNS服务器问题验证域名、使用备用DNS服务器
HTTP 4xx错误请求参数错误、权限问题检查请求头、cookies、认证信息
HTTP 5xx错误服务器内部错误实现重试机制、错峰重试

2.2 实现健壮的页面加载错误处理

使用playwright进行页面加载时,应该实现全面的错误处理机制:

// 健壮的页面加载错误处理示例
async function safeGoto(page: Page, url: string, retries = 3, delayMs = 1000) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const response = await page.goto(url, {
        timeout: 30000, // 30秒超时
        waitUntil: 'networkidle', // 等待网络空闲
      });
      
      if (!response) {
        throw new Error('页面未返回响应');
      }
      
      const status = response.status();
      if (status >= 400) {
        throw new Error(`HTTP错误: ${status}`);
      }
      
      return response; // 成功加载,返回响应
    } catch (error) {
      console.error(`尝试 ${attempt}/${retries} 失败: ${error.message}`);
      
      // 如果是最后一次尝试,抛出错误
      if (attempt === retries) {
        throw new Error(`页面加载失败: ${error.message}`);
      }
      
      // 指数退避重试
      const backoffDelay = delayMs * Math.pow(2, attempt - 1);
      console.log(`等待 ${backoffDelay}ms 后重试...`);
      await new Promise(resolve => setTimeout(resolve, backoffDelay));
    }
  }
}

// 使用示例
const page = await browser.newPage();
try {
  await safeGoto(page, 'https://example.com');
  // 页面加载成功,继续处理
} catch (error) {
  console.error('最终页面加载失败:', error.message);
  // 实现降级策略,如使用缓存数据或跳过此URL
}

2.3 浏览器实例管理错误处理

浏览器实例的创建和管理也可能出现错误,需要妥善处理:

// 安全的浏览器和页面管理
async function withBrowser<T>(action: (browser: Browser) => Promise<T>): Promise<T> {
  let browser: Browser | null = null;
  try {
    browser = await chromium.launch({
      headless: 'new',
      timeout: 60000, // 浏览器启动超时
    });
    return await action(browser);
  } catch (error) {
    console.error('浏览器操作失败:', error.message);
    throw error; // 重新抛出以便上层处理
  } finally {
    if (browser) {
      try {
        await browser.close();
        console.log('浏览器已成功关闭');
      } catch (closeError) {
        console.error('关闭浏览器时出错:', closeError.message);
      }
    }
  }
}

// 使用示例
await withBrowser(async (browser) => {
  const page = await browser.newPage();
  try {
    await safeGoto(page, 'https://example.com');
    // 执行数据提取操作
  } finally {
    await page.close();
  }
});

三、内容预处理错误处理

3.1 预处理流程与潜在错误点

llm-scraper的预处理模块支持多种内容格式转换,每个转换过程都可能出现特定错误:

mermaid

3.2 增强preprocess函数的错误处理

原始的preprocess函数缺乏全面的错误处理,我们可以增强它以提高健壮性:

// 增强的预处理函数错误处理
export async function safePreprocess(
  page: Page,
  options: PreProcessOptions = { format: 'html' }
): Promise<PreProcessResult> {
  const url = page.url();
  let content: string;
  
  try {
    if (options.format === 'raw_html') {
      content = await page.content().catch(err => {
        throw new Error(`获取原始HTML失败: ${err.message}`);
      });
    } 
    else if (options.format === 'markdown') {
      const body = await page.innerHTML('body').catch(err => {
        throw new Error(`获取body内容失败: ${err.message}`);
      });
      
      const turndown = new Turndown();
      content = turndown.turndown(body);
      
      if (!content) throw new Error('Markdown转换结果为空');
    }
    else if (options.format === 'text') {
      const readable = await page.evaluate(async () => {
        try {
          // 使用国内CDN加载Readability库
          const module = await import('https://cdn.bootcdn.net/ajax/libs/readability/0.4.4/Readability.min.js');
          return new module.Readability(document).parse();
        } catch (err) {
          return { error: (err as Error).message };
        }
      });
      
      if (readable.error) throw new Error(`Readability解析失败: ${readable.error}`);
      if (!readable.title || !readable.textContent) {
        throw new Error('Readability返回不完整的内容');
      }
      
      content = `Page Title: ${readable.title}\n${readable.textContent}`;
    }
    else if (options.format === 'html') {
      await page.evaluate(cleanup).catch(err => {
        console.warn(`清理页面时警告: ${err.message}`);
        // 清理失败不中断,继续获取内容
      });
      content = await page.content().catch(err => {
        throw new Error(`获取清理后的HTML失败: ${err.message}`);
      });
    }
    else if (options.format === 'image') {
      const image = await page.screenshot({ 
        fullPage: options.fullPage,
        timeout: 30000 // 截图超时
      }).catch(err => {
        throw new Error(`截图失败: ${err.message}`);
      });
      content = image.toString('base64');
      
      if (!content) throw new Error('截图转换为base64失败');
    }
    else if (options.format === 'custom') {
      if (!options.formatFunction || typeof options.formatFunction !== 'function') {
        throw new Error('自定义模式下必须提供formatFunction');
      }
      
      content = await options.formatFunction(page).catch(err => {
        throw new Error(`自定义处理函数失败: ${err.message}`);
      });
      
      if (content === undefined || content === null) {
        throw new Error('自定义处理函数返回空内容');
      }
    }
    else {
      throw new Error(`不支持的格式: ${(options as any).format}`);
    }
    
    // 验证内容
    if (!content || content.trim() === '') {
      throw new Error(`预处理后内容为空 (格式: ${options.format})`);
    }
    
    return { url, content, format: options.format };
  } catch (error) {
    // 包装错误信息并重新抛出
    throw new Error(`预处理失败 (URL: ${url}): ${(error as Error).message}`);
  }
}

3.3 预处理错误的恢复策略

当预处理失败时,可以实施多种恢复策略:

// 预处理错误的恢复策略
async function preprocessWithFallback(
  page: Page,
  primaryFormat: PreProcessOptions['format'] = 'html'
): Promise<PreProcessResult> {
  // 定义格式降级顺序
  const formatFallbacks: PreProcessOptions['format'][] = [
    primaryFormat,
    'raw_html',
    'text',
    'image'
  ];
  
  // 去重处理
  const uniqueFormats = [...new Set(formatFallbacks)];
  
  for (const format of uniqueFormats) {
    try {
      console.log(`尝试使用格式预处理: ${format}`);
      return await safePreprocess(page, { format });
    } catch (error) {
      console.warn(`格式 ${format} 预处理失败: ${(error as Error).message}`);
      // 继续尝试下一个格式
    }
  }
  
  // 所有格式都失败,抛出最终错误
  throw new Error(`所有预处理格式都失败,URL: ${page.url()}`);
}

// 使用示例
try {
  const preprocessed = await preprocessWithFallback(page, 'markdown');
  console.log('预处理成功,使用格式:', preprocessed.format);
  // 继续处理
} catch (error) {
  console.error('所有预处理策略失败:', error.message);
  // 记录错误并实施进一步的降级策略
}

四、LLM处理与响应错误处理

4.1 LLM交互错误类型与处理

与LLM交互过程中可能遇到多种错误,需要针对性处理:

// LLM错误类型定义
type LLMErrorType = 
  | 'connection_error' 
  | 'timeout_error' 
  | 'rate_limit_error' 
  | 'invalid_response' 
  | 'model_error' 
  | 'authentication_error';

// LLM错误处理函数
function handleLLMError(error: Error): {
  type: LLMErrorType;
  message: string;
  retryable: boolean;
} {
  const errorMessage = error.message.toLowerCase();
  
  if (errorMessage.includes('connect') || errorMessage.includes('network')) {
    return {
      type: 'connection_error',
      message: '网络连接错误,无法连接到LLM服务',
      retryable: true
    };
  }
  
  if (errorMessage.includes('timeout')) {
    return {
      type: 'timeout_error',
      message: 'LLM请求超时',
      retryable: true
    };
  }
  
  if (errorMessage.includes('rate limit') || errorMessage.includes('quota')) {
    return {
      type: 'rate_limit_error',
      message: '已达到LLM速率限制',
      retryable: true
    };
  }
  
  if (errorMessage.includes('invalid') || errorMessage.includes('format')) {
    return {
      type: 'invalid_response',
      message: 'LLM返回了无效格式的响应',
      retryable: false
    };
  }
  
  if (errorMessage.includes('authentication') || errorMessage.includes('api key')) {
    return {
      type: 'authentication_error',
      message: 'LLM认证失败',
      retryable: false
    };
  }
  
  // 默认视为模型错误
  return {
    type: 'model_error',
    message: `LLM处理错误: ${error.message}`,
    retryable: true
  };
}

4.2 增强LLM调用的错误处理

改进generateAISDKCompletions函数,增加全面的错误处理:

// 增强的LLM调用函数
export async function safeGenerateAISDKCompletions<T>(
  model: LanguageModelV1,
  page: PreProcessResult,
  schema: z.Schema<T> | Schema<T>,
  options?: ScraperLLMOptions,
  maxRetries: number = 3
): Promise<{ data: T; url: string }> {
  const content = prepareAISDKPage(page);
  let lastError: Error | null = null;
  
  // 指数退避重试
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`LLM请求尝试 ${attempt}/${maxRetries}`);
      
      const result = await generateObject<T>({
        model,
        messages: [
          { role: 'system', content: options?.prompt || defaultPrompt },
          { role: 'user', content },
        ],
        schema,
        temperature: options?.temperature ?? 0.2, // 默认较低的随机性
        maxTokens: options?.maxTokens ?? 2048, // 设置合理的最大tokens
        topP: options?.topP,
        mode: options?.mode,
        output: options?.output,
        timeout: 60000, // 1分钟超时
      });
      
      if (!result.object) {
        throw new Error('LLM返回空结果');
      }
      
      return {
        data: result.object,
        url: page.url,
      };
    } catch (error) {
      lastError = error as Error;
      const errorInfo = handleLLMError(lastError);
      
      console.error(`LLM请求尝试 ${attempt} 失败: ${errorInfo.message}`);
      
      // 如果不可重试或达到最大重试次数,抛出错误
      if (!errorInfo.retryable || attempt === maxRetries) {
        throw new Error(`LLM处理失败 [${errorInfo.type}]: ${errorInfo.message}`);
      }
      
      // 根据错误类型计算重试延迟
      let delayMs = 1000; // 基础延迟
      
      if (errorInfo.type === 'rate_limit_error') {
        // 速率限制错误,延迟更长
        delayMs = 10000; // 10秒
      } else if (errorInfo.type === 'timeout_error') {
        // 超时错误,适度增加延迟
        delayMs = 5000; // 5秒
      }
      
      // 指数退避
      const backoffDelay = delayMs * Math.pow(2, attempt - 1);
      console.log(`等待 ${backoffDelay}ms 后重试...`);
      await new Promise(resolve => setTimeout(resolve, backoffDelay));
    }
  }
  
  // 理论上不会到达这里,因为循环中已处理所有情况
  throw new Error(`LLM请求失败,已尝试 ${maxRetries} 次: ${lastError?.message}`);
}

4.3 流式处理的错误处理

流式响应需要特殊的错误处理机制:

// 流式处理错误处理
export function safeStreamAISDKCompletions<T>(
  model: LanguageModelV1,
  page: PreProcessResult,
  schema: z.Schema<T> | Schema<T>,
  options?: ScraperLLMOptions
): { stream: AsyncIterable<T>; url: string } {
  const content = prepareAISDKPage(page);
  const url = page.url;
  
  try {
    const { partialObjectStream } = streamObject<T>({
      model,
      messages: [
        { role: 'system', content: options?.prompt || defaultPrompt },
        { role: 'user', content },
      ],
      schema,
      output: options?.output,
      temperature: options?.temperature,
      maxTokens: options?.maxTokens,
      topP: options?.topP,
      mode: options?.mode,
    });
    
    // 包装流以捕获错误
    async function* errorHandlingStream(): AsyncIterable<T> {
      try {
        for await (const chunk of partialObjectStream) {
          if (chunk.error) {
            console.error(`流处理错误: ${chunk.error}`);
            continue; // 跳过错误块,继续处理后续数据
          }
          yield chunk;
        }
      } catch (error) {
        console.error(`流式处理失败: ${(error as Error).message}`);
        // 可以选择抛出错误或返回部分结果
        // throw error;
      }
    }
    
    return {
      stream: errorHandlingStream(),
      url,
    };
  } catch (error) {
    // 初始设置流时的错误
    throw new Error(`初始化流式处理失败: ${(error as Error).message}`);
  }
}

五、数据验证错误处理

5.1 模式验证错误的处理策略

当LLM返回的数据不符合Zod schema时,需要有效的处理策略:

// 数据验证错误处理
async function validateWithRetry<T>(
  data: unknown,
  schema: z.Schema<T>,
  maxRetries: number = 2,
  retryDelayMs: number = 1000
): Promise<T> {
  try {
    // 首次验证
    return schema.parse(data);
  } catch (error) {
    if (!(error instanceof z.ZodError)) {
      throw new Error(`数据验证非Zod错误: ${(error as Error).message}`);
    }
    
    // 记录详细的验证错误
    console.error('Zod验证错误详情:');
    error.errors.forEach((err, index) => {
      console.error(`${index + 1}. ${err.path.join('.')}: ${err.message}`);
    });
    
    // 如果没有重试次数,抛出格式化的错误
    if (maxRetries <= 0) {
      const errorMessage = error.errors.map(
        err => `${err.path.join('.')}: ${err.message}`
      ).join('; ');
      
      throw new Error(`数据验证失败: ${errorMessage}`);
    }
    
    // 如果有重试次数,等待后返回原始数据(供上层处理重试)
    console.log(`数据验证失败,将在 ${retryDelayMs}ms 后重试...`);
    await new Promise(resolve => setTimeout(resolve, retryDelayMs));
    
    // 返回原始数据,由上层决定是否重试LLM调用
    throw new Error(`数据验证失败,需要重试: ${error.message}`);
  }
}

// 集成到LLM调用流程
async function generateAndValidate<T>(
  model: LanguageModelV1,
  page: PreProcessResult,
  schema: z.Schema<T>,
  options?: ScraperLLMOptions
): Promise<{ data: T; url: string }> {
  const maxValidationRetries = 2; // 验证失败后的LLM重试次数
  let lastValidationError: Error | null = null;
  
  for (let attempt = 1; attempt <= maxValidationRetries; attempt++) {
    try {
      // 获取LLM响应
      const llmResult = await safeGenerateAISDKCompletions(
        model, page, schema, options
      );
      
      // 严格验证
      const validatedData = await validateWithRetry(
        llmResult.data, schema, 0 // 这里不重试验证,由外部循环处理
      );
      
      return { data: validatedData, url: page.url };
    } catch (error) {
      lastValidationError = error as Error;
      
      // 检查是否是验证错误
      if ((error as Error).message.includes('数据验证失败')) {
        console.log(`数据验证失败,尝试 ${attempt}/${maxValidationRetries}`);
        
        // 如果还有重试次数,继续循环
        if (attempt < maxValidationRetries) {
          console.log('将重新调用LLM生成数据...');
          continue;
        }
      }
      
      // 其他类型的错误,直接抛出
      throw error;
    }
  }
  
  throw lastValidationError!;
}

5.2 模糊验证与部分提取策略

对于复杂数据,有时需要接受部分有效数据:

// 部分数据提取策略
function extractPartialData<T>(
  data: unknown,
  schema: z.Schema<T>
): Partial<T> | null {
  try {
    // 创建一个宽松的schema版本,所有字段设为可选
    const partialSchema = schema instanceof z.ZodObject 
      ? schema.partial() 
      : schema;
    
    // 使用safeParse而非parse,避免抛出错误
    const result = partialSchema.safeParse(data);
    
    if (result.success) {
      return result.data as Partial<T>;
    }
    
    console.error('部分数据提取失败:', result.error.message);
    return null;
  } catch (error) {
    console.error('部分数据提取错误:', (error as Error).message);
    return null;
  }
}

// 使用示例
try {
  // 尝试严格验证
  const validatedData = await validateWithRetry(data, schema, 0);
  return validatedData;
} catch (error) {
  console.warn('严格验证失败,尝试部分提取...');
  
  // 尝试提取部分数据
  const partialData = extractPartialData(data, schema);
  
  if (partialData && Object.keys(partialData).length > 0) {
    console.log('成功提取部分数据:', Object.keys(partialData));
    // 可以记录部分提取并继续处理
    return partialData as T; // 注意:类型断言可能不安全,根据实际情况处理
  }
  
  // 部分提取也失败
  throw new Error(`完全验证和部分提取均失败: ${(error as Error).message}`);
}

5.3 动态调整提示词以修复验证错误

当验证失败时,可以动态调整提示词引导LLM生成正确格式的数据:

// 动态调整提示词处理验证错误
async function generateWithDynamicPrompt<T>(
  model: LanguageModelV1,
  page: PreProcessResult,
  schema: z.Schema<T>,
  initialOptions?: ScraperLLMOptions
): Promise<{ data: T; url: string }> {
  const baseOptions = initialOptions || {};
  const maxAttempts = 3;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      // 根据尝试次数调整选项
      const options = {
        ...baseOptions,
        // 随着尝试次数增加,降低随机性
        temperature: baseOptions.temperature !== undefined 
          ? Math.max(0, baseOptions.temperature - (attempt - 1) * 0.2)
          : 0.2 - (attempt - 1) * 0.1,
      };
      
      // 获取LLM响应
      const llmResult = await safeGenerateAISDKCompletions(
        model, page, schema, options
      );
      
      // 验证结果
      return {
        data: schema.parse(llmResult.data),
        url: page.url
      };
    } catch (error) {
      // 如果不是验证错误或达到最大尝试次数,抛出错误
      if (attempt === maxAttempts || !(error instanceof z.ZodError)) {
        throw error;
      }
      
      // 构建错误提示
      const errorDetails = (error as z.ZodError).errors
        .map(err => `- ${err.path.join('.')}: ${err.message}`)
        .join('\n');
      
      console.log(`验证失败,调整提示词后重试 (尝试 ${attempt}/${maxAttempts})`);
      
      // 构建新的提示词
      const errorPrompt = `
        Your previous response did not match the required schema. 
        Error details:
        ${errorDetails}
        
        Please correct these errors and provide ONLY the requested JSON data without additional explanations.
        Ensure all required fields are present and of the correct type.
      `;
      
      // 更新基础选项以包含错误提示
      baseOptions.prompt = baseOptions.prompt 
        ? `${baseOptions.prompt}\n\n${errorPrompt}` 
        : `${defaultPrompt}\n\n${errorPrompt}`;
    }
  }
  
  throw new Error(`达到最大尝试次数 ${maxAttempts}`);
}

六、综合错误处理与监控

6.1 构建健壮的scraper调用封装

将所有错误处理机制整合到一个高级封装函数中:

// 高级scraper封装
async function robustScrape<T>(
  browser: Browser,
  model: LanguageModelV1,
  url: string,
  schema: z.Schema<T>,
  options: {
    preprocessOptions?: PreProcessOptions;
    llmOptions?: ScraperLLMOptions;
    maxRetries?: number;
  } = {}
): Promise<{
  data: T | Partial<T>;
  url: string;
  success: boolean;
  errors?: string[];
  formatUsed?: string;
}> {
  const result: {
    data: T | Partial<T> | null;
    url: string;
    success: boolean;
    errors: string[];
    formatUsed?: string;
  } = {
    data: null,
    url,
    success: false,
    errors: [],
  };
  
  let page: Page | null = null;
  
  try {
    // 1. 创建页面
    page = await browser.newPage();
    result.url = url;
    
    // 2. 加载页面(带重试)
    await safeGoto(page, url, options.maxRetries || 3);
    
    // 3. 预处理内容(带格式降级)
    const preprocessed = await preprocessWithFallback(
      page, 
      options.preprocessOptions?.format || 'html'
    );
    result.formatUsed = preprocessed.format;
    
    // 4. 生成并验证数据
    const llmResult = await generateWithDynamicPrompt(
      model, 
      preprocessed, 
      schema, 
      options.llmOptions
    );
    
    // 5. 成功处理
    result.data = llmResult.data;
    result.success = true;
    
    return result;
  } catch (error) {
    // 记录错误信息
    result.errors.push((error as Error).message);
    
    // 如果有部分数据,尝试返回
    if (result.data) {
      console.warn('返回部分成功的数据,尽管发生错误');
      result.success = false; // 部分成功仍视为失败
      return result;
    }
    
    // 完全失败,尝试提取页面基本信息作为最后的回退
    if (page) {
      try {
        const basicInfo = await page.evaluate(() => ({
          title: document.title,
          url: window.location.href,
          timestamp: new Date().toISOString(),
        }));
        
        result.data = { basicInfo } as unknown as Partial<T>;
        console.warn('提取了基本页面信息作为最后的回退');
      } catch (fallbackError) {
        result.errors.push(`回退提取失败: ${(fallbackError as Error).message}`);
      }
    }
    
    return result;
  } finally {
    // 确保页面关闭
    if (page) {
      try {
        await page.close();
      } catch (closeError) {
        result.errors.push(`关闭页面时出错: ${(closeError as Error).message}`);
      }
    }
    
    // 记录结果状态
    if (result.success) {
      console.log(`成功处理: ${url}`);
    } else {
      console.error(`处理失败: ${url}, 错误: ${result.errors.join('; ')}`);
    }
  }
}

6.2 错误监控与报告系统

实现一个简单但有效的错误监控系统:

// 错误监控系统
class ScraperMonitor {
  private errorStats: Record<string, number> = {};
  private successCount = 0;
  private failureCount = 0;
  private startTime: number;
  
  constructor() {
    this.startTime = Date.now();
    // 定期输出统计
    setInterval(() => this.logStats(), 5 * 60 * 1000); // 每5分钟
  }
  
  // 记录成功
  recordSuccess(url: string): void {
    this.successCount++;
    console.log(`✅ 成功: ${url}`);
  }
  
  // 记录失败
  recordFailure(url: string, error: Error | string): void {
    this.failureCount++;
    
    // 提取错误类型
    const errorMessage = typeof error === 'string' ? error : error.message;
    let errorType = 'unknown';
    
    if (errorMessage.includes('预处理失败')) errorType = 'preprocess';
    else if (errorMessage.includes('LLM处理失败')) errorType = 'llm';
    else if (errorMessage.includes('数据验证失败')) errorType = 'validation';
    else if (errorMessage.includes('页面加载失败')) errorType = 'network';
    
    // 更新错误统计
    this.errorStats[errorType] = (this.errorStats[errorType] || 0) + 1;
    
    // 详细日志
    console.error(`❌ 失败 [${errorType}]: ${url} - ${errorMessage}`);
  }
  
  // 记录部分成功
  recordPartialSuccess(url: string, errors: string[]): void {
    this.failureCount++; // 部分成功仍视为失败
    console.warn(`⚠️ 部分成功: ${url} - 错误: ${errors.join('; ')}`);
  }
  
  // 输出统计信息
  logStats(): void {
    const total = this.successCount + this.failureCount;
    const durationHours = (Date.now() - this.startTime) / (1000 * 60 * 60);
    const rate = total / durationHours;
    
    console.log('\n===================== 抓取统计 =====================');
    console.log(`总处理: ${total} 个URL (${rate.toFixed(2)}/小时)`);
    console.log(`成功: ${this.successCount} (${(this.successCount/total*100).toFixed(1)}%)`);
    console.log(`失败: ${this.failureCount} (${(this.failureCount/total*100).toFixed(1)}%)`);
    
    if (this.failureCount > 0) {
      console.log('\n错误类型分布:');
      Object.entries(this.errorStats).forEach(([type, count]) => {
        console.log(`- ${type}: ${count} (${(count/this.failureCount*100).toFixed(1)}%)`);
      });
    }
    console.log('===================================================\n');
  }
  
  // 获取当前统计
  getStats(): {
    total: number;
    success: number;
    failure: number;
    errorStats: Record<string, number>;
    durationHours: number;
  } {
    const total = this.successCount + this.failureCount;
    const durationHours = (Date.now() - this.startTime) / (1000 * 60 * 60);
    
    return {
      total,
      success: this.successCount,
      failure: this.failureCount,
      errorStats: { ...this.errorStats },
      durationHours
    };
  }
}

// 使用示例
const monitor = new ScraperMonitor();

// 在scraper调用中集成
try {
  const result = await robustScrape(browser, model, url, schema);
  
  if (result.success) {
    monitor.recordSuccess(url);
    // 处理成功数据
  } else if (result.data) {
    monitor.recordPartialSuccess(url, result.errors);
    // 处理部分成功数据
  } else {
    monitor.recordFailure(url, result.errors.join('; '));
    // 处理完全失败
  }
} catch (error) {
  monitor.recordFailure(url, error as Error);
}

6.3 完整的生产级错误处理工作流

整合所有错误处理策略,形成完整的工作流:

mermaid

七、最佳实践总结与案例分析

7.1 错误处理最佳实践清单

以下是llm-scraper错误处理的关键最佳实践:

  1. 多层次防御策略

    • 实施"防御性编程"理念,在每个操作前验证前提条件
    • 为每个子系统(网络、预处理、LLM、验证)设置独立的错误处理
    • 使用"快速失败"原则,及早发现并处理错误
  2. 智能重试机制

    • 对网络错误使用指数退避重试策略
    • 根据错误类型决定是否重试(如404错误不应重试)
    • 限制总重试次数,避免无限循环
  3. 优雅降级策略

    • 实现格式降级链(如markdown→html→text→image)
    • 验证失败时尝试部分数据提取
    • 完全失败时返回基本页面元数据
  4. 错误监控与分析

    • 记录详细的错误类型和上下文
    • 实现实时错误统计和监控
    • 定期分析错误模式,持续改进系统
  5. 用户体验优化

    • 提供清晰、具体的错误信息
    • 展示处理进度和成功率
    • 对部分成功和完全失败提供明确指示

7.2 案例分析:从失败中学习

案例1:频繁的LLM超时错误

问题:在处理一批URL时,约30%的请求出现LLM超时错误。

排查过程:

  1. 查看监控发现超时错误集中在特定时间段(高峰期)
  2. 分析日志发现超时错误与模型响应长度正相关
  3. 检查网络状况,发现高峰期网络延迟增加

解决方案:

  • 增加LLM请求超时时间(从30秒到60秒)
  • 实现动态批处理,高峰期减少并发请求数
  • 对长响应内容实施分块处理
  • 增加缓存层,缓存常见URL的结果

改进效果:超时错误率从30%降至5%以下。

案例2:数据验证失败频发

问题:LLM返回的数据经常不符合Zod schema,验证失败率高达40%。

排查过程:

  1. 分析错误日志,发现主要是特定字段缺失或类型错误
  2. 检查提示词,发现对字段格式描述不够明确
  3. 测试不同模型,发现某些模型对复杂schema支持较差

解决方案:

  • 改进提示词,增加对关键字段的明确描述和示例
  • 实现动态提示词调整,验证失败后自动添加错误反馈
  • 对复杂schema实施分步验证和提取
  • 根据schema复杂度自动选择更适合的模型

改进效果:验证失败率从40%降至15%以下。

7.3 未来趋势与持续改进

随着LLM技术的发展,错误处理策略也需要不断演进:

  1. 自适应错误处理:利用机器学习分析错误模式,自动调整处理策略
  2. 多模型协作:结合不同模型的优势,当一个模型失败时自动切换到另一个模型
  3. 增强型提示工程:开发更智能的提示词生成策略,减少验证错误
  4. 实时监控与预警:建立实时错误监控系统,及时发现和解决系统性问题
  5. 用户反馈循环:收集用户对错误处理的反馈,持续优化降级策略

八、结论与下一步行动

llm-scraper的错误处理是确保数据提取稳定性的关键环节。通过实施本文介绍的最佳实践,你可以显著提高系统的健壮性和可靠性,即使在复杂和不稳定的网络环境中也能获得高质量的数据。

下一步行动建议

  1. 审计现有代码,识别错误处理薄弱环节
  2. 优先实施网络和预处理错误处理机制
  3. 集成错误监控系统,建立错误基线
  4. 针对常见错误类型,开发专门的处理策略
  5. 定期分析错误数据,持续优化错误处理流程

记住,优秀的错误处理不仅能提高系统稳定性,还能提供宝贵的 insights,帮助你理解系统的弱点并持续改进。在数据提取的世界里,预料之外的情况是常态,而完善的错误处理机制正是应对这些挑战的关键。

如果你觉得这篇文章有帮助,请点赞、收藏并关注,以便获取更多关于llm-scraper的高级使用技巧和最佳实践!

【免费下载链接】llm-scraper Turn any webpage into structured data using LLMs 【免费下载链接】llm-scraper 项目地址: https://gitcode.com/GitHub_Trending/ll/llm-scraper

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值