【AI】AI大模型之流式传输(前后端技术实现)

流式传输技术详解:从概念到实现的全过程

目录

  1. 什么是流式传输?
  2. 流式传输的实现要求
  3. 流式传输的三个层面
  4. 适配层与包装层的实现
  5. 消息类型分类逻辑
  6. 完整流程示例
  7. 总结与最佳实践

什么是流式传输?

**流式传输(Streaming)**是一种数据传输方式,数据以连续流的形式逐步发送和接收,而不是一次性传输全部数据。

主要特点

  1. 实时性:数据生成后立即发送,无需等待全部完成
  2. 渐进式:接收端可以边接收边处理
  3. 低延迟:用户能更快看到结果
  4. 内存友好:不需要在内存中保存全部数据

应用场景

  • 聊天应用(实时消息)
  • 文件上传/下载进度
  • 实时日志监控
  • AI 模型生成(逐字输出) ← 本文重点
  • 数据导入/导出进度

流式传输的实现要求

⚠️ 重要原则:双方都必须实现

流式传输需要发送方和接收方都实现才能正常工作:

组合 结果
✅ 后端流式 + 前端流式 正常工作,实时显示进度
❌ 后端流式 + 前端非流式 无法正常工作或体验差
⚠️ 后端非流式 + 前端流式 可以工作,但无实时效果
✅ 后端非流式 + 前端非流式 传统方式,等待全部完成

发送方(后端)实现

python 复制代码
# Flask 流式响应
def generate():
    yield f"data: {json.dumps({'type': 'info', 'message': '开始处理...'}, ensure_ascii=False)}\n\n"
    # 处理逻辑...
    yield f"data: {json.dumps({'type': 'token', 'token': '文本片段'}, ensure_ascii=False)}\n\n"

return Response(
    stream_with_context(generate()),
    mimetype='text/event-stream',
    headers={'Cache-Control': 'no-cache', 'Connection': 'keep-alive'}
)

接收方(前端)实现

javascript 复制代码
// Fetch API 流式读取
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();
    
    for (const line of lines) {
        if (line.startsWith('data: ')) {
            const msg = JSON.parse(line.substring(6));
            // 处理消息...
        }
    }
}

流式传输的三个层面

流式传输的实现涉及三个不同的层面,每个层面都有不同的格式要求:

1. 传输协议层(标准统一)

Server-Sent Events (SSE) - Web 标准协议

python 复制代码
# 后端:设置 SSE 响应头
mimetype='text/event-stream'
headers={
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    ...
}

SSE 格式规范:

  • 每行以 data: 开头
  • 每行以 \n\n 结尾(两个换行符)
  • 这是标准格式,所有浏览器都支持

2. 数据格式层(项目自定义)

自定义 JSON 消息格式

python 复制代码
# 后端发送的消息格式
{
    "type": "token",        # 消息类型标识
    "token": "文本片段"     # 实际的文本内容
}

消息类型:

  • info: 状态信息
  • error: 错误信息
  • results: 搜索结果
  • token: 答案片段(流式生成)
  • answer: 完整答案
  • final: 最终数据

3. 大模型 API 层(模型特定)

不同大模型库有不同的流式格式:

模型库 流式格式
Ollama {delta: "文本"}{text: "文本"}
LlamaIndex {delta: "文本"}{response: Object}
OpenAI {delta: "文本"}

适配层与包装层的实现

这是流式传输的核心技术:如何统一不同格式

完整流程

复制代码
大模型库(Ollama/LlamaIndex)
    ↓
    {delta: "文本"} / {text: "文本"} / {response: Object}
    ↓
┌─────────────────────────────────────────┐
│ 🔧 适配层(Adapter Layer)              │
│ rag_generate_with_llamaindex_stream()  │
│                                         │
│ 检查格式 → 提取文本 → 统一为字符串      │
└─────────────────────────────────────────┘
    ↓
    "文本片段" (字符串)
    ↓
