You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用OpenAI+Pinecone构建YouTube语义搜索遇429速率限制错误求助

问题描述
  • 基于OpenAI + Pinecone实现YouTube视频语义搜索,执行命令npx tsx src/bin/process-yt-playlist.ts预处理字幕、获取嵌入向量并插入Pinecone索引时,持续触发OpenAI API的429请求超限错误。
  • 已尝试大幅增加API调用间隔,问题仍未解决,相关代码如下:
import pMap from 'p-map'
import unescape from 'unescape'

import * as config from '@/lib/config'

import * as types from './types'

import pMemoize from 'p-memoize'
import pRetry from 'p-retry'
import pThrottle from 'p-throttle'

// TODO: enforce max OPENAI_EMBEDDING_CTX_LENGTH of 8191

// https://platform.openai.com/docs/guides/rate-limits/what-are-the-rate-limits-for-our-api
// TODO: enforce TPM
const throttleRPM = pThrottle({
  // 3k per minute instead of 3.5k per minute to add padding
  limit: 3000,
  interval: 60 * 1000,
  strict: true
})

type PineconeCaptionVectorPending = {
  id: string
  input: string
  metadata: types.PineconeCaptionMetadata
}

export async function getEmbeddingsForVideoTranscript({
  transcript,
  title,
  openai,
  model = config.openaiEmbeddingModel,
  maxInputTokens = 100, // TODO???
  concurrency = 1
}: {
  transcript: types.Transcript
  title: string
  openai: types.OpenAIApi
  model?: string
  maxInputTokens?: number
  concurrency?: number
}) {
  const { videoId } = transcript

  let pendingVectors: PineconeCaptionVectorPending[] = []
  let currentStart = ''
  let currentNumTokensEstimate = 0
  let currentInput = ''
  let currentPartIndex = 0
  let currentVectorIndex = 0
  let isDone = false

  // const createEmbedding = pMemoize(throttleRPM(createEmbeddingImpl))

  // Pre-compute the embedding inputs, making sure none of them are too long
  do {
    isDone = currentPartIndex >= transcript.parts.length

    const part = transcript.parts[currentPartIndex]
    const text = unescape(part?.text)
      .replaceAll('[Music]', '')
      .replaceAll(/[\t\n]/g, ' ')
      .replaceAll('  ', ' ')
      .trim()
    const numTokens = getNumTokensEstimate(text)

    if (!isDone && currentNumTokensEstimate + numTokens < maxInputTokens) {
      if (!currentStart) {
        currentStart = part.start
      }

      currentNumTokensEstimate += numTokens
      currentInput = `${currentInput} ${text}`

      ++currentPartIndex
    } else {
      currentInput = currentInput.trim()
      if (isDone && !currentInput) {
        break
      }

      const currentVector: PineconeCaptionVectorPending = {
        id: `${videoId}:${currentVectorIndex++}`,
        input: currentInput,
        metadata: {
          title,
          videoId,
          text: currentInput,
          start: currentStart
        }
      }

      pendingVectors.push(currentVector)

      // reset current batch
      currentNumTokensEstimate = 0
      currentStart = ''
      currentInput = ''
    }
  } while (!isDone)
  let index = 0;

  console.log("Entering embeddings calculation")
  // Evaluate all embeddings with a max concurrency
  // const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  const vectors: types.PineconeCaptionVector[] = await pMap(
    pendingVectors,
    async (pendingVector) => {
      // await delay(6000); // add a delay of 1 second before each iteration
      console.log(pendingVector.input + " " + model)


      // const { data: embed } = await openai.createEmbedding({
      //   input: pendingVector.input,
      //   model
      // })

      async function createEmbeddingImpl({
        input = pendingVector.input,
        model = 'text-embedding-ada-002'
      }: {
        input: string
        model?: string
      }): Promise<number[]> {
        const res = await pRetry(
          () =>
            openai.createEmbedding({
              input,
              model
            }),
          {
            retries: 4,
            minTimeout: 1000,
            factor: 2.5
          }
        )
      
        return res.data.data[0].embedding
      }

      const embedding = await pMemoize(throttleRPM(createEmbeddingImpl));
      

      const vector: types.PineconeCaptionVector = {
        id: pendingVector.id,
        metadata: pendingVector.metadata,
        values: await embedding(pendingVector)
      }
      console.log(index + " THIS IS THE NUMBER OF CALLS TO OPENAI Embedding: " + embedding)
      index++;
      return vector
    },
    {
      concurrency
    }
  )

  return vectors
}

