Spring AI alibaba最佳实践-jvm监控诊断agent开发教程

写在文章开头

笔者上一篇文章《Spring AI Alibaba天气预报助手实践》:mp.weixin.qq.com/s/99WcbggTk... 我们初步探讨了AI agent开发核心技术基础实践,并总结出了以下几个核心概念:

  1. Agent(智能体):由大语言模型+工具(Tools)+系统提示词(System prompt),其本质是将LLM推理能力与工具执行能力相结合,打造智能流程自动化的agent
  2. Tool(工具):为LLM提供外部调用的能力(如外部的工具、API、命令行等)
  3. Skill (技能):一组提示词、示例、参考资料这些专家经验打包而成的知识单元,引导模型按照特定的思维框架进行推理工作。
  4. ReAct(Reasoning+Acting):ReAct范式通过思考->心动->观察的循环机制,实现流程智能体自动化

同时,文章还基于Spring AI Alibaba(以下简称SAA)提供的官方天气预报助手案例基础之上,补充了消息修剪、上下文管理、RAG检索增强、Skill抽取等综合实践。

有了这些知识的储备,本文将探讨一个更深入的话题,如何将技术专家确定性经验运用的工具封装为Tool,将可复用的经验即思维推理框架构建为skil,构建可自主执行特定工作的agent。所以这篇文章将基于一个JVM监控诊断助手的案例,演示如何将沉淀的个人经验高度抽象封装为可复用的agent。

详解jvm监控诊断agent需求说明

arthas作为阿里开源的JVM监控诊断工具,其核心价值在于动态字节码增强技术,确保运行时无侵入监控诊断Java应用。但笔者在实际使用中发现,其使用模式存在一个根本性的矛盾,即每个命令的使用场景、参数、结果解读都需要一定的经验积累,面对复杂多变的线上故障时存在效率瓶颈。

仔细分析这种矛盾,本质的原因就是强大的工具仅仅提供原子能力,缺少智能流程的编排的逻辑。所以,现代软件最佳实践是基于AI agent将可复用经验封装为可复用的skill,让AI根据问题动态编排工具调用序列,从而提升日常研发的效率。

从技术架构的角度来看,这是经典的关注点分离的设计:

  • Tool(arhtas):专注提供可靠的底层能力(确定性执行)
  • AI agent:专注诊断流程的编排(不确定性推理)
  • Skill:二者的桥梁,将特定问题经验沉淀,让AI agent能够更好的使用Tool,确保不确定性因素,尽可能正确的执行

这种架构最大的优势就是职责分离模式下,经验的可沉淀性,每次成功的故障排查都可以通过复盘并更新Skill,不断丰富诊断知识库。

详解JVM agent设计思路

需求澄清

本文的案例是制作一个JVM智能监控诊断agent,当出现线上故障时,研发人员只需对agent简要说明进程信息和故障表现,agent就会自动完成故障推理诊断,输出诊断报告:

明确了一个宏观的技术需求,我们再进行一个更细致化的需求澄清:

  1. 用户交互设计:用户需要提示什么信息?我们如何设计系统提示词模板?
  2. 诊断流程编排:agent如何明确正确执行监控诊断,如何设计诊断步骤的依赖关系和执行逻辑?
  3. 系统工具集成:agent如何正确定位具体进程信息?如何实现跨平台通用的命令行执行方案?
  4. 监控工具集成:如何将arthas集成待监控的应用程序中?实现非侵入式远程调用诊断?

用户提示词设计

先来说说用户提示词的设计,本文的JVM agent的设计核心是将笔者的经验内化为可执行的智能,基于这一理念,我们对于用户提示词设计遵循最小信息原则,用户只需简单描述信息,agent就能够自动完成复杂的诊断流程,例如:

erlang 复制代码
demo-service 进程 CPU 使用率 100%,请协助排查问题

这也是笔者一直强调的接口隔离原则,用户无需关心内部的细节实现,只需简单的提词,agent就可以自动完成的复杂的全链路诊断。

agent工作流的封装

第二个问题是对智能体工作流的编排,在笔者在使用arthas进行故障排查的过程中,总结了一套可复用的确定动作链,其本质上就是一个状态依赖的决策过程,例如CPU飙升问题的排查步骤为:

  1. 初始状态:只有进程名和现象等相关信息
  2. 状态转移:每个工具调用都会产生新的数据(定位进程、thread定位线程、jad反编译等),改变系统的状态
  3. 目标状态:定位到问题代码和根因

