Java后端开发AI Agent:工程底座才是真正难点
2026/9/11 4:54:48 网站建设 项目流程

AI Agent 开发是当前后端开发者讨论最多的话题之一。我遇到过不少带着“想转 AI 应用开发”想法来做 Java 面试辅导的同学,也接过许多号称要做 Agent 项目但实际上第一步就卡在 Maven 依赖上的咨询。做了大量一对一辅导和代码问题排查后,我的判断没有变:Agent 开发真正的难点不在提示词,而在工程底座。下面以 Java 后端的视角,拆解 Agent 开发需要的 Java 知识点,给出一个最小可运行的日志分析 Agent 示例,并把高概率出现的报错和排查路径列出来。

1. 先搞清楚:Agent 开发真正考验的是工程底座,不是提示词技巧

1.1 为什么“AI Agent 辅导”听起来热门,却很难直接交付

AI Agent 被很多文章描述成一个“只要会和大模型对话,就能做出智能应用”的方向。实际接触过 Agent 项目的人会很清楚,一个可交付的 Agent 至少要解决这些问题:

  • 用户输入如何进入系统,如何做校验和会话隔离。
  • Agent 如何选择工具,调用 Elasticsearch、数据库、外部接口后如何拿到结构化结果。
  • 大模型返回的 JSON 如何解析,解析失败如何处理。
  • 单次调用超时怎么办,多个工具并发调用如何控制线程池。
  • 上下文怎么保存,重启之后会话记忆是否还在。
  • 模型调用消耗多少 Token,哪一轮调用导致线上故障,如何通过日志还原。

这些能力没有一样是提示词能替代的。纯模板式“Agent 辅导”只能帮助刚入门的人理解什么是 Agent、怎么跑通一个模型调用 demo。一旦进入企业私有化部署、日志检索、权限控制、稳定性治理,问题最终还是落在 Java 后端基本功上。

1.2 Agent 开发到底由哪几部分组成

把一个 Agent 系统拆开看,通常包含五层:

  • 模型层:负责与 LLM 推理服务通信,通常是一个 HTTP 接口,输入 messages,输出 text 或 tool_calls。
  • 工具层:给 Agent 提供真实能力,例如查询日志、执行 SQL、调用订单接口。
  • 记忆层:保存短期对话上下文和长期业务记忆,常见载体是 Redis、MySQL 或向量数据库。
  • 规划层:决定下一步调用哪个工具,可以是一个提示词模板,也可以是一段状态机逻辑。
  • 执行与观测层:真正执行工具调用,记录每一步的输入、输出、耗时、错误和 Token 消耗。

这些层用 Java 实现时,对应技术非常明确。模型层用 HttpClient 或 Feign;工具层用 Elasticsearch Java API Client、RedisTemplate、MyBatis;记忆层用 Redis 和 MySQL;规划层是一段可测试的 Java 逻辑;执行层需要线程池、异步任务和结构化日志。只看框架概念,不看这些工程组件,很难独立完成一个上线的 Agent 服务。

1.3 用 Java 做 Agent,和后端开发的关系

用 Java 做 Agent,本质上不是发明一套新框架,而是把一个后端系统接入大模型。Spring Boot 的 Bean 管理、Maven 的依赖机制、连接池、事务、异常处理、日志、监控、部署,这些知识一个都不能少。很多同学觉得 Java 面试八股文和 Agent 开发没有关系,其实关系非常直接:

  • 面试考线程池参数,因为 Agent 的工具调用天然是高并发场景。
  • 面试考 HashMap 和 Stream,因为要处理模型输出的 JSON 结构。
  • 面试考 Redis 缓存和分布式锁,因为多实例部署时 Agent 会话状态需要共享。
  • 面试考 Elasticsearch 查询,因为很多 Agent 的“检索能力”就是通过 ES 实现的。

所以不要急着把 Java 基础扔掉。先承认 Java 后端能力是 Agent 开发的地基,后面学习 Agent 才会顺。

2. 我在 Java 面试辅导中看到的三类 Agent 学习误区

