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
    }
相关推荐
Ai拆代码的曹操39 分钟前
opencode CLI 源码拆解:yargs + effectCmd 双层架构如何管理 20+ 子命令
架构·ai编程·opencode·源码拆解
AINative软件工程1 小时前
LLM 应用的熔断降级工程实践:Circuit Breaker 不只是重试的升级版
后端·llm·ai编程
学计算机的计算基1 小时前
操作系统内存管理全解:虚拟内存、页表、COW、malloc、OOM一篇搞定
java·笔记·算法
tachibana21 小时前
hot100 前 K 个高频元素(347)
java·数据结构·算法·leetcode
chexus1 小时前
21. 深入 Nginx HTTP 缓存源码:CDN功能
nginx·http·缓存
万亿少女的梦1681 小时前
基于Spring Boot、Java与MySQL的网络订餐系统设计与实现
java·spring boot·mysql·系统设计·网络订餐
倔强的石头_9 小时前
不想每次都从头解释:我用 Doubao-Seed-Evolving 做了一个「稿件接力站」
ai编程
咩咩啃树皮10 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
鱟鲥鳚10 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
大模型码小白12 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程