← Back to 文字

流式响应与接口测试

本文来自《AI 应用开发课程》月份 1 课程文档,已整理为网站文章版本。

学习目标

学完本节后,你应当能够:

  • 使用 FastAPI 提供最小流式响应。
  • /health 和一个核心接口编写最小测试。
  • 理解“服务已启动”不等于“服务可靠”。

前置知识

  • 已完成 /chat/tools/run

1. 为什么这里要补流式响应

因为月份 1 前面你已经学过模型流式输出,这一节要把它变成服务能力。

月份 1 不要求你实现复杂 SSE 协议,但至少要理解:

  • 后端不是只能一次性返回完整文本
  • 流式能力会直接影响用户体验

2. 最小流式思路

FastAPI 中常见做法是使用 StreamingResponse

伪代码思路:

from fastapi.responses import StreamingResponse


async def stream_generator():
    async for chunk in llm_client.stream(messages):
        yield chunk


@router.post("/chat/stream")
async def chat_stream(request: ChatRequest):
    return StreamingResponse(stream_generator(), media_type="text/plain")

月份 1 的重点不是协议细节,而是理解“路由 -> service -> stream 生成器”的分层。

3. 最小接口测试

/health

from fastapi.testclient import TestClient

from app.main import app


client = TestClient(app)


def test_health_should_return_ok() -> None:
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

核心接口

对于 /chat,月份 1 建议先 mock 或替换底层 service,而不是每次都打真实模型。

4. 为什么测试不一定直接打真实模型

因为真实模型调用:

  • 有成本
  • 有随机性
  • 依赖外部网络

学习阶段先确保:

  • 路由行为正确
  • 模型输出经过封装后的接口结构正确

5. 实操任务

  1. /health 写测试
  2. /chat/tools/run 写一个最小测试
  3. 如果实现了流式响应,至少手动验证一次

6. 自测题

  1. 为什么服务接口测试和模型能力测试不是一回事?
  2. 为什么流式响应要单独设计路径和处理方式?
  3. 为什么 /health 是每个服务型项目的基础接口?

7. 作业与验收

验收标准:

  • /health 有自动化测试
  • /chat/tools/run 至少一个有自动化测试
  • 流式响应至少完成手动验证

8. 常见错误

  • 测试直接依赖真实网络,导致结果不稳定
  • 流式接口写出来但从未实际验证
  • 只测 happy path,不测异常路径

9. 本章与前文关系

上一章让你完成了:

  • /chat
  • /tools/run

也就是核心业务链路已经通了。现在要补两件会显著提高项目质量的能力:

  • 流式响应
  • 接口测试

这两者分别对应“更好的交互体验”和“更稳定的回归验证”。

10. 本章在研发助手项目中的位置

在第 4 周综合项目里:

  • 流式响应会让 Demo 体验明显更好
  • 接口测试会让你更敢于重构 service 层

所以本章不是“额外补充”,而是在为最终项目打展示质量和稳定性基础。

11. 流式响应的数据流应该怎样理解

你可以把它拆成这条链路:

flowchart LR
    A["用户请求 /chat/stream"] --> B["FastAPI 路由"]
    B --> C["ChatService / LLMClient.stream"]
    C --> D["模型逐块返回 chunk"]
    D --> E["StreamingResponse 逐块转发"]
    E --> F["客户端持续接收"]

只要你把这条链路理解清楚,流式响应就不会显得神秘。

12. 为什么接口测试不优先打真实模型

因为接口测试的目标是验证:

  • 路由是否挂对
  • schema 是否匹配
  • 基础状态码是否正确
  • service 是否被正确调用

而不是验证模型“今天心情如何”。如果一开始就强依赖真实模型,你会同时引入:

  • 网络因素
  • 成本因素
  • 文本随机性

这对月份 1 学习并不划算。

13. 错误示例 vs 正确示例

错误示例:只写接口,不验证

这种做法最大的问题是:你以为它能跑,但很多问题直到综合项目时才集中爆炸。

正确示例:最少也给 /health 和一个核心接口留测试

这能让你对服务壳子和最重要主链路有基本信心。

14. 一个更完整的流式接口示例

from fastapi.responses import StreamingResponse


@router.post("/chat/stream")
async def chat_stream(request: ChatRequest) -> StreamingResponse:
    raw_messages = [message.model_dump() for message in request.messages]

    async def event_generator():
        async for chunk in llm_client.stream(raw_messages, request.temperature):
            yield chunk

    return StreamingResponse(event_generator(), media_type="text/plain")

这段代码背后的关键点有三个:

  • event_generator 是连接 LLM client 和 HTTP 响应的桥
  • 路由层不直接手写模型请求细节
  • StreamingResponse 负责把生成器变成可持续输出的响应

15. 完整文件级示例:tests/test_api.py

"""月份 1 API 最小测试示例。"""

from fastapi.testclient import TestClient

from app.main import app


client = TestClient(app)


def test_health_should_return_ok() -> None:
    response = client.get("/health")

    assert response.status_code == 200
    assert response.json() == {"status": "ok"}


def test_docs_should_be_available() -> None:
    response = client.get("/docs")

    assert response.status_code == 200

16. 逐段解释这份完整示例

为什么先测 /health

因为它是最稳定、最基础、最不依赖外部环境的接口。

为什么测 /docs

因为它不仅能证明 FastAPI 服务正常,也能证明自动文档链路还在。

为什么这里没有直接测真实 /chat

因为月份 1 当前阶段,更重要的是先把“服务层结构”和“最小接口回归验证”打稳。

17. 一个更接近工程的增强测试方向

后续你可以:

  • 用依赖替身替换真实 LLMClient
  • /chat 构造一个固定返回的假 client
  • 测试响应 schema 是否按预期返回

这样做的好处是:你能测试接口行为,而不依赖真实模型输出。

18. 调试与排错:本章最常见问题

问题一:流式接口看似返回了,但客户端没有持续收到内容

说明你要检查:

  • 生成器是否真的 yield
  • llm_client.stream 是否真的返回 chunk
  • 客户端是否按流式方式消费

问题二:测试导入应用失败

这往往暴露的是项目结构或模块路径问题,而不是 FastAPI 本身问题。

问题三:接口测试过于依赖真实模型

这会导致测试不稳定、运行慢、成本高。

19. 本章完成后你应该具备的能力

完成本章后,你应当做到:

  1. 能解释模型流式结果如何变成 HTTP 流式响应。
  2. 能为服务写出最小接口测试。
  3. 能区分接口测试和模型能力测试。
  4. 能为后续综合项目保留一条更稳的服务化基础。

20. 如果你卡在这里,先回看哪几章

21. 从本章过渡到下一章的桥接说明

接下来进入 06-LangChain核心抽象入门 模块。

到这里,你已经把“纯 API + Tool Calling + FastAPI 服务化”全部走通。现在再引入 LangChain,时机才是对的,因为你已经知道框架要抽象掉的到底是什么,而不会把它当黑盒。

Fin