AskAI系列课程P2.通过脚本批量上传文件到阿里云百炼知识库
【AskAI系列课程】:P2.通过脚本批量上传文件到阿里云百炼知识库
[
新星杯·14天创作挑战营·第15期
10w+人浏览
553人参与
](
)
这是【AskAI系列课程】的第 2 课:通过脚本把站点文档批量上传到阿里云百炼(Model Studio)并接入"知识库(RAG 索引)"。
为什么需要脚本化上传?
当你开始认真使用 RAG 应用时,很快就会遇到一个现实问题:网站内容经常更新,手工上传简直是噩梦。
想象一下这样的场景:
- 你的博客或文档站有几十几百篇文章,每周都有更新
- 每次更新后,你需要手动删除旧文件,重新上传新文件
- 上传后还要等待解析,再手动绑定到知识库
- 一个操作流程下来,轻松消耗半小时
这种重复性工作不仅容易出错,还会严重影响更新积极性。更重要的是,如果想要接入 CI/CD 实现自动化,手工操作根本不可能。
所以我们需要的是:一条命令完成"清空旧内容 → 重新上传 → 等待解析 → 绑定知识库"的完整流程。
理解阿里云百炼的数据架构
在开始写脚本之前,我们先要理解阿里云百炼是如何组织数据的。否则就像盲人摸象,很容易在实现中踩坑。
百炼中围绕 RAG 有两个核心概念:知识库(Index)和应用数据(Data Center)。
知识库(Index)- 检索引擎
知识库就像一个专门的搜索引擎,它的职责是:
- 接收用户的问题,快速检索出相关文档片段
- 为 LLM 提供上下文信息,让回答更准确
- 支持向量检索、关键词检索等多种检索方式
应用数据(Data Center)- 文档仓库
应用数据更像一个文档管理系统,按"类目(Category)“组织文件:
- 负责文档的上传、存储、解析
- 将文档解析成可索引的格式
- 按类目进行文件分组管理
两者的协作关系
这里是关键:知识库不直接存储文档,而是从应用数据中"拉取"已解析的内容建立索引。
具体流程是:
- 先把文件上传到应用数据的某个类目
- 等待百炼解析文档(提取文本、分段等)
- 发起"知识库入库任务”,将指定类目的内容索引到知识库
- 任务完成后,知识库就能检索到这些内容了
理解了这个流程,我们的脚本目标就很明确了:把本地文档推送到默认类目,等待解析完成,然后一次性绑定到知识库。
准备工作:创建知识库
在开始编写脚本之前,我们需要先创建一个知识库。这样可以确保每次运行脚本都写入同一个知识库,避免混乱。
在阿里云百炼控制台手动创建一个知识库,并复制 IndexId。这个 ID 后续会配置到脚本的环境变量中。
实现思路:从痛点到解决方案
现在让我们来分析具体的实现思路。我们要解决的核心问题是:如何将本地的 markdown 文档自动同步到知识库。
主要挑战
在实际实现中,我遇到了几个关键挑战:
- 文件格式兼容性:我的网站用的是
.mdx
格式,但百炼知识库只支持.md
格式 - 文件名冲突:不同目录下可能有同名文件,直接上传会覆盖
- API 限流:批量删除和上传时容易触发频率限制
- 解析状态同步:需要等待所有文件解析完成才能绑定到知识库
解决方案设计
针对这些挑战,我设计了以下解决方案:
1. 智能文件重命名
- 将
.mdx
统一转换为.md
格式 - 使用"目录_文件名.md"的命名规则避免冲突
- 例如:
blog/ai/intro.mdx
→blog_ai_intro.md
2. 分阶段执行流程
- 先清理旧文件(带限流保护)
- 再批量上传新文件
- 统一等待解析完成
- 最后一次性绑定到知识库
3. 容错和重试机制
- 只删除解析成功或失败的文件(解析中的文件无法删除)
- 添加适当的延时避免触发 API 限流
- 支持增量更新和全量更新两种模式
技术架构图解
基于上面的分析,我们来看看整体的技术架构。整个流程可以分为两个层面:数据流转和时序控制。
数据流转架构
遍历 src/content 下 md/mdx
重命名为目录_文件.md
写入 file_caches 目录
申请上传凭证 ApplyFileUploadLease
上传到 OSS
添加到默认类目 AddFile
轮询解析状态直到全部成功
提交知识库入库任务
等待知识库任务完成
时序交互流程
开发者
kb_updater.py
百炼API
阿里云OSS
python kb_updater.py
ListCategory 获取默认类目ID
ListFile 列举现有文件
清理阶段(限流保护)
DeleteFile(仅删除已解析文件)
sleep(0.2) 避免限流
loop
[现有文件]
文件处理阶段
扫描本地文件并重命名
ApplyFileUploadLease 申请上传凭证
上传文件到OSS
AddFile 添加到类目
loop
[每个文件]
同步等待阶段
轮询 ListFile 直到全部解析成功
知识库绑定阶段
SubmitIndexAddDocumentsJob
轮询任务状态直到完成
开发者
kb_updater.py
百炼API
阿里云OSS
核心代码实现详解
现在让我们深入关键的代码实现。这里我会重点解释几个核心函数的实现逻辑,特别是文件重命名的技术细节。
1. 智能文件重命名:解决格式和冲突问题
这是整个脚本中最关键的技术细节之一。我们需要解决两个问题:
.mdx
格式转换为.md
(知识库不支持 mdx)- 避免不同目录下同名文件的冲突
def process_local_files(self):
"""处理本地文件:重命名并写入缓存目录"""
for file_path in self.content_dir.rglob("*.md*"):
if file_path.suffix in ['.md', '.mdx']:
# 计算相对路径,用于构建新文件名
relative = file_path.relative_to(self.content_dir)
parts = list(relative.parts)
# 核心重命名逻辑:目录_文件名.md
if len(parts) > 1:
# 多级目录:blog/ai/intro.mdx → blog_ai_intro.md
new_name = f"{'_'.join(parts[:-1])}_{Path(parts[-1]).stem}.md"
else:
# 根目录文件:intro.mdx → intro.md
new_name = f"{file_path.stem}.md"
# 写入缓存目录
target_path = self.file_caches_dir / new_name
target_path.write_text(file_path.read_text(encoding='utf-8'), encoding='utf-8')
print(f"处理文件: {relative} → {new_name}")
为什么这样重命名?
- 使用下划线连接目录路径,确保文件名唯一性
- 统一转换为
.md
扩展名,满足知识库要求 - 保留原始的目录结构信息,便于后续管理
2. 获取默认类目ID:API调用的基础
def get_default_category_id(self):
"""获取默认类目的真实ID"""
request = ListCategoryRequest()
resp = self.client.list_category_with_options(self.workspace_id, request, headers, runtime)
# 优先查找名为 'default' 的类目
for cate in resp.body.data.category_list:
if getattr(cate, 'category_name', None) == 'default':
return getattr(cate, 'category_id', None)
# 如果没有找到,查找默认类目
for cate in resp.body.data.category_list:
if getattr(cate, 'is_default', False):
return getattr(cate, 'category_id', None)
raise Exception("未找到默认类目")
3. 安全的文件清理:避免API限流
def clean_existing_files(self):
"""清理现有文件,避免重复和冲突"""
files = self.list_files(self.default_category_id)
for f in files:
file_status = getattr(f, 'status', None)
# 只删除已解析完成的文件(解析中的文件无法删除)
if file_status in ('PARSE_SUCCESS', 'PARSE_FAILED', None):
file_id = getattr(f, 'file_id', None) or getattr(f, 'FileId', None)
if file_id:
self.delete_file(file_id)
# 关键:添加延时避免API限流
time.sleep(0.2)
print(f"删除文件: {getattr(f, 'file_name', 'unknown')}")
4. 知识库绑定:按类目一次性入库
def submit_index_add_documents_job(self):
"""将整个类目绑定到知识库"""
request = SubmitIndexAddDocumentsJobRequest(
index_id=self.index_id,
source_type="DATA_CENTER_CATEGORY", # 按类目入库
category_ids=[self.default_category_id] # 指定类目ID
)
resp = self.client.submit_index_add_documents_job(request)
job_id = resp.body.id
# 轮询等待任务完成
return self.wait_for_index_job_complete(job_id)
关键技术要点:
- 使用
DATA_CENTER_CATEGORY
模式,可以一次性处理整个类目 - 避免了逐个文件绑定的复杂性
- 通过轮询确保任务真正完成
使用脚本:一键同步知识库
现在让我们看看如何实际运行这个脚本。整个过程被设计得非常简单,只需要两步配置就能开始使用。
第1步:配置环境变量
在项目根目录创建 .env
文件,配置必要的访问凭证:
# 阿里云访问凭证
ALIBABA_CLOUD_ACCESS_KEY_ID=你的AccessKey
ALIBABA_CLOUD_ACCESS_KEY_SECRET=你的SecretKey
# 百炼工作空间和知识库配置
ALIBABA_CLOUD_BAILIAN_WORKSPACE_ID=你的WorkspaceId
ALIBABA_CLOUD_BAILIAN_KB_INDEX_ID=你的IndexId
如何获取这些配置?
- AccessKey/SecretKey:阿里云控制台 → 用户信息 → AccessKey管理
- WorkspaceId:百炼控制台 → 工作空间设置
- IndexId:百炼控制台 → 知识库 → 复制知识库ID
第2步:运行脚本
一条命令完成所有操作:
python ask-ai-server/bailian/kb_updater.py
脚本执行过程:
执行自动更新阿里云百炼知识库脚本
效果确认:
- 默认类目中的旧文件被安全清理
- 本地文档按重命名规则上传并解析
- 所有内容一次性绑定到知识库
- 整个过程约2-5分钟(取决于文档数量)
验证效果:知识库命中测试
脚本运行完成后,我们可以在知识库详情页测试效果。点击"命中测试"按钮,输入问题,即可看到相关文档片段被检索出来。
这些检索到的片段就是 RAG 系统的"Retrieve"环节输出,它们会作为上下文提供给 LLM 进行"Generate",最终形成准确的回答。
经验总结与优化方向
通过这个脚本的实现和使用,我总结了几个重要的经验:
核心价值
- 效率提升:从手动上传的30分钟缩短到一键执行的2分钟
- 可靠性:自动化避免了人工操作的遗漏和错误
- 可集成性:为后续的 CI/CD 集成奠定了基础
踩过的坑
- API限流:批量删除时必须加延时,否则会被限制访问
- 解析状态:只能删除已解析完成的文件,解析中的文件会删除失败
后续改进方向
- 并发优化:支持配置并发上传数量,提升大批量文件的处理速度
- 增量更新:基于文件修改时间,只更新变更的文档
- 错误重试:增加自动重试机制,处理网络异常等临时问题
- CI/CD集成:在 GitHub Actions 中触发,实现 push 后自动更新知识库
这个脚本虽然解决了当前的痛点,但 RAG 系统的完善还有很长的路要走。下一课我们将探讨如何基于这个知识库构建一个真正可用的问答应用。
参考资料
源码:
我的个人网站: