跳到主要内容
AI 对话应用接口开发:同步接口、SSE 流式与智能体实现 | 极客日志
Java AI 大前端 java
AI 对话应用接口开发:同步接口、SSE 流式与智能体实现 综述由AI生成 了 AI 对话应用的接口开发,对比了同步接口与 SSE 流式接口的差异。基于 Spring Boot 实现了三种 SSE 方案(Flux、ServerSentEvent、SseEmitter),并集成了 AI 智能体的流式运行逻辑。同时介绍了利用 AI 编程工具生成 Vue3 前端页面,解决了跨域问题,并对智能体工具调用策略及最终响应展示进行了优化。
星云 发布于 2026/4/6 更新于 2026/5/21 23 浏览AI 对话应用接口开发
我们平时开发的大多数都是同步接口,也就是到后端处理完再返回。但是对于 AI 应用,特别是响应时间较长的应用,可能会让用户失去耐心等待,因此推荐使用 SSE 技术实现实时流式输出,类似打字机效果,大幅度提升用户体验。
开发 AI 对话同步接口
接下来我们先开发 AI 同步接口,对比学习。首先我们编写一个与"科泰旅游大师"对话的接口,使用常规同步的方式获得对话结果。
在 controller 包中新建 ChatWithAIController。
编写同步接口:
@RestController
@RequestMapping("/ai")
public class ChatWithAIController {
@Resource
private TravelApp travelApp;
@GetMapping("/chat/new")
public String newChat () {
return UUID.randomUUID().toString();
}
@GetMapping("/chat/travel/sync")
public String chat (String chatId, String message) {
return travelApp.chat(chatId, message);
}
}
通过/chat/new 接口,让前端利用 UUID 生成会话 ID,避免 A 用户能够通过猜测会话 ID 获取用户 B 的信息。
同步接口代码也非常简单,直接使用注入的 TravelApp 实例对用户的问题进行回复。
测试同步接口
启动 SpringBoot 项目,浏览器打开 Swagger 页面测试。
获取到 ChatId 后测试/api/ai/chat/travel/sync 时会出现很长的时间进度提示。
开发 AI 对话 SSE 流式接口
Server-Sent Events(SSE,服务器推送事件)
SSE 是 HTML5 定义的基于 HTTP 的单向实时数据推送技术,客户端通过 EventSource API 发起长连接,服务器以文本流持续推送事件,适配实时通知、行情更新等场景。
核心特性
单向通信 :仅服务器向客户端推送数据,客户端反馈需另发 HTTP 请求。
HTTP 原生支持 :基于 HTTP/1.1 分块传输编码,无需协议升级,易穿透代理与防火墙。
自动重连 :浏览器内置重连机制,默认 3 秒重试,可通过 retry 字段自定义。
轻量协议 :文本流格式,支持 data、event、 、 等字段,开销低。
id
retry
应用场景 :实时通知、股票行情、系统日志流、进度更新、IoT 状态推送等单向推送场景。
让 TravelApp 应用支持流式输出 要让接口支持流式输出,首先我们的 TravelApp 也不能使用同步的 call()+chatResponse() 方法返回 String 类型的全部内容。
需要使用到 Flux 响应式对象进行异步操作。在 TravelApp 类增加一个 StreamChat 方法:
public Flux<String> streamChat (String chatId, String message) {
FunctionCallback[] mcpTools = toolCallbackProvider.getToolCallbacks();
Flux<String> result = chatClient.prompt()
.user(message)
.advisors(spec -> spec
.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10 )
)
.tools(aiTools)
.tools(McpToolCallbackProxy.proxyAll(mcpTools))
.toolContext(Map.of("chatId" , chatId))
.stream().content();
return result;
}
实现 SSE 流式接口
直接返回 Flux 对象,但是必须添加 SSE 对应的 MediaType
修改 ChatWithAIController,增加方法 chatWithTravelAppSEE_Flux:
@GetMapping(value = "/chat/travel/sse/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatWithTravelAppSEE_Flux (String chatId, String message) {
return travelApp.streamChat(chatId, message);
}
返回 Flux 对象,并设置泛型为 ServerSentEvent
这种方式可省略 MediaType 的设置,但是要对 String 进行转化。
@GetMapping("/chat/travel/sse/generic")
public Flux<ServerSentEvent<String>> chatWithTravelAppSEE_Generic (String chatId, String message) {
return travelApp.streamChat(chatId, message)
.map(result -> ServerSentEvent.builder(result).build());
}
使用 SseEmiter 类型,通过 send 方法持续向 SseEmiter 对象发送消息
这种方式代码会稍微大一些,但是通用性和可控制性更强。
@GetMapping("/chat/travel/sse/emitter")
public SseEmitter chatWithTravelAppSEE_Emitter (String chatId, String message) {
SseEmitter emitter = new SseEmitter (300000L );
travelApp.streamChat(chatId, message).subscribe(
result -> {
try {
emitter.send(result);
} catch (Exception e) {
emitter.completeWithError(e);
}
},
error -> emitter.completeWithError(error),
() -> emitter.complete()
);
return emitter;
}
AI 智能体应用接口开发
改造智能体支持流式运行 类似于 TravelApp 增加流式对话 StreamChat(),我们也需要改造智能体 BaseAgent 增加支持流式运行的 streamRun() 方法:
public SseEmitter streamRun (String userPrompt) {
SseEmitter emitter = new SseEmitter (300000L );
CompletableFuture.runAsync(() -> {
try {
if (this .state != AgentState.IDLE){
emitter.send("当前 Agent 正在运行中,请勿重复运行!" );
emitter.complete();
return ;
}
if (StrUtil.isBlank(userPrompt)){
emitter.send("请输入用户提示词!" );
emitter.complete();
return ;
}
chatMemory.add(chatId, new UserMessage (userPrompt));
this .state = AgentState.RUNNING;
try {
for (int i = 0 ; i < maxSteps && state != AgentState.FINISHED; i++) {
currentStep = i + 1 ;
log.info("开始执行第{}步..." , currentStep);
String result = "Step " + currentStep + ": " + step();
emitter.send(result);
}
if (currentStep >= maxSteps){
state = AgentState.FINISHED;
emitter.send("已执行最大步骤数,任务结束!,最大步数为" +maxSteps);
}
emitter.complete();
} catch (IOException e) {
state = AgentState.ERROR;
try {
emitter.send("执行出错,请检查!" );
emitter.complete();
} catch (IOException ex) {
throw new RuntimeException (ex);
}
}finally {
cleanResources();
}
} catch (IOException e) {
emitter.completeWithError(e);
});
emitter.onTimeout(() -> {
this .state = AgentState.ERROR;
this .cleanResources();
log.warn("SSE 连接超时,会话 ID: {}" , chatId);
});
emitter.onCompletion(() -> {
if (state == AgentState.RUNNING){
this .state = AgentState.FINISHED;
}
this .cleanResources();
log.debug("SSE 连接完成,会话 ID: {}" , chatId);
});
return emitter;
}
上述代码看上去很复杂,但是大部分都是在原有 run 方法上改造,将结果信息交给 SseEmitter 通过 send 方法推送。
添加智能体异步接口 有了异步运行的方法,我们就可以添加智能体接口了,修改 ChatWithAIController 调用 ktTravelManus 智能体的接口。
@Resource
private ToolCallback[] toolCallbacks;
@Resource
private ChatModel dashscopeChatModel;
@GetMapping("/chat/manus/sse")
public SseEmitter chatWithManus (String message) {
TravelManus travelManus = new TravelManus (toolCallbacks, dashscopeChatModel);
travelManus.setMaxSteps(20 );
return travelManus.streamRun(message);
}
为了保证每次请求都是一个新的 TravelManus 实例,我们通过自己 new 来创建,那么构造函数必要的工具列表 aiTools 和 dashscopeChatModel 对象就需要注入进来。
当然我们也可以直接注入 TravelManus,然后 Controller 添加@Scope 注解值为 request,这样每次请求都会创建一个新的控制器,由于 KtTravelManus 的@Scope 是 prototype,也会注入一个新的 KtTravelManus 实例。
测试智能体接口 使用 API 测试工具测试。
重启 SpringBoot 项目,在 API 测试工具中新增如下接口测试。
参考参数:message=我想在长沙旅游三天预算 3000,帮我规划旅程,并安排附近公里的酒店,并生成 PDF
前端项目的生成和对接 由于我们这个项目不需要复杂的前端页面,我们可以使用 AI 编程工具来快速生成前端代码,极大提高开发效率。
创建前端项目 写这个项目需要具备前端的一些基础知识。在 vscode 打开终端,输入 npm create vite@latest 使用 vite 去创建一个最新的 vue 版本的项目。
接着我们编写提示词给 AI 编程工具
你是一位专业的前端开发,请帮我根据下列信息来生成对应的前端项目代码。
#需求
1、提供主页:可以切换不同应用 (考虑页面样式美观,操作流畅)
2、页面 1:AI 旅游大师应用。页面风格为聊天室,仿 Deepseek 等 AI 聊天界面,首次运行创建新会话,每次创建新会话调用后端 newChat 接口获得新的聊天 ID,老的会话可以通过列表保留 (仅界面支持,后端暂不支持返回会话列表)。聊天室内通过 SSE 的方式调用 chatwithTravelAppsSE Flux 接口,实时显示对话内容。
3、页面 2:AI 旅游智能体应用。页面风格同页面 1,但不保留历史会话,调用 chathithManus 接口,实时显示对话内容,按 Step x:的格式分割气泡展示步骤 (x 为步骤数字值)
###技术选型
1.Vue3+ ElementPlus 布局
2.Axios 请求库
后端接口信息
接口地址前缀:http://localhost:8080/api
SpringBoot 后端接口代码
与 AI 聊天的控制器
@RestController
@RequestMapping("/ai")
public class ChatWithAIController {
// 注入 TravelApp 实例
@Resource
private TravelApp travelApp;
@Resource
private ToolCallback [] aiTools;
@Resource
private ChatModel dashscopeChatModel;
@GetMapping ("/chat/manus/sse" )
public SseEmitter chatWithManus (String message ){
TravelManus travelManus = new TravelManus (aiTools, dashscopeChatModel);
travelManus.setMaxSteps (20 );
return travelManus.streamRun (message);
}
@GetMapping ("/chat/new" )
public String newChat ( ){
return UUID .randomUUID ().toString ();
}
@GetMapping (value = "/chat/travel/sse/flux" , produces = MediaType .TEXT_EVENT_STREAM_VALUE )
public Flux <String > chatWithTravelAppSEE_Flux (String chatId, String message ){
return travelApp.streamChat (chatId, message);
}
然后把提示词发送给 AI 后他会根据我们提示词生成一个页面。
但是在我们运行过程中会有一个错误返回,会话创建失败,然后我们端口控制台可以看到是一个跨域错误。
使用 CorsConfig 配置跨域 在我们后端项目的 config 包中新建 CorsConfig。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings (CorsRegistry registry) {
registry.addMapping("/**" )
.allowCredentials(true )
.allowedOriginPatterns("*" )
.allowedMethods("GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" )
.allowedHeaders("*" )
.exposedHeaders("*" );
}
}
配置完成后我们再次重启后端项目,测试是否可以正常运行了。
优化项目展示最终结果
优化工具提示词 由于工具的使用由 AI 决定,如果它胡乱使用就可能会出现一些异常,例如原本有图片搜索工具,它却选择去使用'网页爬取工具'去爬取百度图片搜索结果,导致读取的页面数据得到大量乱码 (可能百度做了技术防护)。
另外还有一个问题,就是我们仅仅告知了最大执行步骤,并未告知大模型目前在第几步,大模型由于上下文限制,可能会忘记自己现在做到第几步了,我们也可以在提示词中再次提示它。
我们修改一下 KtTravelManus 智能体的 NEXT_STEP_PROMPT 提示词,把这两个问题一起解决一些,看下情况是否能够改善。
根据用户需求,主动选择最合适的工具或工具组合。对于复杂的任务,你可以将问题分解开来,然后分步骤使用不同的工具来解决它。你应该尽量在 %d 个步骤内完成任务,否则任务将可能提前结束,当前已经是第 %d 步。在使用每种工具之后,要清晰地说明执行结果,并提出下一步的建议。如果您想在任何时刻终止这种交互,请使用'终止'工具 / 函数调用重要的选择工具规则:搜索和爬取 1、图片搜索只能使用 searchImage 工具,不允许通过 searchWeb 和 scrapeWebContent 工具 2、如果需要搜索信息,请使用 searchWeb 工具,不允许使用 scrapeWebContent 工具 3、如果需要爬取信息,请使用 scrapeWebContent 工具,不允许使用 searchWeb 工具 4、如果需要地理位置信息,请使用高德地图相关工具 5、如果需要图文并茂的 PDF,先下载图片,并使用 Markdown 格式引用相对路径的图片地址,再传递此 Markdown 内容,使用 generatePdf 工具生成 6、如果用户要求提供文件,在最终响应时提供生成文件的下载链接(A 标签)
智能体注册全部工具 如果想要给智能体用所有工具,那么我们就需要合并本地工具和 MCP 工具提供给 KtTravelManus 的构造函数。
private ToolCallback[] mergeAiTools() {
FunctionCallback[] mcpFunctionCallbacks = McpToolCallbackProxy.proxyAll(toolCallbackProvider.getToolCallbacks());
ToolCallback[] mcpToolCallbacks = Arrays.stream(mcpFunctionCallbacks)
.map(tool -> (ToolCallback)tool).toArray(ToolCallback[]::new );
ToolCallback[] allTools = ArrayUtil.addAll(aiTools, mcpToolCallbacks);
return allTools;
}
让最终结果展示 因为我们只发送了步骤信息,最终结果信息并没有发送,所以我们需要改造 BaseAgent,在最终完成状态时,发送最后一次的 AI 消息。
if (state == AgentState.FINISHED){
String finalAiResponse = getLastValidAiResponse();
if (StrUtil.isNotBlank(finalAiResponse)){
emitter.send("\n=== 智能体最终响应 ===" );
emitter.send(finalAiResponse);
emitter.send("\n=== 智能体最终响应结束 ===" );
}
}
protected String getLastValidAiResponse () {
List<Message> messageList = chatMemory.get(this .chatId, Integer.MAX_VALUE);
if (messageList == null || messageList.isEmpty()) {
return "" ;
}
for (int i = messageList.size() - 1 ; i >= 0 ; i--) {
Message message = messageList.get(i);
if (message instanceof AssistantMessage) {
AssistantMessage assistantMessage = (AssistantMessage) message;
String content = assistantMessage.getText();
if (StrUtil.isNotBlank(content)) {
log.info("Final response: {}" , content);
return content;
}
}
}
return "智能体未返回最终响应" ;
}
先通过 getLastValidAiResponse() 方法获取到最后一次的助理消息
然后通过 emitter.send("\n=== 智能体最终响应===") 发送了响应开始标识,这需要前端代码的支持。
然后发送响应内容 emitter.send(finalAiResponse);
最后发送结束符号 emitter.send("=== 响应结束===")
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online