2.1 误区一:只学 Agent 框架,不读源码,遇到问题只能重启

不少人在学习 Agent 时,第一步是安装某个 Agent 编排框架,第二步是复制一个示例,第三步是跑通 demo。程序能运行时会觉得很顺利,一旦报错就会进入“重启、换模型、改提示词”的死循环。

问题不在于框架不好,而在于没有理解框架内部的调用链。一个 Agent 编排框架至少包含 Tool 注册、模型调用、消息组装、结果解析、异常处理这几层。如果不懂这些组件之间的数据流,看到tool call failed这类错误时,根本不知道是该检查工具入参,还是检查模型返回格式,还是检查鉴权配置。

正确做法是先找一个足够简单的调用链手写一遍。比如 Java 程序发起 HTTP 请求调用模型服务,拿到返回后解析出工具名称和参数,再调用本地方法,把结果拼接回上下文。这个过程不需要复杂框架,但能训练排查问题的能力。

2.2 误区二:把“大模型调用”当成 Agent 的全部

还有一种常见认知是“只要调通了大模型,Agent 就做完了”。实际项目中,大模型调用只是链路中的一环。模型可能返回空内容,可能返回不合法 JSON,可能长时间不响应,可能被恶意 prompt 诱导产生错误行为。这些都需要在工程层面处理。

Java 里有几个非常具体的能力要求:

  • 使用 Jackson 解析模型输出时要处理未知字段和缺失字段。
  • 调用模型接口时要设置连接超时和读取超时。
  • 针对模型接口的 5xx 错误要做重试和熔断。
  • 对用户输入要做长度限制和内容过滤。
  • 对系统提示词和用户输入要拼接成结构化消息,而不是简单字符串拼接。

这些内容才是 Agent 开发和普通 CRUD 明显不同的地方,但它们本质上是后端工程问题。

2.3 误区三:概念学了一堆,Java 工程一个都没写

学习路线错位是最可惜的。有人先背了 Agent、Tool、Memory、Plan 一大串术语,再学框架,最后才发现连 Maven 依赖都不会配置。概念帮助理解方向,但真正检验学习效果的是能不能在本地跑起一个 Java 工程。

我在辅导中经常看到这样的情况:

误区表现在咨询中常见的原话正确做法
只学框架“我学了某个框架的 demo,但改不动了”先读一个工具调用链,写单测覆盖
只调大模型“模型返回不对,我就换提示词”先检查输入上下文、原始日志和解析逻辑
只背概念“Agent 记忆我知道有短期和长期”用 Redis 存会话,用 ES 存日志,动手写一遍

技术学习里,概念只是索引,代码才是正文。每学一个新概念,都要用 Java 实现一个最小闭环。

3. Agent 开发前,先用这份 Java 清单给自己体检

3.1 Java 基础自测清单

不用等到把所有 Java 知识学完再碰 Agent。但下面这张清单里的内容,是 Agent 工程里出现频率最高的 Java 能力项,建议逐条自查。

模块需要掌握到什么程度为什么 Agent 开发需要
集合知道 HashMap 原理,会使用 Stream 处理 List组装消息上下文,处理工具调用结果
并发线程池参数、CompletableFuture、锁Agent 多工具调用、异步归并
IO/HTTPHttpClient、文件读写、流处理调用大模型服务,读取日志文件
JSONJackson 反序列化、泛型处理解析模型输出和 ES 返回结果
数据库连接池、事务、慢查询排查持久化会话与记忆
SpringIoC、AOP、事务、配置外置工程化组织 Agent 服务
网络/认证REST API 概念、鉴权方式工具调用、模型网关接入
日志/监控SLF4J、结构化日志、链路追踪Agent 调试和线上观测

如果清单里超过三项不熟,建议先不要急着做 Agent 项目,把对应的 Java 基础补起来。特别是并发和 JSON,这两项在 Agent 工程里几乎每天都要用。

3.2 并发和资源隔离:Agent 后端最容易翻车的地方

