FastAPI封装本地LLM接口时流式响应中文乱码的编码处理方案

文章导读
FastAPI 封装本地 LLM 接口时出现流式中文乱码,多数情况不是模型本身的输出问题,而是 HTTP 响应层的编码声明与传输格式不一致。排查时先分清乱码出现在“服务端终端打印”“HTTP 响应字节”“客户端解码显示”三层中的哪一层,再针对具体层处理,通常可以避免反复试错。
📋 目录
  1. 先确认乱码发生在哪一层
  2. 服务端 StreamingResponse 编码设置要点
  3. 验证方法与客户端解码注意事项
  4. 常见问题
A A

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(),否则会出现二次编码错误。

FastAPI封装本地LLM接口时流式响应中文乱码的编码处理方案

如果接口需要返回 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 -30

Python 客户端在使用 requests 流式读取时,需要注意 iter_lines 默认不按 Unicode 解码。下面是可以直接运行的验证片段:

FastAPI封装本地LLM接口时流式响应中文乱码的编码处理方案
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 解码。

FastAPI封装本地LLM接口时流式响应中文乱码的编码处理方案

常见问题

为什么设置了 charset=utf-8 仍然出现乱码?

先检查生成器内部产出的字节。若模型返回的是 GBK 或其他编码的 bytes,而服务端按 UTF-8 编码发送,客户端自然无法还原。先在服务端打印原始字节形态,确认源编码后再做解码。另外,部分客户端代理或服务器中间件会覆盖 Content-Type,需要在反向代理层检查响应头是否被改写。

如何区分 SSE 换行问题和中文乱码?

SSE 协议要求每个事件以 data: 开头、\n\n 结尾。如果中文内容被截断或换行错位,通常是生成器内部把长文本拆成了多个 data: 行,但客户端只读取了第一行。用 curl `--raw` 可以看到原始 \n\n 是否完整。乱码则主要表现为字节替换符号,两者可从输出形态上分辨。

处理这类问题,建议固定排查顺序:先确认源字符串、再核对响应头编码声明、最后验证客户端解码方式。每一步用命令或打印确认后再进行下一步,避免在未知字节上反复替换字符集,导致内容二次损坏。