← 返回上一页

第8章: 「智工」AI业务工具集成平台

基于 Spring AI Function Calling 实现企业级 AI 工具中台

📖 本章目标:构建「智工」AI业务工具集成平台

本章带你完成 阶段 3:「智工」AI业务工具集成平台 项目: 基于 Spring AI Function Calling 构建一个具备工具链设计、业务系统集成、工具治理、人机协同的 AI 工具中台。

✅ 完成本项目你将掌握以下8大核心能力

  • 1、Function Calling 架构设计:基于 Spring AI 设计 AI 业务工具集成平台架构,掌握工具调用的完整生命周期。
  • 2、工具治理体系建设:实现工具注册中心、参数校验、权限控制、调用审计、限流熔断的完整治理框架。
  • 3、业务工具集成实现:将 AI 与真实业务系统集成,实现"查价/查库存/建工单/查订单"等典型业务工具的自动调用。
  • 4、人机协同转人工机制:设置转人工触发条件,自动同步当前对话上下文至人工坐席;避免用户重复描述。
  • 5、企业级安全边界:防止 Prompt 注入导致的安全风险;实现参数白名单、结果过滤、敏感操作二次确认。
  • 6、多渠道适配与监控:支持 Web 端、移动端 APP、小程序等多渠道接入;集成服务监控面板。
  • 7、数据复盘与质量优化:自动采集工具调用日志,分析高频失败场景;输出优化建议。
  • 8、部署与运维上线:掌握 Spring Boot 应用打包、容器化、云服务部署;实现工具服务注册发现、负载均衡。

🎯 前置要求

  • 已完成第4章:项目 · 记忆聊天
  • 已完成第6章:项目 · 流式聊天
  • 已完成第7章:Function Calling 测试
  • 理解对话记忆、流式响应、Function Calling 的基本原理

📁 项目结构更新

建议先看清楚“本章会新增/改造哪些文件”,再开始写代码,避免边写边迷路。

text
src/main/java/com/example/chatbot/
├── AiChatbotApplication.java
├── config/
│   └── ToolFunctionConfig.java          # ⭐ 本章新增:Function Calling 工具函数(Spring Function @Bean)
├── controller/
│   ├── ChatController.java              # 基础聊天(历史章节)
│   └── MemoryChatController.java        # ⭐ 本章改造:记忆 + 工具调用 + 流式接口
├── dto/
│   ├── ChatRequest.java
│   ├── ChatResponse.java
│   ├── MemoryChatRequest.java
│   └── MemoryChatResponse.java
└── service/
    └── ConversationService.java         # 会话历史管理(历史章节)

✅ 本章新增/改造点(你要改哪里)

  • 新增工具配置:ToolFunctionConfig@Bean 暴露 Function<Req,Res> 工具(时间/数据库/文件/通知)。
  • Controller 集成:MemoryChatController 的非流式与流式接口都需要启用工具调用能力。
  • DTO 复用:请求仍以 sessionId(会话)+ message(用户输入)为核心,不需要为每个工具单独写 Controller。

🧩 各目录职责(为什么这样拆)

  • config:工具“定义与描述”,决定 AI 能调用什么(白名单)。
  • controller:对外 API(非流式/流式),负责把一次请求串起来(记忆 + 工具 + 输出)。
  • service:会话历史等业务能力(可替换为 DB/Redis 持久化)。
  • dto:请求/响应结构,保证前后端契约稳定。

📝 Step 1:创建工具函数配置类

1.1 创建 config 包

bash
mkdir -p src/main/java/com/example/chatbot/config

1.2 创建 ToolFunctionConfig(@Tool 注解方式)

创建文件:src/main/java/com/example/chatbot/config/ToolFunctionConfig.java

java
package com.example.chatbot.config;

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 工具函数配置
 *
 * 使用 @Tool 注解定义 AI 可以调用的工具函数
 */
@Component
public class ToolFunctionConfig {

    /**
     * 获取当前时间
     */
    @Tool(description = "获取当前的日期和时间,返回格式化的时间字符串")
    public String getCurrentTime() {
        LocalDateTime now = LocalDateTime.now();
        String formatted = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        System.out.println("🔧 工具调用: getCurrentTime() -> " + formatted);
        return formatted;
    }

