作者 :小沛
专栏 :Spring AI Alibaba JManus 技术深度解析
标签 :Spring AI
异步处理
前后端分离
Vue3
消息回显
📖 引言
在现代Web应用开发中,特别是AI对话系统,用户体验的核心在于实时性 和响应性。当用户发送一个复杂请求时,后端可能需要几秒甚至更长时间来处理AI推理、外部API调用等任务。如何让前端优雅地展示处理进度,并在完成后及时回显结果,是一个关键的技术挑战。
Spring AI Alibaba JManus项目通过精心设计的异步消息回显机制 ,完美解决了这一痛点。本文将以用户输入"帮我查询一下今天的天气"为实际案例,深度解析从前端交互到消息完整渲染的全流程技术实现。
🎯 实战案例:用户查询天气的完整交互流程
场景描述
用户在聊天界面输入:"帮我查询一下今天的天气",系统需要:
- 立即响应用户输入
- 调用天气API获取数据
- 实时显示处理进度
- 最终展示天气查询结果
让我们详细追踪这个过程中的每一个技术环节。
🚀 第一阶段:前端用户交互触发
1.1 用户输入处理
当用户在聊天界面输入"帮我查询一下今天的天气"并点击发送按钮时,前端Vue3组件立即执行以下流程:
typescript
// ui-vue3/src/components/chat/index.vue
async handleSendMessage() {
const query = "帮我查询一下今天的天气"
// 1. 立即添加用户消息到聊天界面
const userMessage: ChatMessage = {
id: Date.now().toString(),
content: query,
isUser: true,
timestamp: new Date().toLocaleTimeString(),
avatar: 'user-avatar.png'
}
this.messages.push(userMessage)
// 2. 添加AI助手占位消息(显示思考状态)
const assistantMessage: ChatMessage = {
id: (Date.now() + 1).toString(),
content: '',
isUser: false,
timestamp: new Date().toLocaleTimeString(),
thinking: '正在理解您的请求并准备查询天气信息...',
avatar: 'ai-avatar.png'
}
this.messages.push(assistantMessage)
// 3. 滚动到最新消息
this.$nextTick(() => {
this.scrollToBottom()
})
try {
// 4. 调用后端API
const response = await DirectApiService.sendMessage(query)
// 5. 启动计划执行管理
if (response.planId) {
console.log(`[Chat] 收到planId: ${response.planId},启动轮询管理`)
planExecutionManager.handlePlanExecutionRequested(response.planId, query)
}
} catch (error) {
console.error('[Chat] 发送消息失败:', error)
// 错误处理:更新助手消息显示错误状态
assistantMessage.content = '抱歉,处理您的请求时出现了问题,请稍后重试。'
assistantMessage.thinking = undefined
}
}
1.2 界面即时反馈
此时用户界面会立即显示:
👤 用户 [14:30:52]
帮我查询一下今天的天气
🤖 AI助手 [14:30:52]
💭 正在理解您的请求并准备查询天气信息...
🌐 第二阶段:前端API调用层
2.1 DirectApiService 实现
typescript
// ui-vue3/src/api/direct-api-service.ts
export class DirectApiService {
private static readonly BASE_URL = '/api/executor'
/**
* 发送用户消息到后端执行器
* @param query 用户查询内容
* @returns Promise<ApiResponse> 包含planId的响应
*/
public static async sendMessage(query: string): Promise<any> {
console.log(`[DirectApiService] 发送消息: "${query}"`)
const requestBody = { query }
console.log('[DirectApiService] 请求体:', requestBody)
const response = await fetch(`${this.BASE_URL}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(requestBody)
})
if (!response.ok) {
throw new Error(`API请求失败: ${response.status} ${response.statusText}`)
}
const result = await response.json()
console.log('[DirectApiService] 响应结果:', result)
return result
}
}
2.2 HTTP请求详情
请求信息:
- URL :
POST /api/executor/execute
- 请求头 :
Content-Type: application/json
- 请求体:
json
{
"query": "帮我查询一下今天的天气"
}
⚙️ 第三阶段:后端接口处理
3.1 ManusController 接收请求
java
// src/main/java/.../controller/ManusController.java
@RestController
@RequestMapping("/api/executor")
public class ManusController {
private static final Logger logger = LoggerFactory.getLogger(ManusController.class);
@Autowired
private PlanningFactory planningFactory;
@Autowired
private PlanIdDispatcher planIdDispatcher;
/**
* 异步执行用户查询请求
* @param request 包含用户查询的请求体
* @return 立即返回任务ID和状态
*/
@PostMapping("/execute")
public ResponseEntity<Map<String, Object>> executeQuery(@RequestBody Map<String, String> request) {
String query = request.get("query"); // "帮我查询一下今天的天气"
logger.info("收到用户查询请求: {}", query);
if (query == null || query.trim().isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "查询内容不能为空"));
}
// 1. 创建执行上下文
ExecutionContext context = new ExecutionContext();
context.setUserRequest(query);
// 2. 生成唯一计划ID(时间戳+随机字符)
String planId = planIdDispatcher.generatePlanId();
// 例如: "plan_20250103_143052_weather_abc123"
context.setCurrentPlanId(planId);
context.setRootPlanId(planId);
context.setNeedSummary(true);
logger.info("为查询请求生成planId: {}", planId);
// 3. 创建计划协调器
PlanningCoordinator planningFlow = planningFactory.createPlanningCoordinator(planId);
// 4. 异步执行任务(关键:不阻塞HTTP响应)
CompletableFuture.supplyAsync(() -> {
try {
logger.info("开始异步执行计划: {}", planId);
return planningFlow.executePlan(context);
} catch (Exception e) {
logger.error("执行计划失败: {}", planId, e);
throw new RuntimeException("执行计划失败: " + e.getMessage(), e);
}
});
// 5. 立即返回任务ID和状态(不等待执行完成)
Map<String, Object> response = new HashMap<>();
response.put("planId", planId);
response.put("status", "processing");
response.put("message", "天气查询任务已提交,正在处理中");
response.put("timestamp", System.currentTimeMillis());
logger.info("立即返回响应,planId: {}", planId);
return ResponseEntity.ok(response);
}
}
3.2 后端响应详情
响应信息:
json
{
"planId": "plan_20250103_143052_weather_abc123",
"status": "processing",
"message": "天气查询任务已提交,正在处理中",
"timestamp": 1704268232000
}
响应时间: 通常在 50-100ms 内返回(不等待AI处理)
🔄 第四阶段:后端异步计划执行
4.1 PlanningCoordinator 执行流程
java
// src/main/java/.../coordinator/PlanningCoordinator.java
public class PlanningCoordinator {
private static final Logger log = LoggerFactory.getLogger(PlanningCoordinator.class);
/**
* 执行完整的计划处理流程
* @param context 执行上下文
* @return 执行结果
*/
public ExecutionContext executePlan(ExecutionContext context) {
String planId = context.getCurrentPlanId();
log.info("开始执行完整计划流程,planId: {}", planId);
context.setUseMemory(true);
try {
// 1. 创建执行计划(AI分析用户意图)
log.info("步骤1: 创建执行计划 - {}", planId);
planCreator.createPlan(context);
// 2. 选择合适的执行器并执行
PlanInterface plan = context.getPlan();
if (plan != null) {
PlanExecutorInterface executor = planExecutorFactory.createExecutor(plan);
log.info("步骤2: 选择执行器 {} 处理计划类型: {} - {}",
executor.getClass().getSimpleName(),
plan.getPlanType(),
planId);
// 执行所有步骤(包括调用天气API)
executor.executeAllSteps(context);
log.info("步骤3: 执行所有计划步骤完成 - {}", planId);
} else {
log.error("计划创建失败,无法找到有效计划 - {}", planId);
throw new IllegalStateException("计划创建失败");
}
// 3. 生成执行摘要
log.info("步骤4: 生成执行摘要 - {}", planId);
planFinalizer.generateSummary(context);
log.info("计划执行完成 - {}", planId);
return context;
} catch (Exception e) {
log.error("计划执行过程中发生错误 - {}: {}", planId, e.getMessage(), e);
throw e;
}
}
}
4.2 执行步骤详解
在后台线程中,系统会执行以下步骤:
- 接收请求: AI分析"帮我查询一下今天的天气",识别为天气查询任务
- 计划生成: 创建包含天气API调用的执行计划
- 工具调用: 调用天气查询工具获取实时天气数据
- 结果整理: 将天气数据格式化为用户友好的回复
- 摘要生成: 生成最终的回复内容
📊 第五阶段:前端轮询管理启动
5.1 PlanExecutionManager 启动轮询
typescript
// ui-vue3/src/utils/plan-execution-manager.ts
export class PlanExecutionManager {
private static readonly POLL_INTERVAL = 2000 // 2秒轮询间隔
/**
* 处理计划执行请求
* @param planId 计划ID
* @param query 用户查询
*/
public handlePlanExecutionRequested(planId: string, query: string): void {
console.log(`[PlanExecutionManager] 计划执行请求: ${planId}`)
console.log(`[PlanExecutionManager] 查询内容: "${query}"`)
// 1. 设置活跃计划ID
this.state.activePlanId = planId
// 2. 启动轮询机制
this.initiatePlanExecutionSequence(query, planId)
}
/**
* 启动计划执行序列
* @param query 用户查询
* @param planId 计划ID
*/
public initiatePlanExecutionSequence(query: string, planId: string): void {
console.log(`[PlanExecutionManager] 启动执行序列: "${query}"`)
// 3. 触发对话轮次开始事件
this.emitDialogRoundStart(planId)
// 4. 开始定时轮询
this.startPolling()
}
/**
* 开始轮询计划执行状态
*/
public startPolling(): void {
// 清除之前的轮询定时器
if (this.state.pollTimer) {
clearInterval(this.state.pollTimer)
}
// 设置新的轮询定时器
this.state.pollTimer = window.setInterval(() => {
this.pollPlanStatus()
}, this.POLL_INTERVAL)
console.log(`[PlanExecutionManager] 开始轮询,间隔: ${this.POLL_INTERVAL}ms`)
// 立即执行一次轮询
this.pollPlanStatus()
}
}
5.2 轮询状态检查
typescript
/**
* 轮询计划执行状态
*/
private async pollPlanStatus(): Promise<void> {
if (!this.state.activePlanId) {
console.warn('[PlanExecutionManager] 没有活跃的planId,跳过轮询')
return
}
if (this.state.isPolling) {
console.log('[PlanExecutionManager] 上次轮询仍在进行中,跳过本次')
return
}
try {
this.state.isPolling = true
console.log(`[PlanExecutionManager] 轮询状态: ${this.state.activePlanId}`)
// 调用状态查询API
const details = await CommonApiService.getDetails(this.state.activePlanId)
if (!details) {
console.warn('[PlanExecutionManager] 未收到API响应数据')
return
}
// 更新缓存
if (details.rootPlanId) {
this.setCachedPlanRecord(details.rootPlanId, details)
console.log(`[PlanExecutionManager] 更新缓存: ${details.rootPlanId}`)
}
// 触发UI更新
this.emitPlanUpdate(details.rootPlanId ?? "")
// 检查是否完成
if (details.completed) {
console.log(`[PlanExecutionManager] 计划执行完成: ${details.rootPlanId}`)
this.handlePlanCompletion(details)
} else {
console.log(`[PlanExecutionManager] 计划仍在执行中,继续轮询`)
}
} catch (error) {
console.error('[PlanExecutionManager] 轮询状态失败:', error)
} finally {
this.state.isPolling = false
}
}
🔍 第六阶段:状态查询与数据获取
6.1 后端状态查询接口
java
// ManusController.java
/**
* 获取详细的执行记录
* @param planId 计划ID
* @return 执行记录的JSON表示
*/
@GetMapping("/details/{planId}")
public synchronized ResponseEntity<?> getExecutionDetails(@PathVariable("planId") String planId) {
logger.info("查询执行详情,planId: {}", planId);
// 1. 获取计划执行记录
PlanExecutionRecord planRecord = planExecutionRecorder.getRootPlanExecutionRecord(planId);
if (planRecord == null) {
logger.warn("未找到执行记录,planId: {}", planId);
return ResponseEntity.notFound().build();
}
// 2. 检查用户输入等待状态
UserInputWaitState waitState = userInputService.getWaitState(planId);
if (waitState != null && waitState.isWaiting()) {
planRecord.setUserInputWaitState(waitState);
logger.info("计划 {} 正在等待用户输入", planId);
} else {
planRecord.setUserInputWaitState(null);
}
// 3. 序列化为JSON返回
try {
String jsonResponse = objectMapper.writeValueAsString(planRecord);
logger.debug("返回执行详情,planId: {}, 数据大小: {} 字符", planId, jsonResponse.length());
return ResponseEntity.ok(jsonResponse);
} catch (JsonProcessingException e) {
logger.error("序列化执行记录失败,planId: {}", planId, e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("处理请求时发生错误: " + e.getMessage());
}
}
6.2 轮询请求详情
轮询请求信息:
- URL :
GET /api/executor/details/{planId}
- 频率: 每2秒一次
- 请求头 :
Accept: application/json
响应示例(执行中):
json
{
"planId": "plan_20250103_143052_weather_abc123",
"rootPlanId": "plan_20250103_143052_weather_abc123",
"title": "天气查询任务",
"userRequest": "帮我查询一下今天的天气",
"startTime": "2025-01-03T14:30:52.123",
"status": "RUNNING",
"completed": false,
"steps": [
{
"stepId": "step_1",
"title": "分析用户意图",
"status": "FINISHED",
"result": "识别为天气查询请求"
},
{
"stepId": "step_2",
"title": "调用天气API",
"status": "RUNNING",
"result": "正在获取天气数据..."
}
]
}
🎨 第七阶段:前端渐进式UI更新
7.1 状态更新回调处理
typescript
// ui-vue3/src/components/chat/index.vue
/**
* 处理计划更新事件
* @param rootPlanId 根计划ID
*/
onPlanUpdate(rootPlanId: string) {
console.log(`[Chat] 收到计划更新事件: ${rootPlanId}`)
const cachedRecord = planExecutionManager.getCachedPlanRecord(rootPlanId)
if (cachedRecord) {
// 1. 查找对应的助手消息
const assistantMessage = this.messages.find(msg =>
!msg.isUser && msg.planExecution?.currentPlanId === rootPlanId
)
if (assistantMessage) {
// 2. 更新执行步骤信息
assistantMessage.planExecution = cachedRecord
// 3. 更新思考状态
if (cachedRecord.steps && cachedRecord.steps.length > 0) {
const runningStep = cachedRecord.steps.find(step => step.status === 'RUNNING')
if (runningStep) {
assistantMessage.thinking = `正在执行: ${runningStep.title}`
}
}
// 4. 更新最终回复(如果已完成)
if (cachedRecord.completed && cachedRecord.finalReply) {
assistantMessage.content = cachedRecord.finalReply
assistantMessage.thinking = undefined
console.log(`[Chat] 显示最终回复: ${cachedRecord.finalReply}`)
}
// 5. 滚动到底部
this.$nextTick(() => {
this.scrollToBottom()
})
}
}
}
7.2 UI渐进式更新效果
执行过程中的界面变化:
👤 用户 [14:30:52]
帮我查询一下今天的天气
🤖 AI助手 [14:30:52]
💭 正在执行: 分析用户意图
📋 执行步骤:
✅ 分析用户意图 - 已完成
🔄 调用天气API - 执行中
⏳ 格式化结果 - 等待中
完成后的界面:
👤 用户 [14:30:52]
帮我查询一下今天的天气
🤖 AI助手 [14:30:55]
🌤️ 今天北京的天气情况:
📍 **地点**: 北京市
🌡️ **温度**: 15°C
☁️ **天气**: 多云
💨 **风力**: 东南风 3级
💧 **湿度**: 65%
👁️ **能见度**: 10公里
**温馨提示**: 今天天气较为舒适,适合外出活动,建议穿着轻薄外套。
📋 执行步骤:
✅ 分析用户意图 - 已完成
✅ 调用天气API - 已完成
✅ 格式化结果 - 已完成
📈 第八阶段:完成处理与资源清理
8.1 计划完成处理
typescript
/**
* 处理计划完成
* @param details 计划执行详情
*/
private handlePlanCompletion(details: PlanExecutionRecord): void {
console.log(`[PlanExecutionManager] 处理计划完成: ${details.rootPlanId}`)
// 1. 触发完成事件
this.emitPlanCompleted(details.rootPlanId ?? "")
// 2. 重置序列大小
this.state.lastSequenceSize = 0
// 3. 停止轮询
this.stopPolling()
// 4. 延迟清理资源(5秒后删除执行记录)
setTimeout(async () => {
if (this.state.activePlanId) {
try {
await CommonApiService.deleteDetails(this.state.activePlanId)
console.log(`[PlanExecutionManager] 执行记录已清理: ${this.state.activePlanId}`)
} catch (error) {
console.warn(`[PlanExecutionManager] 清理执行记录失败: ${error.message}`)
}
}
}, 5000)
// 5. 重置状态
if (details.completed) {
this.state.activePlanId = null
this.emitChatInputUpdateState(details.rootPlanId ?? "")
}
}
8.2 停止轮询
typescript
/**
* 停止轮询
*/
public stopPolling(): void {
if (this.state.pollTimer) {
clearInterval(this.state.pollTimer)
this.state.pollTimer = null
console.log('[PlanExecutionManager] 轮询已停止')
}
}
🔄 完整交互时序图
👤用户 🖥️前端Vue3 🌐DirectApiService ⚙️ManusController 🎯PlanningCoordinator 🌤️天气API 📊轮询管理器 输入"帮我查询一下今天的天气" 立即添加用户消息到界面 添加AI占位消息(思考状态) sendMessage(query) POST /api/executor/execute 生成planId 异步执行executePlan() 立即返回{planId, status: "processing"} 返回planId 启动轮询管理(planId) 1. 分析用户意图 2. 调用天气API 返回天气数据 3. 格式化结果 4. 生成最终回复 GET /api/executor/details/{planId} 返回当前执行状态 更新UI显示进度 渐进式更新消息内容 loop [每2秒轮询] par [后端异步处理] [前端轮询检查] 计划执行完成 最后一次状态查询 返回完成状态+天气结果 触发完成事件 显示完整天气信息 展示天气查询结果 停止轮询,清理资源 👤用户 🖥️前端Vue3 🌐DirectApiService ⚙️ManusController 🎯PlanningCoordinator 🌤️天气API 📊轮询管理器
🎯 核心技术要点总结
1. 异步处理架构
- 立即响应: 后端接收请求后立即返回planId,不等待处理完成
- 后台执行: 使用CompletableFuture在独立线程中执行AI推理和API调用
- 非阻塞: HTTP请求不会因为长时间处理而超时
2. 智能轮询机制
- 固定间隔: 每2秒查询一次执行状态
- 状态缓存: 前端缓存执行记录,避免重复处理
- 自动停止: 任务完成后自动停止轮询
3. 渐进式UI更新
- 即时反馈: 用户输入后立即显示在界面上
- 进度展示: 实时显示执行步骤和当前状态
- 流畅体验: 通过Vue3响应式系统实现平滑更新
4. 错误处理与容错
- 网络异常: 处理API调用失败的情况
- 超时处理: 避免长时间等待导致的用户体验问题
- 状态恢复: 支持页面刷新后的状态恢复
5. 性能优化策略
- 资源清理: 任务完成后自动清理执行记录
- 缓存机制: 使用LRU缓存减少重复请求
- 批量更新: 合并多个状态更新减少DOM操作
🎉 总结
Spring AI Alibaba JManus的异步消息回显机制通过精心设计的前后端协作,实现了优秀的用户体验:
- ⚡ 响应迅速: 用户操作后立即获得反馈
- 🔄 状态透明: 清晰展示处理进度和当前状态
- 🎯 结果准确: 完整展示AI处理结果
- 🛡️ 稳定可靠: 完善的错误处理和容错机制
这种架构模式特别适合处理AI对话、复杂计算、外部API调用等耗时操作,为现代Web应用提供了一个优秀的异步处理解决方案。
技术栈 : Spring Boot 3.x + Vue 3 + TypeScript + CompletableFuture
适用场景 : AI对话系统、长时间任务处理、实时状态监控
核心优势: 非阻塞处理、实时反馈、用户体验优化