FastAPI 封装本地 LLM 接口时出现流式中文乱码,多数情况不是模型本身的输出问题,而是 HTTP 响应层的编码声明与传输格式不一致。排查时先分清乱码出现在“服务端终端打印”“HTTP 响应字节”“客户端解码显示”三层中的哪一层,再针对具体层处理,通常可以避免反复试错。
处理方向:先确认本地模型返回的字符串本身是否为正常 Unicode 文本;再检查 StreamingResponse 的 media_type 是否带 charset=utf-8;最后验证客户端是否按 UTF-8 解码。若生成器产出 bytes,FastAPI 不会自动转码,必须保证生成器内部产出的字节序列与媒体类型声明一致。不要盲目替换字符集,否则已损坏的字节会被二次错误解码。
先确认乱码发生在哪一层
在改动代码之前,先做三类检查:
- 在服务端生成器内部打印片段,确认终端能正常显示中文。若终端已乱码,问题在模型输出或日志编码,与 FastAPI 无关。
- 用
curl -i查看响应头,确认Content-Type是否包含charset=utf-8。 - 用
curl `--raw`查看原始字节,观察 JSON 字符串或 SSE 的data:行是否完整。
若 curl 输出中中文显示为 \uXXXX 转义,那是 JSON 的正常表现形式,不是乱码;若显示为 䏿–‡ 这类替换字符,才是 UTF-8 字节被按其他编码显示的典型现象。
服务端 StreamingResponse 编码设置要点
本地 LLM 接口最常用的流式方式是 SSE(Server-Sent Events),通过 StreamingResponse 返回。关键位置是 media_type 参数和生成器产出的数据类型。
from fastapi.responses import StreamingResponse
async def event_stream():
async for chunk in model.aiter_text():
# chunk 应为已正确解码的 str
yield f"data: {chunk}\n\n".encode("utf-8")
return StreamingResponse(
event_stream(),
media_type="text/event-stream; charset=utf-8",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
},
)media_type 中的 charset=utf-8 是告诉客户端按 UTF-8 解码。生成器内部若产出 bytes,则需先确认原本的字节编码,再使用 bytes.decode(...) 转为 str,再统一以 UTF-8 编码输出。不要直接对未知编码的 bytes 做 str.encode(),否则会出现二次编码错误。
如果接口需要返回 JSON 流,而不是 SSE,可以把 media_type 改为 application/json; charset=utf-8,并保证每行输出一个完整 JSON 对象。此时客户端按 JSON Lines 解析即可。
验证方法与客户端解码注意事项
服务端改完之后,先用命令验证,再写客户端测试。推荐以下方式:
# 查看响应头和完整响应内容,注意 -N 让 curl 边收边显示
curl -N -i http://127.0.0.1:8000/chat -H "Content-Type: application/json" -d '{"prompt":"你好"}'
# 查看原始字节,确认 UTF-8 编码是否成形
curl -N `--raw` http://127.0.0.1:8000/chat -H "Content-Type: application/json" -d '{"prompt":"你好"}' | xxd | head -30Python 客户端在使用 requests 流式读取时,需要注意 iter_lines 默认不按 Unicode 解码。下面是可以直接运行的验证片段:
import requests
resp = requests.post(
"http://127.0.0.1:8000/chat",
json={"prompt": "你好"},
stream=True,
)
resp.encoding = "utf-8"
for line in resp.iter_lines(decode_unicode=True):
print(line)这里显式设置 resp.encoding = "utf-8",再让 iter_lines(decode_unicode=True) 按该编码解码。若不设置,requests 可能根据响应头猜测编码,有时会误判为 ISO-8859-1,导致中文乱码。
存在一个实际操作中的边界:部分本地 HTTP 服务端不声明 charset,却返回 UTF-8 字节。客户端不能自动知道编码,只能通过响应头或配置约定。因此建议在服务端显式声明 charset;若调用的是第三方本地模型服务,无法修改响应头,则客户端需强制按 UTF-8 解码。
常见问题
为什么设置了 charset=utf-8 仍然出现乱码?
先检查生成器内部产出的字节。若模型返回的是 GBK 或其他编码的 bytes,而服务端按 UTF-8 编码发送,客户端自然无法还原。先在服务端打印原始字节形态,确认源编码后再做解码。另外,部分客户端代理或服务器中间件会覆盖 Content-Type,需要在反向代理层检查响应头是否被改写。
如何区分 SSE 换行问题和中文乱码?
SSE 协议要求每个事件以 data: 开头、\n\n 结尾。如果中文内容被截断或换行错位,通常是生成器内部把长文本拆成了多个 data: 行,但客户端只读取了第一行。用 curl `--raw` 可以看到原始 \n\n 是否完整。乱码则主要表现为字节替换符号,两者可从输出形态上分辨。
处理这类问题,建议固定排查顺序:先确认源字符串、再核对响应头编码声明、最后验证客户端解码方式。每一步用命令或打印确认后再进行下一步,避免在未知字节上反复替换字符集,导致内容二次损坏。