【创新实训】后端流式响应的实现(Flask框架 + AnythingLLM API)

  • 上一篇实现了Flask框架 + AnythingLLM API
  • 而接下来将通过向/api/v1/workspace/{slug}/stream-chat接口发送POST请求来实现流式响应。

目标

  • 实现生成的答案的分块(chunk by chunk)、实时推送
  • 使得在用户界面中呈现出“打印”效果,而不是等待很长时间后才看到完整的答案,提升用户体验感和交互实时感。

一、准备工作

二、流式响应的核心逻辑

2.1 核心代码

@app.route('/stream-ask', methods=['POST'])
def handle_stream_ask():
    """
    接收问题,调用 AnythingLLM 的 stream-chat 接口,并将 SSE 流转发给前端。
    """
    app.logger.info("收到 /stream-ask 接口的请求")

    # 1. 获取并验证前端请求数据
    try:
        data = request.form
        if not data or 'question' not in data:
            app.logger.warning("错误的请求:JSON 负载中缺少 'question'")
            # 对于流式接口,如果请求本身就有问题,直接返回错误 JSON
            return jsonify({"error": "请求体中缺少 'question' 字段"}), 400
        question = data['question']
        app.logger.info(f"收到的问题 (流式): {question}")
    except Exception as e:
        app.logger.error(f"解析请求 JSON 时出错: {e}")
        return jsonify({"error": "请求体中的 JSON 格式无效"}), 400

    # 2. 检查配置
    if not all([ANYTHINGLLM_BASE_URL, ANYTHINGLLM_API_KEY, ANYTHINGLLM_WORKSPACE_SLUG]):
         app.logger.error("无法处理请求:AnythingLLM 配置缺失。")
         return jsonify({"error": "内部服务器配置错误。"}), 500

    # 3. 准备调用 AnythingLLM 的 stream-chat 接口
    anythingllm_stream_endpoint = f"{ANYTHINGLLM_BASE_URL}/api/v1/workspace/{ANYTHINGLLM_WORKSPACE_SLUG}/stream-chat"
    headers = {
        "Authorization": f"Bearer {ANYTHINGLLM_API_KEY}",
        "Content-Type": "application/json",
        "Accept": "text/event-stream" # **重要:告诉服务器我们期望 SSE 流**
    }
    payload = {
        "message": question,
        # 根据 stream-chat API 文档,可能需要不同的 mode 或其他参数
        "mode": "chat" # 假设模式与非流式相同,请查阅文档确认
    }

    # 4. 定义一个生成器函数来处理和转发 SSE 流
    def event_stream():
        try:
            # **关键:设置 stream=True**
            with requests.post(
                anythingllm_stream_endpoint,
                headers=headers,
                json=payload,
                stream=True, # 启用流式请求
                timeout=180 # 流式连接可能需要更长的超时时间
            ) as response:
                app.logger.info(f"已连接到 AnythingLLM stream-chat: {response.status_code}")
                # 检查初始连接是否成功 (例如,认证失败会在这里报 4xx)
                response.raise_for_status()

                # 迭代处理从 AnythingLLM 收到的每一行数据
                # decode_unicode=True 自动将字节解码为字符串
                for line in response.iter_lines(decode_unicode=True):
                    if line: # 过滤掉空行
                        app.logger.debug(f"收到 SSE 行: {line}") # 调试时可以取消注释
                        if line.startswith("data:"):
                            # 提取 data: 后面的 JSON 字符串
                            data_content = line[len("data:"):].strip()
                            # 检查是否是表示流结束的特殊信号
                            is_complete_signal = False
                            try:
                                parsed_data = json.loads(data_content)
                                if not is_complete_signal:
                                     # 格式化为 SSE 事件并发送给前端
                                     repaired_data = data_content.encode("latin-1")
                                     corrected_data = repaired_data.decode("utf-8")
                                     yield (f"data: {corrected_data}\n\n").encode("utf-8")

                            except json.JSONDecodeError:
                                app.logger.warning(f"无法解析收到的 SSE data: {data_content}")
                                # 可以选择忽略无法解析的数据,或发送错误事件给前端
                                # yield f"event: error\ndata: {{\"message\": \"Invalid data received from source\"}}\n\n"
                            except Exception as e:
                                app.logger.error(f"处理 SSE 数据时发生意外错误: {e}")
                                # 可以发送一个错误事件给前端
                                yield f"event: error\ndata: {{\"message\": \"Error processing stream data\"}}\n\n"
                                break # 出错时可能需要中断流

                        elif line.startswith("event:"):
                             pass # 暂时忽略非 data 事件

                app.logger.info("AnythingLLM 流结束 (iter_lines 完成)")

        # 处理请求 AnythingLLM 期间发生的错误 (连接错误、HTTP错误等)
        except requests.exceptions.Timeout:
            app.logger.error("错误:连接 AnythingLLM stream-chat 超时。")
            yield f"event: error\ndata: {{\"message\": \"Connection to knowledge base timed out.\"}}\n\n"
        except requests.exceptions.HTTPError as e:
            error_message = f"错误:AnythingLLM API 返回状态 {e.response.status_code}。"
            # 尝试获取错误详情
            try:
                error_details = e.response.json()
                error_message += f" Details: {error_details.get('error', e.response.text)}"
            except: # noqa
                 error_message += f" Response body: {e.response.text}"
            app.logger.error(error_message)
            yield f"event: error\ndata: {{\"message\": \"Failed to connect to the knowledge base ({e.response.status_code}).\"}}\n\n"
        except requests.exceptions.RequestException as e:
            app.logger.error(f"错误:访问 AnythingLLM 的网络请求失败: {e}")
            yield f"event: error\ndata: {{\"message\": \"Could not connect to the knowledge base.\"}}\n\n"
        except Exception as e:
            app.logger.error(f"处理流时发生意外错误: {e}", exc_info=True)
            yield f"event: error\ndata: {{\"message\": \"An internal server error occurred while processing the stream.\"}}\n\n"
        finally:
            app.logger.info("流处理生成器结束。")
            # 可以在这里 yield 一个最终的自定义事件,确保前端知道流已关闭
            yield ("event: final_end\ndata: {}\n\n").encode("utf-8")
    # 5. 返回一个流式响应给前端
    # 关键:设置 mimetype='text/event-stream'
    # 使用 stream_with_context 确保在请求上下文之外也能访问 request 等对象
    return Response(stream_with_context(event_stream()), mimetype='text/event-stream')


