Agent实战 #02:一个 Java Agent 从 Demo 到上线,我补了这 5 层工程化能力
引言
Code Review Agent——一个基于 Spring AI Alibaba 构建的代码审查智能体。输入一段代码 diff,它能识别潜在缺陷、风格问题、安全隐患,并给出结构化建议。技术栈看起来不复杂:Spring AI + DashScope + Servlet 流式透传 + MySQL 存储审查记录。
看起来就是调一次 LLM API。
但真正把它从 demo 推到生产之后,发现"调一次 LLM"和"一个能稳定运行的 Agent 服务"之间,差了至少 5 层工程化能力:
- 链路分层——请求从前端到模型之间经历了 4 层网关,每层都有明确职责
- 流式透传——SSE 长连接在多层网关间的稳定传输,是独立要解决的问题
- 结构化 Prompt——不是一段话,而是 6 层结构化模板,覆盖角色、边界、安全
- 模型通道路由——同一个供应商提供了两种调用方式,需要根据模型特性动态选择
- 运行时保障——可观测、可恢复、可调试,三层 ChatModel 装饰器缺一不可
这 5 层能力,框架不会帮你自动补齐,需要工程侧自己搭。下面逐一拆解,包括每一层的设计决策、踩过的坑、以及代码实现。
先看一下完整的请求链路:

前端 ReviewStreamPanel.vue
→ Nginx(TLS 终止 + 静态资源 + 反向代理)
→ dream-saas-gateway:9001(SaaS 网关,租户路由 + 认证鉴权)
→ dream-ai-gateway:8090(AI 网关,模型路由 + 调用配额)
→ code-review:8095(Agent Runtime,Prompt 组装 + 工具调用)
→ DashScope / OpenAI(LLM 推理,2~30s)
→ MySQL review_record(审查记录落库)
6 个节点,4 层网关/服务,每一层都有独立职责。这不是过度设计,是生产环境的务实选择。
第 1 章:请求链路不是你想的那么简单
为什么要拆 4 层
很多人直觉上认为 Agent 服务就是一个 Spring Boot 应用,前端直连就行。Demo 阶段确实可以这么干——一个 Controller 接收请求,调一次 LLM,返回结果,完事。但生产环境会迅速暴露几个问题:
租户隔离。不同租户的 AI 调用配额不同(免费版每天 10 次,专业版不限量),模型权限也不同(基础版只能用 qwen-turbo,旗舰版能用 qwen-max)。这些逻辑放在 Agent Runtime 里做?那是业务逻辑和治理逻辑混在一起,迟早失控。
模型路由。同一个 AI 网关后面可能挂着通义千问、文心一言、GPT-4o 多个供应商。前端不关心用的是哪个模型,只需要传一个模型 ID,AI 网关负责路由到对应的供应商。
服务治理。Agent Runtime 需要独立部署、独立扩缩容。一次全员代码审查的高峰期,code-review 服务可能需要 10 个 Pod,而用户管理服务只需要 2 个。混在一起部署,扩缩容粒度不匹配。
安全边界。认证鉴权在边缘网关完成,内部服务之间走信任网络通信。Agent Runtime 不需要关心 JWT 怎么验、租户信息怎么解析,网关已经处理好了。
于是链路变成了 4 层:
| 层 | 服务 | 端口 | 核心职责 |
|---|---|---|---|
| 边缘网关 | dream-saas-gateway | 9001 | 租户路由、认证鉴权、全局限流、IP 白名单 |
| AI 网关 | dream-ai-gateway | 8090 | 模型路由、通道路由、AI 调用配额、用量统计 |
| Agent Runtime | code-review | 8095 | Agent 业务逻辑、Prompt 组装、工具调用、结果存储 |
| LLM Provider | DashScope/OpenAI | 443 | 模型推理 |
这种"边缘网关 + 领域服务"的分层架构在大厂 AI 平台中很常见。APISIX 做边缘、Kong 做 API 管理、BFF 做业务聚合、Agent Runtime 做推理编排——名字不同,思路一致。
延迟不是问题
每跳一层增加 15ms 延迟,4 层加起来 420ms。但 LLM 一次推理的响应时间是 2~30 秒(取决于模型大小和输入长度),这 20ms 在网络延迟的噪声中完全不可见。架构分层带来的隔离性、可观测性、独立部署能力,远比这点延迟重要。
实际上,如果不用分层架构,Agent Runtime 内部也需要自己做认证、限流、路由、用量统计——这些代码迟早会膨胀成一个隐形的"内部网关"。不如一开始就拆清楚。
排障必须 traceId 贯穿
4 层链路意味着请求在每一跳都可能丢失上下文。如果某一次审查出了问题——模型返回了错误结果,或者响应超时——没有 traceId 贯穿,排查就是大海捞针。需要在第 1 层网关生成 traceId,后续每一层透传并写入日志。
项目中使用 TracingMdcBridge 将 Micrometer Tracing 的 Span 信息注入 MDC(Mapped Diagnostic Context),确保每一层日志都带上同一个 traceId。日志检索时,用 traceId 过滤,一条链路的所有节点一目了然。
@RestController
@RequestMapping("/api/v1/review")
public class ReviewStreamController {
@Autowired
private AgentUpstreamStreamProxyController streamProxy;
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamReview(
@RequestBody ReviewRequest request,
@RequestHeader("X-Trace-Id") String traceId) {
TracingMdcBridge.inject(traceId);
log.info("Review started, model={}, repo={}", request.getModelId(), request.getRepo());
return streamProxy.proxy("/agent/code-review/stream", request)
.map(chunk -> ServerSentEvent.<String>builder()
.event("review")
.data(chunk)
.build())
.doFinally(signal -> log.info("Review finished, signal={}", signal))
.doOnError(e -> log.error("[trace={}] stream error", traceId, e));
}
}
这个接口本身不复杂,但它承载了三个职责:traceId 注入、SSE 事件封装、生命周期日志。流式透传的具体实现,是下一章的重点。
第 2 章:SSE 流式是最容易翻车的
问题出在哪
SSE(Server-Sent Events)是 LLM 流式输出的标准协议。服务端持续推送 token,前端逐字显示,用户体验远好于"等 30 秒突然全出来"。但 SSE 在多层代理架构中的稳定性,是一个独立的工程问题。
Spring Cloud Gateway 和 OpenFeign 都是为标准的请求-响应模式设计的。当上游返回 text/event-stream 时,它们的行为会变得不可预测:
Gateway 默认缓冲响应。Gateway 底层是 Netty,会将上游响应攒到一定大小再下发。SSE 数据被攒到一块再推给前端,前端看到的是"等半天突然全出来",流式效果完全丧失。
Feign 超时和长连接冲突。LLM 推理慢的时候(比如 20 秒),Feign 默认超时可能只有 5 秒,直接报超时错误。调大超时又会影响其他短接口的资源占用。
Nginx 默认开启 proxy_buffering。即使应用层禁用了缓冲,Nginx 作为反向代理也会缓冲响应,导致 SSE 数据在 Nginx 层被攒住。
这些问题在 Demo 阶段很难暴露——本地直连 Agent 服务,数据量小、响应快,一切正常。但上了生产、经过多层网关,一定会遇到。
解法:专用透传 Controller
项目中的做法是写一个专门的 AgentUpstreamStreamProxyController,绕过通用网关组件,用 Java 11 原生 HttpClient 手动做流式透传:
@RestController
public class AgentUpstreamStreamProxyController {
private final HttpClient httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10))
.build();
@Value("${agent.upstream.base-url}")
private String baseUrl;
@PostMapping(value = "/proxy/**", produces = "text/event-stream")
public void proxy(HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 禁用所有层级的缓冲
response.setHeader("Cache-Control", "no-cache");
response.setHeader("X-Accel-Buffering", "no");
response.setHeader("Connection", "keep-alive");
response.setBufferSize(0);
// 构建上游请求
String path = request.getRequestURI().replaceFirst("/proxy", "");
String query = request.getQueryString();
String upstreamUrl = baseUrl + path + (query != null ? "?" + query : "");
HttpRequest upstreamReq = HttpRequest.newBuilder()
.uri(URI.create(upstreamUrl))
.timeout(Duration.ofSeconds(120)) // LLM 推理超时设大
.POST(HttpRequest.BodyPublishers.ofByteArray(readBody(request)))
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.build();
// 发送请求,获取流式响应
HttpResponse<InputStream> upstream = httpClient.send(upstreamReq,
HttpResponse.BodyHandlers.ofInputStream());
// 8KB 缓冲区,循环读取并立即刷出
byte[] buffer = new byte[8192];
try (InputStream in = upstream.body();
OutputStream out = response.getOutputStream()) {
int n;
while ((n = in.read(buffer)) != -1) {
out.write(buffer, 0, n);
out.flush();
}
} catch (Exception e) {
log.error("Upstream stream error", e);
response.sendError(502, "Bad Gateway");
}
}
}
关键配置逐项解释
| 配置项 | 作用 | 为什么需要 |
|---|---|---|
Cache-Control: no-cache |
禁止浏览器和中间代理缓存 SSE 响应 | SSE 是实时流,缓存会导致数据错乱 |
X-Accel-Buffering: no |
告诉 Nginx 不要缓冲这个响应 | Nginx 默认开启 proxy_buffering,会攒住 SSE 数据 |
response.setBufferSize(0) |
Servlet 容器层面禁用输出缓冲 | Tomcat 默认有 8KB 输出缓冲区 |
| 8KB buffer + read→write→flush | 读到多少数据就立即推给客户端 | 核心逻辑:不攒数据,每次 read 后立即 flush |
timeout(120s) |
HTTP 客户端超时设为 120 秒 | LLM 推理可能需要 30 秒以上 |
这套方案的核心思想是:SSE 流式响应是一条数据流,不是一个完整响应。任何试图"等完整响应再处理"的组件都会破坏流式体验。专用透传 Controller 的价值在于:绕过了所有可能缓冲数据的中间层,从上游到客户端建立一条"数据管道"。
这是务实妥协,不是过度设计
有人会说"为什么不上 WebFlux"。WebFlux 确实天然支持响应式流,处理 SSE 更优雅。但项目主体是 Spring MVC(Servlet 栈),为了一个 SSE 透传接口引入响应式编程模型,改造成本太高,且团队需要同时维护两种编程范式。用底层 HttpClient 手动透传,代码量不到 50 行,行为可控,排查容易,是性价比最高的方案。
在工程实践中,"最佳方案"不一定是技术上最优雅的方案,而是在当前约束下代价最小的方案。
第 3 章:Prompt 不是写一段话就行
生产环境的 Prompt 挑战
大多数人的 Prompt 长这样:
你是一个代码审查专家,请审查以下代码并给出建议。
代码:{code}
Demo 够了。生产环境会暴露一系列问题:
- LLM 越界:用户输入"帮我写一首诗",Agent 真的写了一首诗,而不是拒绝
- 工具调用混乱:该调用代码分析工具时不调,不该调的时候乱调
- 输出格式不稳定:有时返回 JSON,有时返回 Markdown,前端解析失败
- 安全问题:恶意输入让 Agent 执行了不应该执行的操作
- Prompt 改了就忘了:改了 Prompt 导致审查结果变化,但没人知道是 Prompt 的问题
这些问题的根源是:Prompt 没有结构化,没有版本管理,没有工程化治理。
6 层结构化 Prompt 设计
项目中将 Prompt 抽象为一个 PromptTemplate 接口,分为 6 个层次。每一层解决一个具体问题:
public interface PromptTemplate {
// L1: 角色定义 —— "你是谁",明确 Agent 的身份和专注领域
String role();
// L2: 能力边界 —— 能做什么 + 不能做什么,显式约束行为范围
List<String> canDo();
List<String> cannotDo();
// L3: 工具定义 —— 每个工具的 When to Use / When NOT to Use
List<ToolDefinition> tools();
// L4: 交互规范 —— 输出格式、置信度要求、语气风格
String interactionRules();
// L5: 安全边界 —— 分级控制:L1拒绝 / L2谨慎 / L3正常
PromptSafetyLevel safetyLevel();
Optional<String> safetySupplement();
// L6: 上下文注入 —— 运行时变量(用户信息、代码 diff、仓库元信息等)
Optional<String> contextBlock();
// 元数据 —— 版本号、描述、最后修改时间
PromptMetadata metadata();
// 组装成最终 Prompt 字符串
default String assemble() {
StringBuilder sb = new StringBuilder();
sb.append("## Role\n").append(role()).append("\n\n");
sb.append("## Capabilities\n");
sb.append("### Can Do\n");
canDo().forEach(c -> sb.append("- ").append(c).append("\n"));
sb.append("### Cannot Do\n");
cannotDo().forEach(c -> sb.append("- ").append(c).append("\n\n"));
if (!tools().isEmpty()) {
sb.append("## Tools\n");
tools().forEach(t -> sb.append(t.toPrompt()).append("\n"));
}
sb.append("## Interaction Rules\n")
.append(interactionRules()).append("\n\n");
sb.append("## Safety Level: ").append(safetyLevel()).append("\n");
safetySupplement().ifPresent(s -> sb.append(s).append("\n\n"));
contextBlock().ifPresent(ctx ->
sb.append("## Context\n").append(ctx).append("\n"));
return sb.toString();
}
}
每一层解决什么问题
| 层级 | 职责 | 解决的具体问题 |
|---|---|---|
| L1 角色定义 | 明确 Agent 身份和专注领域 | 减少幻觉,防止 Agent 跑题到无关领域 |
| L2 能力边界 | 显式列出能做什么、不能做什么 | 防止越界,用户问非代码问题时能正确拒答 |
| L3 工具定义 | 每个工具附带使用条件和不使用条件 | 提高工具调用精准度,减少误调用和漏调用 |
| L4 交互规范 | 规定输出 JSON schema、置信度要求、语气 | 输出格式稳定可解析,审查结果有一致的质量基线 |
| L5 安全边界 | 分级:L1 拒绝 / L2 谨慎 / L3 正常 | 安全可控,高危操作需二次确认 |
| L6 上下文注入 | 运行时变量(diff 内容、仓库元信息) | Prompt 模板与数据分离,模板可复用可版本管理 |
工具定义的细节
工具定义不是简单地给 LLM 一个 function description。每个 ToolDefinition 包含:
name:工具名称,如analyze_code_complexitydescription:功能描述whenToUse:什么场景下应该调用这个工具whenNotToUse:什么场景下不应该调用parameters:参数定义(JSON Schema)
whenToUse 和 whenNotToUse 是减少工具误调用的关键。经验上,只给 description 不加使用条件,LLM 的工具调用准确率大约在 70%80%;加上明确的使用条件后,可以提升到 90% 以上。在生产环境中,这 10%20% 的提升就是"能用"和"不能用"的区别。
Prompt 版本管理
PromptMetadata 记录了版本号、描述、最后修改时间、修改人。每次 Prompt 修改都要更新版本号。配合审查记录可以回溯"某次审查结果为什么变了"——大概率是 Prompt 版本变了,而不是模型变了。这种可追溯性在生产环境中极其重要,尤其是当客户问"为什么上次和这次的审查结果不一样"时。
第 4 章:模型调用要选通道
两个配置不是接了两家供应商
打开项目配置文件,会同时看到两段配置:
spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${DASHSCOPE_API_KEY}
model: qwen-max
dashscope:
api-key: ${DASHSCOPE_API_KEY}
model: qwen-turbo
第一反应是"接了两个供应商"。仔细看 base-url——两个都是 dashscope.aliyuncs.com。实际都是通义千问,区别在于调用方式不同。
两种通道对比
| 维度 | OpenAI 兼容模式 | DashScope 原生 SDK |
|---|---|---|
| 协议 | /v1/chat/completions(OpenAI 标准) |
百炼原生 REST / Java SDK |
| 模型范围 | 通义系列主流模型 | 通义全系列 + 独有模型 |
| 优势 | 通用协议,切换供应商成本低,生态工具兼容 | 深度集成,支持通义独有特性(多模态、插件等) |
| 劣势 | 通义独有特性可能不兼容 | 绑定供应商,迁移成本高 |
| 流式表现 | 标准 SSE,兼容性好 | 部分模型的流式返回格式不完全一致 |
| 适用模型 | qwen-max、qwen-plus 等主流文本模型 | qwen-vl-max(视觉)、qwen-audio(语音)等 |
DashScopeModelChannelResolver:按模型选通道
@Component
public class DashScopeModelChannelResolver {
private final OpenAiChatModel openAiChatModel;
private final DashScopeChatModel dashScopeChatModel;
// 模型 → 通道路由表
private static final Map<String, String> CHANNEL_MAPPING = Map.of(
"qwen-max", "openai-compatible",
"qwen-plus", "openai-compatible",
"qwen-turbo", "dashscope-native",
"qwen-vl-max", "dashscope-native" // 多模态必须走原生
);
public ChatModel resolve(String modelId) {
String channel = CHANNEL_MAPPING.getOrDefault(modelId, "openai-compatible");
return switch (channel) {
case "openai-compatible" -> openAiChatModel;
case "dashscope-native" -> dashScopeChatModel;
default -> throw new UnsupportedModelException(modelId);
};
}
}
为什么需要通道路由
核心原因是不同模型在不同通道上的稳定性和能力覆盖不同。这不是理论推演,是实际踩坑后的产物:
qwen-max走 OpenAI 兼容通道最稳定,且后续如果要切换到 Azure OpenAI 或其他兼容供应商,业务代码不用改qwen-vl-max(视觉模型)必须走 DashScope 原生通道,因为多模态能力(图片输入)在 OpenAI 兼容模式下不完整,会丢参数- 某些模型的原生通道流式返回格式有小差异(比如
data:和data:的区别),需要在通道层做适配
"OpenAI 兼容 = 通用插头"
OpenAI 兼容模式的核心价值不是"用 OpenAI 的 API",而是"用一个通用协议"。通义千问提供了 OpenAI 兼容层,意味着代码中的 ChatModel 调用可以在不改变业务逻辑的前提下切换到其他供应商——只要对方也实现了 OpenAI 兼容协议。
这是一种防御性设计:不绑定单一供应商。在当前的 AI 市场中,模型能力迭代极快,保持切换能力是合理的工程策略。
第 5 章:运行时保障不是可选的
ChatModel vs ChatClient
先厘清两个核心概念:
- ChatModel:底层基础接口,核心方法是
call(Prompt)和stream(Prompt),直接和 LLM API 交互。它是最小的抽象单元。 - ChatClient:高层流畅 API,提供链式调用
.prompt().system("...").user("...").call().content()。它封装了 Prompt 构建、模型调用、结果解析等步骤。ChatClient 底层委托给 ChatModel 实现。
增强逻辑应该加在哪一层?答案是 ChatModel 层。因为 ChatClient 最终调用 ChatModel,在 ChatModel 层做增强可以覆盖所有调用路径——无论是通过 ChatClient 调用还是直接调用 ChatModel。
三层 ChatModel 包装
项目中对 ChatModel 做了三层装饰器(Decorator),每层对应一种运行时能力:
| 类名 | 能力 | 机制 |
|---|---|---|
InstrumentingDebugChatModel |
实时调试 | WebSocket 推送 token 级响应到前端 |
InstrumentingTraceChatModel |
分布式追踪 | Micrometer Tracing,每次调用生成 Span |
RecoveryInstrumentingChatModel |
故障恢复 | Resilience4j 重试/熔断 + SelfCorrectLoop |
三层装饰器的嵌套顺序:
ChatModel model = new RecoveryInstrumentingChatModel(
new InstrumentingTraceChatModel(
new InstrumentingDebugChatModel(
dashScopeChatModel // 最底层:真实的模型调用
)
)
);
最外层是恢复层(先保证可用性),中间是追踪层(记录调用链路),最内层是调试层(捕获实际 token 流)。顺序不是随意的——恢复层在最外才能拦截所有异常,追踪层在中间才能记录包括重试在内的所有调用。
实时调试:看见 Agent 在"想"什么
InstrumentingDebugChatModel 在流式响应过程中,将每个 token 实时推送到前端 WebSocket。开发者(或用户)可以在前端看到 Agent "正在想什么"——输出了什么思考过程、调用了什么工具、传了什么参数。这对调试 Prompt 和排查 Agent 行为极其有用。很多时候,"Agent 给出了错误结果"的根因不在模型,而在 Prompt 的工具定义部分有歧义。没有实时调试,这种问题很难定位。
分布式追踪:让"Agent 响应慢"有数据支撑
InstrumentingTraceChatModel 为每次 LLM 调用创建一个 Micrometer Span,记录:
- 模型 ID、通道类型
- Prompt token 数、completion token 数
- 首 token 延迟(TTFT)
- 总调用耗时
- 是否触发重试、重试次数
这些数据通过 Micrometer 导出到 Prometheus/Grafana,形成 LLM 调用的可观测性面板。生产环境中,"Agent 响应慢"不能只是一个模糊的用户反馈,必须有数据支撑——是模型推理慢、还是 Prompt 太长、还是网络延迟、还是重试次数过多。
故障恢复:不是简单重试
public class RecoveryInstrumentingChatModel implements ChatModel {
private final CircuitBreaker circuitBreaker;
private final Retry retry;
private final SelfCorrectLoop selfCorrectLoop;
private final ChatModel delegate;
@Override
public ChatResponse call(Prompt prompt) {
return circuitBreaker.executeSupplier(() ->
retry.executeSupplier(() -> {
try {
return delegate.call(prompt);
} catch (ModelException e) {
if (e.isRetryable()) {
log.warn("Model call failed, will retry: {}", e.getMessage());
throw e; // 触发 Resilience4j Retry
}
// 不可重试的异常 → 自我修正循环
return selfCorrectLoop.execute(prompt, e);
}
})
);
}
}
这里有两层恢复机制:
第一层:Resilience4j 重试 + 熔断。网络超时、限流等可重试异常,自动重试(指数退避,最多 3 次)。如果连续失败超过阈值,熔断器打开,快速失败,不继续浪费资源。
第二层:SelfCorrectLoop 自我修正。当 LLM 返回格式错误、工具调用参数错误等"可恢复但不可重试"的异常时,将错误信息追加到 Prompt 中让 LLM 自我修正。比如 LLM 返回了不合法的 JSON,把 JSON 解析错误信息和原始输出追加到 Prompt 末尾,让 LLM 重新输出。这和 ReactAgent 的循环机制异曲同工——Agent 不只是调用一次模型,而是在一个循环中不断修正,直到结果满足要求或达到最大循环次数。
生产环境的底线
没有这三层保障,Agent 在生产环境中就是"黑盒":不知道它在想什么(缺调试)、不知道它为什么慢(缺追踪)、不知道它挂了怎么办(缺恢复)。可观测、可恢复、可调试——这三层不是锦上添花,是上线的最低门槛。
结尾
Agent 工程没有"一家通吃"的方案
从 Demo 到生产,这 5 层工程化能力——链路分层、流式透传、结构化 Prompt、模型通道路由、运行时保障——没有一个是从 Spring AI 框架里"开箱即用"的。框架给了核心抽象(ChatModel、ChatClient、Tool Calling、ReactAgent),但生产环境的工程细节需要自己补齐。

