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
    }
相关推荐
王小酱2 小时前
第 29 课:ECC 2.0 — Rust 控制面板与未来方向
ai编程
王小酱2 小时前
第 28 课:跨平台适配与插件机制
ai编程
王小酱2 小时前
第 30 课:综合实战 — 毕业项目
ai编程
王小酱2 小时前
第 23 课:安全(上)— AI 代理特有的威胁
ai编程
王小酱2 小时前
第 18 课:前端框架 — React / Next.js / Vue / Nuxt
openai·ai编程
王小酱2 小时前
第 21 课:API 设计 — RESTful 模式与规范
ai编程
王小酱2 小时前
第 12 课:调用链追踪 — 从 Command 到执行
openai·ai编程·airbnb
王小酱2 小时前
第 13 课:TDD 全流程 — RED-GREEN-IMPROVE
openai·ai编程·aiops
王小酱2 小时前
第 14 课:验证循环 — 从代码到可提交
openai·ai编程·aiops