你好呀,很高兴你来。我是一名大三计算机小菜~,我在这里分享我的平凡经历。谢谢你的到来。
Runnable.astream()
@microsoft/fetch-event-source
LLMs_chain.astream
(langchain中chain的API:流式输出LLM的一个个回复块|原料)
—> chat_sev.stream_chat
(生成器,转换chunk为初步的SSE格式字典|初步加工产品)
—> stream_response_generator
(生成器,转换SSE协议字符串|打包成快递)
—> StreamingResponse
(FastAPI用于流式传输响应体的响应类,以SSE协议发送数据块到客户端|快递员)
—> |前端-fetchEventSource
(第三方库方法,接受SSE数据,累加到ref|收货、组装)
—> Vue响应式更新
—-> 用户眼睛:哇sai!流式输出~
想要实现流式输出,其实很简单。
我们先前搭建了聊天链、RAG 链,并且使用 invoke 方法来传入参数,接收返回的 LLM 回复。
而现在我们只需要三步 ↓
astream
方法 --这时我们的链返回从一次性输出结果变为输出一个个chunk
chunk
:–根据你定义的链的数据结构,解析出你要的数据放到content_piece
content_piece
yield
出去!你是不是感到头昏脑胀?
astream
是什么?SSE
是什么?? 这个yield
又是啥???
Don’t worry! 让我们逐一击破这些概念↓
生成器是一种特殊的 迭代器
(Iterator
)。
迭代器 是你可以逐个访问其元素的对象(比如在 for
循环中使用)。列表、元组、字典、字符串等都是可迭代对象,但它们不是迭代器本身。你可以通过调用 iter()
函数从可迭代对象获取迭代器。迭代器有一个 next() 方法,每次调用它会返回下一个元素,如果没有更多元素了,会引发 StopIteration
异常。
生成器 是一种创建迭代器的简单而强大的方法。它看起来像一个普通的函数,但关键区别在于它使用 yield 关键字来返回值,而不是 return
。
yield
是啥?yield
是一个 Python 关键字,它有两个主要作用:
yield
语句的函数都会自动成为一个生成器函数。调用这个函数不会立即执行函数体,而是返回一个生成器对象(也就是一个迭代器)。yield
语句时:
yield
后面的表达式的值会被返回给调用者(即正在迭代该生成器的代码)。yield
或函数结束。yield
vs return
:return
会彻底终止函数的执行,并返回一个值(或 None
)。函数的状态不会被保存。
yield
只会暂停函数的执行,并返回一个值。函数的状态会被保存,以便下次可以恢复。一个生成器函数可以有多个 yield
语句。
yield
和生成器
,它们俩两个的关系?yield
是用来创建生成器的语法核心。一个函数因为包含了 yield
而成为生成器函数,调用它则得到生成器(迭代器)。生成器函数 (含 yield
的函数): 扮演“蓝图”或“工厂”的角色,用于定义如何按需生成一系列值。
yield
关键字: 扮演“暂停点”和“返回值发射器”的角色。它控制着值的生成和函数执行的暂停/恢复。
生成器对象 (调用生成器函数的结果): 扮演“迭代器”的角色。它实现了迭代协议(__iter__()
和 __next__()
),允许你通过循环或其他方式逐个获取由 yield
产生的值。
yield
一个值,这个值就会被发送到客户端。这使得我们可以逐步发送数据,实现流式传输。 yield {"type": "chunk", "data": content_piece}
这将推送一个字典,包含了我们的content_piece
astream
方法Langchain
的 astream()
方法允许我们以异步迭代的方式获取 Runnable
链(包括 LLM 调用)的输出块。
astream()
返回一个异步迭代器 (AsyncIterator
)。
该方法会以块的形式流式传输最终输出
from langchain.chat_models import ChatAnthropic
model = ChatAnthropic()
chunks = []
async for chunk in model.astream("你好。告诉我一些关于你自己的事情"):
chunks.append(chunk)
print(chunk.content, end="|", flush=True)
print:
你好|!| 我| 的名字| 是| 克劳德|。| 我| 是|一个|由|人类|创建|的|AI|助手|,|旨在|有所帮助|、|无害|和|诚实|。||
如果你希望能够查看整个项目代码,可见仓库地址 ↓
了解了这些前置知识,你是否感觉更清晰明了了呢?
Now,Let’s make it !
chunk
:stream_chat
中,我们异步迭代 astream()
返回的 chunk
。
chunk
的类型。因为我们的基础链可能是普通的 LLM 调用(输出 AIMessageChunk
等 BaseMessage
类型)或 RAG 检索链(输出包含 answer
和 context
的字典 Dict
),需要从中正确提取有效文本内容。async for chunk in stream_iterator:
content_piece = ""
if isinstance(chunk, BaseMessage):
if hasattr(chunk, 'content'):
content_piece = chunk.content
elif isinstance(chunk, dict):
if 'answer' in chunk:
answer_part = chunk['answer']
if isinstance(answer_part, str):
content_piece = answer_part
elif isinstance(answer_part, BaseMessage) and hasattr(answer_part, 'content'):
content_piece = answer_part.content
# ... (处理其他类型或记录日志)
if content_piece:
yield {"type": "chunk", "data": content_piece}
yield
一个包含上下文信息(如知识库名称)的字典:{"type": "context", "data": context_display_name}
。yield
一个:{"type": "chunk", "data": content_piece}
【核心数据–LLM输出数据】yield
一个错误信息:{"type": "error", "data": error_message}
。content_piece
yield
出去!在路由中(src\router\chatRouter.py)
StreamingResponse
:
StreamingResponse
。StreamingResponse
接收一个异步生成器函数作为其 content
参数。@ChatRouter.post(
"/stream",
summary="AI Chat (Streaming)",
description="与 AI 进行流式对话,可选使用知识库。",
)
async def chat_stream_endpoint(
request: ChatRequest, chat_sev: ChatSev = Depends(get_chat_service)
):
"""处理流式聊天请求。"""
if request.knowledge_config:
logging.info(f", kb_id={request.knowledge_config.knowledge_base_id}")
return StreamingResponse(
stream_response_generator(chat_sev, request), media_type="text/event-stream"
)
stream_response_generator
):
async def stream_response_generator(...)
函数。ChatSev
实例的 stream_chat
方法。stream_chat
返回的结构化字典。data: json.dumps(chunk_dict)\n\n
) 并 yield
出去。——在 FastAPI 的 StreamingResponse 中使用生成器时,每次 yield 一个值,这个值就会被发送到客户端。而在StreamingResponse
的 media_type
为 text/event-stream
。async def stream_response_generator(chat_sev: ChatSev, request_data: ChatRequest):
"""异步生成器,用于 StreamingResponse,产生 SSE 格式的事件。"""
logging.info(f"开始为 session_id={request_data.session_id} 生成流式响应")
try:
async for chunk_dict in chat_sev.stream_chat(
question=request_data.question,
api_key=request_data.llm_config.api_key,
supplier=request_data.llm_config.supplier,
model=request_data.llm_config.model,
session_id=request_data.session_id,
knowledge_base_id=request_data.knowledge_config.knowledge_base_id
if request_data.knowledge_config
else None,
filter_by_file_md5=request_data.knowledge_config.filter_by_file_md5
if request_data.knowledge_config
else None,
search_k=request_data.knowledge_config.search_k
if request_data.knowledge_config
else 3,
max_length=None,
temperature=request_data.llm_config.temperature,
):
event_type = chunk_dict.get("type", "message")
yield f"data: {json.dumps(chunk_dict)}\n\n"
logging.debug(
f"Sent chunk: {chunk_dict['type']} for session {request_data.session_id}"
)
except Exception as e:
logging.error(
f"在 stream_response_generator 中发生错误 (session: {request_data.session_id}): {e}",
exc_info=True,
)
error_payload = json.dumps(
{"type": "error", "data": f"流处理中发生严重错误: {e}"}
)
yield f"data: {error_payload}\n\n"
finally:
logging.info(f"结束为 session_id={request_data.session_id} 的流式响应")
这样我们后端的流式输出接口就完成了【详细源码见附录->src/service/ChatSev.py -def stream_chat、src/service/ChatSev.py、src/router/ChatRouter.py】
接着就是前端来接受处理 SSE 了
POST /chat/stream
返回 text/event-stream
类型的数据。type
字段区分 (context
, chunk
, error
)。@microsoft/fetch-event-source
库来处理前端的 SSE 连接。fetchEventSource
()——发送和处理 SSE 请求—— 参数 Accept: ‘text/event-stream’【核心】AbortController
——通过 signal
与 fetchEventSource
挂钩——用于可手动终止 SSE 请求实现用户手动终止 AI 回复【额外功能】fetchEventSource
()中的事件处理回调函数:accumulatedContent += parsedData.data// 将新块追加到累积内容
pnpm add @microsoft/fetch-event-source
- 引入 fetchEventSource。
- 创建一个新的 ref
isStreaming
来跟踪流式响应的状态。- 创建一个 ref
abortController
来控制请求的取消。- 重构 sendMessage 函数,使用
fetchEventSource
调用API,并添加处理 SSE 事件 (onopen, onmessage, onclose, onerror) 的逻辑。- 在
onmessage
中,根据事件类型 (context, chunk, error) 更新消息列表或显示错误。对于chunk
,我们会累加到最后一条 AI 消息的内容中。- 添加错误处理和状态更新逻辑。
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ref } from 'vue'
// ... 其他 import
const isStreaming = ref(false) // 跟踪流式响应状态
const abortController = ref(null) // 控制请求取消
const messages = ref([]) // 存储聊天消息
// ... 其他 refs 和 store
sendMessage
函数:AbortController
,准备用户消息和请求体 messagePayload
。isStreaming.value = true
,添加 AI 消息占位符到 messages
列表。fetchEventSource
: 这是核心逻辑,配置 method
, headers
, body
, signal
。// 8.2 onmessage: 每次收到服务器发送的事件 (data: ...\n\n) 时调用
onmessage(event) {
console.log('Received SSE data:', event.data)
try {
const parsedData = JSON.parse(event.data) // 解析收到的 JSON 字符串
// 找到对应的 AI 消息占位符
const aiMessageIndex = messages.value.findIndex((msg) => msg.id === aiMessageId)
if (aiMessageIndex === -1) {
// 如果找不到,可能消息已被删除或出现异常
console.warn('AI message placeholder not found.')
return
}
// 根据事件类型处理
if (parsedData.type === 'context') {
// 处理上下文信息,这里使用了 ElNotification 弹出通知
console.log('Context received:', parsedData.data)
ElNotification({ title: 'Context', message: parsedData.data, type: 'info' })
} else if (parsedData.type === 'chunk') {
// 核心:处理文本块
accumulatedContent += parsedData.data // 将新块追加到累积内容
// 更新 Vue ref 中对应消息的内容,触发界面响应式更新
messages.value[aiMessageIndex].content = accumulatedContent
scrollToBottom() // 收到新内容,自动滚动到底部
sendMessage
函数: const sendMessage = async () => {
// 1. 前置检查 (Guard Clauses)
if (!OneapiStore.selectedModel) {
ElMessage.info('请先选择一个模型')
return
}
if (!currentSession.value) {
ElMessage.info('请先选择一个话题')
return
}
if (!inputMessage.value.trim() || isStreaming.value) return // 防止重复发送或在流式传输时发送
// 2. 取消之前的流式请求 (如果存在)
if (abortController.value) {
abortController.value.abort()
console.log('Previous stream aborted.')
}
// 3. 初始化本次请求
abortController.value = new AbortController() // 创建新的 AbortController
const userMessageContent = inputMessage.value.trim()
const userMessage = {
// 准备用户消息对象
id: Date.now(),
type: 'human',
content: userMessageContent,
}
messages.value.push(userMessage) // 将用户消息添加到聊天记录
// 4. 构建请求体 (Payload)
const messagePayload = {
question: userMessageContent,
session_id: currentSession.value?._id,
chat_config: chat_config.value,
llm_config: llm_config.value,
...(knowledge_config.value ? { knowledge_config: knowledge_config.value } : {}), // 动态添加知识库配置
}
// 5. 更新 UI 状态 (开始流式传输)
inputMessage.value = '' // 清空输入框
isStreaming.value = true // 设置为流式状态 (会禁用输入框,改变发送按钮)
// 6. 添加 AI 消息占位符
const aiMessageId = Date.now() + 1 // 为 AI 回复生成唯一 ID
messages.value.push({
id: aiMessageId,
type: 'ai',
content: '', // 初始内容为空,等待后续 chunk 更新
})
let accumulatedContent = '' // 用于累积收到的文本块
// 7. 调用 fetchEventSource 发起 SSE 请求
try {
console.log('发送流式消息 payload:', messagePayload)
await fetchEventSource(baseURL + '/chat/stream', {
// baseURL 来自 @/utils/request
method: 'POST', // 使用 POST 方法
headers: {
'Content-Type': 'application/json', // 告知后端发送的是 JSON
Accept: 'text/event-stream', // 表明希望接收 SSE 流
},
body: JSON.stringify(messagePayload), // 将 JS 对象序列化为 JSON 字符串
signal: abortController.value.signal, // 关联 AbortController,用于取消请求
// 8. 事件处理回调函数
// 8.1 onopen: 连接成功建立时调用
onopen(response) {
if (
response.ok && // 确保 HTTP 状态码是 2xx
response.headers.get('content-type')?.includes('text/event-stream') // 确认响应类型正确
) {
console.log('SSE connection opened.')
// 连接成功,AI 消息占位符已添加,等待 onmessage
} else {
// 如果连接不成功或响应类型不对,则认为失败
isStreaming.value = false // 重置流式状态
throw new Error(`Failed to connect: ${response.status} ${response.statusText}`) // 抛出错误,会被外层 catch 或 onerror 捕获
}
},
// 8.2 onmessage: 每次收到服务器发送的事件 (data: ...\n\n) 时调用
onmessage(event) {
console.log('Received SSE data:', event.data)
try {
const parsedData = JSON.parse(event.data) // 解析收到的 JSON 字符串
// 找到对应的 AI 消息占位符
const aiMessageIndex = messages.value.findIndex((msg) => msg.id === aiMessageId)
if (aiMessageIndex === -1) {
// 如果找不到,可能消息已被删除或出现异常
console.warn('AI message placeholder not found.')
return
}
// 根据事件类型处理
if (parsedData.type === 'context') {
// 处理上下文信息,这里使用了 ElNotification 弹出通知
console.log('Context received:', parsedData.data)
ElNotification({ title: 'Context', message: parsedData.data, type: 'info' })
} else if (parsedData.type === 'chunk') {
// 核心:处理文本块
accumulatedContent += parsedData.data // 将新块追加到累积内容
// 更新 Vue ref 中对应消息的内容,触发界面响应式更新
messages.value[aiMessageIndex].content = accumulatedContent
scrollToBottom() // 收到新内容,自动滚动到底部
} else if (parsedData.type === 'error') {
// 处理流内由后端报告的错误
console.error('Stream error reported:', parsedData.data)
messages.value[aiMessageIndex].content += `\n\n**错误:** ${parsedData.data}` // 将错误信息附加到消息末尾
ElMessage.error(`流式响应出错: ${parsedData.data}`)
// 发生错误,尝试中止连接
if (abortController.value) {
abortController.value.abort()
}
}
} catch (e) {
// 处理 JSON 解析失败的情况
console.error('Failed to parse SSE data:', e, 'Raw data:', event.data)
const aiMessageIndex = messages.value.findIndex((msg) => msg.id === aiMessageId)
if (aiMessageIndex !== -1 && event.data && typeof event.data === 'string') {
// 尝试将原始错误数据附加到消息中
messages.value[aiMessageIndex].content +=
`\n\n**解析错误,原始数据:** ${event.data}`
}
ElMessage.error('接收到无效的数据格式')
// 解析错误,中止连接
if (abortController.value) {
abortController.value.abort()
}
}
},
// 8.3 onclose: 连接正常关闭时调用 (服务器关闭或客户端调用 abort())
onclose() {
console.log('SSE connection closed.')
isStreaming.value = false // 重置流式状态
abortController.value = null // 重置 AbortController 引用
scrollToBottom() // 确保最后滚动到底部
},
// 8.4 onerror: 发生错误时调用 (网络错误、onopen/onmessage 中抛出的错误、AbortError)
onerror(err) {
console.error('SSE error:', err)
isStreaming.value = false // 只要出错,就重置流式状态 (除了 AbortError)
// 找到 AI 消息
const aiMessageIndex = messages.value.findIndex((msg) => msg.id === aiMessageId)
// 特殊处理 AbortError (用户手动取消)
if (err.name === 'AbortError') {
console.log('Stream aborted by user.')
// 如果 AI 消息还是空的,就把它从列表里移除
if (aiMessageIndex !== -1 && !messages.value[aiMessageIndex].content) {
messages.value.splice(aiMessageIndex, 1)
}
// **重要:** 对于 AbortError,我们直接 return,不执行后续错误处理,
// 也不抛出错误,以防止库尝试重连。状态重置由 handleStopStreaming 或 onclose 处理。
return
}
// 处理其他类型的错误 (网络、连接等)
ElMessage.error(`连接错误: ${err.message || '未知错误'}`)
if (aiMessageIndex !== -1) {
// 在 AI 消息末尾附加错误信息
messages.value[aiMessageIndex].content +=
`\n\n**连接错误:** ${err.message || '未知错误'}`
} else {
// 如果连 AI 占位符都没有(可能 onopen 就失败了),则添加一条错误消息
messages.value.push({
id: Date.now(),
type: 'ai',
content: `**连接错误:** ${err.message || '未知错误'}`,
})
}
abortController.value = null // 重置 AbortController 引用
// **重要:** 对于非 AbortError,必须抛出错误 (throw err) 或不返回。
// 这是 fetchEventSource 库的设计,抛出错误会阻止它默认的重连尝试。
throw err
},
})
} catch (err) {
// 9. 捕获 fetchEventSource 启动时的错误
// 这个 catch 主要捕获 fetchEventSource 启动时就发生的错误,
// 例如 DNS 解析失败、网络连接无法建立等,这些错误发生在 onopen 之前。
// onerror 中 throw 的错误也会在这里被捕获,但我们主要处理非 AbortError。
console.error('Error initiating SSE request:', err)
isStreaming.value = false // 确保重置状态
if (err.name !== 'AbortError') {
// AbortError 已在 onerror 处理
ElMessage.error(`请求失败: ${err.message || '未知错误'}`)
// 标记用户消息发送失败
const userMessageIndex = messages.value.findIndex(
(msg) => msg.id === userMessage.id,
)
if (userMessageIndex !== -1) {
messages.value[userMessageIndex].content += ' (发送失败)'
}
// 移除可能已添加的 AI 占位符
const aiMessageIndex = messages.value.findIndex((msg) => msg.id === aiMessageId)
if (aiMessageIndex !== -1) {
messages.value.splice(aiMessageIndex, 1)
}
}
abortController.value = null // 重置 AbortController 引用
}
}
到这里教学就结束啦!
我把更多核心源码放在这里了,欢迎大家0积分下载:【免费】langchain项目如何实现流式输出经验分享前端流式输出.pdf
如果你希望能够查看整个项目代码,可见仓库地址 ↓
参考文献: