1. 问题
目前 Agent 已经可以在工作目录中创建和处理文件,但这些文件都保存在后端服务器上,前端无法直接访问。
因此需要增加一个中间环节,把 Agent 生成的文件交给用户。
这里采用对象存储的方式:
Agent生成文件
↓
工作目录
↓
打包
↓
MinIO
↓
生成下载链接
↓
前端下载
这样 Agent 仍然只负责处理自己的工作目录,文件交付由后续流程完成。
2. 对象存储
**对象存储(Object Storage)**主要用于保存图片、视频、压缩包等非结构化文件。
和普通文件系统相比,对象存储更适合通过程序进行文件上传和访问。可以简单理解成一个可以通过 API 操作的网盘。
本项目使用 MinIO。
MinIO 是一个开源对象存储服务,可以直接部署在自己的服务器上,并提供 Python SDK。
这里使用 MinIO 主要是因为项目本身可以本地部署,没有必要为了文件存储再依赖第三方云存储。
3. MinIO 部署
项目使用 Docker 部署 MinIO。
docker pull minio/minio
mkdir -p /usr/local/minio-data
docker run -p 9000:9000 -p 9190:9090 \\
–name minio \\
-d –restart=always \\
-e "MINIO_ROOT_USER=minio" \\
-e "MINIO_ROOT_PASSWORD=minio123" \\
-v /usr/local/minio-data:/data \\
minio/minio server /data –console-address ":9090"
启动后可以通过:
http://服务器IP:9190
进入 MinIO 管理页面。
如果是在本机部署,则使用:
http://localhost:9190
登录后即可看到 MinIO 的管理界面。
4. Python SDK
安装 MinIO 的 Python SDK:
pip install minio
最基本的操作就是连接 MinIO、创建 Bucket、上传文件以及生成下载链接。
from minio import Minio
client = Minio(
"localhost:9000",
access_key="minio",
secret_key="minio123",
secure=False
)
项目实际只需要用到其中两个操作:
上传文件
生成下载链接
下载链接使用 presigned_get_object() 生成。
它返回的是一个带有效期的 URL,用户拿到链接后可以直接下载文件,不需要知道 MinIO 的账号密码。
5. 文件传递方案
5.1 为什么需要打包
一个 Agent 任务不一定只生成一个文件。
例如:
工作目录/
├── result.docx
├── data.xlsx
├── image.png
└── output.pdf
如果逐个上传并生成链接,前端还需要处理多个文件。
因此这里直接将整个工作目录打包成一个 ZIP:
工作目录
↓
output.zip
↓
MinIO
↓
一个下载链接
用户只需要下载一个文件即可。
5.2 为什么放到 Agent 结束之后处理
文件上传不属于具体工具的功能。
例如 create_file 工具只负责创建文件,如果让它同时负责上传,就会让工具本身承担文件交付的工作。
因此将这部分放到 FileMiddleware 中,在 Agent 执行完成后统一处理。
6. 开始实现
6.1 添加 ZIP 压缩方法
在 utils/doc_utils 下增加 zip_files.py。
使用 Python 自带的 shutil 直接压缩目录:
import os
import shutil
def compress_dir(folder_path):
"""
将目录压缩成 zip
"""
output_path = shutil.make_archive(
base_name=folder_path,
format="zip",
root_dir=os.path.dirname(folder_path),
base_dir=os.path.basename(folder_path)
)
return output_path
这里没有额外引入压缩库,项目目前的需求比较简单,标准库就够用了。
6.2 添加 MinIO 配置
MinIO 的地址和认证信息放到 .env 中:
HOST_IP=localhost
MINIO_PORT=9000
MINIO_ACCESS_KEY=minio
MINIO_SECRET_KEY=minio123
然后在配置文件中统一读取:
HOST_IP = os.getenv("HOST_IP")
MINIO_PORT = os.getenv("MINIO_PORT")
MINIO_ENDPOINT = f"{HOST_IP}:{MINIO_PORT}"
MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY")
MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY")
这样代码中不需要直接写 MinIO 的连接信息,部署到其他环境时只需要修改环境变量。
6.3 封装 MinIO 连接
在 conn 下新建 minio_conn.py。
不直接在 Middleware 中操作 MinIO SDK,而是简单封装一层:
from minio import Minio
from base import configs as cfg
class MinioConn:
def __init__(self):
self.client = self.create_client()
self.bucket_name = "stores"
self._init_bucket()
def create_client(self):
return Minio(
cfg.MINIO_ENDPOINT,
access_key=cfg.MINIO_ACCESS_KEY,
secret_key=cfg.MINIO_SECRET_KEY,
secure=False
)
def _init_bucket(self):
if not self.client.bucket_exists(self.bucket_name):
self.client.make_bucket(self.bucket_name)
def upload_obj(self, object_name, file_path):
self.client.fput_object(
self.bucket_name,
object_name,
file_path
)
def gen_presigned_url(self, object_name):
return self.client.presigned_get_object(
self.bucket_name,
object_name
)
这里暂时只保留项目需要的两个方法:
upload_obj()
gen_presigned_url()
Bucket 在初始化时检查,不存在就自动创建。
7. 接入 FileMiddleware
MinIO 部分准备好之后,最后需要把它接到 Agent 的执行流程中。
首先在 FileMiddleware 初始化时创建 MinIO 连接:
class FileMiddleware(AgentMiddleware[CustomState]):
def __init__(self):
super().__init__()
self.mc = MinioConn()
然后利用 before_agent 和 aafter_agent 两个生命周期节点完成文件处理。
7.1 记录开始时间
在 before_agent 中记录当前任务的开始时间。
原因是工作目录可能本身就存在一些文件,不能每次 Agent 执行结束都把整个目录当成新文件。
因此需要知道:
Agent 开始执行之后,有没有产生新的文件。
7.2 Agent 执行结束后处理文件
主要逻辑放在 aafter_agent:
async def aafter_agent(self, state, runtime):
dir_path = rt.get_root_thread_dir()
# 判断任务期间是否产生新文件
if not await self._check_if_new_file(
dir_path,
state["start_work_time"]
):
return None
# 压缩目录
local_path = await asyncio.to_thread(
compress_dir,
dir_path
)
# 上传 MinIO
object_name = os.path.basename(local_path)
await asyncio.to_thread(
self.mc.upload_obj,
object_name,
local_path
)
# 生成下载链接
url = self.mc.gen_presigned_url(object_name)
return {
"messages": [
AIMessage(content="可通过如下地址下载:"),
AIMessage(content=url)
]
}
这里压缩和上传都属于比较明显的文件 I/O 操作,因此使用:
asyncio.to_thread()
执行,避免直接阻塞当前异步流程。
如果任务期间没有产生文件,则直接返回,不进行压缩和上传。
8. 测试
可以直接在 Agent Chat UI 中测试,例如:
做个 a.txt,随便写点什么
Agent 完成后,可以看到返回结果中多了一条下载链接。
整个过程实际上就是:
创建 a.txt
↓
Agent 执行结束
↓
检测到新文件
↓
工作目录压缩成 ZIP
↓
上传 MinIO
↓
生成 Presigned URL
↓
返回给前端
点击链接即可下载 Agent 生成的文件。
9. 修改后的目录
本次主要涉及以下几个文件:
.env
# MinIO 配置
base/
└── configs.py
# 读取 MinIO 配置
conn/
└── minio_conn.py
# MinIO 连接、上传、生成下载链接
content/
└── middles/
└── file_manager_middle.py
# Agent 执行结束后的文件处理
utils/
└── doc_utils/
└── zip_files.py
# 工作目录压缩
10. 小结
这一部分实际上没有改变 Agent 原来的文件处理方式,只是在原来的工作目录后面增加了一个文件交付流程:
Agent工作目录
↓
检测新文件
↓
ZIP
↓
MinIO
↓
Presigned URL
↓
用户
Agent 继续在自己的工作目录中操作文件,Middleware 负责在任务结束后把文件整理并交给用户。这样文件生成和文件传递就分开了。




