AI agent实现打字机效果

搭建AI核心工作流程

1、流式传输,实现AI打字机效果

1、首先进行流式调用大模型,参考相关api。

Flux<ChatResponse> streamResponse = chatModel.stream(new Prompt(new UserMessage(prompt)));

java 复制代码
/**
 * 调用 LLM(流式输出)
 */
private String callLlmWithStreaming(String prompt, Consumer<String> streamHandler, SseMessageTypeEnum messageType) {
    StringBuilder contentBuilder = new StringBuilder();

    Flux<ChatResponse> streamResponse = chatModel.stream(new Prompt(new UserMessage(prompt)));

    streamResponse
    .doOnNext(response -> {
        String chunk = response.getResult().getOutput().getText();
        if (chunk != null && !chunk.isEmpty()) {
            contentBuilder.append(chunk);
            streamHandler.accept(messageType.getStreamingPrefix() + chunk);
        }
    })
    .doOnError(error -> log.error("LLM 流式调用失败, messageType={}", messageType, error))
    .blockLast();

    return contentBuilder.toString();
}
2、在接收流式调用时,传递一个回调函数对象。【Consumer<String> streamHandler】

1、该对象accept信息时,会触发回调。

2、回调一直往上传,传递到初始的定义方

【import java.util.function.Consumer;】

【public void executeArticleGeneration(ArticleState state, Consumer<String> streamHandler) {】

【agent2GenerateOutline(state, streamHandler);】

3、在往上传,就是调用方。再调用方产生了回调。

【// 执行智能体编排,并通过 SSE 推送进度

articleAgentService.executeArticleGeneration(state, message -> {

handleAgentMessage(taskId, message, state);

});】

4、handleAgentMessage回调函数会调用SSE进行流式推送到前端页面。

3、SSE流式调用管理代码
java 复制代码
package com.panda.multiagent.manager;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.panda.multiagent.constant.ArticleConstant.SSE_RECONNECT_TIME_MS;
import static com.panda.multiagent.constant.ArticleConstant.SSE_TIMEOUT_MS;

/**
 * @Author panda
 * @Date 2026-04-02
 * @Des
 */
@Component
@Slf4j
public class SseEmitterManager {
    /**
     * 存储所有的 SseEmitter
     */
    private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();

    /**
     * 创建 SseEmitter
     *
     * @param taskId 任务ID
     * @return SseEmitter
     */
    public SseEmitter createEmitter(String taskId) {
        SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS);

        // 设置超时回调
        emitter.onTimeout(() -> {
            log.warn("SSE 连接超时, taskId={}", taskId);
            emitterMap.remove(taskId);
        });

        // 设置完成回调
        emitter.onCompletion(() -> {
            log.info("SSE 连接完成, taskId={}", taskId);
            emitterMap.remove(taskId);
        });

        // 设置错误回调
        emitter.onError((e) -> {
            log.error("SSE 连接错误, taskId={}", taskId, e);
            emitterMap.remove(taskId);
        });

        emitterMap.put(taskId, emitter);
        log.info("SSE 连接已创建, taskId={}", taskId);

        return emitter;
    }

    /**
     * 发送消息
     *
     * @param taskId  任务ID
     * @param message 消息内容
     */
    public void send(String taskId, String message) {
        SseEmitter emitter = emitterMap.get(taskId);
        if (emitter == null) {
            log.warn("SSE Emitter 不存在, taskId={}", taskId);
            return;
        }

        try {
            SseEmitter.SseEventBuilder builder = SseEmitter.event();
            builder.data(message).reconnectTime(SSE_RECONNECT_TIME_MS);
            emitter.send(builder);
            log.debug("SSE 消息发送成功, taskId={}, message={}", taskId, message);
        } catch (IOException e) {
            log.error("SSE 消息发送失败, taskId={}", taskId, e);
            emitterMap.remove(taskId);
        }
    }

    /**
     * 完成连接
     *
     * @param taskId 任务ID
     */
    public void complete(String taskId) {
        SseEmitter emitter = emitterMap.get(taskId);
        if (emitter == null) {
            log.warn("SSE Emitter 不存在, taskId={}", taskId);
            return;
        }

        try {
            emitter.complete();
            log.info("SSE 连接已完成, taskId={}", taskId);
        } catch (Exception e) {
            log.error("SSE 连接完成失败, taskId={}", taskId, e);
        } finally {
            emitterMap.remove(taskId);
        }
    }

    /**
     * 检查 Emitter 是否存在
     *
     * @param taskId 任务ID
     * @return 是否存在
     */
    public boolean exists(String taskId) {
        return emitterMap.containsKey(taskId);
    }
}
4、前端使用

