📝 前言
Function Calling(函数调用)技术让AI不再局限于简单问答,而是能够像人类一样"使用工具"完成实际任务。
本文将带你使用 Spring AI 开发一个数据库运维机器人,它能:
✅ 理解自然语言(如"数据库CPU占用率太高了")

✅ 自动查询慢SQL Top 10

✅ 根据指令终止慢SQL进程

✅ 具备对话记忆,支持流式响应


技术栈:Spring Boot 3.x + Spring AI 1.1.0 + 通义千问

🎯 一、项目背景与需求分析

传统运维痛点:

  •         凌晨收到告警"数据库CPU 95%"
  •         手动登录服务器查询慢SQL
  •         逐个分析、手动kill

AI解决方案:

  •         告诉AI"帮我查一下慢SQL"
  •         AI自动查询并展示结果
  •         说"kill掉id为3和6的慢SQL"
  •         AI执行后自动展示最新状态

🏗️ 二、项目结构

function-call/
├── DbOpsController.java      # REST控制器
├── DbOpsTools.java           # AI工具类(Function Calling)
├── DbOpsService.java         # 业务逻辑层
├── DbOpsTicket.java          # 工单数据模型
├── DbChatStatus.java         # 工单状态枚举
└── db_ops_system_prompt.pt   # 系统提示词

💻 三、环境准备


