欢迎光临
我们一直在努力

跟着百炼学流式输出:打字机一样的回复是怎么来的

1. 前言:为啥要看流式输出?

最近在玩百炼大模型的 API,发现它支持一个很酷的功能——流式输出。说白了就是模型的回复不像普通接口那样等半天一口气丢给你,而是像打字机一样一个字一个字往外蹦。

用下来感觉流式输出真的很香,主要是这三个原因:

  • 体感快:不用盯着屏幕转圈圈干等,第一段文字几乎立刻就能看到,心理上舒服很多。
  • 体验好:看着文字一行行冒出来,感觉就是模型真的在思考,比突然弹一大段冷冰冰的文本有意思多了。
  • 省资源:不用等模型把整段话全生成完再一次性返回,服务端的压力也小一些。

这篇文章就是我在学习过程中整理的一个小笔记,用最简单的例子把单轮对话的流式输出跑通,希望对刚接触的同学有点帮助。

2. 先搞个 API Key

首先得准备一个百炼的 API Key,去官网申请一下就有了。拿到之后配成环境变量:

export DASHSCOPE_API_KEY="你的sk-xxx"

这样代码里就不用把 Key 写死了,既安全又方便。

3. 初始化客户端

整个项目对接大模型,其实就是初始化一个 OpenAI 客户端,然后指向百炼的兼容地址:

import os
from openai import OpenAI

client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://ws-f5fcw8tsv6v3ajb4.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
)

这里要特别注意:base_url 跟你的 API Key 是绑定的,不同地域的 Key 对应的地址不一样,写错了会直接报错,我一开始在这里踩过坑。

4. messages 是个啥?

刚开始看文档的时候我也挺懵的,为啥跟大模型对话不是直接传一句「你好」,而是要传一个叫 messages 的列表?

后来搞明白了,每条 message 其实就是一个角色在说话:

字段含义
role 谁在说话:system(给模型定规矩)、user(你说的)、assistant(模型回的)
content 具体说了什么

单轮对话最简单,就塞一条 user 消息进去就行:

messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "请介绍一下自己"},
]

system 那行不是必须的,但加上的话可以给模型定个基调,比如让它扮演某个角色或者限定回答风格,挺好用的。

5. 最基础的流式调用

好了,万事俱备,直接上代码:

import os
from openai import OpenAI

client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://ws-f5fcw8tsv6v3ajb4.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
)

# 关键就是 stream=True
completion = client.chat.completions.create(
model="qwen-plus",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "请介绍一下自己"},
],
stream=True, # 开启流式
stream_options={"include_usage": True}, # 顺便带回 token 用量
)

chunks = []
for chunk in completion:
if chunk.choices:
choice = chunk.choices[0]
if choice.delta: # delta 就是这一小段新增内容
delta = choice.delta
if delta.content:
print(delta.content, end="", flush=True) # 逐字打印
chunks.append(delta.content)

res = "".join(chunks) # 全部拼起来就是完整的回复

跑起来的效果就是文字一个接一个往外蹦,跟 ChatGPT 聊天一个感觉。

这里有个小技巧:用列表 chunks 暂存每一块,最后 join 拼起来,比用 += 拼接字符串效率高很多,算是个 Python 基本功吧。

6. 封装成 Web 接口

自己玩够了,下一步就是把它包成接口给别人调用。我用的是 FastAPI + SSE(Server-Sent Events),这样前端也能实时看到流式效果。

6.1 生成器函数——负责产出一块一块的文本

import os
from openai import OpenAI

def stream_chunk(user_question: str):
client = OpenAI(
api_key=os.environ["DASHSCOPE_API_KEY"],
base_url="https://ws-f5fcw8tsv6v3ajb4.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
)

completion = client.chat.completions.create(
model="qwen-plus",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_question},
],
stream=True,
stream_options={"include_usage": True},
)

for chunk in completion:
if chunk.choices:
choice = chunk.choices[0]
if choice.delta and choice.delta.content:
# SSE 格式:以 "data: " 开头,两个换行结尾
yield f"data: {choice.delta.content}\\n\\n"

yield "data: [done]\\n\\n" # 告诉前端:我说完了

yield 就是生成器的精髓,每次只吐出一小块,外层不用等全部生成完就能拿到数据,天然适合做流式。

6.2 路由——用 StreamingResponse 把生成器变成 HTTP 流

from fastapi import APIRouter
from starlette.responses import StreamingResponse

llm_day01_router = APIRouter(prefix="/llm-day01", tags=["LLM-DAY01"])

@llm_day01_router.post("/case2", summary="单轮对话流式输出")
async def case2_api(llmCase1Request: LLMCase1):
return StreamingResponse(
content=stream_chunk(llmCase1Request.question),
media_type="text/event-stream", # SSE 标准类型
)

6.3 请求体定义

from pydantic import BaseModel, Field