前端(EventSource) ←── SSE 推送 ←── 后端(SseEmitter)

解析消息类型

更新页面状态

sse.ts

javascript 复制代码
export interface SSEMessage {
  type: string
  data?: any
  [key: string]: any
}

export interface SSEOptions {
  onMessage: (message: SSEMessage) => void
  onError?: (error: Event) => void
  onComplete?: () => void
}


/**
 * 建立 SSE 连接
 */
export const connectSSE = (taskId: string, options: SSEOptions): EventSource => {
  const { onMessage, onError, onComplete } = options

  const eventSource = new EventSource(`/api/article/progress/${taskId}`, { withCredentials: true })

  eventSource.onmessage = (event) => {
    try {
      const message: SSEMessage = JSON.parse(event.data)
      onMessage(message)
      
      // 检查是否完成
      if (message.type === 'ALL_COMPLETE' || message.type === 'ERROR') {
        eventSource.close()
        onComplete?.()
      }
    } catch (error) {
      console.error('SSE 消息解析失败:', error)
    }
  }

  eventSource.onerror = (error) => {
    console.error('SSE 连接错误:', error)
    onError?.(error)
    eventSource.close()
  }

  return eventSource
}

/**
 * 关闭 SSE 连接
 */
export const closeSSE = (eventSource: EventSource | null) => {
  if (eventSource) {
    eventSource.close()
  }
}

在业务中直接使用就可以。

javascript 复制代码
try {
        // 创建任务
        const res = await createArticle({ topic: topic.value })
        taskId.value = res.data.data as string


        // 建立 SSE 连接
        eventSource = connectSSE(taskId.value, {
            onMessage: handleSSEMessage,
            onError: handleSSEError,
            onComplete: handleSSEComplete,
        })
    } catch (error: any) {
        message.error(error.message || '创建任务失败')
        isCreating.value = false
    }
相关推荐
星马梦缘17 小时前
数据库 第十三章 未完结版本
java·网络·数据库
程序猿乐锅17 小时前
【JAVASE | 第十六篇】多线程
java·开发语言
做个文艺程序员17 小时前
第01篇:Redis 从入门到上手:核心数据结构与 Java Spring Boot 实战详解
java·redis数据结构·redis入门·redis教程·java集成redis
影寂ldy17 小时前
C# 多接口、同名冲突、显式实现、接口继承 完整笔记
java·笔记·c#
JAVA面经实录91717 小时前
Spring Cloud Alibaba 微服务企业实战完整文档(架构+规范+调优+故障+源码)
java·运维·spring cloud·微服务
布局呆星17 小时前
Spring Boot + JWT + Spring Security 认证授权实战:双角色、双 Token、方法级权限,一次讲透
java·开发语言
大G的笔记本17 小时前
生产级 Spring Boot 网关完整实现方案
java·笔记·gateway
LucianaiB17 小时前
Swarm管理面板的多项目配置策略与模型别名机制的效率分析
java·服务器·前端
qq_25183645717 小时前
基于Spring Boot的数据标注与质检系统设计与实现
java·spring boot·后端
ANnianStriver17 小时前
PetLumina 09 — 全局日期格式化与通知详情完善
ai·ai编程·路由·日期格式化