8 - AI 服务化 - AI 超级智能体项目教程

我们团队最近在疯狂的研究AI,期间调研学习了大量的资料,感谢大佬们的分享。

期间不仅做了几个还不错的项目,也踩坑不少,我们也发光发热,把我们总结的经验以专栏的方式分享出来,希望对大家有帮助。

这是专栏内容的第8篇 ,这是专栏链接,没看之前文章的朋友,建议先看之前的内容。

本节重点

AI 服务化是指将原本只能本地运行的 AI 能力转化为可远程调用的接口服务,使更多人能够便捷地访问 AI 能力。通过本节学习,⁠你将掌握如何将 AI 智能体转变为可供他人调用的服务

具体内容包括:

  • AI 应用接口开发
  • AI 智能体接口开发

在开始之前,先给大家提个醒,Spring AI 版本更新飞快,有些代码的写法随时可能失效,尽量以 官方文档 为准。

一、AI 应用接口开发

我们平时开发的大多数接口都是同步接口,也就是等后端处理完再返回。但是对于 AI 应用,特别是响应时间较长的对话类应用,可能会让⁠用户失去耐心等待,因此推荐使用 SSE(Server-Sent Events)技术实现实时流式输出,类似打字机效果,大幅提升用户体验。

接下来我们会同时提供同步接口(一次性完整返回)⁠和基于 SSE 的流式输出接口。

开发

1、支持流式调用

首先,我们需要为 InterviewAPP 添加流式调用方法⁠,通过 stream 方法就可以返回 Flux 响应式对象了:

java 复制代码
public Flux<String> doChatByStream(String message, String chatId) {
    return chatClient
            .prompt()
            .user(message)
            .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                    .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
            .stream()
            .content();
}

💡 建议不要直接使用 ChatResponse 作为返回类型,因为这会导致返回内容膨⁠胀,影响传输效率。所以上述代码中我们使用 content 方法,只返回 AI 输出的文本信息。

2、开发同步接口

在 controller 包下新建 AiController,将所有的接口都写在这个文件内。

先编写一个同步接口:

java 复制代码
@RestController@RequestMapping("/ai")public class AiController {

    @Resourceprivate InterviewAPP interviewAPP;

    @Resourceprivate ToolCallback[] allTools;

    @Resourceprivate ChatModel dashscopeChatModel;

    @GetMapping("/interview_app/chat/sync")public String doChatWithInterviewAPPSync(String message, String chatId) {
        return interviewAPP.doChat(message, chatId);
    }
}

3、开发 SSE 流式接口

然后编写基于 SSE 的流式输出接口,有几种常⁠见的实现方式:

1) 返回 Flux 响应式对象,并且添加 S⁠SE 对应的 MediaType:

java 复制代码
@GetMapping(value = "/interview_app/chat/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> doChatWithInterviewAPPSSE(String message, String chatId) {
    return interviewAPP.doChatByStream(message, chatId);
}

2)返回 Flux 对象,并且设置泛型为 Serv⁠erSentEvent。使用这种方式可以省略 MediaType:

java 复制代码
@GetMapping(value = "/interview_app/chat/sse")public Flux<ServerSentEvent<String>> doChatWithInterviewAPPSSE(String message, String chatId) {
    return interviewAPP.doChatByStream(message, chatId)
            .map(chunk -> ServerSentEvent.<String>builder()
                    .data(chunk)
                    .build());
}

3)使用 SSEEmiter,通过 send 方法⁠持续向 SseEmitter 发送消息(有点像 IO 操作):

java 复制代码
@GetMapping("/interview_app/chat/sse/emitter")public SseEmitter doChatWithInterviewAPPSseEmitter(String message, String chatId) {
    // 创建一个超时时间较长的 SseEmitterSseEmitter emitter = new SseEmitter(180000L); // 3分钟超时// 获取 Flux 数据流并直接订阅
    interviewAPP.doChatByStream(message, chatId)
            .subscribe(
                    // 处理每条消息
                    chunk -> {
                        try {
                            emitter.send(chunk);
                        } catch (IOException e) {
                            emitter.completeWithError(e);
                        }
                    },
                    // 处理错误
                    emitter::completeWithError,
                    // 处理完成
                    emitter::complete
            );
    // 返回emitterreturn emitter;
}

测试接口

开发完成后,我们可以通过 Swagger 接口文档来测试接口功能、验证会话上下文是否正常⁠工作。但是,浏览器控制台可能无法实时查看 SSE 返回的内容,这时我们不妨使用 CURL 工具进行测试。

一般 Linux 和 Mac 系统自带了 CU⁠RL 工具,打开终端,输入下列命令:

java 复制代码
curl 'http://localhost:8123/api/ai/interview_app/chat/sse?message=hello&chatId=1'

控制台会持续不断地输出文本片段

💡 在浏览器 F12 控制台中,可以直接选中⁠网络请求来复制 CURL 命令,非常便于测试

当然,如果你无法使用 CURL,也可以使用 IDEA 自带的 HT⁠TP Client 工具进行测试。点击接口旁边的绿豆就能自动生成测试代码:

二、AI 智能体接口开发

由于智能体执行过程通常包含多个步骤,执行时间较长,使用同步方法会导⁠致用户体验不佳。因此,我们采用 SSE 技术将智能体的推理过程实时分步输出给用户。

开发

1)首先在 BaseAgent 类中添加流式输出方法:

java 复制代码
/**
 * 运行代理(流式输出)
 *
 * @param userPrompt 用户提示词
 * @return SseEmitter实例
 */public SseEmitter runStream(String userPrompt) {
    // 创建SseEmitter,设置较长的超时时间SseEmitter emitter = new SseEmitter(300000L); // 5分钟超时// 使用线程异步处理,避免阻塞主线程
    CompletableFuture.runAsync(() -> {
        try {
            if (this.state != AgentState.IDLE) {
                emitter.send("错误:无法从状态运行代理: " + this.state);
                emitter.complete();
                return;
            }
            if (StringUtil.isBlank(userPrompt)) {
                emitter.send("错误:不能使用空提示词运行代理");
                emitter.complete();
                return;
            }

            // 更改状态
            state = AgentState.RUNNING;
            // 记录消息上下文
            messageList.add(new UserMessage(userPrompt));

            try {
                for (int i = 0; i < maxSteps && state != AgentState.FINISHED; i++) {
                    int stepNumber = i + 1;
                    currentStep = stepNumber;
                    log.info("Executing step " + stepNumber + "/" + maxSteps);

                    // 单步执行String stepResult = step();
                    String result = "Step " + stepNumber + ": " + stepResult;

                    // 发送每一步的结果
                    emitter.send(result);
                }
                // 检查是否超出步骤限制if (currentStep >= maxSteps) {
                    state = AgentState.FINISHED;
                    emitter.send("执行结束: 达到最大步骤 (" + maxSteps + ")");
                }
                // 正常完成
                emitter.complete();
            } catch (Exception e) {
                state = AgentState.ERROR;
                log.error("执行智能体失败", e);
                try {
                    emitter.send("执行错误: " + e.getMessage());
                    emitter.complete();
                } catch (Exception ex) {
                    emitter.completeWithError(ex);
                }
            } finally {
                // 清理资源this.cleanup();
            }
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
    });

    // 设置超时和完成回调
    emitter.onTimeout(() -> {
        this.state = AgentState.ERROR;
        this.cleanup();
        log.warn("SSE connection timed out");
    });

    emitter.onCompletion(() -> {
        if (this.state == AgentState.RUNNING) {
            this.state = AgentState.FINISHED;
        }
        this.cleanup();
        log.info("SSE connection completed");
    });

    return emitter;
}

上述代码虽然看着很复杂,但是大部分都是在原有 run 方⁠法的基础上进行改造,补充给 SseEmitter 推送消息的代码。

注意,上述代码中使用 CompletableFuture.runAsync() 实现非阻塞式异步执行,否则会长时间占用 Web 服务器线程池资源。

2)在 AiController 中编写新的接口,注意每次对话都要创建一个新的实例:

java 复制代码
@Resourceprivate ToolCallback[] allTools;

@Resourceprivate ChatModel dashscopeChatModel;

/**
 * 流式调用 Manus 超级智能体
 *
 * @param message
 * @return
 */@GetMapping("/manus/chat")public SseEmitter doChatWithManus(String message) {
    YuManus yuManus = new YuManus(allTools, dashscopeChatModel);
    return yuManus.runStream(message);
}

测试接口

跟前面一样,使用 CURL 工具进行测试,效果如图:

后端支持跨域

为了让前端项目能够顺利调用后端接口,我们需要在后端⁠配置跨域支持。在 config 包下创建跨域配置类,代码如下:

java 复制代码
/**
 * 全局跨域配置
 */@Configurationpublic class CorsConfig implements WebMvcConfigurer {

    @Overridepublic void addCorsMappings(CorsRegistry registry) {
        // 覆盖所有请求
        registry.addMapping("/**")
                // 允许发送 Cookie
                .allowCredentials(true)
                // 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("*")
                .exposedHeaders("*");
    }
}

注意,如果 .allowedOrigins("*").allowCredentials(true) 同时配置会导致冲突,因为出于安全考虑,跨域请求不能同时允许所有域名访问和发送认证信息(比如 Cookie)。

结语

AI智能体,AI编程感兴趣的朋友可以在掘金私信我,或者直接加我微信:wangzhongyang1993。

后面我还会更新更多跟AI相关的文章,欢迎关注我一起学习

相关推荐
长桥夜波3 小时前
【第二十周】机器学习笔记09
人工智能·笔记·机器学习
流烟默3 小时前
基于Optuna 贝叶斯优化的自动化XGBoost 超参数调优器
人工智能·python·机器学习·超参数优化
饕餮怪程序猿3 小时前
C++:大型语言模型与智能系统底座的隐形引擎
c++·人工智能
hzp6663 小时前
基于大语言模型(LLM)的多智能体应用的新型服务框架——Tokencake
人工智能·语言模型·大模型·llm·智能体·tokencake
摘星编程3 小时前
昇腾NPU性能调优实战:INT8+批处理优化Mistral-7B全记录
人工智能·华为·gitcode·昇腾
中科岩创3 小时前
陕西某地煤矿铁塔自动化监测服务项目
人工智能·物联网·自动化
亚马逊云开发者3 小时前
Agentic AI基础设施实践经验系列(三):Agent记忆模块的最佳实践
人工智能
小花皮猪3 小时前
多模态 AI 时代的数据困局与机遇,Bright Data 赋能LLM 训练以及AEO场景
人工智能·多模态·ai代理·aeo
爱吃烤鸡翅的酸菜鱼4 小时前
深度解析《AI+Java编程入门》:一本为零基础重构的Java学习路径
java·人工智能·后端·ai