大模型流式输出与工具调用之间如何保持状态一致?

考查重点:增量展示与执行状态关联。流式片段只作增量展示,完整参数校验后才执行工具;事件携调用 ID 与阶段,最终状态由执行结果确认

本题目录

参考答案

核心回答

把文本展示、工具参数收集和工具执行分开,由后端统一维护调用状态。 文本片段可以立即上屏,工具参数片段则先按调用标识归并;收到完整工具请求、完成解析和校验后,才交给执行器。

每次工具调用都要有可关联的 ID。执行结束后,把结果与原调用配对写回消息历史,再让模型继续生成;前端的“调用中、成功、失败”也由这些执行事件驱动,不能从模型说了“已经完成”推断真实状态。

一轮模型输出结束,可能只是要求执行工具,还没有得到最终答案。工具执行完成、模型回答完成和整个业务任务完成,是不同的状态。写文件、构建等操作还应根据实际结果确认,不能用连接结束或流结束代替成功。

增量参数怎样变成一次调用

流式工具参数往往不是完整的 JSON。例如写文件请求的参数,可能先到达文件路径,再分多次收到正文内容;函数名称和 ID 也可能只出现在部分数据块中。

应用应按供应商协议,用响应内的 index 或调用 ID 找到对应缓冲区,保留名称和 ID,顺序追加参数片段。多次调用分别收集,不把所有参数拼到同一个字符串里。请求完整后,再检查工具是否存在、参数能否解析、字段与业务条件是否满足。

即使某个中间片段暂时能解析成 JSON,也不代表供应商已经发送完该次调用;执行应等待协议或框架的完整请求信号。增量回调可以展示“正在准备写入文件”,不应一边收到正文片段,一边重复执行写文件工具。

执行结果怎样接回模型和页面

可以把一次调用的过程组织为“参数收集中、待执行、执行中、成功或失败”,由同一个执行协调层更新,再发出对应事件。事件同时关联本轮任务和工具调用,避免同一会话的下一轮回答混入上一轮工具状态。

模型上下文中保留原来的助手工具请求,再追加匹配调用 ID 的工具结果。模型据此决定继续调用工具还是生成最终回答。工具返回错误时,应把实际错误或受控的失败结果交回,页面也显示失败,不能只因为方法返回了字符串就标成成功。

前端的重复事件过滤与后端的执行幂等是两件事:前者防止同一调用出现多条“正在执行”,后者防止重试造成重复副作用。涉及写入或外部办理时,可按任务与调用记录执行状态和结果,重试前先确认上次操作是否已经生效;不能因为前端去重了,就认为工具只会执行一次。

怎样确认这一轮真的结束

后端应区分模型本轮停止、工具返回、最终回答完成,以及业务产物可用。收到工具调用请求时,先执行并回填结果,仍可能需要下一轮模型请求;流中断或页面断开时,则保留已完成操作的事实,不把半段回答记成完整成功,也不假定已执行的副作用自动回滚。

验证时可以重放参数被拆成多个片段、一次包含多个工具、工具返回失败、执行后连接中断等情况,检查是否只在完整参数形成后执行、结果是否匹配原调用、页面状态是否与真实操作一致。针对代码生成,还要把文件写入和构建结果分开确认。

AI代码生成平台Java

基于 AI代码生成平台

核心回答

AI 代码生成平台把普通代码文本流和带工具调用的 Vue 工程生成分开处理。HTML、多文件模式边展示边收集文本,流结束后统一解析并保存;Vue 模式使用 LangChain4j 的 TokenStream,分别处理文本片段、工具请求和工具执行回调,再转换成页面可读内容,通过 SSE 推送。

这个版本的关键是:部分工具请求只驱动展示,真正执行发生在完整模型响应形成之后。 工具结果按原请求写入模型记忆,再继续调用模型。展示层按工具 ID 避免重复提示,但这还不是完整的执行幂等机制;流结束也不代表工程一定构建成功。

聊天请求经过哪些层

AppController 接收应用 ID 和用户消息,调用 AppServiceImpl.chatToGenCode();服务层校验应用与用户,先记录用户消息,再进入 AiCodeGeneratorFacade.generateAndSaveCodeStream(),按生成类型选择对应的 LangChain4j 服务。这里的聊天链路直接进入模型和工具循环,不需要经过另一个 LangGraph4j 工作流。

HTML 和多文件模式返回 Flux<String>。门面类收集全部文本,在正常结束时调用代码解析器和文件保存器;简单流处理器再收集展示文本,写入聊天记录。这条路线没有把每个文本片段当成一次文件写入。

