为什么建议国内的AI对话服务不采用EventSource协议?

        EventSource协议随着ChatGPT的兴起,越来越受到开发者的关注。

        众所周知,在Chrome检查模式下(F12),我们可以看到,一般情况下:POST请求发送给服务器,服务器会一次性返回Response信息回来(如下图,抓取登录百度的数据包)。

 

        而ChatGPT对话服务却稍微有所不同。虽然同样采用POST请求,将问题发送给服务器,而服务器却返回EventStream,而且EventStream的内容随着时间(Timing)一直在不断变化,直到输出内容结束。EventStream协议接受的请求头类型也是我们平时比较少见的:Content-Type:text/event-stream

        这就让我们产生一个疑问:EventStream究竟是什么?为什么Chatgpt采用EventStream进行数据的传输?通过直接询问Chatgpt,它给出了如下的答案:

EventStream 和 EventSource 是两个在 Web 开发中经常使用的术语。它们之间存在一定的关系。

EventStream:
EventStream 是一种基于消息传递的编程模型,通常用于实现实时数据流的处理。它是一种持续不断地从数据源接收事件,并将这些事件流传送给消费者的机制。EventStream 可以被视为一个无限的、按时间顺序排列的事件序列。

EventSource:
EventSource 是 Web API 的一部分,是为了实现服务器端向客户端推送事件而设计的。它定义了一种从服务器获取事件的方式,利用了浏览器的长连接技术,以便服务器能够主动将事件通过 HTTP 协议推送给客户端。EventSource 接口提供了 DOM 事件模型,允许开发者通过特定的 URL 与服务器建立连接,并通过监听事件的方式接收服务器推送的事件。

它们之间的关系:
EventStream 是一个抽象的概念,用于描述连续的事件流。它可以采用不同的实现方式,比如通过推送技术实现的服务器端主动推送、通过轮询实现的客户端主动拉取等。而 EventSource 是实现了服务器端主动推送的一种具体技术,它利用了长连接和事件监听机制,可以在浏览器与服务器之间建立持久性的连接,并实时接收服务器端推送的事件。

可以说,EventSource 是一种使用 EventStream 进行连接和事件推送的具体实现方式。

总结:
EventStream 是一种抽象的事件流机制,而 EventSource 是基于 EventStream 的一种具体实现,用于在 Web 开发中实现服务器端向客户端推送事件的技术。它们之间的关系是 EventSource 是一种使用 EventStream 实现的具体机制。

        EventSource协议并不是什么新玩意。早在十几年前,html5协议起草发布时,就已经作为重要的内容纳入其中,目的是为了实现服务器端往客户端更实时推送信息。

        阅读到这里,对网络传输协议稍微了解的读者不免产生一个疑问:很多系统的聊天功能、推送服务、游戏实时数据都是采用WebSocket协议,实现客户端和服务端的双通道通讯。那么,EventSource协议的优势是什么?通过对比,不难发现,EventSource 在某些场景下更适合,它的优势如下:

  1. EventSource 的实现更简单:WebSocket 是一种全双工通信协议,与传统的 HTTP 不同,它需要在客户端和服务器之间建立一个持久的、双向的连接。实现 WebSocket 需要在服务器和客户端都编写特定的逻辑,这对于一些简单的实时推送场景来说可能过于繁琐,而 EventSource 则提供了更简单的实现方式。它使用了长连接和纯文本传输,没有复杂的握手和协议解析过程,使得服务器和客户端的开发和维护变得更加容易。

  2. EventSource 的实现更轻量:WebSocket 建立了双向的持久连接,并且在整个通信过程中保持连接状态。这对服务器来说会增加一定的资源消耗,尤其是在需要同时处理大量连接时。EventSource 采用的是服务器向客户端单向推送数据的方式,是基于 HTTP/1.1 协议的一种机制,它使用了 HTTP 的长连接和基于文本的传输,相对 WebSocket 的二进制传输来说,它的实现更加轻量。这使得 EventSource 可以在各种环境下更广泛地应用,并且更容易适配现有的服务器架构。

  3. 浏览器支持度更好:尽管现代浏览器已经广泛支持 WebSocket,但 EventSource 的支持度更好。EventSource 在早期版本的浏览器中也得到了良好的支持,并且在一些限制性环境下(如移动应用)更容易使用。

        这也就不难解释为什么OpenAI团队在实现ChatGPT服务的时候,采用的是EventSource协议,而不是WebSocket协议了。ChatGPT这种服务是服务器向客户端推送回复内容的单向通讯,基本上不需要用到复杂的WebSocket协议。

        既然是这样,读者肯定会问:EventSource协议优势那么多,而且还是ChatGPT也在采用的协议,为什么你不建议国内的AI对话采用EventSource协议呢?

        笔者一开始在开发Bmob后端云的AI服务时,也是基于上述的分析,计划采用EventSource协议进行开发。可随着分析的不断深入,发现国内不少厂商在支持EventSource协议上的不足。由于Bmob后端云通过SDK的形式,提供简单易用的API调用给开发者,开发者的应用类型多种多样,包括小程序、Android、iOS、快应用、HTML5、uni-app等等,这就要求开发的AI人工智能SDK方案要兼容所有这些平台和服务。

        可遗憾的是,作为开发者数量比较多的小程序,首先就不支持EventSource,而且这个问题早在2018年前就有开发者提出,官方却始终没有解决。

         同样,uni-app也一样不支持EventSource,这就让这个协议雪上加霜。

         所以,我们在策划开发Bmob后端云的AI人工智能SDK时,综合考虑到这些,还是选择了WebSocket协议。虽然这种协议相对复杂,但无疑能够兼容国内几乎所有的平台。这也是笔者标题中提到的,如果你想开发一套能够兼容国内绝大多数平台的AI人工智能服务,建议抛弃EventSource协议,采用WebSocket协议。

        Over!

        欢迎有不同意见的朋友拍砖。

        欢迎加本人微信(xiaowon12)聊技术聊想法。