┌─────────────────────────────────────────┐
│ 📦 包装层(Wrapper Layer)              │
│ search_alarm() 路由函数                 │
│                                         │
│ 字符串 → JSON对象 → JSON字符串 → SSE    │
└─────────────────────────────────────────┘
    ↓
    data: {"type":"token","token":"文本片段"}\n\n
    ↓
前端接收并显示

适配层详解

位置rag_generate_with_llamaindex_stream() 函数

作用:将不同大模型库的格式统一为字符串

场景1:Ollama LLM 直接调用
python 复制代码
# 调用 Ollama 的流式 API
response_stream = Settings.llm.stream_complete(prompt)

# 适配层 - 处理 Ollama 的不同返回格式
for token in response_stream:
    # 情况1:Ollama 返回 {delta: "文本片段"}
    if hasattr(token, 'delta') and token.delta:
        yield token.delta          # ← 提取 delta 属性
    
    # 情况2:Ollama 返回 {text: "文本片段"}
    elif hasattr(token, 'text'):
        yield token.text           # ← 提取 text 属性
    
    # 情况3:Ollama 返回其他格式(字符串或对象)
    else:
        token_str = str(token)     # ← 转换为字符串
        if token_str:
            yield token_str

适配逻辑:

  • ✅ 检查 delta 属性 → 提取文本
  • ✅ 检查 text 属性 → 提取文本
  • ✅ 其他情况 → 转换为字符串

统一输出:所有格式最终都 yield 字符串

场景2:LlamaIndex ChatEngine
python 复制代码
# 调用 LlamaIndex ChatEngine 的流式 API
response_stream = chat_engine.stream_chat(query)

# 适配层 - 处理 LlamaIndex 的不同返回格式
for response_chunk in response_stream:
    # 情况1:LlamaIndex 返回 {delta: "文本片段"}
    if hasattr(response_chunk, 'delta') and response_chunk.delta:
        yield response_chunk.delta
    
    # 情况2:LlamaIndex 返回 {response: ResponseObject}
    elif hasattr(response_chunk, 'response'):
        response_text = str(response_chunk.response)
        if response_text:
            yield response_text
    
    # 情况3:直接是字符串或其他格式
    else:
        chunk_str = str(response_chunk)
        if chunk_str:
            yield chunk_str
场景3:LlamaIndex QueryEngine
python 复制代码
# 调用 QueryEngine 的流式 API
streaming_response = query_engine.query(enhanced_query)

# 适配层 - 处理 QueryEngine 的不同返回格式
if hasattr(streaming_response, 'response_gen'):
    # 情况1:返回 StreamingResponse 对象,有 response_gen 生成器
    for text_chunk in streaming_response.response_gen:
        if text_chunk:
            yield text_chunk

elif hasattr(streaming_response, '__iter__') and not isinstance(streaming_response, str):
    # 情况2:直接是生成器
    for chunk in streaming_response:
        if hasattr(chunk, 'delta') and chunk.delta:
            yield chunk.delta
        else:
            chunk_str = str(chunk)
            if chunk_str:
                yield chunk_str
else:
    # 情况3:非流式,返回完整响应
    response_text = str(streaming_response)
    yield response_text

包装层详解

位置search_alarm() 路由函数

作用:将适配层输出的字符串包装成自定义 JSON 格式,并封装为 SSE 格式

包装过程
python 复制代码
# 调用适配层,获取字符串流
answer_stream = rag_generate_with_llamaindex_stream(query, results, chat_history_list=current_history)

# 包装层 - 将字符串包装成 JSON + SSE 格式
for token in answer_stream:                    # ← token 是字符串(来自适配层)
    if token is None:
        continue
    
    # 确保是字符串类型
    token_str = str(token) if not isinstance(token, str) else token
    
    if token_str:
        # 包装成自定义 JSON 格式
        json_data = {
            'type': 'token',      # ← 消息类型
            'token': token_str    # ← 文本片段
        }
        
        # 转换为 JSON 字符串
        json_str = json.dumps(json_data, ensure_ascii=False)
        
        # 包装成 SSE 格式:data: {...}\n\n
        yield f"data: {json_str}\n\n"
包装格式说明
  1. 自定义 JSON 格式:
json 复制代码
{
    "type": "token",        // 消息类型标识
    "token": "文本片段"     // 实际的文本内容
}
  1. SSE 格式:

    data: {"type":"token","token":"文本片段"}\n\n

设计优势

  1. 适配层:屏蔽不同库的格式差异,统一输出字符串
  2. 包装层:将字符串包装为应用层统一的消息格式
  3. 解耦:更换大模型库时只需修改适配层
  4. 扩展:新增消息类型只需在包装层添加

这就是适配器模式 + 包装器模式的组合应用。


消息类型分类逻辑

后端根据流程阶段和状态决定发送哪种类型的消息:

消息类型定义

消息类型 发送时机 发送条件 数据内容
info 流程关键节点 搜索开始/完成、RAG开始 message 字符串
error 出现错误 验证失败、配置错误、执行异常 message 错误信息
results 搜索完成 搜索完成后(无论是否有结果) data.results 结果列表
token RAG流式生成 每个文本片段生成时 token 文本片段
answer RAG生成完成 生成成功且有答案 answer 完整答案
final 流程结束 所有情况(成功/失败) data 包含所有最终数据

分类逻辑详解

1. info - 状态信息

发送时机:流程中的关键节点

python 复制代码
# 搜索开始时
yield f"data: {json.dumps({'type': 'info', 'message': '开始搜索知识库...'}, ensure_ascii=False)}\n\n"

# 找到精确匹配结果时
yield f"data: {json.dumps({'type': 'info', 'message': f'找到 {len(results)} 个精确匹配结果'}, ensure_ascii=False)}\n\n"

# 开始向量搜索时
yield f"data: {json.dumps({'type': 'info', 'message': '正在进行向量搜索...'}, ensure_ascii=False)}\n\n"

# 搜索完成时
yield f"data: {json.dumps({'type': 'info', 'message': f'找到 {len(results)} 个相关结果'}, ensure_ascii=False)}\n\n"

# 开始生成RAG回答时
yield f"data: {json.dumps({'type': 'info', 'message': '开始生成智能回答...'}, ensure_ascii=False)}\n\n"

分类逻辑:

  • ✅ 流程开始:搜索开始
  • ✅ 搜索阶段:精确匹配完成、向量搜索开始、搜索完成
  • ✅ RAG阶段:开始生成回答
2. error - 错误信息

发送时机:出现错误时

python 复制代码
# 查询为空
if not query:
    yield f"data: {json.dumps({'type': 'error', 'message': '查询内容不能为空'}, ensure_ascii=False)}\n\n"

# LLM未初始化
if Settings.llm is None:
    error_msg = "LlamaIndex LLM 未初始化,无法生成回答。请检查 Ollama 服务是否运行。"
    yield f"data: {json.dumps({'type': 'error', 'message': error_msg}, ensure_ascii=False)}\n\n"

# 流式生成失败
if not full_answer:
    error_msg = "流式生成失败,未收到有效回答"
    yield f"data: {json.dumps({'type': 'error', 'message': error_msg}, ensure_ascii=False)}\n\n"

# 异常捕获
except Exception as e:
    error_msg = f"流式生成过程出错: {str(e)}"
    yield f"data: {json.dumps({'type': 'error', 'message': error_msg}, ensure_ascii=False)}\n\n"

分类逻辑:

  • ✅ 输入验证错误:查询为空
  • ✅ 配置错误:LLM未初始化、RAG未启用
  • ✅ 执行错误:生成失败、异常
3. results - 搜索结果

发送时机:搜索完成后

python 复制代码
# 搜索完成后发送结果
yield f"data: {json.dumps({
    'type': 'results', 
    'data': {
        'results': results,           # 搜索结果列表(包含 images 字段)
        'count': len(results),        # 结果数量
        'search_mode': search_mode    # 搜索模式
    }
}, ensure_ascii=False)}\n\n"

分类逻辑:

  • ✅ 条件:搜索完成(无论是否有结果)
  • ✅ 数据:结果列表、数量、搜索模式
  • 图片数据 :每个结果对象的 images 字段包含图片路径数组
4. token - 答案片段(流式生成)