Vue 模式则把三类回调转换成内部 JSON 消息:AI_RESPONSE 携带模型文本,TOOL_REQUEST 携带工具请求信息,TOOL_EXECUTED 携带执行请求和返回结果。随后交给 JsonMessageStreamHandler,处理器把它们转成展示字符串,控制器再以 {"d": "展示片段"} 包装成 SSE 数据,并在正常结束后追加 done 事件。浏览器接收的不是这三类原始工具消息的直接透传。

部分工具回调为什么不会直接执行

项目内包含自定义的 dev/langchain4j 实现,回调语义需要沿这层看。在 OpenAiStreamingChatModel 中,工具片段到达时会保留调用 ID、名称并追加参数;传给 onPartialToolExecutionRequest() 的 arguments 是本次参数片段,不是已经拼好的完整参数。对应实现说明了这一点。

模型响应收集完成后,AiServiceStreamingResponseHandler.onCompleteResponse() 读取完整 AiMessage 中的工具请求,逐个交给 ToolExecutor。执行返回后,先把对应的 ToolExecutionResultMessage 加入记忆,再触发 onToolExecuted(),并带着更新后的消息请求下一轮模型;模型不再要求调用工具时,才回调应用层的最终完成处理。完整请求执行分支与部分回调是两条不同职责。

所以,参数分成多块时,应用层只是不断获知“这个工具正在形成请求”,不会由展示处理器拿半段参数重复执行工具。

展示状态与真实执行还有哪些区别

JsonMessageStreamHandler 在一次流处理内维护 seenToolIds。首次看到某个工具 ID 时,返回该工具的调用提示,后续同 ID 的请求片段返回空字符串,避免页面反复展示同一条提示。这个集合控制的是本次流内的提示去重,不能证明跨请求重试时工具只执行一次。

收到 TOOL_EXECUTED 后,处理器解析请求参数,调用工具的 generateToolExecutedResult() 生成展示内容,并追加到聊天记录缓冲区。虽然内部消息包含实际 result,当前展示分支没有读取它,而是依据参数格式化内容。例如写文件工具会根据文件路径和内容生成代码块;工具方法即使返回“文件写入失败”,该展示分支也没有据此明确标成失败。处理器和写文件工具共同体现了这个区别。

最终完成状态也有类似边界:Vue 的完成回调调用 buildProject(),但没有检查其布尔返回值,随后仍结束流;HTML 和多文件模式的解析保存异常也只记录日志。因此,当前 done 可以表示响应流正常走完,不能据此确认文件保存或构建一定成功。

进一步完善时,应把工具实际返回结果中的成功或失败传给展示层,构建失败也要进入明确的失败状态,再决定是否发送业务完成事件。这样模型记忆、页面提示与工程产物才能依据同一份执行事实更新,而不是都依据一段“看起来成功”的文字。

对应教程

  • 《Tools 工具调用》:参考模型发起工具请求、程序执行、工具结果回填的分工,理解通用消息闭环。
  • 《Agent 智能体》:参考流式展示中间步骤与多轮工具调用的关系;本题的 Java 回调、消息转换和构建行为来自所链接的项目源码。

参考资料

  • 国内:阿里云百炼:Function Calling:采用按 index 归并增量参数、补全助手工具请求后继续对话的示例,解释流式参数为什么先收集再执行。
  • 国内:DeepSeek:Tool Calls:参考模型只产生调用意图、工具由程序执行,以及通过 tool_call_id 回传结果的分工。
  • 国内:Spring AI Alibaba:Messages 消息:采用助手工具请求与工具结果按同一 ID 配对的消息结构,说明执行事实如何进入后续上下文。
  • 国内:腾讯云:对话接口的流式工具调用示例:参考示例中的参数片段合并与 tool_calls 结束原因,区分“本轮模型要求调用工具”和“最终业务完成”;这里采用的是协议示例,不推荐接入其待下线接口。
  • 国外:LangChain4j:Response Streaming:采用部分文本、部分工具请求、完整工具请求和完整模型响应的职责区分;项目具体回调以本文固定提交中的扩展实现为准。
  • 国外:LangChain4j:Tools (Function Calling):参考工具调用片段、调用索引与完整请求的关联,以及执行结果回传模型的闭环,解释展示回调与执行流程为何分开。

相关面经

1 篇

快手

Agent 开发工程师
一面
查看原面经 →
如何处理大模型流式输出与工具调用之间的状态一致性?

收录题集

1 个来源,共1 份题集

相关题目

4 题