3.1 Maven依赖

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring AI OpenAI兼容模型(支持通义千问) -->
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
        <version>1.1.0</version>
    </dependency>

    <!-- Lombok(简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

💡 关键点:spring-ai-starter-model-openai 虽然是OpenAI的名字,但通过配置 base-url 可以对接任何兼容OpenAI API的模型(如通义千问、智谱AI等)。

3.2 配置文件(application.yml)

server:
  port: 8013

spring:
  application:
    name: db-ops-bot
  
  ai:
    openai:
      api-key: your-dashscope-api-key  # 替换为你的通义千问API Key
      base-url: https://dashscope.aliyuncs.com/compatible-mode/
      chat:
        options:
          model: qwen-plus              # 使用通义千问Plus模型
          temperature: 0.7              # 创造性程度(0-1)
          maxTokens: 5000               # 最大响应长度

🔑 如何获取API Key:

  1. 访问阿里云百炼平台:https://bailian.console.aliyun.com/
  2. 注册并开通通义千问服务
  3. 在API-KEY管理页面创建密钥

3.3 数据模型

📦 DbChatStatus.java(工单状态枚举)
package cn.eavon.llm.mentor.model;

public enum DbChatStatus {
    TICKET_CREATED,    // 工单已创建
    INVESTIGATING,     // 正在处理
    RESOLVED,          // 已解决
    TICKET_ERROR       // 处理出错
}

💡 为什么用枚举?

  1. 限定状态只能是预定义的几种,防止非法状态值
  2. 代码可读性强(TICKET_CREATED 比字符串 "created" 更清晰)
  3. IDE可以提供自动补全

📦 DbOpsTicket.java(工单记录)
package cn.eavon.llm.mentor.model;

import com.fasterxml.jackson.annotation.JsonPropertyDescription;

public record DbOpsTicket(
        @JsonPropertyDescription("工单ID") String ticketId,
        @JsonPropertyDescription("用户ID") String userId,
        @JsonPropertyDescription("数据库服务器IP") String serverIp,
        @JsonPropertyDescription("工单状态") DbChatStatus status,
        @JsonPropertyDescription("问题描述") String issueDescription
) {
}

🔥 核心技术点:
1. record 关键字(Java 14+)

  •         专门用于存储不可变数据的类
  •         自动生成构造函数、getter、equals、hashCode、toString
  •         等价于传统类但代码量减少70%

2. @JsonPropertyDescription

  •         告诉JSON序列化器字段的含义
  •         在Spring AI中,这个注解会被AI模型读取,帮助AI理解返回数据的结构

🔧 DbOpsService.java(业务逻辑层)
package cn.eavon.llm.mentor.service;

import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Random;

@Service
public class DbOpsService {

    private final Random random = new Random();

    /**
     * 查询Top 10慢SQL(模拟实现)
     */
    public String getTop10SlowSql(String serverIp) {
        StringBuilder result = new StringBuilder();
        result.append("=== 数据库服务器 ").append(serverIp).append(" 慢SQL Top 10 ===\n");
        result.append("查询时间: ").append(
            LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
        ).append("\n");
        result.append("=".repeat(80)).append("\n\n");

        // 模拟常见的慢SQL语句
        String[] sampleSqls = {
                "SELECT * FROM orders WHERE status='pending' ORDER BY create_time DESC",
                "SELECT u.*, o.* FROM users u LEFT JOIN orders o ON u.id=o.user_id WHERE u.age > 30",
                "UPDATE inventory SET stock=stock-1 WHERE product_id IN (SELECT id FROM products WHERE category='electronics')",
                // ... 更多SQL
        };

        for (int i = 1; i <= 10; i++) {
            int sqlIndex = (i - 1) % sampleSqls.length;
            long execTime = random.nextInt(5000) + 1000;  // 随机执行时间 1000-6000ms
            int sqlId = random.nextInt(100) + 1;
            
            result.append("【排名 ").append(i).append("】\n");
            result.append("  SQL ID: ").append(sqlId).append("\n");
            result.append("  执行时间: ").append(execTime).append(" ms\n");
            result.append("  执行次数: ").append(random.nextInt(100) + 1).append(" 次\n");
            result.append("  平均锁等待: ").append(random.nextInt(500)).append(" ms\n");
            result.append("  SQL语句: ").append(sampleSqls[sqlIndex]).append("\n");
            result.append("-".repeat(80)).append("\n\n");
        }

        result.append("\n💡 建议操作:\n");
        result.append("  1. 对于执行时间超过3000ms的SQL,建议立即终止\n");
        result.append("  2. 使用工具 kill_slow_sql 终止指定的慢SQL进程\n");
        result.append("  3. 建议为高频查询添加索引优化\n");
        
        return result.toString();
    }

    /**
     * 终止指定的SQL进程(模拟实现)
     */
    public String killSqlProcess(String serverIp, String sqlId) {
        System.out.println("正在终止服务器 " + serverIp + " 上的SQL进程 [ID: " + sqlId + "]");
        
        boolean success = random.nextBoolean();  // 随机成功/失败
        
        if (success) {
            String message = "✅ 成功终止SQL进程 [ID: " + sqlId + "] 在服务器 " + serverIp + " 上\n" +
                    "   释放资源: CPU " + (random.nextInt(20) + 5) + "%, 内存 " + (random.nextInt(500) + 100) + "MB\n" +
                    "   操作时间: " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            return message;
        } else {
            throw new RuntimeException("❌ 终止SQL进程失败 [ID: " + sqlId + "]\n" +
                    "   错误原因: 进程可能已完成或不存在");
        }
    }
}

💡 设计思路:
为什么返回String而不是对象?

  • AI大模型更擅长处理结构化的文本
  • 格式化的文本可以直接展示给用户
  • 减少AI解析JSON的复杂度

🛠️ DbOpsTools.java 工具层(AI的"工具箱")
package cn.eavon.llm.mentor.function;

import cn.eavon.llm.mentor.service.DbOpsService;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class DbOpsTools {

    @Autowired
    private DbOpsService dbOpsService;

    @Tool(
            name = "query_slow_sql",
            description = "查询当前数据库服务器上执行时间最长的慢SQL,返回排名前10的慢查询语句及其详细信息。" +
                    "调用场景:1)用户反馈数据库服务器CPU占用率高;2)用户主动要求查看慢SQL;" +
                    "3)执行kill操作后需要展示最新状态。"
    )
    public String querySlowSql(
            @ToolParam(description = "数据库服务器IP地址,从对话上下文中获取") String serverIp
    ) {
        System.out.println("查询慢SQL - 服务器IP: " + serverIp);
        String result = dbOpsService.getTop10SlowSql(serverIp);
        System.out.println("慢SQL查询结果:\n" + result);
        return result;
    }

    @Tool(
            name = "kill_slow_sql",
            description = "终止指定的慢SQL进程。调用前必须明确告知用户即将执行的操作。" +
                    "调用条件:1)用户已明确提供要kill的SQL ID列表;2)用户已确认执行kill操作。" +
                    "执行完成后应再次调用query_slow_sql展示最新状态。"
    )
    public String killSlowSql(
            @ToolParam(description = "要终止的SQL进程ID列表,多个ID用逗号分隔,例如:3,6") String sqlIds,
            @ToolParam(description = "数据库服务器IP地址,从对话上下文中获取") String serverIp
    ) {
        System.out.println("执行kill操作 - 服务器IP: " + serverIp + ", SQL IDs: " + sqlIds);
        
        String[] ids = sqlIds.split(",");
        StringBuilder result = new StringBuilder();
        
        for (String id : ids) {
            String trimmedId = id.trim();
            try {
                String killResult = dbOpsService.killSqlProcess(serverIp, trimmedId);
                result.append(killResult).append("\n");
            } catch (Exception e) {
                result.append("终止SQL进程失败 [ID: ").append(trimmedId).append("]: ")
                      .append(e.getMessage()).append("\n");
            }
        }
        
        String finalResult = result.toString();
        System.out.println("Kill操作结果:\n" + finalResult);
        return finalResult;
    }
}

🔥 核心注解详解:

💡 Function Calling 工作原理:

用户:"数据库CPU占用率太高了"
         ↓
AI分析用户意图,发现需要查询慢SQL
         ↓
AI决定调用 query_slow_sql 工具
         ↓
Spring AI框架自动提取参数(serverIp)
         ↓
执行 DbOpsTools.querySlowSql("192.168.1.100")
         ↓
将返回结果传递给AI
         ↓
AI根据结果生成自然语言回复给用户

🎯 关键理解:AI不是直接执行代码,而是:

  1. 分析用户输入
  2. 判断是否需要调用工具
  3. 决定调用哪个工具、传什么参数
  4. Spring AI框架执行工具
  5. 将执行结果返回给AI
  6. AI根据结果生成回复

🌐 DbOpsController.java 控制器层(REST接口)
package cn.eavon.llm.mentor.function;

import cn.eavon.llm.mentor.model.DbChatStatus;
import cn.eavon.llm.mentor.model.DbOpsTicket;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

import java.util.UUID;

import static org.springframework.ai.chat.memory.ChatMemory.CONVERSATION_ID;

@RestController
@RequestMapping("/db/ops")
@Slf4j
public class DbOpsController {

    @Autowired
    private OpenAiChatModel chatModel;

    private ChatClient chatClient;

    @Autowired
    private DbOpsTools dbOpsTools;

    @Value("classpath:prompts/db_ops_system_prompt.pt")
    private Resource systemText;

    /**
     * 创建新工单
     */
    @GetMapping("/newTicket")
    public DbOpsTicket newTicket(String userId, String serverIp, 
                                  HttpServletResponse httpServletResponse) {
        httpServletResponse.setCharacterEncoding("UTF-8");

        String ticketId = UUID.randomUUID().toString();

        log.info("newTicket userId:{}, serverIp:{}, ticketId:{}", 
                 userId, serverIp, ticketId);

        return chatClient
                .prompt()
                .user(String.format(
                    "我需要运维支持,我的用户id是%s,数据库服务器IP是: %s," +
                    "工单ID是: %s,当前状态是 %s",
                    userId, serverIp, ticketId, DbChatStatus.TICKET_CREATED.name()
                ))
                .advisors(spec -> spec.param(CONVERSATION_ID, ticketId)
                        .param("chat_memory_retrieve_size", 100))
                .call()
                .entity(DbOpsTicket.class);
    }

    /**
     * 用户提问(支持流式响应)
     */
    @GetMapping("/ask")
    public Flux<String> ask(String question, String ticketId, 
                            HttpServletResponse httpServletResponse) {
        httpServletResponse.setCharacterEncoding("UTF-8");

        return chatClient
                .prompt()
                .user(question)
                .tools(dbOpsTools)
                .advisors(spec -> spec.param(CONVERSATION_ID, ticketId)
                        .param("chat_memory_retrieve_size", 100))
                .stream()
                .content();
    }

    /**
     * 初始化ChatClient(应用启动时执行一次)
     */
    @PostConstruct
    public void init() {
        // 创建对话记忆(最多保留10条消息)
        ChatMemory chatMemory = MessageWindowChatMemory.builder()
                .maxMessages(10)
                .build();

        // 构建聊天客户端
        chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
                .defaultSystem(systemText)
                .build();
    }
}

关键技术点:

  1. @PostConstruct:应用启动时执行一次,初始化ChatClient
  2. ChatMemory(对话记忆):让AI记住之前的对话,不用重复提供服务器IP
  3. .tools(dbOpsTools):启用Function Calling,让AI可以调用工具
  4. CONVERSATION_ID:区分不同工单的对话历史
🐛db_ops_system_prompt.pt(系统提示词)
# Role
你是一名资深的数据库运维专家,负责快速诊断和解决数据库性能问题。

# Task
第一步:问题诊断
- 识别用户描述的性能问题关键词(CPU高、慢查询等)
- 从上下文获取服务器IP

第二步:执行诊断
- 调用 query_slow_sql 工具查询Top 10慢SQL
- 分析结果并给出建议

第三步:执行优化
- 用户要求kill时,先确认再执行
- 执行后再次查询展示最新状态

# Limit
- 仅处理数据库性能问题
- kill操作前必须获得用户确认
- 使用专业但通俗的语言

💡 提示词工程(Prompt Engineering)要点:
1. 角色定义(Role)
       · 明确AI的身份和职责
       · 影响AI的语言风格和专业程度
2. 任务流程(Task)
        · 分步骤指导AI如何处理对话
        · 类似"业务流程图"
3. 限制条件(Limit)
        · 防止AI越权操作
        · 定义边界和安全规则
4. 标准话术
        · 提供模板,保证回复的专业性
        · 避免AI自由发挥导致不规范的回复

🚀 四、运行与测试

localhost:8013/db/ops/newTicket?userId=admin001&serverIp=192.168.1.100

localhost:8013/db/ops/ask?ticketId=0138a92c-7861-4dbc-ab63-2b4751a796a8&question=数据库服务器CPU占用率太高了,帮我查一下慢SQL

localhost:8013/db/ops/ask?ticketId=0138a92c-7861-4dbc-ab63-2b4751a796a8&question=帮我kill掉id为17、50、66、36的慢SQL

localhost:8013/db/ops/ask?ticketId=0138a92c-7861-4dbc-ab63-2b4751a796a8&question=确认

🎯 五、核心技术总结

技术 作用 类比
ChatClient 与AI交互的高级API JdbcTemplate
Function Calling 让AI调用Java方法 AI的"工具箱"
ChatMemory 对话记忆 记事本
流式响应 逐字输出 ChatGPT打字机效果
System Prompt 定义AI行为规范 员工手册


Spring AI的价值:让Java开发者无需深入学习AI,就能构建智能应用。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