ReAct(Reasoning+Acting)这笔者的经验是高度契合的:

  1. 推理阶段的贝叶斯更新:基于用户提出的问题,得到观测数据,更新对问题的概率估计, 推例如CPU飙升采用thread显示CPU占用率极高的线程时,系统对于线程的相关代码怀疑度会显著提升。
  2. 行动阶段的最有工具选择:基于状态选择信息增益最大的工具,例如thread命令的信息增益远大于执行memory命令。
  3. 观察阶段的状态收缩:每个工具的执行结果都会不断缩小问题空间,无线逼近真实的根因。

这种设计巧妙之处在于,它将笔者的个人经验转换为agent的状态转移规则,让AI基于真实的观测数据动态调整诊断路径。

例如CPU飙升问题,我们的agent思考和执行流程为:

  1. 需要先定位进程号 -> 执行jps -l
  2. 获取pid后需要查询端口 -> lsof -p <pid>
  3. 定位线程号和执行栈帧 -> thread + thread <id>
  4. 分析thread结果,定位问题代码段 -> jad <class>

命令行工具的设计

第三个问题即服务器级别的工具,即跨平台的兼容性问题,考虑到市面上开发Java的系统涉及Linux、windows和macOS,基于进程名称定位pid的指令有所差异。

所以针对命令行选择需要考虑统一适配,对此,笔者也通过AI检索到一条通用的、可识别不同系统的指令:

ini 复制代码
# 输入指令
java -XshowSettings:properties -version 2>&1 

## 输出操作系统基本信息

os.name = Mac OS X

这条指令巧妙之处在于,它利用JVM的统一抽象层,让其启动加载平台相关的本地库,间接获得Java配置以及宿主操作系统的信息

完成了命令行层面的设计考量之后,我们还需要考虑系统命令行调用的工具选型,结合市面上主流的轮子,笔者最终还是考虑hutoolRuntimeUtil,它通过适配器模式将不同操作系统的命令调用统一封装为Java接口,用起来十分的便捷且强大,对应代码示例如下所示:

arduino 复制代码
   // 先执行top命令获取输出
        String output = RuntimeUtil.execForStr("jps -l");
        Console.log(output);

arthas的集成设计的哲学

针对arthas官方文档的通篇阅读,笔者了解到Spring boot应用可通过集成Arthas Spring Boot Starter完成arthas server自动装配。同时arthas还支持通过HTTP API的方式发送指令对远程服务进行线上监控诊断,比如获取arthas版本号的命令如下所示:

vbnet 复制代码
curl -Ss -XPOST http://localhost:8563/api -d '
{
  "action":"exec",
  "command":"version"
}
'

所以对于arthas的集成,我们只需:

  1. Arthas Spring Boot Starte集成到项目中
  2. 对外暴露一个HTTP API端口
  3. agent集成并通过HTTP客户端发起调用进行监控诊断

这种设计充分体现了微服务架构思想,将诊断能力封装为独立的服务,让服务的边界有了明确且清晰的划分:

  • arthas服务(待监控的进程):专注JVM诊断能力的提供
  • AI agent:专注于诊断逻辑的流程编排
  • HTTP接口:两者通信的桥梁,符合现代微服务的通信标准

同时,对于arthas http接口的端口号设计,笔者也进行的深度的考量,本着约定大于配置的原则 ,所有应用的装配artahs服务端的API端口号都在进程web请求的端口基础上-1000,例如demo-service的端口号为9563,那么arhtas的http端口号就是8563。

通过信息编码为规则,确保零配置定位端口,还能保证系统规范的一致性。

架构设计的系统思维

完成需求澄清后,我们就有了下面这张架构图,总体来说,这个架构图充分体现了笔者将复杂的诊断流程拆解为可组合的标准化步骤:

  1. 用户接口层:接收自然语言描述,承担问题描述的标准化转换
  2. skill管理层:结合问题加载相关经验模板,实现上下文增强
  3. 工具执行层:将抽象的执行意图转为明确的工具调用序列
  4. 数据分析层:对于多工具执行结果进行推导分析,生成结构化报告

这种架构的核心优势就是可组合、可复用、可沉淀:

  1. 新的工具随着可以灵活增加或组合到工具执行层
  2. 新的skill可以封装为skill沉淀
  3. 数据分析算法可以随着历史案例不断优化

例如,基于JVM agent的一次完整的JVM监控诊断步骤为:

  1. 用户->agent:用户输入自然语言描述问题
  2. agent->skill:加载skill并通过skill对上下文进行增强
  3. skill->tool:基于意图,发起命令行工具调用,定位系统信息和java进程信息
  4. tool->arthas:通过标准化调用http请求对已装配arthas的程序进行监控诊断
  5. arthas->tool :收集监控诊断响应结果输出故障诊断报告

详解JVM agent落地

工具封装

首先是工具的封装,我们先来说说命令行工具RuntimeExecTool,集成hutool之后,通过agent传入的参数作为执行的命令执行返回即可:

typescript 复制代码
/**
 * 系统命令执行工具
 */
@Slf4j
public class RuntimeExecTool implements BiFunction<String, ToolContext, String> {
    @Override
    public String apply(@ToolParam(description = "传入要执行的系统命令") String command, ToolContext toolContext) {
        log.info("执行命令:{}", command);
        String result = RuntimeUtil.execForStr(command);
        log.info("命令执行结果:{}", result);
        return result;
    }
}

同理,本着结构化契约的思想,我们将arthas http请求地址和指令用list传入,交由HttpUtil发起远程调用并将执行结果返回,需要注意的是,因为ArthasTool涉及列表参数,为了保证模型传参的准确性,笔者在description给了详尽的说明,确保agent能够理解和使用工具:

typescript 复制代码
@Slf4j
public class ArthasTool implements BiFunction<List<String>, ToolContext, String> {


    @Override
    public String apply(@ToolParam(description = """
    Arthas 命令执行参数列表,必须包含 2 个元素:
    - 参数 1(index=0):Arthas HTTP API 完整地址,格式为 http://localhost:<端口>/api,例如:http://localhost:8563/api
    - 参数 2(index=1):要执行的 Arthas 命令,如 thread、memory、jad com.example.MyService、heapdump 等
    """) List<String> args,
                        ToolContext toolContext) {
        //解析请求地址和命令
        String url = args.get(0);
        String command = args.get(1);
        log.info("arthas url: {}", url);
        log.info("arthas command: {}", command);


        Map<String, Object> params = new HashMap<>();
        params.put("action", "exec");
        params.put("command", command);

        String result = HttpUtil.post(url, JSONUtil.toJsonStr(params));

        log.info("arthas执行结果: {}", result);
        return result;
    }
}

Skill技能封装

接下来便是skill的封装,它是我们agent构建的核心所在,本质上个人内化的经验构建AI可理解的知识表示,然后将多变的、不确定的结果迭代交由AI进程推理决策,将确定性的执行封装为流程编排中的工具。

所以笔者所设计的skill着重强调不同的问题的场景和解决步骤,对于生成结果并没有过多的干预。

对应的skill目录结构如下:

  1. SKILL.md:诊断逻辑和模型推理规则
  2. references给出常见的使用命令和响应格式,即稳定的工具基础知识
  3. examples:实际场景约束和最佳实践
python 复制代码
jvm-monitor-diagnostician
├── SKILL.md # 诊断技能的逻辑编码说明
├── examples # 可扩展的诊断逻辑
│   └── cpu-high-example.md 
└── references # 稳定的经验知识
    ├── arthas-commands.md
    └── response-format.md

对应这里也给出skill.md示例,基本上就是笔者对于个人经验和方法论的复用和封装:

arthas-commands.md则是对于一些常见的命令的参考文章,需要注意的是,该文档是笔者处于skill完整性所编写的。按照当前模型的储备,这些相对早起知识语料理应具备:

同时,结合AI多轮对话,设计了针对CPU飙升问题的完整示例文档,理解JVM agent线上监控诊断的标准流程。

agent构建的系统架构原理

