对象存储,AI Agent 文件的存储底座, OSS
AI Agent 运行时会持续产生、读取大量文件——用户上传的 PDF、程序生成的图表、多模态的音视频,普通本地文件夹根本扛不住高并发读写。这篇先讲清「向量库、关系库、对象存储」三个库在 RAG 里各管什么,再带你用 30 秒拉起本地私有化存储,学会用 @aws-sdk/client-s3 一套 SDK 对接阿里云 OSS、MinIO、RustFS,最后讲透从文件上传到溯源展示的完整闭环,适合所有在搭知识库或多模态 Agent 的开发者
Leo
2026.09.06 · 更新于 2026.09.10
对象存储,AI Agent 文件的存储底座:RAG 溯源到多模态, S3 与三种落地方案
为什么写这篇:Agent 不落地文件,知识就永远是「看过就忘」
先说一个 Agent 开发里逃不掉的现象:AI Agent 运行起来,就是在不停地生产文件和消费文件。
- 用户往知识库里丢一份 PDF,系统要读它、切它、回溯源时还要再读一次;
- 你的 Agent 跑个数据分析,程序自动生成一张图表要存下来;
- 语音 Agent 合成一段回答,生成的音频要给前端播放;
- 多模态 Agent 更是图片、视频一起上。
这些文件有大有小,但有一个共同点:它们不是几行文字,是实实在在的大体积二进制数据。问题来了——把文件存哪?
大多数新手的第一反应是「放本地文件夹」。但本地文件夹有三个硬伤:
- 并发扛不住:海量文件被多个进程同时读写时,本地磁盘很快就成了性能瓶颈;
- 容量没有上限:本地磁盘是固定的,而 Agent 产出的文件是无上限的;
- 水平扩展难:服务一旦横向扩到多台机器,A 机写的文件 B 机根本读不到。
所以做 AI 知识库、多模态 Agent 项目,必须上对象存储。它才是支撑各种 Agent 文件读写的底层地基。这篇的目的,就是帮你把「为什么非得是对象存储、对象存储怎么选、代码怎么写」一次讲透。
先说结论:
AI Agent 的文件存储,正确姿势是「三个库各管一段」:原始大文件 → 对象存储长期归档;文件元数据 → 关系库;文本切片 → 向量库。只有对象存储能统一承载图片、PDF、音视频这类大容量二进制素材。
先看全景:RAG 里三个「库」到底谁管什么
新手最容易犯的错,是以为一个数据库就能搞定一切。实际上一条 RAG 链路走下来,至少涉及三个存储,它们分工完全不同:
| 存储 | 存什么 | 拿什么找 | 为什么是它 |
|---|---|---|---|
| 向量库(Milvus 等) | 文本切片的向量 | 语义相似度 | 只擅长「按意思找」,存不了大文件 |
| 关系库(PostgreSQL 等) | 文件元数据:文件名、来源、切片位置 | SQL、主键、JOIN | 擅长结构化记录,但不存二进制内容 |
| 对象存储(OSS/MinIO/RustFS) | 图片、PDF、音视频等原始大文件 | 对象 Key(路径) | 唯一能统一承载大体积素材的底座 |
把这三个库串起来,就是一条完整的 RAG 链路:
把这条链路拆开看,对象存储出现在两个关键节点上:
- 入库时:解析完的原始文件完整存入对象存储,长期归档不动——这是所有溯源工作的前提;
- 回答时:用户提问,向量库返回匹配片段并带上文件 ID,程序拿 ID 去关系库读文件基础信息,再通过对象存储拉回完整原始文档做溯源展示。
为什么向量库和数据库都顶不上这个位置?一句话:向量库只存向量、数据库只存文字信息,它们都存不了大体积二进制文件。 只有对象存储能同时承载图片、PDF、音视频,所以在多模态时代它几乎是唯一解。
先搞懂对象存储本身:Bucket、Key、预签名 URL
动手对接前,先花两分钟把对象存储的几个核心概念建立起来——它们和你熟悉的「文件夹」很不一样。
对象存储不是文件系统
普通文件系统是「目录树 + 文件」:一层层文件夹套下去,可以原地改文件内容。对象存储是扁平的——里面只有一个个「对象」(Object),每个对象就是一个文件加上它的元数据。
那为什么我们看到的对象 Key 都长得像路径,比如 docs/meeting/2026-09/raw.pdf?因为对象存储允许你用 / 模拟出层级关系,方便人看,但它本质是一个超大的 KV 存储:Key 是地址,Value 是文件二进制。这也是它天然适合海量并发的根本原因——读写一个对象和读写十万个对象没有本质区别。
三个必须记住的概念:
- Bucket(桶):最顶层的命名空间,相当于「一个仓库」。通常一个项目/一个用途开一个桶,桶名全局唯一;
- Object Key(对象名):桶内每个对象的唯一地址,用
/模拟目录层级,例如uploads/meeting-1/source.pdf; - 预签名 URL(Presigned URL):给文件生成一个带时效的临时下载链接。因为对象默认是私有的,你不想把文件设为公开读,就靠它把「某段时间内可下载」的权限临时交出去。
要点:Key 即路径,存的时候随手把逻辑路径组织好,溯源时会省很多事;Bucket 是权限边界,公开桶和私有桶要分清;私有桶 + 预签名 URL 才是正规做法,别为图省事把所有文件设成公开读。
为什么不是 MySQL 存大字段、也不是本地文件
有人会问:那把文件塞进 MySQL 的 BLOB 字段不就行了?能存,但不该存:数据库是为结构化记录优化的,往里塞几 MB 的二进制会把整张表拖慢,备份、扩容都跟着遭殃。文件就该交给专门管文件的存储,数据库只留一行元数据记着「文件在哪」。
而对象存储里的文件不能像文件夹那样「原地改一行」,它是整存整取的——上传即覆盖、整体读写。这个特性对「原始素材只读、需要追溯」的 RAG 场景反而刚刚好。
三类主流方案怎么选:阿里云 OSS / MinIO / RustFS
方案层面,现在主流是这三类:阿里云 OSS、MinIO、RustFS。它们底层全部遵循 S3 标准协议,核心读写能力基本一致——真正拉开差距的是「谁来维护」和「商用约束」。
| 维度 | 阿里云 OSS | MinIO | RustFS |
|---|---|---|---|
| 形态 | 公有云托管服务 | 可 Docker 私有化部署 | 专为私有化海量文件场景打造 |
| 谁来维护 | 阿里云运维,按量自动扩容 | 自己部署自己管 | 自己部署自己管 |
| 典型场景 | 线上 SaaS,不想投入运维人力的团队 | 本地测试、小型知识库 | 多模态国产化、海量文件私有化项目 |
| 可视化后台 | 完善的控制台 | 新版社区版已阉割可视化管理 | 自带 Web 控制台 |
| 商用版权 | 商业授权,无风险 | AGPL 开源协议,商用有版权风险 | 商用无约束 |
| 底层技术 | 阿里云自研 | Go 语言 | Rust 底层,内存占用低、并发稳定 |
选型用一句话粗暴概括:要省运维就上云 OSS,要私有部署就二选一——图省事图免费选 MinIO,图商用无风险、图海量并发选 RustFS。
具体到你该选哪个,可以顺着这张决策图走:
要点:OSS 的优势是免运维 + 按量自动扩容,代价是数据在云上、有成本;MinIO 的优势是上手最快,社区版有 AGPL 商用风险且新版阉割了可视化管理;RustFS 的优势是 Rust 底层内存占用低、并发稳定、商用无约束,适合国产化和海量文件场景。
上手第一步:30 秒在本地拉起一个私有化存储
选型定了,先别管代码,先把存储跑起来。以 RustFS 为例,一个 docker-compose.yml 就够,用的是 S3 协议、Web 控制台、日志三件套齐全的最小配置:
# docker compose up -d 一条命令拉起一个 S3 兼容对象存储
services:
rustfs:
image: rustfs/rustfs:latest
container_name: rustfs-server
restart: always
ports:
- "9000:9000" # S3 API 端口(程序对接用)
- "9001:9001" # Web 控制台端口(浏览器用)
environment:
# S3 接口 / 控制台登录账号密钥
RUSTFS_ACCESS_KEY: admin
RUSTFS_SECRET_KEY: Admin@123456
# 开启 Web 可视化管理控制台
RUSTFS_CONSOLE_ENABLE: "true"
volumes:
# 数据持久化到本地文件夹
- ./rustfs-data:/data
- ./rustfs-logs:/logs
command: server /data
启动后浏览器打开 http://localhost:9001 就能看到管理后台。想先试 MinIO 也简单,把镜像换成 minio/minio 即可——端口、账号体系、S3 接口全一样,这也是「都遵循 S3 协议」带来的好处。
要点:9000 是程序连的 API 口,9001 是给人看的控制台口,别搞混;RustFS/MinIO 都认一套 AccessKey + SecretKey;本地测试选哪个都行,上生产前先过一遍商用版权那关。
接入第二步:一套 SDK 对接所有 S3 服务
存储起来了,就该写代码了。好消息是:核心能力都遵循 S3 协议,所以对接方式高度统一。 安装 @aws-sdk/client-s3 这一个 AWS 官方的包,就能对接 OSS/MinIO/RustFS 所有 S3 兼容服务;当然也可以用 ali-oss、minio 各自的 SDK,更贴合厂商习惯。
姿势一:统一用 @aws-sdk/client-s3(RustFS / MinIO 通吃)
这是最推荐的姿势——一份代码,换环境变量就能切换存储。核心就三件事:配 endpoint、开 forcePathStyle、发 PutObjectCommand。
// npm i @aws-sdk/client-s3 dotenv
import 'dotenv/config';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import fs from 'fs';
// 一套 SDK,私有化部署的 RustFS / MinIO 通吃
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT, // 例如 http://localhost:9000
region: 'auto', // 本地私有存储随便填,不影响
forcePathStyle: true, // 私有化存储必须 true
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
});
// 流式上传:大文件用 fs 流,内存不爆炸
async function upload(key, filePath, contentType) {
await s3.send(new PutObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key, // 对象名,用 / 组织层级
Body: fs.createReadStream(filePath), // 读文件为流
ContentType: contentType, // 图片 / PDF / 音频 各自类型
}));
console.log('上传完成:', key);
}
// 例:归档一份用户上传的会议纪要 PDF
await upload('docs/meeting/2026-09/raw.pdf', './raw.pdf', 'application/pdf');
要点:
forcePathStyle: true是本地私有存储的关键开关,不开会连不上;Key 就是对象地址,存的时候把业务路径编好;Body 传流而不是整个 Buffer,大文件也能低内存上传;别把密钥硬编码,走环境变量。
姿势二:用 ali-oss 官方 SDK(阿里云 OSS 专用)
如果选了阿里云 OSS,用官方 ali-oss SDK 最顺手——它替你封装好了 OSS 的签名、分片、断点续传等云上能力。
// npm i ali-oss
import OSS from 'ali-oss';
import fs from 'fs';
const client = new OSS({
region: 'oss-cn-hangzhou', // Bucket 所在地域
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
bucket: 'my-agent-bucket', // 桶名,创建时全局唯一
});
async function upload() {
// putStream 流式上传,返回结果里带着可访问地址
const res = await client.putStream(
'docs/meeting/2026-09/raw.pdf', // 对象名(路径)
fs.createReadStream('./raw.pdf')
);
console.log('上传完成:', res.url);
}
要点:
region必须是 Bucket 真实所在地域,写错连不上;桶名全局唯一,创建后不好改;官方 SDK 对云上复杂场景(分片、续传、防盗链)支持最全,线上 OSS 优先用它。
姿势三:用 minio SDK(MinIO 专用)
想用 MinIO 时,官方 minio SDK 也有一套几乎一样的写法,只是域名格式稍有差异(不能带 http://)。
// npm i minio
import Minio from 'minio';
import fs from 'fs';
const client = new Minio.Client({
endPoint: 'localhost', // 注意:不带协议前缀
port: 9000,
useSSL: false,
accessKey: process.env.MINIO_ACCESS_KEY,
secretKey: process.env.MINIO_SECRET_KEY,
});
async function upload() {
// putObject:桶 + 对象名 + 文件流
await client.putObject('my-bucket', 'docs/meeting/2026-09/raw.pdf',
fs.createReadStream('./raw.pdf'));
console.log('上传完成');
}
三种姿势放在一起看,会发现骨架完全一样:都是「配账号密钥 → 指到桶 → 把流塞给某个对象路径」。差异只在每个 SDK 的字段命名上——这正是「底层全是 S3 协议」带给开发者的红利:学一次,处处用。
溯源闭环:怎么把文件再「拿回来」
RAG 里对象存储的另一半使命,是回答时的溯源展示——向量库给了你「这段话来自某份文件」,你得有能力把那份完整原文拉出来。这一步分两种诉求,各有各的取法。
场景一:程序内部要读文件内容(Agent 继续处理)
程序拿到 Key 后用 GetObjectCommand 直接拉流,比如把原始 PDF 交给下一个处理环节:
import { GetObjectCommand } from '@aws-sdk/client-s3';
async function download(key) {
const { Body } = await s3.send(new GetObjectCommand({
Bucket: process.env.S3_BUCKET,
Key: key,
}));
// Body 是流,可以直接写盘、转 Buffer、或再喂给下游处理
return Body;
}
场景二:要给前端做展示 / 下载(溯源查看原始文档)
前端不能直接用 AccessKey 去拉文件。正规做法是生成一个几分钟内有效的预签名 URL,把它交给前端去展示——文件本身保持私有,权限却按需放出:
// 统一 SDK 生成预签名 URL
// npm i @aws-sdk/s3-request-presigner
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const url = await getSignedUrl(s3,
new GetObjectCommand({ Bucket: process.env.S3_BUCKET, Key }),
{ expiresIn: 300 } // 300 秒 = 5 分钟后过期
);
console.log('临时下载链接:', url);
如果用阿里云 OSS,官方 SDK 一句 signatureUrl 就搞定,原理一样:
// ali-oss 生成预签名 URL,expires 单位是秒
const url = client.signatureUrl('docs/meeting/2026-09/raw.pdf', { expires: 300 });
要点:程序内处理用 GetObjectCommand 拉流,给用户展示用预签名 URL,两种场景别混;预签名 URL 自带过期时间,是私有桶对外提供临时读权限的标准姿势;拿到的 URL 有隐私风险,别把它永久存进数据库,要用时再生成。
结尾总结:一张表想清楚「怎么选、怎么放」
兜了一大圈,其实你只需要记住两件事:文件放对象存储、元数据放关系库、切片向量放向量库;对象存储选哪个,看你的「运维投入」和「商用约束」。
| 你的处境 | 建议方案 | 一句话理由 |
|---|---|---|
| 线上 SaaS,不想管服务器 | 阿里云 OSS | 免运维、按量扩容,只操心业务 |
| 本地开发测试、小知识库 | MinIO | Docker 一条命令起来,够用 |
| 私有化、商用合规、海量并发 | RustFS | Rust 底层内存低、并发稳,无版权负担 |
| 多模态国产化项目 | RustFS | 私有化 + 商用无约束,最省心 |
存储落位则始终是这套铁三角,缺一不可:
对象存储不是什么高深的技术,它只是把「文件放哪儿」这件事,从脆弱的本地磁盘,搬到了一个专为海量并发而生的底座上。理解了它和向量库、关系库的各自分工,再面对 RAG 溯源、多模态素材归档,你就不会再纠结「文件到底该塞进哪个库」——大文件永远先问一句:对象存储安排上了吗?
读者留言
COMMENTS · 0发表留言