如何在R语言Shiny应用中实现LLM响应流式输出而非等待全量结果
在Shiny应用中实现LLM流式响应输出
核心实现思路
- 放弃
ellmer同步的chat()方法,改用支持流式输出的stream_async()接口 - 借助Shiny的
ExtendedTask处理异步流式任务,确保UI不被阻塞 - 通过Shiny的自定义消息机制,将LLM返回的文本片段实时传递到前端,用JavaScript逐步更新页面内容
完整实现代码
library(shiny) library(ellmer) library(promises) # 初始化LLM客户端(二选一) # Ollama本地模型 chat <- ellmer::chat_ollama(model = "gemma3:4b") # DeepSeek模型(测试用密钥) # chat <- chat_deepseek( # api_key = "API-Key-removed", # model = "deepseek-chat" # ) ui <- fluidPage( tags$div( tags$pre("Show me a prime number: "), id = "user_prompt" ), actionButton("ask_llm", "Ask", icon("robot")), tags$div( id = "llm_response", style = "padding: 10px; border: 1px solid #eee; border-radius: 4px; margin-top: 10px;" ), # 自定义JS处理流式内容更新 tags$script(HTML(" Shiny.addCustomMessageHandler('stream_chunk', function(message) { const responseDiv = document.getElementById('llm_response'); responseDiv.innerHTML += message.chunk; }); ")) ) server <- function(input, output, session) { # 创建异步流式任务 stream_task <- ExtendedTask$new( function(client, prompt) { # 获取流式响应 stream <- client$stream_async(prompt) # 迭代处理每个响应片段 stream$then(function(res) { for (chunk in res) { # 将片段发送到前端 session$sendCustomMessage("stream_chunk", list(chunk = chunk)) # 可选:控制输出速度,模拟真实打字效果 Sys.sleep(0.05) } }) } ) # 监听按钮点击事件 observeEvent(input$ask_llm, { # 清空之前的响应内容 session$sendCustomMessage("stream_chunk", list(chunk = "")) # 触发流式任务 stream_task$invoke(chat, "Show me a prime number: ") }) } shinyApp(ui, server)
关键细节说明
- 前端处理:通过
Shiny.addCustomMessageHandler监听后端发送的stream_chunk消息,每次收到片段就追加到响应容器中 - 异步任务:
ExtendedTask确保流式处理在后台运行,不会卡住Shiny的UI线程 - 流式迭代:遍历
stream_async()返回的响应流,逐个片段传递到前端,可选添加Sys.sleep()模拟自然打字速度 - 内容重置:点击按钮时先发送空字符串清空响应区域,避免多次点击内容叠加
内容的提问来源于stack exchange,提问作者LIANG Chen
相关产品推荐
相关产品推荐