通过上述的铺垫,我们完成的工具的整合和技能的编写,接下来我们就需要基于这些组件编排agent流程,完成构建,对应代码如下,整体步骤为:

  1. 创建系统提示词SYSTEM_PROMPT,通过systemPrompt方法完成配置,这里采用了约束编程的思想,确保用户简要的提示词通过系统提示词增强后,agent依然能够按照正确的路径执行
  2. 基于编写的工具创建工具回调ToolCallback,通过tool方法完成注册
  3. 创建ChatModel构建模型的抽象
  4. 通过ClasspathSkillRegistry加载resources目录下的技能文件,并通过钩子方法hooks加载到hooks容器中
  5. 将完整的ReactAgent已聚合关系作为JVMAgent的成员变量,严格遵守组合优先于继承的软件设计原则,确保设计灵活、安全、且易于维护
ini 复制代码
@Bean
    public JVMAgent jvmAgent() {

        //系统提示词简化为角色定位和输出格式要求
        String SYSTEM_PROMPT = """
                # JVM 监控诊断助手
                
                ## 角色定位
                你是一位专业的 JVM 监控诊断工程师,负责 Java 应用的性能监控与故障诊断。
                
                ## ⚠️ 强制流程规则
                **必须严格遵守以下顺序执行诊断,禁止跳过任何步骤**:
                
                1. **第一步**:调用 runtimeExecTool 执行 `jps -l` 定位目标进程 PID
                2. **第二步**:调用 runtimeExecTool 执行 `lsof -p <PID>` 或 `netstat -tlnp` 获取应用端口号
                   - ⚠️ **必须等待 runtimeExecTool 返回结果**
                   - ⚠️ **必须从返回结果中提取实际的端口号**(9000-9999 范围)
                3. **第三步**:计算 Arthas 端口 = 应用端口 - 1000
                4. **第四步**:构建 Arthas HTTP API 地址 `http://localhost:<实际端口>/api/`
                5. **第五步**:调用 arthasTool 执行诊断命令
                   - ⚠️ **禁止在未执行第二步的情况下直接调用 arthasTool**
                   - ⚠️ **禁止假设或硬编码端口号为 8563**
                   - ⚠️ **arthasTool 的第一个参数必须是基于实际端口构建的 URL**
                
                ## 技能使用
                你已掌握 "jvm-monitor-diagnostician" 技能,当用户报告 Java 应用性能问题时,请按照该技能定义的任务流程进行诊断分析。
                
                ## 示例参考
                处理 CPU 飙升问题时,请参考 "examples/cpu-high-example.md" 中的完整诊断流程和输出示例。
                
                ## 适用场景
                - CPU 使用率异常
                - 内存泄漏问题
                - 频繁 GC
                - 线程死锁
                - 应用响应缓慢
                - 其他 JVM 性能相关问题
                
                """;

        // ========== 工具配置开始 ==========
        // runtimeExecTool:用于执行系统命令,在 JVM 诊断流程中负责进程定位、端口查询、系统信息获取等底层操作
        ToolCallback getRuntimeExecTool = FunctionToolCallback
                .builder("runtimeExecTool", new RuntimeExecTool())
                .description("""
                        执行系统命令,用于 JVM 诊断流程中的进程定位、端口查询等操作。
                        常用命令示例:
                        - jps -l:列出所有 Java 进程
                        - jps -l | grep <进程名>:查找指定进程 PID
                        - lsof -p <PID>:查看进程打开的端口(推荐)
                        - netstat -tlnp | grep <PID>:查看进程监听的端口
                        - java -XshowSettings:properties -version 2>&1:检测操作系统类型
                        """)
                .inputType(String.class)
                .build();

        // arthasTool:Arthas 远程诊断工具,通过 HTTP API 向目标 Java 进程发送诊断命令
        // ⚠️ 必须在 runtimeExecTool 获取实际端口后才能使用,禁止跳过前置流程直接调用
        ToolCallback getArthasTool = FunctionToolCallback
                .builder("arthasTool", new ArthasTool())
                .description("""
                        Arthas 远程诊断工具,通过 HTTP API 向目标 Java 进程发送诊断命令。
                        
                        ⚠️ 强制前置流程(必须严格遵守):
                        1. 先调用 runtimeExecTool 执行 jps -l 获取进程 PID
                        2. 再调用 runtimeExecTool 执行 lsof -p <PID> 获取应用端口号(9000-9999 范围)
                        3. 计算 Arthas 端口 = 应用端口 - 1000
                        4. 构建 API 地址:http://localhost:<实际端口>/api/
                        
                        参数要求(必须包含 2 个元素的 List):
                        - 参数 1(index=0):Arthas HTTP API 完整地址,格式为 http://localhost:<端口>/api
                          例如:http://localhost:8563/api(禁止硬编码,必须基于步骤 2 的实际输出)
                        - 参数 2(index=1):要执行的 Arthas 命令
                          常用命令:thread、memory、jad com.example.MyService、heapdump、dashboard 等
                        
                        ⚠️ 重要提醒:
                        - 禁止在未执行端口查询命令前直接调用 arthasTool
                        - 禁止假设或硬编码端口号为 8563
                        - 必须等待 runtimeExecTool 返回结果并从中提取实际端口号
                        """)
                .inputType(List.class)
                .build();
        // ========== 工具配置结束 ==========

        // 创建 DashScope API
        DashScopeApi dashScopeApi = DashScopeApi.builder()
                .apiKey(apiKey)
                .build();
        //基于 dashscope api 创建 chatmodel
        ChatModel chatModel = DashScopeChatModel.builder()
                .dashScopeApi(dashScopeApi)
                .defaultOptions(DashScopeChatOptions.builder()
                        .withModel(DashScopeChatModel.DEFAULT_MODEL_NAME)
                        .withTemperature(0.0) //控制输出的随机性(0.0-1.0),值越高越有创造性
                        .withMaxToken(1000) // 最大输出长度 更多参数请参考 ChatModel 适配
                        .build())
                .build();

        // 创建技能并加载
        SkillRegistry registry = ClasspathSkillRegistry.builder()
                .classpathPath("skills")
                .build();

        SkillsAgentHook skillsHook = SkillsAgentHook.builder()
                .skillRegistry(registry)
                .build();

        ReactAgent agent = ReactAgent.builder()
                .name("JVM 监控诊断助手")
                .model(chatModel)
                .tools(getRuntimeExecTool, getArthasTool)
                .systemPrompt(SYSTEM_PROMPT)//系统提示词
                .hooks(skillsHook)
                .saver(new MemorySaver())//Agent 通过状态自动维护对话历史。使用 MemorySaver 配置持久化存储,默认使用 HashMap
                .build();
        //将其聚合到 JVM Agent中
        JVMAgent jvmAgent = new JVMAgent();
        jvmAgent.setAgent(agent);
        return jvmAgent;

    }