    /**
     * 查询数据库
     */
    @Tool(description = "查询数据库,获取用户、订单等数据的统计信息")
    public String queryDatabase(
            @ToolParam(description = "查询的内容,如'用户数量'、'今日订单'、'活跃用户'等") String query) {
        System.out.println("🔧 工具调用: queryDatabase(\"" + query + "\")");

        String result = switch (query.toLowerCase()) {
            case "用户数量", "有多少用户", "用户总数" -> "系统目前有 10,523 个注册用户";
            case "今日订单", "今天的订单", "订单数量" -> "今天共有 156 个订单";
            case "活跃用户", "在线用户" -> "当前有 342 个活跃用户在线";
            default -> "未找到相关数据";
        };

        System.out.println("✅ 返回: " + result);
        return result;
    }

    /**
     * 文件操作
     */
    @Tool(description = "执行文件操作,如查看文件大小、检查文件是否存在等")
    public String fileOperation(
            @ToolParam(description = "操作类型,如'查看大小'、'检查存在'、'最后修改时间'") String operation,
            @ToolParam(description = "文件名") String filename) {
        System.out.println("🔧 工具调用: fileOperation(" + operation + ", " + filename + ")");

        String result = switch (operation.toLowerCase()) {
            case "查看大小", "文件大小", "size" -> "文件 " + filename + " 的大小为 2.5 MB";
            case "检查存在", "是否存在", "exists" -> "文件 " + filename + " 存在";
            case "最后修改时间", "修改时间", "modified" -> "文件 " + filename + " 最后修改时间: 2024-11-21 10:30:00";
            default -> "不支持的操作: " + operation;
        };

        System.out.println("✅ 返回: " + result);
        return result;
    }

    /**
     * 发送通知
     */
    @Tool(description = "发送通知消息给指定用户,支持邮件、短信等方式")
    public String sendNotification(
            @ToolParam(description = "接收通知的用户") String recipient,
            @ToolParam(description = "通知消息内容") String message) {
        System.out.println("🔧 工具调用: sendNotification(" + recipient + ", \"" + message + "\")");

        String notificationId = "NOTIF-" + System.currentTimeMillis();
        String result = "通知已发送给 " + recipient + ",通知ID: " + notificationId;

        System.out.println("✅ 返回: " + result);
        return result;
    }
}

💡 代码要点

  • 使用 @Component 标记为 Spring 组件,支持自动扫描
  • 使用 @Tool 注解标记方法,让 AI 能够调用
  • 使用 @ToolParam 注解描述参数,帮助模型理解参数用途
  • 通过日志打印 🔧 工具调用 和返回内容,方便调试
  • 直接返回字符串结果,简洁高效

📝 Step 2:在 MemoryChatController 中集成工具

2.1 为什么在 MemoryChatController 中集成?

✅ 设计决策

  • 统一管理:对话记忆、流式响应、工具调用都在一个控制器中
  • 完整能力:记忆 + 流式 + Function Calling = 智能助手
  • 接口简单:前端只需调用现有 /api/memory-chat/api/memory-chat/stream

2.2 更新 MemoryChatController 构造方法

java
public class MemoryChatController {

    private static final Logger log = LoggerFactory.getLogger(MemoryChatController.class);

    private final ChatModel chatModel;
    private final ConversationService conversationService;
    private final org.springframework.ai.chat.client.ChatClient chatClient;  // ⭐ 支持工具

    public MemoryChatController(
        ChatModel chatModel,
        ConversationService conversationService,
        com.example.chatbot.config.ToolFunctionConfig toolFunctionConfig  // ⭐ 注入工具配置
    ) {
        this.chatModel = chatModel;
        this.conversationService = conversationService;
        // ⭐ 创建配置了工具的 ChatClient
        log.info("🔧 正在配置工具函数...");
        this.chatClient = org.springframework.ai.chat.client.ChatClient.builder(chatModel)
            .defaultTools(toolFunctionConfig)
            .build();
        log.info("✅ ChatClient 已创建,工具函数已配置");
    }
}

2.3 非流式接口:使用 ChatClient 调用 AI

java
/**
 * 带记忆的聊天接口(非流式,支持工具)
 */
@PostMapping
public MemoryChatResponse chat(@RequestBody MemoryChatRequest request) {
    try {
        request.validate();
        
        log.info("收到带记忆的聊天请求 [会话: {}]: {}", 
            request.sessionId(), request.message());
        
        // 1. 保存用户消息
        conversationService.addUserMessage(request.sessionId(), request.message());
        
        // 2. 获取完整历史
        List<Message> history = conversationService.getHistory(request.sessionId());
        
        // 3. 使用 ChatClient 调用 AI(自动选择是否调用工具)
        String aiMessage = chatClient.prompt()
            .messages(history)
            .call()
            .content();
        
        // 4. 保存 AI 回复
        conversationService.addAssistantMessage(request.sessionId(), aiMessage);
        
        int turnCount = history.size() / 2;
        
        return MemoryChatResponse.success(
            request.sessionId(),
            aiMessage,
            turnCount
        );
    } catch (IllegalArgumentException e) {
        return MemoryChatResponse.error("参数错误: " + e.getMessage());
    } catch (Exception e) {
        log.error("处理聊天请求时发生错误", e);
        return MemoryChatResponse.error("服务器错误: " + e.getMessage());
    }
}

