上个星期在客户现场部署 RAGFlow,所有的服务容器都拉起来了,唯独 ragflow-server 一直在重启。docker logs 拉出来一看,满屏都是 tiktoken 下载词表的报错,核心就是 cl100k_base.tiktoken 这个文件拉不下来。客户环境是隔离内网,没有外网出口,这个文件下载失败,服务就卡在初始化,整个知识库流程根本推不动。
RAGFlow 做文档解析、文本切片的时候,默认要用 tiktoken 对文本做 token 化,这是 OpenAI 开源的一个分词库。cl100k_base 是它内置的编码词表,对应 GPT-4、GPT-3.5 这些模型用的 token 规则。联网环境第一次运行会自动下载,几秒钟解决;但到了离线环境,这个下载请求就变成了拦路虎。这篇文章就专门解决这个问题,把我试过有效的方法,连同 RAGFlow 离线部署时其他几个高频坑一起梳理出来,照着做就能填平。
1. 问题复盘:一个 1.35MB 的词表文件为什么能卡死服务
1.1 分词器在 RAGFlow 里的定位
很多人第一次看到 cl100k_base.tiktoken 报错会懵,因为这不是 RAGFlow 自己的组件,而是 Python 库 tiktoken 运行时的下载动作。要理解这里面的逻辑,得先知道 RAGFlow 处理一份 PDF 的流程:上传文档之后,DeepDoc 做版面解析和 OCR,把 PDF 转成结构化的文本块,然后再做文本切片。切片的时候,RAGFlow 需要把文本转成 token 序列,用 token 数量来控制每个 chunk 的长度,这样向量化的结果才均匀、检索才准。
tiktoken 就是干这个转 token 的活。它本身是纯 Python 库,安装很快,但是首次调用tiktoken.get_encoding("cl100k_base")时,如果本地缓存没有这个词表,就会尝试从 OpenAI 的对象存储下载。这个文件只有 1.35MB 左右,在联网环境里是秒下,但内网环境直接超时失败,异常抛出来,文档解析链路全部停摆。
1.2 报错现场长什么样
RAGFlow 的日志里会出现类似这样的信息:
Encountered error downloading https://openaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken或者这样:
Connection error, and we cannot find the requested files in the local cache还有一种情况是报 HTTP 403 或者 SSL 错误,但根因都一样:tiktoken 在尝试联网下载,而网络不通。这个报错经常出现在 ragflow-server 容器刚启动、或者第一次创建知识库上传文档的时候,所以很多人一开始会误以为是 RAGFlow 其他配置有问题,折腾半天才发现只是 tokenizer 词表没就绪。
1.3 tiktoken 的查找顺序
要把这个问题彻底解决,得先明白 tiktoken 找文件的顺序。它的逻辑不复杂:
- 查内存缓存,进程内已经加载过就直接复用。
- 查环境变量
TIKTOKEN_CACHE_DIR指定的目录。 - 查默认缓存路径
~/.cache/tiktoken。 - 都没命中,就向远程 URL 发 HTTP 请求下载,下载成功后把文件写入缓存目录,文件名的规则是 URL 的 SHA1 哈希。
也就是说,英文资料里说的“下载 cl100k_base.tiktoken”,其实下载完之后它并不叫这个名字,而是以哈希命名的无扩展名文件,存在缓存目录里。这个细节很重要,因为我见过有人手动创建一个叫 cl100k_base.tiktoken 的文件塞进缓存目录,结果根本不生效。后来搞清楚了查找规则,整个解决方案就顺理成章了:我们把缓存文件和目录结构准备好,骗过它的查找逻辑,让它在离线环境下命中本地缓存。
2. 解决思路选型:试过绕路,最后还是回归缓存预置
2.1 几个试了但没根治的方案
先说踩过的弯路,避免大家重复投入。
第一种思路是把 tiktoken 整个包离线安装到内网,也就是拿 whl 文件 pip install。这个做了之后,import tiktoken没问题,但那句报错依然存在。原因很简单,pip 只是把包装进了 site-packages,词表数据文件还是要运行时联网拉取。
第二种思路是在容器里配 HTTP 代理或者改 DNS。离线内网的网络拓扑里,很多环境压根没有外网出口,改网关和 DNS 都没用,代理服务器也不一定找得到。这条路对真正隔离的客户网络来说基本走不通。
第三种思路是写代码绕过出错点,比如 catch 异常然后 fake 一个 encoding 对象。这样做异常是不抛了,但 RAGFlow 后续的切片逻辑会拿不到真实的 token 计数,切出来的 chunk 大小偏差很大,知识库的质量直接受影响。这个方案我调试到一半就放弃了,太不优雅。
2.2 三个真正靠谱的方案对比
真正靠谱的方案其实都是围绕“让 tiktoken 在本地找到词表文件”这个核心思路展开的。
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 方案A:预置缓存目录 | 在联网机器上生成缓存,拷贝到内网并挂载进容器 | 不碰代码,升级重建容器后依然有效,可沉淀为部署资产 | 需要提前准备,第一次部署多花十分钟 |
| 方案B:改包源码 | 修改 tiktoken 内部的加载逻辑,把远程 URL 替换成本地路径 | 完全不依赖缓存目录,逻辑改完一劳永逸 | 每次升级镜像或升级版本要重新改,易维护性差 |
| 方案C:低层 API 加载 | 用Encoding类手动读本地文件构造编码对象 | 对业务代码可控性最强 | 需要在代码层面嵌入,RAGFlow 又不是自己写的,根本没法改干净 |
我最终选择的是方案A,把缓存目录做成挂载卷。理由很简单:RAGFlow 官方镜像更新频率高,docker compose pull之后容器重建,方案B和方案C说不定就丢了,但挂载卷是独立于容器生命周期的,文件一直在。而且这个缓存目录后续可以放进团队内部的知识库资产里,新环境部署直接拷一份,全网通用。
2.3 为什么说“放缓存文件”不是临时补丁
我看到有些帖子说这个是“治标不治本”,这个说法我不太认同。tiktoken 本身就内置了缓存目录机制,环境变量TIKTOKEN_CACHE_DIR就是官方留的口子。我们做的只是把“下载”这一步在联网机器上先执行完,把“产物”放到内网环境,这正是离线部署的标准做法,和离线安装 Python 包、离线拉 Docker 镜像是一个道理。
举个例子,docker pull 在联网机器上拉镜像,再 save 成 tar 包拷进内网 load,这是所有内网部署的常规操作。tiktoken 缓存预置和这个完全同构,属于正规的离线软件分发,不是 hack。
3. 动手实操:三步解决 cl100k_base.tiktoken,照着做就行
3.1 在能联网的机器上生成缓存文件
找一台能访问外网的 Linux 机器,最好 Python 版本和 RAGFlow 容器内的版本接近。执行下面这段命令:
python3 - <<'EOF' import tiktoken enc = tiktoken.get_encoding("cl100k_base") print(enc.encode("hello world")) EOF第一次执行会联网下载,看到输出一串数字就代表成功了。然后看一下缓存目录:
ls -l ~/.cache/tiktoken/正常情况下会看到一个没有扩展名的文件,文件名是一长串哈希,大小约 1.35MB。这就是 cl100k_base 词表在缓存目录里的真实形态。
这里有个细节提醒一下:如果目录下已经有多个文件,不用纠结哪个是哪个,全部一起拷走就行。比如有些机器上还缓存过r50k_base或者p50k_base,一并拷过去也没坏处,后面可能会用到。
3.2 拷贝到离线服务器并调整权限
把~/.cache/tiktoken/目录整个拷贝到离线服务器的固定路径,比如/data/ragflow/tiktoken-cache:
mkdir -p /data/ragflow/tiktoken-cache cp ~/.cache/tiktoken/* /data/ragflow/tiktoken-cache/ chmod -R a+r /data/ragflow/tiktoken-cache/最后一步chmod很容易被忽略。RAGFlow 容器里的进程不一定以 root 运行,如果文件权限是 600 或者更严格,容器内用户读不了,照样报错。我之前就遇到过这种情况,缓存文件位置放对了,权限不对,日志里报 Permission denied,排查了半小时才反应过来。
3.3 修改 docker-compose.yml,给 ragflow-server 挂载缓存
进入 RAGFlow 部署目录,编辑docker-compose.yml。找到ragflow-server这个服务,在environment里加上TIKTOKEN_CACHE_DIR环境变量,在volumes里把宿主机目录挂载进去:
ragflow-server: ... environment: - TIKTOKEN_CACHE_DIR=/opt/tiktoken volumes: - /data/ragflow/tiktoken-cache:/opt/tiktoken保存后重启:
docker compose down docker compose up -d注意volumes的挂载路径和TIKTOKEN_CACHE_DIR必须一致,否则容器内找到了环境变量,但目录里没有文件,还是会走下载逻辑。我用过/opt/tiktoken做容器内路径,避免和容器已有的数据目录冲突。
3.4 容器内验证:缓存是否真正生效
重启完别急着去界面操作,先进容器做一次单测,确认 tiktoken 不再尝试联网:
docker exec -it ragflow-server bash python3 -c "import tiktoken; enc = tiktoken.get_encoding('cl100k_base'); print(enc.encode('hello world'))"正常输出类似[15339, 2437]这样的 token id 列表,而不是抛异常。看到这个输出,就说明 tiktoken 在离线状态下成功读到了本地缓存。
如果这一步还是报错,先检查两个地方:第一,挂载路径是否写对了,进容器执行ls -l /opt/tiktoken看看文件在不在;第二,文件是否有可读权限,容器内执行cat /opt/tiktoken/<哈希文件> | head -c 100试试。
3.5 回到 RAGFlow 界面做端到端验证
单测过了,还要把业务链路走一遍。登录 RAGFlow 管理界面,新建一个知识库,选择嵌入模型,上传一个测试用的小 PDF,观察解析状态。如果之前卡在 pending,现在变成了 done,说明文档解析链路里 tiktoken 相关的问题已经清零。
我习惯用一个单页的 PDF 做验证,解析快,容易看出问题。如果上传后状态还是长时间 pending,就去 docker logs 里看 ragflow-server 有没有别的报错,重点是定位是不是还有第二个网络请求被卡住。
4. 不只是 tokenizer:离线部署的模型链路也要一起验证
4.1 嵌入模型和重排模型的离线方案
tiktoken 的问题解决后,RAGFlow 还有一个大头:嵌入模型和重排模型。创建知识库时,RAGFlow 要用嵌入模型把文本块转成向量,检索时要用重排模型对候选结果做二次排序。这两个模型在离线环境通常不会自动下载,需要提前准备好。
常见的做法是把模型文件放在一台能联网的机器上下载好,按 Hugging Face 的目录结构整体拷进内网,然后用 Ollama 或者 Xinference 加载。以 Xinference 为例,启动时可以指定本地路径加载模型,配置好之后把模型的 Base URL 填到 RAGFlow 的模型供应商里。RAGFlow 官方的模型列表里支持 Xinference,填http://<xinference服务IP>:<端口>就行。
这里提醒一个容易踩的坑:RAGFlow 创建知识库时下拉框里的模型,必须先在“模型供应商”页面注册成功,否则选不到。很多人部署完发现模型列表是空的,不是模型文件的问题,而是注册步骤没完成。
4.2 创建知识库的完整流程和默认模型设置
RAGFlow 创建知识库的流程是这样的:先配置好模型供应商,再点“创建知识库”,填写名称、选择嵌入模型和重排模型,完成之后上传文档。这里有一个“默认模型”的概念,如果你希望后续所有知识库默认使用某个本地模型,可以在系统设置里把默认模型指过去,这样每次新建知识库就不用反复选择。
文档上传之后,解析过程是异步的。我建议盯一眼解析日志,确认嵌入模型的调用地址是内网 IP 而不是公网地址。有些配置模板里默认填的是外网模型地址,离线环境里虽然 tokenizer 问题解决了,但模型调用还是失败,知识库照样起不来。
4.3 其他同类工具的离线部署经验
这套思路不光适用于 RAGFlow。如果你在用 AnythingLLM 或者其他 RAG 工具做本地化部署,同样会遇到模型加载、嵌入模型配置这些问题。AnythingLLM 的离线部署主要卡在本地模型路径配置上,处理方式和 RAGFlow 类似,都是提前把模型文件下载好,然后通过环境变量或者配置页面指定本地路径。
区别在于,AnythingLLM 默认对 tiktoken 的依赖没有 RAGFlow 那么深,它的文本切片逻辑有自己的实现。但不管什么工具,离线部署的核心思路是一致的:把所有需要联网获取的组件,在能联网的环境里准备好,以文件形式放入内网,再告诉软件“去哪读本地文件”。
4.4 用 Helm 部署 RAGFlow 时的等效做法
如果你用的是 Kubernetes 环境,通过 Helm 部署 RAGFlow,处理方式原理相同,只是“挂载卷”变成了 PVC。具体来说,先把 tiktoken 缓存文件放到共享存储里,然后在 Helm values 里配置环境变量TIKTOKEN_CACHE_DIR和对应的 volumeMounts。如果临时没有共享存储,也可以用 initContainer 在容器启动前把文件拷进去,本质上和 docker 的挂载是一回事。
另外一个在 Helm 环境里需要注意的点是,tiktoken 缓存文件是二进制文件,不适合放到 ConfigMap 里。ConfigMap 是用来存放配置文本的,二进制文件要塞进去很容易出编码问题。正确做法是 PVC 挂载,或者把文件打包进自定义镜像。
5. 高频报错与排查经验
5.1 tiktoken 相关报错速查表
把我在部署过程中遇到过的几个典型报错,以及对应的处理方式整理成了一张表,方便你排查时直接对照:
| 报错信息 | 原因 | 解决办法 |
|---|---|---|
Ran out of entries in remote openai/encode static files | 词表文件未下载或缓存不完整 | 重新生成缓存目录,并确保挂载路径正确 |
Connection error, and we cannot find the requested files in the local cache | 网络不通,本地也没有缓存 | 按第3章步骤预置缓存文件 |
Permission denied访问/opt/tiktoken时 | 容器内用户无缓存文件读权限 | 宿主机上执行chmod -R a+r |
| 单测通过但知识库解析仍失败 | tiktoken之外的问题,比如嵌入模型地址不可达 | 检查模型供应商配置、模型进程状态 |
| 容器重建后问题复现 | 用了docker cp临时缓存,没有持久化挂载 | 改用 docker-compose volumes 挂载 |
5.2 RAGFlow 启动成功后一直报连接不上 Redis
这是另一个我在离线部署中经常被问到的问题。现象是 RAGFlow 的界面能打开,但服务日志里不断报 Redis 连接失败。排查顺序很重要,别一上来就改配置。
第一步,确认 Redis 容器是不是健康:
docker ps | grep redis docker logs <redis容器名> --tail 50第二步,进入 ragflow-server 容器,测试能不能连上 Redis 服务名:
docker exec -it ragflow-server redis-cli -h redis ping如果能 PONG,说明容器网络 OK,问题大概率出在.env里的密码或端口配置上。RAGFlow 的.env文件里有REDIS_HOST、REDIS_PORT、REDIS_PASSWORD等字段,离线部署时如果 Redis 密码没设置,而.env里却配了密码,就会出现反复重试连接的情况。把密码字段清空或者改成与 Redis 实际配置一致即可。
5.3 创建知识库时模型列表为空
这个问题的原因,十有八九是模型没有注册成功。RAGFlow 里的模型不是“上传文件”就有了,必须先在模型供应商页面里把本地嵌入模型的 API 地址和模型名称配好,测试连通成功后,才能在创建知识库时选到。
还有一种情况是模型注册了但状态显示不可用,这时候要看模型服务的日志。比如 Xinference 加载模型失败,多半是模型文件路径不对或者显存不足。先把模型服务调通,再回到 RAGFlow 界面刷新重试,不要反着来。
5.4 Windows 部署场景的路径注意点
如果你是在 Windows 上用 Docker Desktop 跑 RAGFlow,挂载路径的写法要特别注意。TIKTOKEN_CACHE_DIR是容器内路径,保持 Linux 风格/opt/tiktoken;宿主机挂载路径写成 Windows 格式,比如D:\ragflow\tiktoken-cache:/opt/tiktoken。权限问题上,Windows 挂载到 Linux 容器偶尔会带上奇怪的权限位,遇到 Permission denied 时,在容器里执行chmod -R a+r /opt/tiktoken也能救急。
还有一个 Windows 特有的坑:Docker Desktop 的文件共享设置里,默认只共享了部分盘符。如果你的缓存文件放在 D 盘,但 Docker Desktop 没有把 D 盘加入共享目录,挂载就会失败。遇到挂载不生效,先去 Docker Desktop 的 Settings -> Resources -> File Sharing 里确认盘符已勾选。
5.5 部署预检清单:把离线问题在开机前解决
踩过几次坑之后,我给自己整理了一份 RAGFlow 离线部署预检清单,每次上新环境都按这个过一遍,能省掉大半烦恼:
- tiktoken 缓存目录是否就绪,权限是否为 a+r。
- 嵌入模型和重排模型的本地路径是否就位,Xinference 或 Ollama 能否正常启动。
.env文件中的 Redis、MySQL 配置是否与依赖容器一致。- 所有需要的内网 IP 和端口是否在防火墙上放通。
- 如果是 K8s 环境,PVC 是否创建成功,initContainer 是否执行完成。
这份清单不复杂,但能解决的问题很实际。离线部署最怕的不是某一个技术点有多难,而是环境默认联网、实际又不联网这种假设错位。预检清单的价值,就是把所有“需要联网”的假设提前标出来,逐个替换成本地方案。
我个人的感觉是,tiktoken 这个坑看似小,但它暴露的是离线部署的一个通用规律:部署前先盘一遍所有需要运行时下载的文件,把它们变成部署资产的一部分。现在我在公司内部搭了一套离线部署制品库,tiktoken 缓存、模型文件、离线 Docker 镜像全放一起,新环境部署基本半小时搞定。这个小习惯,算是这个坑给我留下的最大收获。