Agent 调用大模型通常是阻塞式 HTTP 调用,单次耗时可能几秒到几十秒。如果使用 Tomcat 默认线程池处理请求,一个用户的多轮问答就可能占满线程。更危险的是,多个用户同时触发 Agent 工具调用时,线程池很可能被打满,随后出现接口超时和线程饥饿。

常见做法是给 Agent 模型调用单独配置线程池,并把工具调用与业务线程隔离。下面是一个最小示例:

ExecutorService agentPool = new ThreadPoolExecutor( 8, // 核心线程数 16, // 最大线程数 60L, // 空闲线程存活时间 TimeUnit.SECONDS, new LinkedBlockingQueue<>(200), new ThreadPoolExecutor.CallerRunsPolicy() );

核心线程数不能开太大,因为大模型接口有并发限制和成本限制。队列长度也不宜无限,否则内存可能先被打满。CallerRunsPolicy是一种降级策略,线程池满时由调用线程执行任务,虽然会拖慢调用方,但至少不会直接丢弃任务。

3.3 环境准备:JDK、Maven、Elasticsearch

做下面的日志分析 Agent 示例前,先确认环境符合要求:

  • JDK 17 或更高版本。
  • Maven 3.8 或更高版本。
  • Elasticsearch 8.x,本地或远程可访问。
  • 可选:一个兼容 OpenAI 格式的大模型推理服务。

先执行下面三条命令检查环境:

java -version mvn -version curl http://localhost:9200/_cluster/health

最后一条命令如果返回包含"status" : "green""status" : "yellow"的 JSON,说明 ES 可以访问。如果连接不上,需要先排查 ES 服务是否启动、端口是否正确、是否开启了认证。

注意:Elasticsearch Java API Client 的服务端版本和客户端版本尽量保持一致,至少大版本不能差异过大,否则协议不兼容,会出现序列化或请求失败的问题。

4. 最小可运行案例:Java + Elasticsearch REST API 实现日志分析 Agent

4.1 需求设计

这个例子的目标是模拟“日志分析 Agent”:用户输入一个关于错误日志的问题,Java 程序从 Elasticsearch 查询最近 ERROR 日志,把日志内容组装成提示词,调用大模型服务,返回分析结论。

分成三个模块:

  • LogQueryTool:查询 Elasticsearch,返回日志文本。
  • LlmClient:调用大模型 HTTP 接口。
  • LogAnalyzerAgent:把日志查询和模型调用组合起来。

如果本地没有大模型服务,可以先让LogAnalyzerAgent只打印 Prompt,确认日志查询链路没有问题后再接入模型。

4.2 工程结构

在本地创建一个 Maven 项目,目录结构如下:

log-analyzer-agent/ ├── pom.xml └── src/main/java/com/beifeng/agent/ ├── LogAnalyzerApp.java ├── LogAnalyzerAgent.java ├── LogQueryTool.java ├── LlmClient.java └── LogEntry.java

为了保持示例简单,不引入 Spring Boot。实际生产项目可以把这些类注册为 Spring Bean,外包 RestClient 和线程池。

4.3 依赖配置

pom.xml中使用 Java 17,并加入 Elasticsearch Java Client 和 Jackson 依赖:

<project> <modelVersion>4.0.0</modelVersion> <groupId>com.beifeng</groupId> <artifactId>log-analyzer-agent</artifactId> <version>1.0.0</version> <properties> <maven.compiler.release>17</maven.compiler.release> </properties> <dependencies> <dependency> <groupId>co.elastic.clients</groupId> <artifactId>elasticsearch-java</artifactId> <version>8.11.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.16.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency> </dependencies> </project>

版本号可能随官方发布变化,落地前要确认你所用的 ES 服务端版本与客户端版本是否兼容。Lombok 依赖可以根据团队习惯取舍,这个例子中即便不用 Lombok,手写 getter/setter 也能运行。

4.4 核心代码:查询日志、组装 Prompt、调用模型

先定义日志实体LogEntry.java

package com.beifeng.agent; public class LogEntry { private String level; private String message; private String timestamp; public String getLevel() { return level; } public void setLevel(String level) { this.level = level; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getTimestamp() { return timestamp; } public void setTimestamp(String timestamp) { this.timestamp = timestamp; } }

定义工具层LogQueryTool.java,负责查询 Elasticsearch 中的 ERROR 日志:

package com.beifeng.agent; import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch.core.SearchRequest; import co.elastic.clients.elasticsearch.core.SearchResponse; import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.json.jackson.JacksonJsonpMapper; import co.elastic.clients.transport.ElasticsearchTransport; import co.elastic.clients.transport.rest_client.RestClientTransport; import org.apache.http.HttpHost; import org.elasticsearch.client.RestClient; import java.io.IOException; import java.util.stream.Collectors; public class LogQueryTool { private final ElasticsearchClient esClient; private final String index; public LogQueryTool(String host, int port, String index) { RestClient restClient = RestClient.builder(new HttpHost(host, port, "http")).build(); ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); this.esClient = new ElasticsearchClient(transport); this.index = index; } public String queryErrorLogs(int size) throws IOException { SearchRequest request = SearchRequest.of(s -> s .index(index) .query(q -> q.match(t -> t.field("level").query("ERROR"))) .size(size) ); SearchResponse<LogEntry> response = esClient.search(request, LogEntry.class); return response.hits().hits().stream() .map(Hit::source) .filter(source -> source != null) .map(LogEntry::getMessage) .collect(Collectors.joining("\n")); } }

这里的关键点是查询条件。示例只匹配了level=ERROR,生产环境通常还要加时间范围,避免一次查出全量历史日志。还应该限制返回条数并做分页,防止 ES 返回超大数据导致内存溢出。

定义模型调用客户端LlmClient.java,使用 Java 自带的HttpClient

package com.beifeng.agent; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; public class LlmClient { private final String endpoint; private final String apiKey; private final HttpClient httpClient = HttpClient.newHttpClient(); private final ObjectMapper objectMapper = new ObjectMapper(); public LlmClient(String endpoint, String apiKey) { this.endpoint = endpoint; this.apiKey = apiKey; } public String chat(String prompt) throws Exception { String safePrompt = prompt.replace("\\", "\\\\") .replace("\"", "\\\"") .replace("\n", "\\n"); String body = """ { "model": "log-analyzer", "messages": [ {"role": "system", "content": "你是日志分析助手,回答要简洁、可操作。"}, {"role": "user", "content": "%s"} ] } """.formatted(safePrompt); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(endpoint)) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + apiKey) .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { throw new RuntimeException("LLM 服务返回异常,HTTP " + response.statusCode() + ",响应:" + response.body()); } return extractContent(response.body()); } private String extractContent(String responseBody) throws Exception { JsonNode root = objectMapper.readTree(responseBody); return root.path("choices").path(0).path("message").path("content").asText(); } }

