- 上一篇实现了Flask框架 + AnythingLLM API
- 而接下来将通过向
/api/v1/workspace/{slug}/stream-chat
接口发送POST
请求来实现流式响应。
目标
- 实现生成的答案的分块(chunk by chunk)、实时推送
- 使得在用户界面中呈现出“打印”效果,而不是等待很长时间后才看到完整的答案,提升用户体验感和交互实时感。
一、准备工作
- 环境变量的配置和Flask应用框架的搭建在上一篇推文中已经详细说明了,不再赘述。
- 前提条件也请见上一篇推文。
二、流式响应的核心逻辑
2.1 核心代码
@app.route('/stream-ask', methods=['POST'])
def handle_stream_ask():
"""
接收问题,调用 AnythingLLM 的 stream-chat 接口,并将 SSE 流转发给前端。
"""
app.logger.info("收到 /stream-ask 接口的请求")
try:
data = request.form
if not data or 'question' not in data:
app.logger.warning("错误的请求:JSON 负载中缺少 'question'")
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
if not all([ANYTHINGLLM_BASE_URL, ANYTHINGLLM_API_KEY, ANYTHINGLLM_WORKSPACE_SLUG]):
app.logger.error("无法处理请求:AnythingLLM 配置缺失。")
return jsonify({"error": "内部服务器配置错误。"}), 500
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"
}
payload = {
"message": question,
"mode": "chat"
}
def event_stream():
try:
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}")
response.raise_for_status()
for line in response.iter_lines(decode_unicode=True):
if line:
app.logger.debug(f"收到 SSE 行: {line}")
if line.startswith("data:"):
data_content = line[len("data:"):].strip()
is_complete_signal = False
try:
parsed_data = json.loads(data_content)
if not is_complete_signal:
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}")
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
app.logger.info("AnythingLLM 流结束 (iter_lines 完成)")
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:
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 ("event: final_end\ndata: {}\n\n").encode("utf-8")
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")
