400 行 Java 代码手搓 AI Agent,ReAct 循环 + Tool Call,我跑起来了
大家好,我是二哥呀。
说实话,用 Claude Code、Qoder CLI 和 Codex 进行 AI编程也有一段时间了,我一直很好奇这玩意儿到底是怎么实现的。
它们看起来就是简单的命令行界面,但背后却能理解我的需求、调用各种工具、读写文件、执行命令,甚至能自己改代码。

这到底是什么原理?
今天这篇文章,我们就从零开始,用 Java 实现一个最简单的 Agent CLI。它能配置 GLM-5.1 的 API Key,能接收你的输入,有 Agent Loop,能 Tool Call,能根据上下文编程或输出内容。
一个超级简化版的 Claude Code,大概 400 行代码就能跑起来。

01、Agent 的核心原理
在动手写代码之前,先搞清楚 Agent 到底是什么东西。
Agent 的核心就三个东西:推理(Reasoning)、行动(Acting)、观察(Observing)。这三者循环起来,就是著名的 ReAct 模式。

流程是这样的:
- 你输入一个任务
- LLM 思考:我需要做什么?要不要调用工具?
- 如果需要工具,LLM 输出工具调用请求
- Agent 执行工具,拿到结果
- 把结果喂给 LLM,继续思考
- 重复 2-5,直到 LLM 觉得任务完成
- 输出最终结果
这个循环就是 Agent 的灵魂。Claude Code、Qoder CLI、OpenClaw,本质上都是这个循环的不同实现。
02、项目结构
我们用 Java 17 + Maven 来搭建项目,不依赖任何第三方 Agent 框架,从零手写。
paicli/
├── pom.xml
├── .env
└── src/main/java/com/paicli/
├── cli/Main.java # 入口类
├── agent/Agent.java # Agent 核心(ReAct 循环)
├── llm/GLMClient.java # GLM-5.1 API 客户端
└── tool/ToolRegistry.java # 工具注册表

Maven 依赖
我们需要这几个依赖:
- Jackson:处理 JSON
- OkHttp:发送 HTTP 请求
- SLF4J:简单日志
com.fasterxml.jackson.core
jackson-databind
2.16.0
com.squareup.okhttp3
okhttp
4.12.0
org.slf4j
slf4j-simple
2.0.9
03、GLM-5.1 API 客户端
Agent 要能理解我们的提示词,得能调用大模型。所以我们需要先封装一个 GLMClient,支持普通对话和工具调用。
public class GLMClient {
private static final String API_URL =
"https://open.bigmodel.cn/api/paas/v4/chat/completions";
private static final String MODEL = "glm-5.1";
private final String apiKey;
private final OkHttpClient httpClient;
public GLMClient(String apiKey) {
this.apiKey = apiKey;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.build();
}
}
核心是 chat 方法,它接收消息历史和一个工具列表,返回 LLM 响应。
消息格式
GLM-5.1 的 API 兼容 OpenAI 格式,消息有三种角色:
system:系统提示,定义 Agent 的身份和能力user:用户输入assistant:助手回复,可以包含文本或工具调用tool:工具执行结果
public record Message(String role, String content,
List toolCalls, String toolCallId) {
public static Message system(String content) {
return new Message("system", content, null, null);
}
public static Message user(String content) {
return new Message("user", content, null, null);
}
public static Message assistant(String content) {
return new Message("assistant", content, null, null);
}
public static Message tool(String toolCallId, String content) {
return new Message("tool", content, null, toolCallId);
}
}
工具定义格式
要让 LLM 知道有哪些工具可用,需要按照特定格式定义工具:
public record Tool(String name, String description, JsonNode parameters) {}
parameters 是一个 JSON Schema,描述工具需要哪些参数、参数类型是什么。比如 write_file 工具的参数定义:
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "文件路径"
},
"content": {
"type": "string",
"description": "文件内容"
}
},
"required": ["path", "content"]
}
LLM 会根据这个定义,在需要时生成正确的参数。
完整的 chat 方法
public ChatResponse chat(List messages, List tools)
throws IOException {
// 构建请求体
ObjectNode requestBody = mapper.createObjectNode();
requestBody.put("model", MODEL);
// 添加消息历史
ArrayNode messagesArray = requestBody.putArray("messages");
for (Message msg : messages) {
ObjectNode msgNode = messagesArray.addObject();
msgNode.put("role", msg.role());
msgNode.put("content", msg.content());
// 如果有工具调用,序列化 tool_calls
if (msg.toolCalls() != null && !msg.toolCalls().isEmpty()) {
ArrayNode toolCallsArray = msgNode.putArray("tool_calls");
for (ToolCall tc : msg.toolCalls()) {
ObjectNode tcNode = toolCallsArray.addObject();
tcNode.put("id", tc.id());
tcNode.put("type", "function");
ObjectNode functionNode = tcNode.putObject("function");
functionNode.put("name", tc.function().name());
functionNode.put("arguments", tc.function().arguments());
}
}
// 如果是工具结果,添加 tool_call_id
if (msg.toolCallId() != null) {
msgNode.put("tool_call_id", msg.toolCallId());
}
}
// 添加工具定义
if (tools != null && !tools.isEmpty()) {
ArrayNode toolsArray = requestBody.putArray("tools");
for (Tool tool : tools) {
ObjectNode toolNode = toolsArray.addObject();
toolNode.put("type", "function");
ObjectNode functionNode = toolNode.putObject("function");
functionNode.put("name", tool.name());
functionNode.put("description", tool.description());
functionNode.set("parameters", tool.parameters());
}
}
// 发送 HTTP 请求
RequestBody body = RequestBody.create(
requestBody.toString(),
MediaType.parse("application/json")
);
Request request = new Request.Builder()
.url(API_URL)
.header("Authorization", "Bearer " + apiKey)
.post(body)
.build();
// 解析响应
try (Response response = httpClient.newCall(request).execute()) {
String responseBody = response.body().string();
JsonNode root = mapper.readTree(responseBody);
// 提取消息内容、工具调用、token 使用等信息
// ...
}
}
这里有个关键点:工具调用。
当 LLM 决定调用工具时,它会返回一个 tool_calls 数组,包含工具名和参数。Agent 执行完工具后,要把结果以 tool 角色的消息返回给 LLM,这样 LLM 才能继续思考。
这个往返过程是 ReAct 模式的核心。
LLM 不直接执行任务,而是通过工具调用来“行动”,然后观察行动结果,再决定下一步。这种分离能让 LLM 专注于推理,工具专注于执行,各司其职。
为什么用 GLM-5.1
选择 GLM-5.1 有几个原因:
第一,工具调用支持好。GLM-5.1 对 Function Calling 的支持非常稳定,能准确理解工具定义,生成正确的参数。
第二,刚好我是max会员。哈哈哈。
当然,这个框架很容易扩展到其他模型。只要模型支持 OpenAI 格式的 API,改一下 API_URL 和 MODEL 就能用。
04、工具注册表
Agent 要能干实事,得有一套工具。我们实现几个最基础的工具:
read_file:读取文件write_file:写入文件list_dir:列出目录execute_command:执行 Shell 命令create_project:创建项目结构
public class ToolRegistry {
private final Map tools = new HashMap<>();
public ToolRegistry() {
registerFileTools();
registerShellTools();
registerCodeTools();
}
}
工具的定义
每个工具包含四部分:名字、描述、参数定义、执行逻辑。
public record Tool(
String name,
String description,
Jso...
热门评论
14 条评论
回复