function getNumTokensEstimate(input: string): number {
  const numTokens = (input || '')
    .split(/\s/)
    .map((token) => token.trim())
    .filter(Boolean).length

  return numTokens
}
问题分析与解决方案

你的代码节流、重试逻辑存在核心问题,导致429错误无法缓解,以下是针对性修复步骤:

1. 修复节流逻辑的作用域错误

你在pMap循环内部每次创建新的createEmbeddingImpl和embedding实例,导致节流器throttleRPM无法跨请求共享状态,等于没生效。把节流、重试、memoize逻辑移到函数外部,确保所有请求共用同一个限制实例:

// 移到函数外部,全局复用
async function createEmbeddingImpl({
  input,
  model = 'text-embedding-ada-002',
  openai
}: {
  input: string
  model?: string
  openai: types.OpenAIApi
}): Promise<number[]> {
  const res = await pRetry(
    () =>
      openai.createEmbedding({
        input,
        model
      }),
    {
      retries: 4,
      minTimeout: 1000,
      factor: 2.5,
      // 新增失败日志,方便排查
      onFailedAttempt: (error) => {
        console.log(`请求失败,重试次数 ${error.attemptNumber},原因:${error.message}`);
      }
    }
  )

  return res.data.data[0].embedding
}

// 全局节流实例,确保所有请求共享RPM限制
const throttledCreateEmbedding = throttleRPM(createEmbeddingImpl);
const memoizedEmbedding = pMemoize(throttledCreateEmbedding);

然后在pMap回调中直接调用这个全局函数:

const vectors: types.PineconeCaptionVector[] = await pMap(
  pendingVectors,
  async (pendingVector) => {
    console.log(pendingVector.input + " " + model)
    // 直接调用全局的节流+缓存函数
    const embedding = await memoizedEmbedding({
      input: pendingVector.input,
      model,
      openai
    });
    console.log(`${index} 已完成OpenAI嵌入调用`)
    index++;
    return {
      id: pendingVector.id,
      metadata: pendingVector.metadata,
      values: embedding
    }
  },
  {
    concurrency: 1 // 保持并发为1,避免叠加请求突破限制
  }
)

2. 修复令牌数估算偏差

你的getNumTokensEstimate用空格分割单词估算令牌数,和OpenAI实际计算方式偏差极大,可能导致单请求令牌数超过TPM(每分钟令牌数)限制触发429。改用官方tiktoken库准确计算:

首先安装依赖:

npm install tiktoken

然后修改估算函数:

import { encoding_for_model } from 'tiktoken';

function getNumTokensEstimate(input: string, model: string = 'text-embedding-ada-002'): number {
  const encoding = encoding_for_model(model);
  const numTokens = encoding.encode(input).length;
  encoding.free();
  return numTokens;
}

3. 额外排查点

  • 登录OpenAI控制台确认API密钥的配额(RPM、TPM)是否足够,若配额本身不足,节流也无法解决问题;
  • 检查是否有其他进程同时使用同一API密钥发起请求,导致总请求量超限。

最终整合后的完整代码

import pMap from 'p-map'
import unescape from 'unescape'
import { encoding_for_model } from 'tiktoken';

import * as config from '@/lib/config'
import * as types from './types'
import pMemoize from 'p-memoize'
import pRetry from 'p-retry'
import pThrottle from 'p-throttle'

// TODO: enforce max OPENAI_EMBEDDING_CTX_LENGTH of 8191