发送时机:RAG流式生成过程中

python 复制代码
# 流式生成答案片段
for token in answer_stream:
    if token is None:
        continue
    token_str = str(token) if not isinstance(token, str) else token
    if token_str:
        full_answer += token_str
        # 每个文本片段都发送一次 token 消息
        yield f"data: {json.dumps({'type': 'token', 'token': token_str}, ensure_ascii=False)}\n\n"

分类逻辑:

  • ✅ 条件:启用RAG且流式生成中
  • ✅ 频率:每个文本片段发送一次
  • ✅ 数据:单个文本片段
5. answer - 完整答案

发送时机:RAG生成完成后

python 复制代码
# 生成完成后发送完整答案
if full_answer:
    yield f"data: {json.dumps({
        'type': 'answer', 
        'answer': full_answer,                    # 完整答案
        'references': results[:5] if results else []  # 参考来源(包含 images)
    }, ensure_ascii=False)}\n\n"

分类逻辑:

  • ✅ 条件:RAG生成成功且有答案
  • ✅ 数据:完整答案、参考来源
  • 图片数据references 中每个对象的 images 字段
6. final - 最终数据

发送时机:整个流程结束时

python 复制代码
# RAG成功时(有答案)
yield f"data: {json.dumps({
    'type': 'final', 
    'data': {
        'success': True, 
        'answer': full_answer, 
        'results': results,              # 所有结果(包含 images)
        'references': results[:5],       # 参考来源(包含 images)
        'session_id': session_id
    }
}, ensure_ascii=False)}\n\n"

# 不使用RAG时(只有搜索结果)
yield f"data: {json.dumps({
    'type': 'final', 
    'data': {
        'success': True, 
        'results': results,              # 所有结果(包含 images)
        'session_id': session_id
    }
}, ensure_ascii=False)}\n\n"

分类逻辑:

  • ✅ 条件:流程结束(成功或失败)
  • ✅ 数据:包含所有最终数据(答案、结果、错误、会话ID等)
  • 图片数据resultsreferences 中都包含 images 字段

完整流程示例

场景1:传统搜索模式(不使用RAG)

复制代码
1. info: "开始搜索知识库..."
   ↓
2. info: "找到 5 个相关结果" 或 "正在进行向量搜索..."
   ↓
3. results: {
     results: [
       {id: 1, images: ["img1.jpg"], ...},
       {id: 2, images: ["img2.jpg"], ...},
       ...
     ],
     count: 5
   }
   ↓
4. final: {
     success: true,
     results: [{images: [...]}, ...],
     session_id: "..."
   }

图片返回时机:

  • results 消息:所有搜索结果的图片
  • final 消息:所有搜索结果的图片(再次返回)

场景2:RAG模式(成功)

复制代码
1. info: "开始搜索知识库..."
   ↓
2. info: "找到 5 个相关结果"
   ↓
3. results: {
     results: [{images: [...]}, ...],
     count: 5
   }
   ↓
4. info: "开始生成智能回答..."
   ↓
5. token: "根据"
   ↓
6. token: "知识库"
   ↓
7. token: "中的信息"
   ... (多个token,逐字显示)
   ↓
8. answer: {
     answer: "完整答案...",
     references: [{images: [...]}, ...]  // 前5条结果的图片
   }
   ↓
9. final: {
     success: true,
     answer: "完整答案...",
     results: [{images: [...]}, ...],      // 所有结果的图片
     references: [{images: [...]}, ...],    // 参考来源的图片
     session_id: "..."
   }

图片返回时机:

  • results 消息:所有搜索结果的图片(第一次)
  • answer 消息:前5条参考结果的图片(第二次)
  • final 消息:所有结果和参考来源的图片(第三次和第四次)

场景3:RAG模式(失败)

复制代码
1. info: "开始搜索知识库..."
   ↓
2. results: {results: [{images: [...]}, ...], count: 5}
   ↓
3. info: "开始生成智能回答..."
   ↓
4. error: "LlamaIndex LLM 未初始化..."
   ↓
