AI 工具

用 SSE 流式客户端测量 GroqCloud 首 Token 延迟

GroqCloud 主打低延迟。要量化“快”,至少测量到首 token 时间(TTFT)。 这篇用标准库读 SSE data: 流,实现 streamchat.py,在 mock 上打印内容并记录 TTFT。

BINBHO · 图文教程

GroqCloud 主打低延迟。要量化“快”,至少测量到首 token 时间(TTFT)。 这篇用标准库读 SSE data: 流,实现 stream_chat.py,在 mock 上打印内容并记录 TTFT。

先看清成品

  • mock 流式输出「快速回复:测试」,TTFT≈58ms(本机 mock,不代表真实 LPU 延迟)。
  • 401 覆盖;2 项 pytest 通过。
  • 无效 Key 打真实 API 返回 401 Invalid API Key。

合规提醒:使用自己的 GroqCloud 账号与 API Key;语音内容需你有权处理。模型与价格以 groq.com/pricing 与控制台为准,本文不写价格。遵守各模型许可。

运行说明:没有可用的 Groq 密钥。真实运行过:流式聊天 / Whisper / tool-calling 客户端对本地 mock 的请求,以及对 https://api.groq.com 使用无效密钥得到 HTTP 401(2026-10-23,UTC+8)。未上传真实用户音频,未产生计费。

做完之后你会得到什么?

成果 说明
stream_chat.py 流式客户端 + TTFT
mock_groq.py SSE mock
test_94a.py 2 项测试

需要准备哪些环境?

项目 说明
Python 3.11+ 标准库;pip install pytest
Groq Key(可选) https://console.groq.com → API Keys
音频样例(B) 小于 100MB,官方格式之一

关键事实

事实 来源
Chat Completions 兼容 OpenAI:POST /openai/v1/chat/completions,stream: true 官方 OpenAI Compatibility / Text Chat
流式响应为 SSE,data: {...},结束为 data: [DONE] 官方 Streaming
Base URL 一般为 https://api.groq.com 官方文档
可用模型以控制台为准 官方 Models

文件结构

94-groqcloud-A-src/
├── stream_chat.py
├── mock_groq.py
└── test_94a.py

第 1 步:流式客户端

#!/usr/bin/env python3
"""Groq-compatible streaming chat; measure time-to-first-token (TTFT)."""
from __future__ import annotations

import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.request


