Agent 接收用户上传文件

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 读取文件

具体分为两部分:

  1. 修改 Agent Chat UI,使上传组件支持任意文件,并将文件信息放入 upload_files

  2. 在后端 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 开始执行之前,因此放在 FileMiddlewareabefore_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 处理。

相关推荐
知识分享小能手6 小时前
深度学习学习教程,从入门到精通,深度生成模型 —— 知识点详解与代码实现(20)
人工智能·深度学习·学习
hzxxxz7 小时前
26%的研发交给AI之后-人的位置换到了哪里
人工智能
m0_587383007 小时前
深圳24小时自助健身房系统软件开发实战:架构设计与部署指南
人工智能·数据挖掘·系统架构·需求分析
Joy T7 小时前
Spring AI 2.0 Agent 进阶:Memory、State 与 Context Engineering 常见技术全景
java·人工智能·后端·spring·agent入门·agent state
估值探索者7 小时前
【Python量化系统工程化 #08】关了 SSH 就停?systemd 让脚本开机自启 + 异常自动拉起
java·c++·人工智能·分类·数据挖掘
西柚研究生1234567 小时前
论文分析17:YOLOv11_UAVNet:无人机航拍图像专用目标检测算法
人工智能·python·深度学习·算法·目标检测
m4Rk_7 小时前
【论文阅读】Agent 记忆机制(74):CompassMem——从相似度检索走向事件图上的记忆导航
论文阅读·人工智能·学习·开源·github
镭封7 小时前
短剧多人对白怎么配?一人完成多角色配音的办法
人工智能·音视频·语音识别·媒体
小李不想当小白7 小时前
DMA直接存储器存取(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·分享