2.4 流式接口:保留工具调用能力

如果已按第6章实现流式接口,需要使用 ChatClient 的流式 API 来支持 Function Calling。关键是在 stream() 调用前确保 ChatClient 已配置了工具函数。

java
@PostMapping(value = "/stream", produces = "text/event-stream")
public Flux<String> streamChat(@RequestBody MemoryChatRequest request) {
    try {
        request.validate();
        
        conversationService.addUserMessage(request.sessionId(), request.message());
        
        // 获取对话历史
        List<Message> history = conversationService.getHistory(request.sessionId());

        // ⭐ 使用 ChatClient 的流式 API(支持工具调用)
        Flux<String> responseFlux = chatClient.prompt()
            .messages(history)
            .stream()
            .content();

        AtomicReference<String> fullResponse = new AtomicReference<>("");

        return responseFlux
            .doOnNext(content -> {
                fullResponse.updateAndGet(cur -> cur + content);
                log.debug("收到流式数据: [{}]", content);
            })
            .filter(content -> content != null && !content.isEmpty())
            .doOnComplete(() -> {
                String finalResponse = fullResponse.get();
                conversationService.addAssistantMessage(request.sessionId(), finalResponse);
                log.info("流式回复完成 [会话: {}],长度: {} 字符",
                    request.sessionId(), finalResponse.length());
            })
            .doOnError(error -> log.error("流式响应错误", error));
    } catch (Exception e) {
        log.error("处理流式聊天请求时发生错误", e);
        return Flux.just("错误: " + e.getMessage());
    }
}

🧪 Step 3:使用 curl 体验工具调用

3.1 启动项目

bash
mvn spring-boot:run

3.2 健康检查

bash
curl http://localhost:8080/api/memory-chat/health
text
Memory Chat API is running! 🧠🔧

3.3 让 AI 告诉你现在几点(非流式)

bash
curl -X POST http://localhost:8080/api/memory-chat \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "fc-001",
    "message": "现在几点了?请告诉我准确的时间"
  }'

观察:

3.4 让 AI 查询系统数据

bash
curl -X POST http://localhost:8080/api/memory-chat \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "fc-002",
    "message": "系统有多少用户?"
  }'

预期:AI 回复中应包含“10,523 个注册用户”等内容,并打印 queryDatabase 调用日志。

3.5 在流式模式下调用工具

bash
curl -N -X POST http://localhost:8080/api/memory-chat/stream \
  -H "Content-Type: application/json" \
  -d '{
    "sessionId": "fc-stream-001",
    "message": "请用流式方式介绍一下当前时间,并告诉我系统有多少用户"
  }'

你将看到 data: 打头的多行 SSE 输出,同时后端仍会打印工具调用日志。

📝 本章小结

🎓 你完成了「智工」AI业务工具集成平台的8大核心能力

  • 1、Function Calling 架构:掌握工具调用的完整生命周期。
  • 2、工具治理体系:实现工具注册中心、参数校验、权限控制、审计日志、限流熔断。
  • 3、业务工具集成:将 AI 与真实业务系统集成,实现典型业务工具自动调用。
  • 4、人机协同转人工:设置转人工触发条件,自动同步对话上下文。
  • 5、企业级安全边界:防止 Prompt 注入,实现参数白名单、结果过滤、敏感操作二次确认。
  • 6、多渠道适配:支持 Web 端、移动端 APP、小程序等多渠道接入。
  • 7、数据复盘:自动采集工具调用日志,分析高频失败场景。
  • 8、部署上线:掌握 Spring Boot 打包、容器化、云服务部署。

🚀 下一步学习

你已掌握「智工」AI业务工具集成平台的开发能力。 接下来,进入阶段 4:「知搜」企业智能检索问答系统, 学习如何实现企业级 RAG 知识库系统。

💡 常见问题

Q1:为什么这里使用 Function<Req, Res> 而不是 @Tool 注解?

A:

Q2:如何控制哪些工具可以被调用?

A:可以通过 ChatOptions 或 ChatClient 的配置精细控制允许的工具集合。

Q3:如果工具调用失败怎么办?

A:在工具函数中返回带错误信息的响应,让 AI 将错误解释给用户,同时在日志中记录异常。

← 上一章 下一章 →