def stream_chat(*, base_url: str, api_key: str, model: str, message: str) -> tuple[str, float, float]:
    url = f"{base_url.rstrip('/')}/openai/v1/chat/completions"
    payload = {
        "model": model,
        "messages": [{"role": "user", "content": message}],
        "stream": True,
    }
    req = urllib.request.Request(
        url,
        data=json.dumps(payload).encode(),
        headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    t0 = time.perf_counter()
    ttft = None
    parts: list[str] = []
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            while True:
                line = resp.readline()
                if not line:
                    break
                line = line.decode("utf-8", errors="replace").strip()
                if not line.startswith("data:"):
                    continue
                data = line[5:].strip()
                if data == "[DONE]":
                    break
                obj = json.loads(data)
                delta = (obj.get("choices") or [{}])[0].get("delta") or {}
                content = delta.get("content")
                if content:
                    if ttft is None:
                        ttft = time.perf_counter() - t0
                    parts.append(content)
                    print(content, end="", flush=True)
    except urllib.error.HTTPError as e:
        raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e
    print()
    total = time.perf_counter() - t0
    return "".join(parts), (ttft or total), total


def main(argv=None) -> int:
    p = argparse.ArgumentParser(description=__doc__)
    p.add_argument("message", nargs="?", default="用一句话说明低延迟为什么重要")
    p.add_argument("--base", default=os.environ.get("GROQ_BASE_URL", "https://api.groq.com"))
    p.add_argument("--key", default=os.environ.get("GROQ_API_KEY", ""))
    p.add_argument("--model", default=os.environ.get("GROQ_MODEL", "llama-3.1-8b-instant"))
    args = p.parse_args(argv)
    if not args.key:
        print("设置 GROQ_API_KEY", file=sys.stderr)
        return 2
    text, ttft, total = stream_chat(base_url=args.base, api_key=args.key, model=args.model, message=args.message)
    print(f"TTFT={ttft*1000:.1f}ms total={total*1000:.1f}ms chars={len(text)}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

第 2 步:Mock

#!/usr/bin/env python3
from __future__ import annotations
import json, time
from http.server import BaseHTTPRequestHandler, HTTPServer

API_KEY = "gsk-test"


class H(BaseHTTPRequestHandler):
    def log_message(self, *a):
        pass

    def do_POST(self):
        if self.path not in ("/openai/v1/chat/completions", "/v1/chat/completions"):
            self.send_error(404); return
        if self.headers.get("Authorization") != f"Bearer {API_KEY}":
            self.send_response(401); self.end_headers(); self.wfile.write(b'{"error":{"message":"Invalid API Key"}}'); return
        body = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
        msg = body["messages"][-1]["content"]
        if body.get("stream"):
            self.send_response(200)
            self.send_header("Content-Type", "text/event-stream")
            self.end_headers()
            time.sleep(0.05)  # simulate TTFT
            for i, ch in enumerate(["快", "速", "回", "复", f":{msg[:20]}"]):
                chunk = {"id": "chatcmpl-x", "choices": [{"delta": {"content": ch}, "index": 0}]}
                self.wfile.write(f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n".encode())
                self.wfile.flush()
                time.sleep(0.01)
            self.wfile.write(b"data: [DONE]\n\n")
            return
        resp = {"id": "c", "choices": [{"message": {"role": "assistant", "content": f"ok:{msg}"}}],
                "usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8},
                "model": body.get("model")}
        self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
        self.wfile.write(json.dumps(resp, ensure_ascii=False).encode())


if __name__ == "__main__":
    import os
    HTTPServer(("127.0.0.1", int(os.environ.get("PORT", "3095"))), H).serve_forever()

第 3 步:测试

import threading
from http.server import HTTPServer
import mock_groq
from stream_chat import stream_chat

def serve():
    httpd = HTTPServer(("127.0.0.1", 0), mock_groq.H)
    threading.Thread(target=httpd.serve_forever, daemon=True).start()
    return httpd, httpd.server_address[1]

def test_stream_ttft():
    httpd, port = serve()
    try:
        text, ttft, total = stream_chat(base_url=f"http://127.0.0.1:{port}", api_key=mock_groq.API_KEY,
                                        model="llama-3.1-8b-instant", message="测试")
        assert "测试" in text or "快速" in text
        assert ttft > 0
        assert total >= ttft
    finally:
        httpd.shutdown()

def test_401():
    httpd, port = serve()
    try:
        try:
            stream_chat(base_url=f"http://127.0.0.1:{port}", api_key="bad", model="m", message="x")
            assert False
        except SystemExit as e:
            assert "401" in str(e)
    finally:
        httpd.shutdown()

第 4 步:运行

# 2026-10-23 12:37 UTC+8 mock
快速回复:测试
TTFT=58.0ms total=108.8ms chars=7

# 真实 API + 无效密钥
$ curl ... https://api.groq.com/openai/v1/chat/completions -H 'Authorization: Bearer gsk_invalid' ...
401 {"error":{"message":"Invalid API Key","type":"invalid_request_error","code":"invalid_api_key"}}

$ python -m pytest -q
2 passed

怎么检查结果?

  • 能边收边打印;TTFT > 0 且 ≤ total。
  • mock 延迟不等于生产 TTFT,上线后用真实 Key 再测。

调试示例

readline 与缓冲。 若用 resp.read() 一次读完就失去流式意义。本客户端按行读 data:。注意某些代理会缓冲 SSE,测量 TTFT 时应尽量直连。

常见问题

现象 原因 处理
没有输出 stream 解析错 检查 data: 前缀与 JSON delta.content
401 Key 无效 控制台重建 Key
比官网慢 网络路径 换近的出口;TTFT 含 RTT

模板规则

  • 流式按行解析 SSE。
  • TTFT 与 total 一并记录。
  • 密钥放环境变量。

项目资源

用途 地址
Streaming(官方) https://console.groq.com/docs/text-chat#streaming
OpenAI Compatibility(官方) https://console.groq.com/docs/openai
Pricing https://groq.com/pricing

“快”要用 TTFT 说话,不能只看体感。