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相关的文章,欢迎关注我一起学习。

相关推荐
2501_938140071 小时前
做内容很省心,2026好用AI笔记,支持抖音小红书视频无水印保存
人工智能
sell-cloud4 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国6 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI6 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区6 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris8 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy8 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V8 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI8 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技9 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人