这里默认模型服务返回的是常见的choices[0].message.content结构。不同服务商的字段可能不同,实际接入时需要根据 API 文档调整请求体和响应解析。

定义编排层LogAnalyzerAgent.java

package com.beifeng.agent; public class LogAnalyzerAgent { private final LogQueryTool logQueryTool; private final LlmClient llmClient; public LogAnalyzerAgent(LogQueryTool logQueryTool, LlmClient llmClient) { this.logQueryTool = logQueryTool; this.llmClient = llmClient; } public String analyze(String question) throws Exception { String logs = logQueryTool.queryErrorLogs(20); String prompt = "你是日志分析助手。请根据给定 ERROR 日志回答用户问题。" + "要求:先写关键错误,再写可能原因,最后写排查建议。\n\n" + "用户问题:\n" + question + "\n\n" + "最近错误日志:\n" + logs; return llmClient.chat(prompt); } }

最后写入口LogAnalyzerApp.java

package com.beifeng.agent; public class LogAnalyzerApp { public static void main(String[] args) throws Exception { String esHost = args.length > 0 ? args[0] : "localhost"; int esPort = args.length > 1 ? Integer.parseInt(args[1]) : 9200; String index = args.length > 2 ? args[2] : "app-logs"; String question = args.length > 3 ? args[3] : "分析最近的错误日志,给出核心原因和排查建议"; String llmEndpoint = System.getenv("LLM_ENDPOINT"); String llmApiKey = System.getenv("LLM_API_KEY"); if (llmEndpoint == null || llmEndpoint.isBlank()) { throw new IllegalArgumentException("请先设置 LLM_ENDPOINT 环境变量"); } LogQueryTool logQueryTool = new LogQueryTool(esHost, esPort, index); LlmClient llmClient = new LlmClient(llmEndpoint, llmApiKey == null ? "" : llmApiKey); LogAnalyzerAgent agent = new LogAnalyzerAgent(logQueryTool, llmClient); String answer = agent.analyze(question); System.out.println(answer); } }