// https://platform.openai.com/docs/guides/rate-limits/what-are-the-rate-limits-for-our-api
// TODO: enforce TPM
const throttleRPM = pThrottle({
  // 3k per minute instead of 3.5k per minute to add padding
  limit: 3000,
  interval: 60 * 1000,
  strict: true
})

async function createEmbeddingImpl({
  input,
  model = 'text-embedding-ada-002',
  openai
}: {
  input: string
  model?: string
  openai: types.OpenAIApi
}): Promise<number[]> {
  const res = await pRetry(
    () =>
      openai.createEmbedding({
        input,
        model
      }),
    {
      retries: 4,
      minTimeout: 1000,
      factor: 2.5,
      onFailedAttempt: (error) => {
        console.log(`请求失败,重试次数 ${error.attemptNumber},原因:${error.message}`);
      }
    }
  )

  return res.data.data[0].embedding
}

const throttledCreateEmbedding = throttleRPM(createEmbeddingImpl);
const memoizedEmbedding = pMemoize(throttledCreateEmbedding);

type PineconeCaptionVectorPending = {
  id: string
  input: string
  metadata: types.PineconeCaptionMetadata
}

export async function getEmbeddingsForVideoTranscript({
  transcript,
  title,
  openai,
  model = config.openaiEmbeddingModel,
  maxInputTokens = 100,
  concurrency = 1
}: {
  transcript: types.Transcript
  title: string
  openai: types.OpenAIApi
  model?: string
  maxInputTokens?: number
  concurrency?: number
}) {
  const { videoId } = transcript

  let pendingVectors: PineconeCaptionVectorPending[] = []
  let currentStart = ''
  let currentNumTokensEstimate = 0
  let currentInput = ''
  let currentPartIndex = 0
  let currentVectorIndex = 0
  let isDone = false

  // Pre-compute the embedding inputs, making sure none of them are too long
  do {
    isDone = currentPartIndex >= transcript.parts.length

    const part = transcript.parts[currentPartIndex]
    const text = unescape(part?.text)
      .replaceAll('[Music]', '')
      .replaceAll(/[\t\n]/g, ' ')
      .replaceAll('  ', ' ')
      .trim()
    const numTokens = getNumTokensEstimate(text, model)

    if (!isDone && currentNumTokensEstimate + numTokens < maxInputTokens) {
      if (!currentStart) {
        currentStart = part.start
      }

      currentNumTokensEstimate += numTokens
      currentInput = `${currentInput} ${text}`

      ++currentPartIndex
    } else {
      currentInput = currentInput.trim()
      if (isDone && !currentInput) {
        break
      }

      const currentVector: PineconeCaptionVectorPending = {
        id: `${videoId}:${currentVectorIndex++}`,
        input: currentInput,
        metadata: {
          title,
          videoId,
          text: currentInput,
          start: currentStart
        }
      }

      pendingVectors.push(currentVector)

      // reset current batch
      currentNumTokensEstimate = 0
      currentStart = ''
      currentInput = ''
    }
  } while (!isDone)
  let index = 0;

  console.log("Entering embeddings calculation")
  const vectors: types.PineconeCaptionVector[] = await pMap(
    pendingVectors,
    async (pendingVector) => {
      console.log(pendingVector.input + " " + model)
      const embedding = await memoizedEmbedding({
        input: pendingVector.input,
        model,
        openai
      });
      console.log(`${index} 已完成OpenAI嵌入调用`)
      index++;
      return {
        id: pendingVector.id,
        metadata: pendingVector.metadata,
        values: embedding
      }
    },
    {
      concurrency
    }
  )

  return vectors
}

function getNumTokensEstimate(input: string, model: string = 'text-embedding-ada-002'): number {
  const encoding = encoding_for_model(model);
  const numTokens = encoding.encode(input).length;
  encoding.free();
  return numTokens;
}

内容的提问来源于stack exchange,提问作者Helpinghand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 16:27:59