前言(痛点暴击)
你是不是也遇到过:
- LLM 接口本身明明很快(2~3s)
- 一用上 LangChain + OpenAI 兼容接口
- 国内环境直接30s~60s+ 超时
- 服务一重启就集体卡死
- 日志疯狂报
tiktoken连接超时?
导致用户体验极差,接口响应时间从预期的 3-5 秒膨胀到 30-60 秒。
现象:重启即炸,超时拉满
典型症状
- 服务首次启动后,LLM 调用正常(2-3 秒返回)
- 服务重启后:所有 LLM 调用集体超时
- 响应时间从预期 3~5s → 暴冲到 30~60s+
- 控制台疯狂报错:(出现大量 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/openaiChatOpenAI- 国内网络 / 无代理 / 代理不稳定
那这篇文章就是为你写的。
根因: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:20 | LLM 意图分析 | 成功(tiktoken 已缓存) |
| 16:45:41 | LLM 意图分析 | 成功(tiktoken 已缓存) |
| 16:49:50 | LLM 意图分析 | 成功(tiktoken 已缓存) |
| 17:09:10 | 服务重启 | tiktoken 缓存丢失 |
| 17:09:17 | LLM 意图分析 | 超时(tiktoken 下载失败) |
| 17:16:15 | LLM 意图分析 | 超时 |
| 17:19:14 | LLM 意图分析 | 超时 |
| ... | 后续所有调用 | 全部超时 |
这就是典型的:LLM 本身不慢,tiktoken 慢到你怀疑人生。
方案:直接绕开!不依赖代理、不改环境
核心思路
抛弃 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~40s | 2~3s |
| 对话回复 | 30~40s | 2~5s |
| 整体接口 | 基本不可用 | 稳定流畅 |
一句话总结:从不可用直接拉满到生产可用。
其他方案(为什么我不推荐)
1、预下载 tiktoken 文件
- 优点:保留精确 token 计算
- 缺点:要维护本地文件、升级易炸、麻烦
2、走代理
- 优点:不用改代码
- 缺点:部署复杂、线上环境不一定允许、不稳定
3、加大超时到 60s
- 优点:最简单
- 缺点:用户直接跑光
4、最优方案:绕开 tiktoken(本方案)
- 零依赖
- 不改环境
- 不搞代理
- 上线即生效
- 速度直接拉满
注意事项(生产必看)
- 此方法不支持流式输出,需要流式再单独封装 SSE。(这个也简单)
- 不计算 token,如需计费 / 限流,要自己实现轻量分词。(几乎不用)
- 需要 tool calling / 复杂 Agent 场景,仍可保留 LangChain。(酌情处理)
- 兼容所有 OpenAI 格式接口:自建 / 第三方 / 阿里 / 百度 / 字节 等。(几乎也能满足)
总结
很多时候 LLM 慢不是模型慢,而是你用的库在偷偷做你不需要的事情。
tiktoken 这个坑,国内 90% 使用 LangChain.js 的团队都踩过。希望这篇实战优化,能帮你把 LLM 接口从超时重灾区拉回秒级体验。