2.2 代码分析

  • 定义请求路由/stream-ask
  • 在请求头headers中,设置参数"Accept": "text/event-stream",表示期望从AnythingLLM 那里获取 SSE 流
    • SSE:Server-Sent Events,一种服务器主动向客户端推送数据的技术。在与大模型进行对话时,模型会逐字逐句生成内容。
  • 定义event_stream()函数来处理并转发从AnythingLLM 接收到的 SSE 流
    • request.post()方法中,设置参数stream=True开启流式请求;另外,设置参数timeout=180来控制流式连接的超时时间,防止无限等待。
    • 遍历接收到的每一行数据(字节码),并将其自动解码为字符串;
    • 再将AnythingLLM 的响应中,data:后的数据提取出来,作为返回给前端的内容;
    • 将上一步提取出的数据,重新包装成 SSE 消息data: {corrected_data}\n\n,并yield给 Flask 应用。Flask 会将yield的内容,流式地传输到前端。
    • 设置结束信号,通过向Flask yield一个消息event:final_end\ndata: {}\n\n来实现。
  • 错误处理:分别对连接超时网络请求失败等异常发生时,通过event: error\ndata: {{\"message\": \"xxxxxx"}}\n\n的格式,向前端反馈错误信息供前端处理,而不是前端因为后端的崩溃而功能异常,增强程序的鲁棒性。
  • 最终返回一个Response对象,设置参数mimetype='text/event-stream',表示返回流式响应

2.3 遇到的问题(乱码问题)

在这里插入图片描述

  • 如上图,在测试流式响应时发现,返回的文本出现了乱码的情况。
    • 通过查询乱码的编码类型,发现是latin-1编码方式。
    • 因此,将从AnythingLLM接收到的响应,首先使用latin-1编码后,再使用utf-8解码后再yield,问题就解决了。
    • 以下是代码片段测试结果
repaired_data = data_content.encode("latin-1")
corrected_data = repaired_data.decode("utf-8")
yield (f"data: {corrected_data}\n\n").encode("utf-8")

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值