1. 问题与目标
前面的工作已经实现了用户端文件上传,但目前文件主要用于用户侧操作。下一步需要让 Agent 本身能够接收这些文件。
对于通用 Agent 来说,文件不应该只限制在图片。例如用户上传一个 Word 文档,希望 Agent 阅读并分析其中的内容;或者上传 Excel、PDF、Markdown 等文件,再让 Agent 根据文件执行任务。
LangGraph CLI 创建的工程默认已经支持文件上传,但前端组件支持的类型主要是:
pjp, jfif, pjpeg, jpeg, jpg, png, gif, webp, pdf
这些类型本质上都是图片格式,PDF 在默认情况下也会作为图片处理。
如果项目只要求 Agent 接收图片,那么后端只需要将模型替换成支持视觉输入的 VLM 即可,其他部分基本不需要调整。
但本项目后续还需要处理 Word、Excel、PDF、Markdown 等文件,因此这里先解决一个更基础的问题:
让 Agent 能够接收任意类型的用户文件,并将文件保存到当前 Agent 工作目录。
文件内容的具体解析、转换和处理属于后续工作。
2. 实现方案
整体流程比较简单:
用户上传文件
↓
Agent Chat UI
↓
文件转换为 Base64
↓
AgentState.upload_files
↓
before_agent Middleware
↓
Base64 解码
↓
user_uploads/
↓
Agent 读取文件
具体分为两部分:
-
修改 Agent Chat UI,使上传组件支持任意文件,并将文件信息放入
upload_files。 -
在后端
before_agent中读取upload_files,将 Base64 文件恢复成实际文件。
3. 前端文件数据
前端接收到用户上传的文件后,不直接将二进制文件放进 JSON,而是使用 Base64 字符串表示文件内容。
例如,一个上传文件的数据结构如下:
{
"type": "file",
"mimeType": "image/jpeg",
"data": "DQoNCiMjIEFJIEFnZW50...",
"metadata": {
"filename": "a.md",
"size": 8238,
"lastModified": 1757389979338
}
}
其中:
-
type:固定为file -
mimeType:文件 MIME 类型 -
data:文件内容对应的 Base64 字符串 -
metadata.filename:原始文件名 -
metadata.size:文件大小 -
metadata.lastModified:文件最后修改时间
多个文件则组成一个列表,并通过 Agent State 传递:
from langchain.agents.middleware import AgentState
class CustomState(AgentState):
upload_files: list
3.1 为什么使用 Base64
Base64 的作用只是将二进制数据转换成字符串,方便放进 JSON 等文本格式中进行传输。
例如:
原始文件
↓
二进制数据
↓ Base64 编码
字符串
↓
JSON
↓
后端
↓ Base64 解码
原始文件
需要注意,Base64 不是加密,同时会使数据体积增加约三分之一,因此这里只把它作为前后端之间的传输方式,而不是文件存储方式。
4. 后端实现
4.1 Base64 文件处理
在 content/utils 下新增 base64_util.py,负责将 Base64 数据恢复成文件。
import base64
import os
def save_base64_file_from_content_block(content_block, save_dir):
"""
从前端文件内容块中提取 Base64 数据并保存为文件
"""
return save_base64_file(
base64_data=content_block["data"],
filename=content_block["metadata"]["filename"],
save_dir=save_dir
)
def save_base64_file(
base64_data: str,
filename: str,
save_dir: str
):
"""
将 Base64 数据保存为文件
"""
file_path = os.path.join(save_dir, filename)
file_data = base64.b64decode(base64_data)
with open(file_path, "wb") as f:
f.write(file_data)
return {
"success": True,
"file_path": str(file_path),
"filename": filename,
}
这里将具体的 Base64 解码逻辑独立出来,Middleware 只负责处理 Agent 生命周期和状态,不直接处理文件编码细节。
4.2 配置上传目录
在 base/configs.py 中增加:
USER_UPLOAD_PATH = "user_uploads"
后续如果需要调整目录,只修改配置即可,不需要再修改 Middleware。
4.3 在 Middleware 中处理上传文件
文件上传发生在 Agent 开始执行之前,因此放在 FileMiddleware 的 abefore_agent 中处理。
首先增加相关状态和依赖:
from base.configs import USER_UPLOAD_PATH
from content.utils import base64_util as bu
class CustomState(AgentState):
upload_files: list
start_work_time: float
file_update_time: float
然后修改 abefore_agent:
async def abefore_agent(self, state, runtime):
file_update_time = time.time()
# 当前线程对应的工作目录
dir_path = os.path.join(
rt.get_root_thread_dir(),
USER_UPLOAD_PATH
)
# 创建用户上传目录
await asyncio.to_thread(
os.makedirs,
dir_path,
exist_ok=True
)
files = state.get("upload_files")
if files:
content = "用户上传了如下文件,文件路径如下:"
for file in files:
# Base64 解码并保存文件
result = await asyncio.to_thread(
bu.save_base64_file_from_content_block,
file,
dir_path
)
# 返回给 Agent 相对路径
content += f"\n{rt.get_out_path(result['file_path'])}"
return {
"messages": [
ToolMessage(
content=content,
tool_call_id=get_uuid(),
name="file_upload"
)
],
"upload_files": None,
"start_work_time": time.time(),
"file_update_time": file_update_time
}
return {
"start_work_time": file_update_time,
"file_update_time": file_update_time
}
这里有两个需要注意的地方。
第一,文件操作没有直接阻塞异步流程。
os.makedirs 和文件写入属于同步 I/O,因此使用:
await asyncio.to_thread(...)
放到线程中执行,避免直接阻塞 Agent 的异步执行流程。
第二,处理完成后清空 upload_files。
文件已经落盘后,就没有必要继续将完整的 Base64 数据保存在 Agent State 中:
"upload_files": None
这样可以避免后续 Agent 状态继续携带较大的文件数据。
此时 Agent 实际拿到的是类似下面的信息:
用户上传了如下文件,文件路径如下:
./user_uploads/a.md
./user_uploads/test.pdf
./user_uploads/data.xlsx
后续 Agent 可以根据路径继续调用文件处理工具。
5. 测试
前端选择多个不同类型的文件进行上传,例如:
test.pdf
test.docx
data.xlsx
demo.md
image.png
上传后检查当前线程对应的工作目录:
<thread_work_dir>/
└── user_uploads/
├── test.pdf
├── test.docx
├── data.xlsx
├── demo.md
└── image.png
确认文件能够正常生成,并且文件内容与原文件一致,即可说明前后端文件传输链路已经打通。
6. 最终目录结构
本次只列出新增和修改的部分:
├─ base
│ └─ configs.py
│ # 添加 USER_UPLOAD_PATH
│
├─ content
│ ├─ middles
│ │ └─ file_manager_middle.py
│ │ # 处理 Agent 接收用户文件
│ │
│ └─ utils
│ ├─ runtime_util.py
│ └─ base64_util.py
│ # 新增,负责 Base64 文件解码
7. 小结
这一阶段没有处理文件内容,只解决了 Agent 接收文件 这个基础问题。
目前链路为:
前端上传任意文件
↓
Base64
↓
AgentState.upload_files
↓
FileMiddleware.abefore_agent
↓
Base64 解码
↓
当前线程 / user_uploads/
↓
Agent 获取文件路径
至此,Agent 已经可以接收任意类型的用户文件。
下一步只需要在这个基础上继续增加文件解析能力,例如:
Word / PDF / Markdown
↓
文本提取
↓
Markdown / 纯文本
↓
Agent 分析
而图片、Excel 等文件则可以根据实际类型交给对应的工具或 SubAgent 处理。