我们团队最近在疯狂的研究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 响应式对象,并且添加 SSE 对应的 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 对象,并且设置泛型为 ServerSentEvent。使用这种方式可以省略 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 系统自带了 CURL 工具,打开终端,输入下列命令:
java
curl 'http://localhost:8123/api/ai/interview_app/chat/sse?message=hello&chatId=1'
控制台会持续不断地输出文本片段
💡 在浏览器 F12 控制台中,可以直接选中网络请求来复制 CURL 命令,非常便于测试
当然,如果你无法使用 CURL,也可以使用 IDEA 自带的 HTTP 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相关的文章,欢迎关注我一起学习。