class LLMCase1(BaseModel):
question: str = Field(..., title="问题", description="用户的问题")

6.4 完整数据流

我把整个数据流向画了一下,方便理解:

#mermaid-svg-2FJnMz6SwFxvetKi{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2FJnMz6SwFxvetKi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2FJnMz6SwFxvetKi .error-icon{fill:#552222;}#mermaid-svg-2FJnMz6SwFxvetKi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2FJnMz6SwFxvetKi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2FJnMz6SwFxvetKi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2FJnMz6SwFxvetKi .marker.cross{stroke:#333333;}#mermaid-svg-2FJnMz6SwFxvetKi svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2FJnMz6SwFxvetKi p{margin:0;}#mermaid-svg-2FJnMz6SwFxvetKi .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-2FJnMz6SwFxvetKi .cluster-label text{fill:#333;}#mermaid-svg-2FJnMz6SwFxvetKi .cluster-label span{color:#333;}#mermaid-svg-2FJnMz6SwFxvetKi .cluster-label span p{background-color:transparent;}#mermaid-svg-2FJnMz6SwFxvetKi .label text,#mermaid-svg-2FJnMz6SwFxvetKi span{fill:#333;color:#333;}#mermaid-svg-2FJnMz6SwFxvetKi .node rect,#mermaid-svg-2FJnMz6SwFxvetKi .node circle,#mermaid-svg-2FJnMz6SwFxvetKi .node ellipse,#mermaid-svg-2FJnMz6SwFxvetKi .node polygon,#mermaid-svg-2FJnMz6SwFxvetKi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2FJnMz6SwFxvetKi .rough-node .label text,#mermaid-svg-2FJnMz6SwFxvetKi .node .label text,#mermaid-svg-2FJnMz6SwFxvetKi .image-shape .label,#mermaid-svg-2FJnMz6SwFxvetKi .icon-shape .label{text-anchor:middle;}#mermaid-svg-2FJnMz6SwFxvetKi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2FJnMz6SwFxvetKi .rough-node .label,#mermaid-svg-2FJnMz6SwFxvetKi .node .label,#mermaid-svg-2FJnMz6SwFxvetKi .image-shape .label,#mermaid-svg-2FJnMz6SwFxvetKi .icon-shape .label{text-align:center;}#mermaid-svg-2FJnMz6SwFxvetKi .node.clickable{cursor:pointer;}#mermaid-svg-2FJnMz6SwFxvetKi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2FJnMz6SwFxvetKi .arrowheadPath{fill:#333333;}#mermaid-svg-2FJnMz6SwFxvetKi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2FJnMz6SwFxvetKi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2FJnMz6SwFxvetKi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2FJnMz6SwFxvetKi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2FJnMz6SwFxvetKi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2FJnMz6SwFxvetKi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2FJnMz6SwFxvetKi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2FJnMz6SwFxvetKi .cluster text{fill:#333;}#mermaid-svg-2FJnMz6SwFxvetKi .cluster span{color:#333;}#mermaid-svg-2FJnMz6SwFxvetKi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2FJnMz6SwFxvetKi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2FJnMz6SwFxvetKi rect.text{fill:none;stroke-width:0;}#mermaid-svg-2FJnMz6SwFxvetKi .icon-shape,#mermaid-svg-2FJnMz6SwFxvetKi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2FJnMz6SwFxvetKi .icon-shape p,#mermaid-svg-2FJnMz6SwFxvetKi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2FJnMz6SwFxvetKi .icon-shape .label rect,#mermaid-svg-2FJnMz6SwFxvetKi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2FJnMz6SwFxvetKi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2FJnMz6SwFxvetKi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2FJnMz6SwFxvetKi :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

用户发起请求

FastAPI 路由接收

stream_chunk 生成器启动

大模型逐块返回内容

生成器逐块 yield

SSE 流推送到浏览器

还有更多内容吗?

yield data: [done] 结束

  • 用户在前端输入问题,请求打到 FastAPI 路由;
  • 路由调用 stream_chunk 生成器,生成器内部请求大模型;
  • 大模型每吐一小块文本,生成器就 yield 出去,包装成 SSE 格式;
  • 浏览器实时收到并渲染,直到收到 [done] 信号结束。
  • 7. 小结

    学完这一套,我对流式输出有了比较直观的理解。核心就三个点:

    • stream=True 打开流式开关,返回值变成生成器;
    • delta.content 是每次迭代新增的那一小段内容;
    • StreamingResponse + SSE 把后端逐块产出的数据实时推给前端。

    掌握了这些,后面就可以在这个基础上玩更多花样了,比如多轮对话、上下文记忆、前端打字机效果等等。希望这篇笔记能帮到正在踩坑的你!

    赞(0)
    未经允许不得转载:171主机测评 » 跟着百炼学流式输出:打字机一样的回复是怎么来的
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址