LLM 调用从 60s 卡死降到 3s!彻底绕过 tiktoken 网络阻塞(LangChain.js 必看)

0 阅读4分钟

前言(痛点暴击)

你是不是也遇到过:

  • LLM 接口本身明明很快(2~3s)
  • 一用上 LangChain + OpenAI 兼容接口
  • 国内环境直接30s~60s+ 超时
  • 服务一重启就集体卡死
  • 日志疯狂报 tiktoken 连接超时?

导致用户体验极差,接口响应时间从预期的 3-5 秒膨胀到 30-60 秒。

WX20240826-175445@2x.png

现象:重启即炸,超时拉满

典型症状

  1. 服务首次启动后,LLM 调用正常(2-3 秒返回)
  2. 服务重启后:所有 LLM 调用集体超时
  3. 响应时间从预期 3~5s → 暴冲到 30~60s+
  4. 控制台疯狂报错:(出现大量 tiktoken 连接超时错误)
Failed to calculate number of tokens, falling back to approximate count TypeError: fetch failed
  [cause]: ConnectTimeoutError: Connect Timeout Error
    (attempted addresses: ***.**.**.**:443, ***.**.**.***:443, timeout: 10000ms)
    code: 'UND_ERR_CONNECT_TIMEOUT'

如果你用的是:

  • @langchain/openai
  • ChatOpenAI
  • 国内网络 / 无代理 / 代理不稳定

那这篇文章就是为你写的

WX20240826-180254@2x.png

根因:90% 的人都不知道的 LangChain 暗坑

谁在偷偷拖慢你?—— tiktoken

tiktoken 是 OpenAI 官方分词器,用来精确计算 token 数量

问题出在这里:LangChain.js 在每次 llm.invoke() 时,都会强制调用 tiktoken 计算 token!

超时链路(一图看懂)

llm.invoke(prompt)
  -> LangChain 内部调用 getNumTokens()
    -> js-tiktoken 需要加载编码数据
      -> 从 https://tiktoken.pages.dev/js/{encoding}.json 下载
        -> 该域名托管在 Cloudflare,国内网络不稳定/不可达
          -> p-retry 库重试 3-4 次,每次 10s 超时
            -> 总计阻塞 30-40-> 最终 fallback 到近似计算,但时间已浪费

为什么重启才炸?

  • tiktoken 编码数据首次下载成功后,缓存在 Node.js 进程内存
  • 服务运行期间,后续调用直接使用内存缓存,不再下载
  • 服务重启后,内存缓存清空,tiktoken 重新尝试下载
  • 如果此时网络不通(Cloudflare 在国内不稳定),就会反复超时

时间线示例(来自实际日志)

时间事件结果
16:43:20LLM 意图分析成功(tiktoken 已缓存)
16:45:41LLM 意图分析成功(tiktoken 已缓存)
16:49:50LLM 意图分析成功(tiktoken 已缓存)
17:09:10服务重启tiktoken 缓存丢失
17:09:17LLM 意图分析超时(tiktoken 下载失败)
17:16:15LLM 意图分析超时
17:19:14LLM 意图分析超时
...后续所有调用全部超时

这就是典型的:LLM 本身不慢,tiktoken 慢到你怀疑人生。

WX20240826-180009@2x.png

方案:直接绕开!不依赖代理、不改环境

核心思路

抛弃 LangChain 调用层,直接用 fetch 调用 LLM API。

  • 绕开 tiktoken
  • 绕开 LangChain 内部逻辑
  • 只保留最核心的 chat completions
  • 响应时间直接回到 2~3s

最终代码(可直接复制使用)

为方便大家快速理解,当前已关闭流式输出(SSE) 。若需使用该功能,可在现有基础上调整实现。

src/services/llm.service.ts 中新增 invokeLLM 函数:

import { config } from '../config';

/**
 * 直接调用 LLM(绕过 LangChain & tiktoken 阻塞)
 * @param prompt 用户提示词
 * @param options 温度、超时等配置
 * @returns 模型返回内容
 */
export async function invokeLLM(
  prompt: string,
  options?: { temperature?: number; timeout?: number }
): Promise<string> {
  const { baseUrl, apiKey, model, temperature: defaultTemp } = config.llm;
  const url = `${baseUrl}/chat/completions`;

  const controller = new AbortController();
  const timeoutId = setTimeout(
    () => controller.abort(),
    options?.timeout || 10000
  );

  try {
    const res = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        model,
        temperature: options?.temperature ?? defaultTemp,
        messages: [{ role: 'user', content: prompt }],
      }),
      signal: controller.signal,
    });

    if (!res.ok) {
      throw new Error(`LLM 接口异常 ${res.status}`);
    }

    const data = await res.json();
    return data.choices?.[0]?.message?.content || '';
  } catch (err: any) {
    if (err.name === 'AbortError') {
      throw new Error('LLM 调用超时');
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}

使用对比

改造前(LangChain,受 tiktoken 阻塞,会超时):

const llm = getLLM();
const response = await llm.invoke(prompt);

改造后(直接 fetch,不受 tiktoken 影响,飞一般的速度)

const content = await invokeLLM(prompt);

效果对比(肉眼可见的提升)

表格

指标改造前(LangChain)改造后(直接 fetch)
意图分析30~60s+ / 超时2~3s
消息预处理30~40s2~3s
对话回复30~40s2~5s
整体接口基本不可用稳定流畅

一句话总结:不可用直接拉满到生产可用

其他方案(为什么我不推荐)

1、预下载 tiktoken 文件

  • 优点:保留精确 token 计算
  • 缺点:要维护本地文件、升级易炸、麻烦

2、走代理

  • 优点:不用改代码
  • 缺点:部署复杂、线上环境不一定允许、不稳定

3、加大超时到 60s

  • 优点:最简单
  • 缺点:用户直接跑光

4、最优方案:绕开 tiktoken(本方案)

  • 零依赖
  • 不改环境
  • 不搞代理
  • 上线即生效
  • 速度直接拉满

WX20240826-180523@2x.png

注意事项(生产必看)

  1. 此方法不支持流式输出,需要流式再单独封装 SSE。(这个也简单)
  2. 不计算 token,如需计费 / 限流,要自己实现轻量分词。(几乎不用)
  3. 需要 tool calling / 复杂 Agent 场景,仍可保留 LangChain。(酌情处理)
  4. 兼容所有 OpenAI 格式接口:自建 / 第三方 / 阿里 / 百度 / 字节 等。(几乎也能满足)

总结

很多时候 LLM 慢不是模型慢,而是你用的库在偷偷做你不需要的事情

tiktoken 这个坑,国内 90% 使用 LangChain.js 的团队都踩过。希望这篇实战优化,能帮你把 LLM 接口从超时重灾区拉回秒级体验