最近实习在做 AI Agent 的时候,出现了一个上传文件的场景:你要上传一个文件,让 AI 帮你处理这个文件。
那么在这个场景下,我生出来一个疑问:文件是怎么上传的?
一、上传文件示例
首先,我们来看这样一个例子:
我要对 agent 进行测试,所以我手动构造了一个模拟的"上传文件对象"。
代码如下:
ini
pdf_file = File(
content=pdf_content,
filename=pdf_info['filename'],
content_type='application/pdf'
)
response = self.agent.run(
prompt,
files=[pdf_file], # 正确的文件上传方式
stream=False
)
我们可以看到,构造的文件被上传了,我们的 Agent 也认识了我们的文件。
这类对象的本质,就是模仿浏览器发送给后端的文件结构。 简单了解常见的文件
二、Agno 服务端如何处理上传的文件
那么浏览器发送给后端,我们后端的 Agno 是如何处理这个文件的?
Agno 在服务端使用 FastAPI 来提供 REST 接口。
通过 AgentOS.get_app()
会得到一个内置的 FastAPI 应用。
默认情况下,发送给 Agent 的请求使用 POST /agents/{agent_id}/runs
这个端点,该接口要求 multipart/form-data
格式,其中必须包含 message
(文本消息)等字段,支持可选的 files
字段上传文件。
因此,上传接口由 FastAPI 定义,主要依赖 fastapi.UploadFile
类型来接收文件。
三、Agno Agent 如何识别并获取上传文件
在 FastAPI 收到文件后,框架会将它们封装为 UploadFile
对象(提供 .filename
、.content_type
、.file
等属性)。
Agno 的 AgentOS 运行时代码会根据文件的 MIME 类型进行分类处理:
- 对于图像(PNG/JPEG/WebP 等)、音频(WAV/MP3 等)、视频(MP4/WebM 等)文件,分别转换为内部的
Image
、Audio
、Video
对象(通常是把文件流编码为 Base64 形式); - 对 PDF、CSV、DOCX、TXT、JSON 等文档类型,则转换为通用的
File
对象。
总之,Agent 通过检查上传文件的内容类型,并将其读入内存或临时文件,再创建相应的媒体对象进行后续处理。
四、Agno Agent 文件存储在哪里
FastAPI 的 UploadFile
默认使用内存与临时文件混合存储:
- 小文件保存在内存;
- 大文件超过阈值后会写入临时文件系统(
SpooledTemporaryFile
)。
总体来说,文件首先存在 FastAPI 提供的临时存储中,开发者也可以自行保存到指定目录后续使用。
五、文件处理及后续使用
上传后,文件通常有两种用途:
1. 传给模型
注意:一定是多模态模型,不然会报错!
如果模型支持多媒体输入(如多模态模型),可以直接将 Image
/ Audio
等对象的 Base64 内容发送给模型。
这依赖配置变量 send_media_to_model
(默认为 True
),表示是否将媒体内容附加到模型上下文。
如果设置为 False
,则媒体文件不会传给模型,而是留待工具处理。
例如:我使用的 deepseek 不支持多模态,于是我将 send_media_to_model
设置为 False
,这样这个文件就不会直接发送给大模型,进而避免报错。
ini
pdf_translator_agent = Agent(
name="pdf_translator_agent",
model=TencentCloudDeepseek(
id="deepseek-chat",
base_url=config['api']['deepseek']['base_url'],
thinking_enabled=False,
),
tools=[
DocumentProcessingTools()
]
# 这个配置可以确保模型不会收到任何媒体文件,从而避免模型因为不支持 pdf 文件而报错
send_media_to_model=False,
)
2. 传给 Tool 或解析
Agno 支持在 Agent 中使用自定义工具处理文件。
例如,可以编写一个 @tool
函数,参数为文件路径或 File
对象,然后在其中使用 Python 代码打开并解析该文件(如对 PDF 进行文本提取、对图片做 OCR)。
这里给出这种方式的一种 tools 写法:
python
"""
PDF文档处理工具模块
包含PDF翻译所需的所有工具类和辅助类
"""
from typing import Optional, Sequence
from agno.tools import Toolkit
from agno.media import File
import subprocess
import os
import uuid
from datetime import datetime
class DocumentProcessingTools(Toolkit):
"""PDF文档处理工具集"""
def __init__(self):
super().__init__(
name="document_processing_tools",
tools=[
self.save_pdf_to_directory
]
)
def save_pdf_to_directory(
self,
files: Optional[Sequence[File]] = None,
target_directory: str = "/home/teams/pdf_translator/files"
) -> str:
"""
将用户上传的 PDF 文件保存到指定目录
这个工具可以访问用户上传的文件(自动注入),并将它们保存到文件系统中。
用户上传的文件最初只存在于内存中,必须先保存到文件系统,pdf2zh 才能处理。
Args:
files: 用户上传的文件列表(自动注入)
target_directory: 目标保存目录,默认为 /home/teams/pdf_translator/files
Returns:
保存结果信息,包括保存的文件列表和路径
Example:
用户上传 document.pdf
→ 自动保存到 /home/teams/pdf_translator/files/document.pdf
→ 返回保存成功的消息和完整路径
"""
if not files:
return "❌ 错误: 没有检测到上传的文件。请确保用户已上传 PDF 文件。"
print(f"\n{'='*60}")
print(f"💾 开始保存上传的文件")
print(f"📂 目标目录: {target_directory}")
print(f"📄 文件数量: {len(files)}")
print(f"{'='*60}\n")
# 确保目标目录存在
os.makedirs(target_directory, exist_ok=True)
saved_files = []
errors = []
for i, file in enumerate(files, 1):
try:
# 获取文件名
filename = file.filename if hasattr(file, 'filename') and file.filename else f"uploaded_file_{i}.pdf"
# 检查文件内容
if not file.content:
error_msg = f"文件 {i} ({filename}): 内容为空"
print(f"⚠️ {error_msg}")
errors.append(error_msg)
continue
# 构建完整路径
file_path = os.path.join(target_directory, filename)
# 获取文件大小
file_size = len(file.content)
file_size_mb = file_size / (1024 * 1024)
print(f"📝 正在保存文件 {i}/{len(files)}:")
print(f" 文件名: {filename}")
print(f" 大小: {file_size_mb:.2f} MB ({file_size:,} bytes)")
print(f" 路径: {file_path}")
# 写入文件
with open(file_path, 'wb') as f:
f.write(file.content)
# 验证文件已保存
if os.path.exists(file_path):
actual_size = os.path.getsize(file_path)
if actual_size == file_size:
print(f" ✅ 保存成功!验证通过")
saved_files.append({
'filename': filename,
'path': file_path,
'size': file_size
})
else:
error_msg = f"文件 {filename}: 大小不匹配 (期望: {file_size}, 实际: {actual_size})"
print(f" ❌ {error_msg}")
errors.append(error_msg)
else:
error_msg = f"文件 {filename}: 保存后无法找到"
print(f" ❌ {error_msg}")
errors.append(error_msg)
print()
except Exception as e:
error_msg = f"文件 {i}: 保存失败 - {str(e)}"
print(f"❌ {error_msg}")
errors.append(error_msg)
import traceback
traceback.print_exc()
# 生成返回消息
print(f"{'='*60}")
if saved_files:
result_lines = [
f"✅ 成功保存 {len(saved_files)} 个文件:\n"
]
for file_info in saved_files:
size_mb = file_info['size'] / (1024 * 1024)
result_lines.append(
f"📄 {file_info['filename']}\n"
f" 路径: {file_info['path']}\n"
f" 大小: {size_mb:.2f} MB\n"
)
if errors:
result_lines.append(f"\n⚠️ {len(errors)} 个文件失败:\n")
for error in errors:
result_lines.append(f" - {error}\n")
result = "".join(result_lines)
print(result)
print(f"{'='*60}\n")
return result
else:
error_result = f"❌ 所有文件保存失败\n\n错误详情:\n" + "\n".join(f"- {e}" for e in errors)
print(error_result)
print(f"{'='*60}\n")
return error_result
六、提示词的注意事项
要注意的是,要配合提示词一起使用,不然 AI 常常会不调用工具就直接返回「文件没上传」。
markdown
*当用户说"翻译PDF"或提到文件时,不要假设没有文件!**
- ✅ 必须立即调用 `save_pdf_to_directory` 工具
- ✅ 工具会自动检测是否有上传的文件
- ✅ 如果有文件,工具会保存并返回结果
- ✅ 如果没有文件,工具会返回错误消息
- ❌ 绝对不要在调用工具前就说 "没有检测到文件"
- ❌ 绝对不要要求用户 "上传文件" 而不先调用工具