引言

Code Review Agent——一个基于 Spring AI Alibaba 构建的代码审查智能体。输入一段代码 diff,它能识别潜在缺陷、风格问题、安全隐患,并给出结构化建议。技术栈看起来不复杂:Spring AI + DashScope + Servlet 流式透传 + MySQL 存储审查记录。

看起来就是调一次 LLM API。

但真正把它从 demo 推到生产之后,发现"调一次 LLM"和"一个能稳定运行的 Agent 服务"之间,差了至少 5 层工程化能力:

  1. 链路分层——请求从前端到模型之间经历了 4 层网关,每层都有明确职责
  2. 流式透传——SSE 长连接在多层网关间的稳定传输,是独立要解决的问题
  3. 结构化 Prompt——不是一段话,而是 6 层结构化模板,覆盖角色、边界、安全
  4. 模型通道路由——同一个供应商提供了两种调用方式,需要根据模型特性动态选择
  5. 运行时保障——可观测、可恢复、可调试,三层 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_complexity
  • description:功能描述
  • whenToUse:什么场景下应该调用这个工具
  • whenNotToUse:什么场景下不应该调用
  • parameters:参数定义(JSON Schema)

whenToUsewhenNotToUse 是减少工具误调用的关键。经验上,只给 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 时,需要补齐的核心认知。框架在进化,生态在成熟,但工程化的基本功——分层、可观测、可恢复、可追溯——永远是自己的。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