成熟度现状
| 维度 | 成熟度 | 说明 |
|---|---|---|
| 单轮 LLM 调用 | ★★★★★ | 稳定可靠,Spring AI / LangChain 均支持良好 |
| RAG | ★★★★☆ | 向量检索 + 分块策略基本成熟,评估体系仍在完善 |
| Agent 编排 | ★★★☆☆ | ReactAgent / Graph 可用,复杂编排场景仍需大量定制 |
| 生产治理 | ★★☆☆☆ | 可观测性、故障恢复、成本控制缺乏统一标准 |
| 协议层(MCP) | ★★★☆☆ | 协议本身在快速发展,生态工具链仍在上升期 |
Java 做 Agent 的优势与定位
Java 的优势:
- 类型安全:Prompt 结构化、工具定义、配置管理都有编译期保障,减少运行时错误
- 微服务生态成熟:网关、限流、链路追踪、配置中心、服务注册——都是现成的,不需要重新造轮子
- 企业级治理能力:RBAC、审计、合规、多租户隔离——Java 生态有大量的基础设施可以直接复用
对比 Python 生态:
- LangChain / LangGraph 在 Agent 编排上更灵活,社区更活跃,新论文和新方法通常先出 Python 版本
- FastAPI 做 Agent 服务轻量快速,但大规模微服务治理不如 Spring 生态成熟
- Python 社区在 AI 领域的迭代速度更快,框架层面的创新往往先出现在 Python 生态
没有哪个生态是"最佳选择"。Java 做 Agent 的价值在于:当 Agent 需要嵌入一个已有的企业级系统中时(这个系统可能已经有 50 个微服务、完善的 CI/CD、多租户架构),Java 生态的治理能力和基础设施是最大的杠杆。不需要为了一个 Agent 重写整个技术栈。
这 5 层工程化能力,就是 Java 工程师从传统后端转向 AI Agent 时,需要补齐的核心认知。框架在进化,生态在成熟,但工程化的基本功——分层、可观测、可恢复、可追溯——永远是自己的。
更多推荐





所有评论(0)