5. final: {
     success: true,
     results: [{images: [...]}, ...],
     rag_error: "...",
     session_id: "..."
   }

图片数据的返回时机

重要发现:图片不是单独返回的

图片数据不是单独返回 ,而是作为搜索结果的一部分,每个结果对象都有一个 images 字段(数组)。

图片返回的多个时机

返回时机 消息类型 包含字段 说明
搜索完成后 results data.results[].images 所有搜索结果的图片
RAG生成完成后 answer references[].images 前5条参考结果的图片
流程结束时 final data.results[].images data.references[].images 所有结果和参考来源的图片

图片数据结构

json 复制代码
{
  "id": 1,
  "alarm_code": "E001",
  "alarm_message": "温度传感器故障",
  "solution": "检查传感器连接...",
  "images": [
    "uploads/images/sensor1.jpg",
    "uploads/images/sensor2.jpg"
  ],
  ...
}

总结与最佳实践

核心技术要点

  1. 三个层面分离

    • 传输协议层(SSE):标准统一
    • 数据格式层(JSON):项目自定义
    • 大模型API层:模型特定
  2. 适配器模式

    • 适配层:统一不同库的格式差异
    • 包装层:统一应用层的消息格式
  3. 消息类型设计

    • 按流程阶段分类
    • 实时反馈 + 最终数据
    • 错误处理完善

实现建议

  1. 统一定义消息类型

    python 复制代码
    # 使用常量定义
    class MessageType:
        INFO = 'info'
        ERROR = 'error'
        RESULTS = 'results'
        TOKEN = 'token'
        ANSWER = 'answer'
        FINAL = 'final'
  2. 错误处理

    • 所有错误都发送 error 消息
    • 最终都发送 final 消息(包含错误信息)
  3. 性能优化

    • 快速搜索直接返回完整结果
    • RAG模式流式生成,实时显示
  4. 用户体验

    • 实时状态反馈(info)
    • 逐字显示答案(token)
    • 完整数据保证(final)

设计模式应用

  • 适配器模式:适配不同大模型库的格式
  • 包装器模式:统一应用层的消息格式
  • 观察者模式:前端监听流式消息并更新UI

结语

流式传输是一个涉及多个层面的复杂技术,需要:

  1. 理解三个层面:传输协议、数据格式、大模型API
  2. 实现适配层:统一不同库的格式差异
  3. 实现包装层:统一应用层的消息格式
  4. 设计消息类型:按流程阶段分类
  5. 前后端配合:双方都必须实现流式处理

通过这种设计,我们实现了:

  • 🚀 实时反馈:用户可以看到搜索和生成进度
  • ✍️ 逐字显示:答案逐字显示,类似 ChatGPT
  • 🔄 向后兼容:传统搜索模式不受影响
  • 🛡️ 错误处理:完善的错误处理和提示

希望这篇文章能帮助你理解流式传输的完整实现过程!


相关推荐
黑客思维者1 天前
二次函数模型完整训练实战教程,理解非线性模型的拟合逻辑(超详细,零基础可懂)
人工智能·语言模型·非线性拟合·二次函数模型
小途软件1 天前
ssm607家政公司服务平台的设计与实现+vue
java·人工智能·pytorch·python·深度学习·语言模型
WJSKad12351 天前
传送带物体检测识别_基于YOLO11与RGCSPELAN改进算法_工业视觉检测系统
人工智能·算法·视觉检测
富唯智能1 天前
重新定义“自动化搬运项目”:15分钟部署的复合机器人如何革新柔性生产
人工智能·机器人·自动化
zxy28472253011 天前
利用C#对接BotSharp本地大模型AI Agent示例(2)
人工智能·c#·api·ai agent·botsharp
初次攀爬者1 天前
RAG知识库核心优化|基于语义的智能文本切片方案(对比字符串长度分割)
人工智能·后端
宋情写1 天前
JavaAI05-Chain、MCP
java·人工智能
whaosoft-1431 天前
51c~目标检测~合集3
人工智能
掘金一周1 天前
高德地图与Three.js结合实现3D大屏可视化 | 掘金一周 1.8
前端·人工智能·后端