4.5 运行验证

先用 Maven 编译:

mvn clean compile

然后设置大模型服务地址:

export LLM_ENDPOINT=http://localhost:8000/v1/chat/completions export LLM_API_KEY=local-test-key

运行入口类时,需要让 Maven 执行 Java 程序。可以在pom.xml中引入exec-maven-plugin,然后在命令行执行:

mvn exec:java -Dexec.mainClass=com.beifeng.agent.LogAnalyzerApp \ -Dexec.args="localhost 9200 app-logs 分析最近的错误日志"

在 Elasticsearch 中有 ERROR 日志,且大模型服务可用的情况下,控制台会输出分析结论。

关键错误:系统出现大量数据库连接超时。 可能原因:连接池满、慢 SQL 占用连接、数据库负载过高。 排查建议:先看连接池使用率,再查慢查询日志,最后检查数据库节点负载。

如果不想先接大模型,可以临时把LlmClient.chat改成打印 prompt 并返回固定字符串,先把 ES 查询链路验证通,再接入真实模型。

注意:运行成功不等于链路可用。还要验证索引名是否正确、查询条件是否命中日志、大模型响应是否被正确解析。建议先用 curl 直接查询 ES,确认原始数据存在。

5. 从 demo 到生产:Agent 项目的分层、配置和可观测性

5.1 分层设计

上面的示例是一个最小结构。生产环境建议按职责拆成更多模块,避免所有逻辑都堆在一个 Agent 类里。

层级职责典型实现
接入层暴露 HTTP API,做参数校验和鉴权Spring Boot Controller
编排层管理 Agent 状态、调用工具、组装多轮上下文AgentService、StateMachine
工具层封装 ES、数据库、Redis、外部系统LogQueryTool、OrderQueryTool
模型层统一封装大模型调用、重试、超时LlmClient、ModelGateway
存储层保存会话、记忆、日志MySQL、Redis、Elasticsearch

分层的好处是每一层都可以单独测试。比如LogQueryTool不依赖模型,可以单独写单元测试;LlmClient不依赖业务逻辑,可以用 Mock 服务验证。

5.2 配置外置和密钥管理

不要把 ES 密码、API Key 写在代码里。本地示例可以通过环境变量传入,生产环境建议使用配置中心或密钥管理服务。

下面是一个环境变量的配置示例:

export ES_HOST=10.0.0.10 export ES_PORT=9200 export ES_USERNAME=elastic export ES_PASSWORD=your-password export LLM_ENDPOINT=https://your-llm-gateway.internal/v1/chat/completions export LLM_API_KEY=your-api-key

Java 里使用System.getenv读取,而不是在代码中写死。生产环境还应该为模型服务和 ES 都设置网络白名单,避免接口暴露到公网。

5.3 日志、链路追踪和评估

Agent 调试比普通接口调试更麻烦,因为一次回答可能涉及多轮模型调用和多次工具调用。建议给每个会话生成一个traceId,并在日志中记录:

  • 用户输入。
  • 每一步工具调用的入参和返回摘要。
  • 大模型请求的消息数量、Token 消耗、耗时。
  • 工具调用是否失败。
  • 最终回答的截断摘要。

日志结构可以打印成 JSON,便于在 Elasticsearch 或 SkyWalking 中聚合分析。例如

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询