手撸一个桌面文件整理 Agent:让你的电脑学会自己收拾
告别混乱的桌面!这篇教程带你从零实现一个能整理文件的智能体,包含文件扫描、分类、移动、重命名等实用工具,让你的电脑学会自己收拾。
写在前面
如果你读过前面的通用的大模型客户端封装,你已经掌握了如何让你的程序真正"思考"。
但是,一个只会聊天的程序,就像一个只会纸上谈兵的军师——它能理解你,指点你,却不能帮你做事儿。
今天,我们来给智能体装上"手"和"脚",让它能够真正操作你的电脑。
想象一下:你的桌面乱七八糟,文件散落一地。你只需要对智能体说一句"帮我整理一下桌面",它就自动帮你:
- 扫描桌面上的所有文件
- 让大模型根据文件类型(文档、图片、视频、代码……)做分类决策
- 创建对应的文件夹
- 把文件一个个搬进去
全程自主决策,不需要你写分类规则。
这不是科幻,这是今天我们要一起实现的东西。
你将学到什么
- 工具系统设计:如何让智能体"学会"使用工具
- 文件操作工具:扫描、移动、重命名、删除文件
- Agent 循环:让大模型自己决定调用什么工具,实现自循环、自验证
前置知识
如果你还没有读过以下文章,建议先阅读:
- Agent开发入门——从理解到实践 - 理解 Agent 的基本概念
- 接入大模型:从第一个 Hello World 到多模态对话 - 了解如何调用大模型
- 给你的智能体换一颗温暖的大脑:封装 LLM Client,让思考真正发生 - 掌握 LLM Client 和记忆管理
说明:本文使用 Gson 做 JSON 序列化,使用 Java 11+ 内置的
HttpClient发送 HTTP 请求。除此之外没有任何第三方依赖。
第一部分:先看全局
整个系统只有一条核心链路:
你说话 → Agent 思考该调哪个工具 → 工具操作真实文件 → 结果返回给 Agent → Agent 再想下一步
这就是 ReAct 模式(Reasoning + Acting):想一步,做一步,看一步,再想。
你丢给它一个乱七八糟的目录路径,它会:
- 扫描目录里的所有文件
- 让大模型根据文件类型做分类决策
- 创建对应的文件夹
- 把文件一个个搬进去
如果文件名太丑,它还能帮你重命名。
代码结构
一共就 两个文件:
desktop-agent/
├── DesktopAgent.java ← 全部逻辑,约 400 行
└── lib/
└── gson-2.11.0.jar ← 唯一的依赖
工具不是接口、不是抽象类,就是一个个普通的 Java 方法。工具描述是写死的 JSON 字符串,直接塞给大模型。能跑通才是硬道理。
第二部分:理解工具系统
什么是 Function Calling/Tools?
简单来说,Function Calling/Tools 就是让大模型能够"调用函数"。 在这里我就不细说 Function Calling,因为这不是本篇文章的重点,但 Function Calling 这个概念非常重要,你可以通过阅读 Function Calling 详解来了解它。
第三部分:完整代码
下面是 DesktopAgent.java 的完整代码。我会在关键位置加上注释,但你也可以先整体看一遍,再回头研究细节。
编译和运行:
# 下载 Gson(唯一的依赖) curl -L -o gson.jar https://repo1.maven.org/maven2/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar # 编译 javac -cp .:gson.jar DesktopAgent.java # 运行(默认整理桌面) java -cp .:gson.jar DesktopAgent # 或者指定目录 java -cp .:gson.jar DesktopAgent /tmp/test-desktopWindows 用户请把
:换成;。
import com.google.gson.*;
import java.io.*;
import java.net.http.*;
import java.net.URI;
import java.nio.file.*;
import java.text.SimpleDateFormat;
import java.util.*;
public class DesktopAgent {
// ============================================
// 配置项:改成你自己的 API Key 和地址
// ============================================
private static final String API_KEY = "sk-xxxxxxxxxxxxxxxx";
private static final String API_URL = "https://api.deepseek.com/v1/chat/completions";
private static final String MODEL = "deepseek-chat";
private static final int MAX_STEPS = 15;
private static final Gson gson = new Gson();
// ============================================
// 工具定义:告诉大模型你有哪些"手"
// 格式是 OpenAI Function Calling 标准
// ============================================
private static final List<String> TOOL_DEFINITIONS = List.of(
"""
{
"type": "function",
"function": {
"name": "list_files",
"description": "列出指定目录下的所有文件和子目录,返回文件名、大小和修改时间",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "目录路径"
}
},
"required": ["path"]
}
}
}
""",
"""
{
"type": "function",
"function": {
"name": "create_folder",
"description": "创建文件夹(如果不存在)。支持多级目录",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要创建的文件夹路径"
}
},
"required": ["path"]
}
}
}
""",
"""
{
"type": "function",
"function": {
"name": "move_file",
"description": "移动文件到目标路径。如果目标目录不存在会自动创建",
"parameters": {
"type": "object",
"properties": {
"source": {
"type": "string",
"description": "源文件路径"
},
"destination": {
"type": "string",
"description": "目标文件路径(含文件名)"
}
},
"required": ["source", "destination"]
}
}
}
""",
"""
{
"type": "function",
"function": {
"name": "rename_file",
"description": "重命名文件或文件夹",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件当前路径"
},
"new_name": {
"type": "string",
"description": "新的文件名(不含目录路径)"
}
},
"required": ["path", "new_name"]
}
}
}
""",
"""
{
"type": "function",
"function": {
"name": "delete_file",
"description": "删除文件或文件夹。注意:此操作不可撤销!",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要删除的文件或文件夹路径"
},
"confirm": {
"type": "boolean",
"description": "必须为 true 才执行删除"
}
},
"required": ["path", "confirm"]
}
}
}
""",
"""
{
"type": "function",
"function": {
"name": "get_file_type",
"description": "根据文件扩展名判断文件类型分类,返回:文档、图片、视频、音频、代码、压缩包、安装包、其他",
"parameters": {
"type": "object",
"properties": {
"filename": {
"type": "string",
"description": "文件名(含扩展名)"
}
},
"required": ["filename"]
}
}
}
"""
);
// ============================================
// 主函数:程序入口
// ============================================
public static void main(String[] args) {
String dir = args.length > 0
? args[0]
: System.getProperty("user.home") + "/Desktop";
System.out.println("桌面整理 Agent 已启动");
System.out.println("目标目录:" + dir);
System.out.println();
Scanner scanner = new Scanner(System.in);
// 对话历史:第一条是系统提示,告诉大模型它的角色
List<Map<String, Object>> messages = new ArrayList<>();
messages.add(Map.of(
"role", "system",
"content", """
你是一个桌面文件整理助手。
你的能力:扫描文件、创建文件夹、移动文件、重命名文件、删除文件、识别文件类型。
工作流程:
1. 先用 list_files 扫描目录
2. 用 get_file_type 判断每个文件的类型
3. 制定分类方案,告诉用户你要怎么做,等用户确认
4. 创建文件夹、移动文件
5. 给出整理报告
注意:
- 不要删除重要文件,删除前一定要用户确认
- 整理方案先告诉用户,等用户说"继续"或"可以"后再执行
"""
));
System.out.println("你:(输入你的需求,输入 quit 退出)");
while (true) {
System.out.print("你:");
String input = scanner.nextLine().trim();
if ("quit".equalsIgnoreCase(input)) break;
if (input.isEmpty()) continue;
messages.add(Map.of("role", "user", "content", input));
String reply = agentLoop(messages);
System.out.println("助手:" + reply);
System.out.println();
}
scanner.close();
}
// ============================================
// Agent 循环:这就是智能体的核心
//
// 不断地:调大模型 → 有工具调用就执行 → 把结果送回去 → 再调
// 直到大模型返回纯文本(不再需要调工具)
// ============================================
private static String agentLoop(List<Map<String, Object>> messages) {
for (int step = 0; step < MAX_STEPS; step++) {
// 1) 把对话历史 + 工具定义一起发给大模型
JsonObject body = buildRequestBody(messages);
JsonObject resp = callLlm(body);
JsonObject msg = resp.getAsJsonArray("choices")
.get(0).getAsJsonObject()
.getAsJsonObject("message");
// 2) 如果大模型没有调用工具,任务完成,返回文本
if (!msg.has("tool_calls")
|| msg.get("tool_calls").isJsonNull()) {
String content = msg.has("content")
&& !msg.get("content").isJsonNull()
? msg.get("content").getAsString()
: "";
// 把助手的最终回复加入对话历史
messages.add(Map.of("role", "assistant", "content", content));
return content;
}
// 3) 大模型要求调用工具
// 先把助手的消息(含 tool_calls)加入对话历史
messages.add(Map.of(
"role", "assistant",
"tool_calls", msg.getAsJsonArray("tool_calls"),
"content", msg.get("content") // 可能是 null
));
// 4) 逐个执行工具,把结果加入对话历史
for (JsonElement tcElem : msg.getAsJsonArray("tool_calls")) {
JsonObject tc = tcElem.getAsJsonObject();
String callId = tc.get("id").getAsString();
JsonObject func = tc.getAsJsonObject("function");
String funcName = func.get("name").getAsString();
JsonObject args = JsonParser.parseString(
func.get("arguments").getAsString()
).getAsJsonObject();
// 执行
String result = executeTool(funcName, args);
// 在终端打印执行情况(方便你看清楚发生了什么)
System.out.println(" 🔧 " + funcName + " → "
+ (result.length() > 100
? result.substring(0, 100) + "..."
: result));
// 把工具执行结果加入对话历史
messages.add(Map.of(
"role", "tool",
"tool_call_id", callId,
"content", result
));
}
// 循环继续 → 大模型会看到工具结果,继续思考
}
return "(达到最大思考轮次,任务可能未完成)";
}
// ============================================
// 工具执行:根据名字分发到对应的 Java 方法
// ============================================
private static String executeTool(String name, JsonObject args) {
try {
return switch (name) {
case "list_files"
-> toolListFiles(args.get("path").getAsString());
case "create_folder"
-> toolCreateFolder(args.get("path").getAsString());
case "move_file"
-> toolMoveFile(
args.get("source").getAsString(),
args.get("destination").getAsString());
case "rename_file"
-> toolRenameFile(
args.get("path").getAsString(),
args.get("new_name").getAsString());
case "delete_file"
-> toolDeleteFile(
args.get("path").getAsString(),
args.get("confirm").getAsBoolean());
case "get_file_type"
-> toolGetFileType(args.get("filename").getAsString());
default -> "未知工具:" + name;
};
} catch (Exception e) {
return "工具执行出错:" + e.getMessage();
}
}
// ============================================
// 6 个工具方法:每个就是一个普通的 Java 方法
// ============================================
/** 工具 1:列出目录里的所有文件 */
private static String toolListFiles(String path) {
File dir = new File(path);
if (!dir.exists() || !dir.isDirectory())
return "目录不存在:" + path;
File[] files = dir.listFiles();
if (files == null || files.length == 0)
return "目录为空:" + path;
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm");
StringBuilder sb = new StringBuilder();
sb.append("共 ").append(files.length).append(" 个项目:\n");
for (File f : files) {
sb.append(f.isDirectory() ? "[目录]" : "[文件]")
.append(" ").append(f.getName())
.append(" | ").append(formatSize(f.length()))
.append(" | ").append(fmt.format(new Date(f.lastModified())))
.append("\n");
}
return sb.toString();
}
/** 工具 2:创建文件夹 */
private static String toolCreateFolder(String path) {
File folder = new File(path);
if (folder.exists()) return "文件夹已存在:" + path;
return folder.mkdirs()
? "已创建:" + path
: "创建失败,请检查权限:" + path;
}
/** 工具 3:移动文件 */
private static String toolMoveFile(String source, String destination) {
File src = new File(source);
if (!src.exists()) return "源文件不存在:" + source;
File dest = new File(destination);
File destDir = dest.getParentFile();
if (destDir != null && !destDir.exists()) destDir.mkdirs();
try {
Files.move(src.toPath(), dest.toPath(),
StandardCopyOption.REPLACE_EXISTING);
return "移动成功:" + src.getName() + " → " + destination;
} catch (Exception e) {
return "移动失败:" + e.getMessage();
}
}
/** 工具 4:重命名 */
private static String toolRenameFile(String path, String newName) {
File file = new File(path);
if (!file.exists()) return "文件不存在:" + path;
File renamed = new File(file.getParent(), newName);
if (renamed.exists()) return "目标名已存在:" + newName;
return file.renameTo(renamed)
? "重命名成功:" + file.getName() + " → " + newName
: "重命名失败,请检查权限";
}
/** 工具 5:删除(需要 confirm=true) */
private static String toolDeleteFile(String path, boolean confirm) {
if (!confirm) return "未确认,取消删除";
File file = new File(path);
if (!file.exists()) return "文件不存在:" + path;
if (file.isDirectory()) {
deleteDir(file);
return "已删除目录:" + path;
}
return file.delete()
? "已删除:" + path
: "删除失败";
}
private static void deleteDir(File dir) {
File[] items = dir.listFiles();
if (items != null)
for (File f : items)
if (f.isDirectory()) deleteDir(f); else f.delete();
dir.delete();
}
/** 工具 6:根据扩展名判断文件类型 */
private static String toolGetFileType(String filename) {
String ext = "";
int dot = filename.lastIndexOf('.');
if (dot > 0 && dot < filename.length() - 1)
ext = filename.substring(dot + 1).toLowerCase();
return switch (ext) {
case "doc","docx","pdf","txt","rtf",
"xls","xlsx","ppt","pptx","csv"
-> "文档";
case "jpg","jpeg","png","gif","bmp",
"svg","webp","ico","tiff"
-> "图片";
case "mp4","avi","mov","mkv","wmv","flv"
-> "视频";
case "mp3","wav","flac","aac","ogg","m4a"
-> "音频";
case "java","py","js","ts","html","css",
"cpp","c","go","rs","rb","php","sh","sql",
"json","xml","yaml","yml"
-> "代码";
case "zip","rar","7z","tar","gz","bz2"
-> "压缩包";
case "exe","msi","app","dmg","deb","rpm"
-> "安装包";
default -> "其他";
};
}
// ============================================
// 构建发送给大模型的请求体
// ============================================
private static JsonObject buildRequestBody(
List<Map<String, Object>> messages) {
JsonObject body = new JsonObject();
body.addProperty("model", MODEL);
body.add("messages", gson.toJsonTree(messages));
// 工具定义:把 JSON 字符串解析后组成 JSON 数组
JsonArray tools = new JsonArray();
for (String def : TOOL_DEFINITIONS)
tools.add(JsonParser.parseString(def));
body.add("tools", tools);
return body;
}
// ============================================
// 调用大模型 API
// ============================================
private static JsonObject callLlm(JsonObject body) {
try {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + API_KEY)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
HttpResponse<String> resp = HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString());
return JsonParser.parseString(resp.body()).getAsJsonObject();
} catch (Exception e) {
throw new RuntimeException("调用大模型失败:" + e.getMessage(), e);
}
}
// ============================================
// 工具方法:格式化文件大小
// ============================================
private static String formatSize(long bytes) {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
if (bytes < 1024L * 1024 * 1024)
return String.format("%.1f MB", bytes / (1024.0 * 1024));
return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024));
}
}
就是这一个文件,大约 400 行。你可以复制粘贴,改一下 API_KEY,编译运行。
第四部分:它是怎么运转的
整个程序的核心只有 一个循环——agentLoop() 方法。让我用一个具体场景解释它的工作过程。
假设你对它说:"帮我整理桌面,路径是 /tmp/test-desktop"
第一轮:扫描
- Agent 把你的消息和工具定义发给大模型
- 大模型回复:"我需要先扫描目录",并请求调用
list_files(path="/tmp/test-desktop") - Agent 执行工具,得到文件列表
- 工具结果被送回对话历史
🔧 list_files → 共 9 个项目:
[文件] report.pdf | 156 KB | 2025-05-10
[文件] notes.txt | 3.2 KB | 2025-05-15
[文件] photo1.jpg | 4.8 MB | 2025-04-01
[文件] demo.py | 2.1 KB | 2025-05-16
...
第二轮:制定方案
大模型看到了文件列表,思考后回复一个整理方案,并等你确认(这是系统提示要求的)。
第三轮起:执行
你说"可以",大模型开始连续调用工具:create_folder 创建分类目录,然后 move_file 逐个移动文件。每一轮它都能看到上一步的结果,所以如果有文件移动失败,它会自动处理。
最后一轮:汇报
大模型判断活干完了,返回一段整理报告——不再是工具调用,而是纯文本。循环结束。
关键设计:为什么不让程序自动执行,而要等用户确认?
因为移动和删除文件是不可逆操作。通过在系统提示里要求"先告知方案,等用户确认",我们在大模型层面加了一道安全阀。这比在代码里写 if 判断更灵活——大模型能理解什么情况需要确认,什么情况可以直接做。
第五部分:实际运行效果
准备测试目录
mkdir -p /tmp/test-desktop
touch /tmp/test-desktop/{report.pdf,notes.txt,data.xlsx}
touch /tmp/test-desktop/{photo.jpg,screenshot.png}
touch /tmp/test-desktop/demo.mp4
touch /tmp/test-desktop/{Main.java,index.html}
touch /tmp/test-desktop/mystery.xyz
运行效果
桌面整理 Agent 已启动
目标目录:/tmp/test-desktop
你:帮我整理一下这个目录
🔧 list_files → 共 9 个项目:[文件] report.pdf | 156 KB ...
助手:我扫描了目录,发现 9 个文件。建议按以下方式分类:
📁 文档:report.pdf, notes.txt, data.xlsx
📁 图片:photo.jpg, screenshot.png
📁 视频:demo.mp4
📁 代码:Main.java, index.html
📁 其他:mystery.xyz
是否继续?
你:可以
🔧 create_folder → 已创建:/tmp/test-desktop/文档
🔧 create_folder → 已创建:/tmp/test-desktop/图片
🔧 create_folder → 已创建:/tmp/test-desktop/视频
🔧 create_folder → 已创建:/tmp/test-desktop/代码
🔧 create_folder → 已创建:/tmp/test-desktop/其他
🔧 move_file → 移动成功:report.pdf → /tmp/test-desktop/文档/report.pdf
🔧 move_file → 移动成功:photo.jpg → /tmp/test-desktop/图片/photo.jpg
...
助手:整理完成!创建了 5 个文件夹,移动了 9 个文件。
整理后的目录结构
/tmp/test-desktop/
├── 文档/
│ ├── report.pdf
│ ├── notes.txt
│ └── data.xlsx
├── 图片/
│ ├── photo.jpg
│ └── screenshot.png
├── 视频/
│ └── demo.mp4
├── 代码/
│ ├── Main.java
│ └── index.html
└── 其他/
└── mystery.xyz
第六部分:代码拆解——三个核心
整个程序 400 行,但真正需要你理解的核心只有三个部分。
核心 1:工具定义(写死的 JSON)
TOOL_DEFINITIONS 是一个 JSON 字符串列表,每个元素描述一个工具的名字、用途和参数。这些 JSON 会原样发给大模型,告诉它"你可以用这些工具"。
格式是 OpenAI Function Calling 标准,几乎所有主流大模型都兼容这个格式。你只需要写一次,换任何模型都能用。
核心 2:工具执行(switch 分发)
executeTool() 就是一个 switch-case:大模型说调 list_files,就调 Java 的 toolListFiles() 方法;说调 move_file,就调 toolMoveFile()。
每个工具方法就是一个普通的 Java 方法,读参数、操作文件、返回字符串。没有任何特殊接口,没有任何继承关系。
核心 3:Agent 循环(agentLoop)
这是最精彩的部分。整个逻辑可以浓缩为一句话:
调大模型 → 有工具调用就执行 → 把结果送回去 → 再调。直到大模型不再需要调工具。
这个循环之所以强大,是因为大模型在每一轮都能看到所有历史消息——包括之前的工具调用和工具结果。这意味着它能做连续的、有上下文的决策:
- 看到文件列表 → 决定分类方案
- 看到文件夹创建成功 → 开始移动文件
- 看到某个文件移动失败 → 自动跳过或重试
- 看到所有文件都处理完 → 生成整理报告
这就是 Agent 的"自循环、自验证"——它不需要你写 if-else 来规定流程,它自己看着办。
第七部分:扩展与优化
我们的桌面整理 Agent 已经可以工作了,但还有很多可以改进的地方。我来说几个例子,然后你可以试着实现,当然你也可以举一反三。自由的给他扩展完善。
还记得我最早学编程的时候,老师讲到一半,我脑海里总是会冒出各种稀奇古怪的花式玩法,总是迫不及待的想试一试。现在我把这个机会留给你,你可以试着实现下面的迭代。
支持交互式确认
如果你想让 Agent 在执行每个移动操作前都先问你,可以在系统提示里加上:
每移动一个文件前,先告诉用户你要做什么,等用户说"可以"再执行。
一个提示词的改变,行为就完全不同——这就是大模型驱动的灵活性。
添加撤销功能
你可以在工具执行后记录日志,然后添加一个 "undo_move" 工具,让大模型在用户说"撤销"时,把文件移回原来的位置。
支持自定义分类
你可以在系统提示里加上用户偏好:
用户的工作文件以"周报"、"会议"开头,请把它们归到"工作"分类,而不是"文档"。
不需要写代码,只需要修改提示词,大模型就会照做。
性能优化
对于大量文件(几百个以上),可以考虑:
- 并行移动文件(使用
ExecutorService) - 批量处理(一次请求移动多个文件)
- 只处理变化的文件(增量扫描)
这个 400 行的程序是一个完整的 Agent。你已经掌握了 Agent 开发的核心套路:
- 定义工具 —— 写 JSON 描述 + 实现 Java 方法
- 写循环 —— 调大模型 → 执行工具 → 送结果 → 再调
- 结束 —— 大模型返回纯文本,循环退出
同样的套路,你可以做很多事:
- 把
toolListFiles换成查数据库 → 数据库查询 Agent - 加上发送邮件的工具 → 邮件助理 Agent
- 加上读写 Excel 的工具 → 数据分析 Agent
- 加上调 GitHub API 的工具 → 代码审查 Agent
变化的只是工具,不变的是那个循环。
最后的话
回到最初的问题:一个大模型能帮你做什么?
在没有工具之前,它只能帮你"想"。有了工具之后,它能帮你"做"。而把这两样连起来的,就是一个不到 400 行的循环。
这不是什么高深的架构,不是什么复杂的框架。它就是一个循环、几个方法、一堆 JSON。但就是这么朴素的东西,让一个 AI 真正走进了你的电脑,帮你做了一件具体的事。
去试试吧。你的桌面也在等你。
动手试试吧! 当你看到你的桌面被自动整理得井井有条时,你会感受到一种奇妙的成就感。那不仅仅是技术的成功,更是你用心创造的价值。