服务接口封装

最后,我们需要将call接口封装暴露给外部,考虑call调用在未来的迭代可能作为项目中大部分agent都需要暴露的方法,本着DRY原则(Don't Repeat Yourself) ,笔者利用一个公共抽象类AbstractAgent完成ReactAgent聚合和call方法的暴露。后续需要暴露对话行为的agent,只需继承这个抽象类直接直接复用这些属性和方法:

对应抽象类AbstractAgent代码如下:

arduino 复制代码
@Data
public class AbstractAgent {
    //聚合ReactAgent
    private ReactAgent agent;


    //对外暴露查询调用
    public String call(String message, RunnableConfig runnableConfig) throws Exception {
        return agent.call(message, runnableConfig).getText();
    }

    public String call(String message) throws Exception {
        return agent.call(message).getText();
    }
}

完成后,JVMAgent可直接继承并获取其行为:

java 复制代码
public class JVMAgent extends AbstractAgent {

}

最后,我们简单编写一个controller将外部请求参数作为用户提词,完成jvmAgent的call调用,并将结果返回:

java 复制代码
 private final JVMAgent jvmAgent;

/**
     * JVM性能分析入口
     * @param request 分析请求参数
     * @return 分析结果
     */
    @PostMapping("/analyze")
    public JVMAnalysisResponse analyze(@RequestBody JVMAnalysisRequest request) {
        try {
            log.info("收到JVM分析请求: {}", JSONUtil.toJsonStr(request));
            
            // 调用JVMAgent进行分析
            String result = jvmAgent.call(request.getProblemDescription());
            
            JVMAnalysisResponse response = new JVMAnalysisResponse();
            response.setSuccess(true);
            response.setResult(result);
            response.setMessage("分析完成");
            
            log.info("JVM分析完成: {}", result);
            return response;
            
        } catch (Exception e) {
            log.error("JVM分析失败", e);
            JVMAnalysisResponse response = new JVMAnalysisResponse();
            response.setSuccess(false);
            response.setMessage("分析失败: " + e.getMessage());
            return response;
        }
    }

功能验收

功能验收的核心是确认AI agent是否能够替代人工诊断,对此,笔者在本地起了死循环的代码段打满单核CPU时间片:

less 复制代码
@Slf4j
@RestController
public class TestController {
    @RequestMapping("cpu-100")
    public  void cpu() {
        while (true){ 
        }
    }
}

随后,我们请求http://localhost:8080/api/jvm/analyze发起调用开始对agent关键能力进行验证:

json 复制代码
{
    "problemDescription": "WebBaseApplication程序CPU飙升,请协助排查"
}

第一步:按照skill的说明,agent执行的第一步是通过jps -l定位进程号,skill路径选择正确,工具调用验证成功:

第二步:再使用跨平台java指令定位系统信息,为后续端口号查询指令做铺垫:

第三步:基于mac平台兼容指令执行lsof -p 19634

第四步:构建arthas请求地址和参数进行JVM监控诊断:

最后:给出故障诊断报告和建议,自此,我们完成端到端的验证,完成agent在实际场景中的流程闭环:

小结

以笔者的实践经验,AI应用本职业是对于可复用行为的封装,相较于过去的开发模式,我们可以:

  1. 将一些复杂的经验性思维框架即推理部分,封装为skill,让AI处理灵活多变的部分。
  2. 将确定性经验部分封装为Tool

开发者不再是编写复杂的业务逻辑,而是通过提词引导AI按照正确的路径执行,将自己从繁琐的细节中解放,专注与更高层次经验沉淀。

所以要想构建一个提升自己生产力的agent,要了解自己的需求,用经验构建一个完整的流程编排,明确需要可变和不可变的部分,通过提词、skill、tool构建出一个智能agent。

本文的JVM agent为例,本质上就是基于个人对工具线上问题诊断经验所沉淀出一份说明书和提词,结合固定的工具链,完成工作流程编排和工具调用序列的综合落地方案。

本文到此结束,希望笔者的理念,对你有所帮助。

你好,我是 SharkChili ,Java Guide 核心维护者之一,对 Redis、Nightingale 等知名开源项目有深度源码研究经验。熟悉 Java、Go、C 等多语言技术栈,现任某知名黑厂高级开发工程师,专注于高并发系统架构设计与性能优化。

🌟 开源项目贡献

  • mini-redis :教学级 Redis 精简实现,助力分布式缓存原理学习
    🔗 github.com/shark-ctrl/...(欢迎 Star & Contribute)

📚 公众号价值 分享企业级架构设计、性能优化、源码解析等核心技术干货,涵盖分布式系统、微服务治理、大数据处理等实战领域,并探索面向AI的vibe coding等现代开发范式。

👥 加入技术社群 关注公众号,回复 【加群】 获取联系方式,与众多技术爱好者交流分布式架构、微服务等前沿技术!

参考

命令行工具-RuntimeUtil:www.bookstack.cn/read/hutool...

Arthas Spring Boot Starter:arthas.aliyun.com/doc/spring-...

Arthas Http API:arthas.aliyun.com/doc/http-ap...

本文使用 markdown.com.cn 排版

相关推荐
颜酱2 小时前
从0到1实现LRU缓存:思路拆解+代码落地
javascript·后端·算法
IT_陈寒2 小时前
JavaScript这5个隐藏技巧,90%的开发者都不知道!
前端·人工智能·后端
JaguarJack3 小时前
PHP 的异步编程 该怎么选择
后端·php·服务端
风象南3 小时前
AI 写代码效果差?大多数人第一步就错了
人工智能·后端
BingoGo3 小时前
PHP 的异步编程 该怎么选择
后端·php
焗猪扒饭13 小时前
redis stream用作消息队列极速入门
redis·后端·go
树獭非懒13 小时前
AI大模型小白手册|Embedding 与向量数据库
后端·python·llm
IT_陈寒16 小时前
SpringBoot实战:5个让你的API性能翻倍的隐藏技巧
前端·人工智能·后端