作为一名长期从事AI应用开发的工程师,我经常遇到开发者询问:"Agent到底是什么?如何从零开始构建一个?"今天,我将带你用最原始的方式,完全不用任何框架,仅用Java标准库手写一个功能完整的Agent。这种方式虽然"笨拙",但能让你看清Agent的每个齿轮如何转动。
Agent本质上是一个智能循环系统,它的核心工作流程被称为ReAct模式(Reasoning + Acting)。这个模式包含以下几个关键阶段:
这个循环会持续进行,直到任务完成为止。听起来简单?让我们用代码实现它。
确保你的环境满足:
提示:本文使用阿里云DashScope的兼容OpenAI接口,你也可以替换为其他兼容API端点。
我们将所有代码放在一个文件中,用内部类组织:
code复制SimpleAgent.java
├── Tool接口
├── WeatherTool(天气查询)
├── CalculatorTool(数学计算)
├── TimeTool(时间查询)
├── ChatHistory(对话历史管理)
├── SimpleAgent(核心逻辑)
└── main方法(入口)
这种单文件设计虽然不符合大型项目规范,但非常适合教学目的,让你一目了然地看到所有组件。
首先定义所有工具都必须实现的接口:
java复制interface Tool {
String name(); // 工具唯一标识
String description(); // 工具功能描述
String parametersSchema(); // 参数JSON Schema
String execute(Map<String, String> args); // 执行方法
}
这个设计遵循了以下原则:
java复制static class WeatherTool implements Tool {
@Override public String name() { return "get_weather"; }
@Override public String description() { return "查询指定城市的天气情况"; }
@Override public String parametersSchema() {
return """
{
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名称" }
},
"required": ["city"]
}
""";
}
@Override
public String execute(Map<String, String> args) {
String city = args.getOrDefault("city", "未知");
// 模拟数据 - 实际项目应调用天气API
Map<String, String> fakeWeather = Map.of(
"北京", "晴,15°C,东北风3级",
"上海", "多云,18°C,东风2级"
);
return fakeWeather.getOrDefault(city, city + ":晴,20°C,微风");
}
}
这个工具实现了完整的四则运算解析器:
java复制static class CalculatorTool implements Tool {
// ...省略接口方法...
private double evalSimple(String expr) {
expr = expr.replaceAll("\\s+", "");
return parseExpr(new int[]{0}, expr);
}
// 解析表达式
private double parseExpr(int[] pos, String s) {
double result = parseTerm(pos, s);
while (pos[0] < s.length() && (s.charAt(pos[0]) == '+' || s.charAt(pos[0]) == '-')) {
char op = s.charAt(pos[0]++);
double term = parseTerm(pos, s);
result = op == '+' ? result + term : result - term;
}
return result;
}
// 解析项
private double parseTerm(int[] pos, String s) {
double result = parseFactor(pos, s);
while (pos[0] < s.length() && (s.charAt(pos[0]) == '*' || s.charAt(pos[0]) == '/')) {
char op = s.charAt(pos[0]++);
double factor = parseFactor(pos, s);
result = op == '*' ? result * factor : result / factor;
}
return result;
}
// 解析因子
private double parseFactor(int[] pos, String s) {
if (s.charAt(pos[0]) == '(') {
pos[0]++;
double result = parseExpr(pos, s);
pos[0]++;
return result;
}
// ...处理数字...
}
}
这个解析器实现了完整的运算符优先级和括号处理,虽然不如第三方库强大,但足够演示用途。
java复制static class ChatHistory {
private final List<Map<String, Object>> messages = new ArrayList<>();
private final int maxTokens;
// 添加各种类型消息的方法
void addMessage(String role, String content) {
messages.add(Map.of("role", role, "content", content));
trim();
}
void addAssistantToolCall(JsonNode toolCallsNode) {
Map<String, Object> msg = new LinkedHashMap<>();
msg.put("role", "assistant");
msg.put("content", (Object) null);
msg.put("tool_calls", toolCallsNode);
messages.add(msg);
}
// 简单的token管理
private void trim() {
while (messages.size() > maxTokens / 200 && messages.size() > 2) {
if ("system".equals(messages.get(1).get("role"))) {
messages.remove(2);
} else {
messages.remove(1);
}
}
}
}
java复制String chat(String userInput) throws Exception {
history.addMessage("user", userInput);
int iterations = 0;
while (iterations < MAX_ITERATIONS) {
iterations++;
JsonNode response = callModel();
JsonNode choice = response.path("choices").get(0);
String finishReason = choice.path("finish_reason").asText();
if ("stop".equals(finishReason)) {
String content = choice.path("message").path("content").asText();
history.addMessage("assistant", content);
return content;
}
if ("tool_calls".equals(finishReason)) {
JsonNode toolCalls = choice.path("message").path("tool_calls");
history.addAssistantToolCall(toolCalls);
for (JsonNode toolCall : toolCalls) {
String toolCallId = toolCall.path("id").asText();
String toolName = toolCall.path("function").path("name").asText();
String argsJson = toolCall.path("function").path("arguments").asText();
String result = executeTool(toolName, argsJson);
history.addToolResult(toolCallId, toolName, result);
}
continue;
}
return "(回复被截断)";
}
return "(超过最大迭代次数)";
}
java复制private JsonNode callModel() throws Exception {
ObjectNode body = mapper.createObjectNode();
body.put("model", MODEL);
// 构建消息列表
ArrayNode messagesNode = mapper.createArrayNode();
for (Map<String, Object> msg : history.getMessages()) {
ObjectNode msgNode = mapper.createObjectNode();
msg.forEach((k, v) -> {
if (v instanceof JsonNode) {
msgNode.set(k, (JsonNode) v);
} else if (v != null) {
msgNode.put(k, v.toString());
} else {
msgNode.putNull(k);
}
});
messagesNode.add(msgNode);
}
body.set("messages", messagesNode);
// 构建工具定义
ArrayNode toolsNode = mapper.createArrayNode();
for (Tool tool : tools.values()) {
ObjectNode toolNode = mapper.createObjectNode();
toolNode.put("type", "function");
ObjectNode funcNode = mapper.createObjectNode();
funcNode.put("name", tool.name());
funcNode.put("description", tool.description());
funcNode.set("parameters", mapper.readTree(tool.parametersSchema()));
toolNode.set("function", funcNode);
toolsNode.add(toolNode);
}
body.set("tools", toolsNode);
// 发送HTTP请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(API_URL))
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + apiKey)
.POST(HttpRequest.BodyPublishers.ofString(body.toString()))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw new RuntimeException("API错误: " + response.body());
}
return mapper.readTree(response.body());
}
编译并运行:
bash复制export DASHSCOPE_API_KEY=your_key
javac SimpleAgent.java
java SimpleAgent
对话示例1 - 简单查询:
code复制你:北京天气如何?
助手:北京今天晴天,气温15°C,东北风3级
对话示例2 - 组合查询:
code复制你:上海和广州的气温差多少?
助手:上海18°C,广州22°C,相差4度
API连接失败
工具调用失败
上下文丢失
虽然我们的Agent已经能工作,但还有很大改进空间:
工具管理
性能优化
持久化
生产级特性
这个手写版本虽然简陋,但它清晰地展示了Agent的核心原理。在实际项目中,建议使用成熟的框架如Spring AI或LangChain4j,它们已经解决了上述大部分问题。