<think>我们正在处理用户的问题:用户在使用Spring AI调用Tool后,发现前端输出再是流式输出,希望了解原因并找到解决方案。 根据引用[1]中的信息,当Spring AI调用Tool时,会涉及到ToolExecutionEligibilityPredicate类中的isToolExecutionRequired方法。如果toolCalls为空,则判断为需要执行工具调用,因此会执行调用。这可能与流式输出中断有关。 另外,引用[2]提到了使用SSE(Server-Sent Events)实现流式输出的关键依赖,包括spring-boot-starter-webflux和spring-ai-mcp-server-webflux。因此,流式输出通常依赖于WebFlux和SSE技术。 可能的原因分析: 1. 当调用Tool时,Spring AI可能中断了原有的流式响应,转而执行工具调用。工具调用完成后,可能没有正确地恢复流式输出。 2. 工具调用的执行是同步的,导致整个响应被阻塞,直到工具调用完成,从而破坏了流式传输。 3. 在工具调用期间,响应类型可能从流式SSE被转换为普通的HTTP响应。 解决方案思路: 1. 确保在工具调用后,能够继续以流式的方式返回结果。这可能需要调整工具调用的处理逻辑,使其能够与流式输出兼容。 2. 使用异步方式执行工具调用,并在调用完成后继续流式输出结果。 3. 检查Spring AI的配置,确保在调用工具时仍然保持流式响应。 根据引用[1]中提到的代码位置(OpenAiChatModel类中调用ToolExecutionEligibilityPredicate),我们可以推断,当工具调用被触发时,Spring AI会执行工具调用,然后将工具调用的结果作为消息的一部分返回。但是,这个过程中可能中断了流式输出。 具体步骤: 1. 在调用工具之前,Spring AI可能已经开始了流式输出(例如,生成消息的一部分)。当需要调用工具时,它必须暂停流式输出,因为工具调用的结果需要被插入到消息中。 2. 工具调用完成后,Spring AI可能会继续生成后续的消息。但是,如果工具调用是同步的,那么整个等待过程会导致流式中断,前端可能只能接收到一个完整的响应(包含工具调用结果和后续生成的消息)。 因此,要维持流式输出,我们需要确保工具调用本身也是以流式友好的方式处理的。然而,目前Spring AI对工具调用的支持可能还完全适配流式场景。 参考引用[3],其中提到Spring AI集成Ollama调用本地大模型,实现聊天对话功能。这说明在非工具调用的场景下,流式输出是可行的。因此,问题在于工具调用与流式输出的兼容性。 可能的解决方法: 1. 避免在需要流式输出的情况下使用工具调用。如果必须使用工具,考虑将工具调用的结果提前获取,然后以流式方式输出整个响应(包括工具调用结果和AI生成的内容)。但这可能破坏了交互的实时性。 2. 修改Spring AI的源代码,使其在工具调用期间也能保持流式输出。例如,在工具调用执行时,先输出一个占位符,然后异步执行工具调用,在工具调用完成后再将结果插入到流中。但这需要深入了解Spring AI的内部机制。 3. 使用Spring AI的异步API,将工具调用放在一个单独的线程中执行,同时保持主响应流中断。但需要注意,这样可能会导致消息顺序错乱。 由于用户使用的是Spring AI,我们可以查看官方文档或社区是否有关于此问题的解决方案。另外,引用[2]中提到的MCP协议的SSE实时流式服务,可能提供了在工具调用时保持流式的方法。 具体到代码层面,根据引用[1],我们可以关注ToolExecutionEligibilityPredicate的判断逻辑。如果toolCalls为空,则执行工具调用,那么流式输出可能正常进行。而当需要执行工具调用时(toolCalls为空),就会进入工具调用流程,从而中断流式。 因此,一个临时的解决方案是:在需要工具调用时,正常使用流式输出;在需要工具调用时,放弃流式输出,改为同步响应。但这并能满足用户同时使用工具调用和流式输出的需求。 深入思考:Spring AI的流式输出通常是通过返回Flux<ChatResponse>来实现的。当调用工具时,这个Flux可能会被中断,因为工具调用需要阻塞等待结果。因此,我们需要将工具调用也封装成非阻塞的方式,例如使用Mono或Flux,然后将其与原来的Flux合并。 但是,在Spring AI的当前版本中,可能还没有这样的支持。因此,我们需要寻找替代方案。 建议步骤: 1. 检查Spring AI的版本,查看是否有相关的更新或修复。 2. 在调用模型时,尝试配置参数,避免在流式调用中使用工具。例如,在OpenAI的API调用中,可以设置`functions`参数为空,或者使用工具调用的功能。 3. 如果必须使用工具调用,并且需要流式输出,可以尝试以下思路: - 将工具调用的执行与AI的流式输出分离。即,当AI需要调用工具时,先返回一个特殊的标记(例如<tool_invocation>)给前端,然后前端根据这个标记再发起一个请求来执行工具并获取结果。之后,前端将工具调用的结果作为上下文再次请求AI,继续流式输出。这样,虽然整体流程变复杂,但每一步都可以保持流式。 由于Spring AI目前可能支持在流式过程中嵌入工具调用并保持流式,因此可能需要自定义处理。 总结原因:Spring AI在调用Tool时,会中断流式输出,因为工具调用是同步阻塞的,且当前实现没有将工具调用的结果以流式方式整合到后续的响应中。 解决方案: 1. 等待Spring AI未来版本支持流式工具调用。 2. 自定义修改Spring AI的源代码,将工具调用异步化,并重新设计流式响应流程。 3. 采用上述的分离方案,将工具调用从前端分离,保持AI响应的流式。 考虑到用户可能希望立即解决,我们推荐第三种方案,即分离工具调用和AI流式响应。 具体实施步骤(前端配合): 1. 前端请求AI,使用流式方式(SSE)。 2. 当AI需要调用工具时,返回一个特殊事件,例如: event: tool_call data: { "tool_name": "get_weather", "parameters": { "location": "Beijing" } } 3. 前端接收到这个事件后,发起一个独立的请求到后端执行工具调用(非流式),获取结果。 4. 前端将工具调用的结果作为上下文,再次发起流式请求(携带之前的历史消息和工具调用结果),继续对话。 5. 这样,每次需要工具调用时,就中断当前的流式连接,然后由前端重新发起新的流式请求,从而保持每次响应的流式特性。 这种方案需要前后端协作,并且需要修改AI的调用方式,使其在需要工具调用时提前返回。 在Spring AI中,我们可以通过自定义ChatModel来实现上述逻辑。具体来说: - 继承现有的ChatModel(如OpenAiChatModel),重写流式调用的方法,当检测到需要工具调用时,立即返回一个包含工具调用信息的特殊消息,而是等待工具调用完成。 - 然后,由前端处理工具调用,并将结果传回,后端再继续调用AI模型(流式)。 注意:这种方式改变了原有的交互协议,但可以保持流式输出。 如果用户希望在后端一次性完成,而依赖前端的多次请求,那么目前可能无法实现流式输出工具调用的结果。因为工具调用本身需要时间,而流式输出要求持续断地发送数据。 因此,我们根据用户的需求和场景,提供两种选择: 方案一:放弃在工具调用时使用流式输出,改为同步等待整个响应(包括工具调用结果和AI生成的内容)。 方案二:采用上述前后端协作的方案,将工具调用拆分为独立的请求,从而保持每次响应的流式特性。 最后,我们还可以参考Spring AI社区是否有更好的解决方案。 由于引用内容有限,以上分析和解决方案基于现有信息推理得出。 相关问题: 1. 如何配置Spring AI以避免在流式输出中调用工具? 2. Spring AI中如何自定义ChatModel以实现工具调用与流式输出的分离? 3. 使用Spring AI时,如何保持长对话的流式输出?</think>### Spring AI调用Tool导致前端非流式输出的原因及解决方法 #### 原因分析 1. **工具调用中断流式响应** 当Spring AI需要执行Tool调用时(如函数调用),系统会中断原有的流式响应流程。引用[^1]指出:`OpenAiChatModel`类中的`ToolExecutionEligibilityPredicate.isToolExecutionRequired()`方法会检查是否需要执行工具调用。若`toolCalls`为空(即无需调用工具),流式输出正常;但当需要执行工具调用时,系统会**暂停流式响应**,转而处理工具调用逻辑,导致前端无法继续接收流式数据。 2. **同步阻塞问题** 工具调用通常是同步操作(如数据库查询、API调用),这会阻塞响应线程。在工具执行完成前,后续的流式消息无法生成和推送,破坏了SSE(Server-Sent Events)的持续推送机制。 3. **响应类型转换** 在工具调用过程中,Spring AI可能将响应类型从`application/x-ndjson`(SSE流式格式)转换为普通`application/json`响应,导致前端无法以流式方式解析数据。 --- #### 解决方案 ##### 方法一:强制跳过工具调用(简单场景) 通过配置禁用工具调用,确保流式输出持续: ```java @Bean public OpenAiChatModel openAiChatModel(OpenAiApi openAiApi) { OpenAiChatOptions options = OpenAiChatOptions.builder() .withFunctionCall("none") // 禁用所有工具调用 .build(); return new OpenAiChatModel(openAiApi, options); } ``` > **适用场景**:需要工具调用的简单对话。但会牺牲工具调用功能[^1]。 ##### 方法二:异步工具调用(推荐) 改造工具调用逻辑,使其异步执行并重新接入流: 1. **自定义`ToolFunctionExecutor`** 使用`Mono`异步处理工具调用: ```java @Component public class AsyncToolExecutor implements FunctionCallback { @Override public Mono<ChatResponse> execute(ChatRequest request) { return Mono.fromCallable(() -> { // 同步工具调用逻辑(如调用外部API) return toolService.execute(request.getToolName(), request.getArguments()); }).subscribeOn(Schedulers.boundedElastic()); // 切换到弹性线程池 } } ``` 2. **集成到响应流** 在`ChatModel`中合并异步结果: ```java public Flux<ChatResponse> streamChat(ChatRequest request) { return Flux.create(sink -> { // 原始流式响应 chatModel.stream(request).subscribe(sink::next); // 检测到工具调用时 if (requiresToolCall(request)) { asyncToolExecutor.execute(request) .flatMap(toolResult -> // 将工具结果作为新消息追加到流 chatModel.stream(buildNewRequest(toolResult)) ) .subscribe(sink::next); } }); } ``` ##### 方法三:SSE通道分离(复杂场景) 将工具调用与AI响应拆分为独立SSE通道: 1. **前端监听双通道** ```javascript // 通道1:AI响应流 const aiEventSource = new EventSource("/ai-stream"); // 通道2:工具调用结果流 const toolEventSource = new EventSource("/tool-results"); ``` 2. **后端返回工具调用指令** 当需要工具调用时,返回特殊指令而非阻塞流: ```java if (requiresToolCall(request)) { sink.next(ChatResponse.of( "TOOL_CALL:get_weather?location=Beijing" // 自定义指令格式 )); sink.complete(); // 结束当前流 } ``` 前端解析到`TOOL_CALL`指令后,主动请求工具接口并获取结果,再发起新对话。 --- #### 关键配置验证 确保依赖和配置正确(引用[^2]): ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> <!-- 必须使用WebFlux --> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-mcp-server-webflux</artifactId> <!-- SSE支持 --> </dependency> ``` 在`application.properties`中启用响应流: ```properties spring.ai.chat.options.stream=true ``` > **效果验证**:工具调用期间,前端应持续收到`data: {...}`格式的SSE消息,而非单次完整响应。 --- ### 相关问题 1. **如何确保异步工具调用结果与原始对话的上下文一致性?** 2. **在Spring AI中,如何监控工具调用的执行延迟对流式响应的影响?** 3. **前端应如何设计以兼容混合流式响应(AI消息 + 工具调用指令)?** [^1]: 工具调用逻辑中断流式处理流程 [^2]: WebFlux依赖实现SSE流式传输 [^3]: Spring AI基础集成模式
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值