- AI 应用
- API网关
- LLM 网关
- 后端
- 前端
【免费下载链接】axonhub
⚡️ Open-source AI Gateway — Use any SDK to call 100+ LLMs. Built-in failover, load balancing, cost control & end-to-end tracing.
本指南以 AxonHub 仓库中的 streaming 集成测试文档 为主体,结合 streaming_test.go 及网关底层流式实现源码,系统讲解如何基于 OpenAI SDK(openai-gov3)验证 AxonHub 网关的流式对话(Chat Completions Streaming)能力。读完本文,你将掌握流式响应的分块接收与聚合原理、流式模式下工具调用(含单轮多次并行工具调用)的验证方法、长文本流式输出的参数配置,以及流式错误处理与客户端中断等边界场景的测试套路,并能在自己的项目里照搬这套集成测试方案。
一、测试场景概览:四类流式用例
集成测试目录 integration_test/openai/chat/streaming 中,README.md 定义了四类核心测试用例,每一类都对应流式生产环境中的一个关键能力点:
| 测试函数 | 覆盖能力 | 核心验证点 |
|---|---|---|
TestStreamingChatCompletion | 基础流式对话 | 分块实时接收、内容聚合、非空校验 |
TestStreamingWithTools | 流式 + 工具调用 | 流式过程中工具调用被检测、收集并执行 |
TestStreamingLongResponse | 长文本流式输出 | 多 chunk 累积、内容长度与关键词校验 |
TestStreamingErrorHandling | 流式错误处理 | 非法参数优雅失败、不 panic |
所有用例共用一个测试框架:通过 testutil 提供的TestHelper构造带 AxonHub 追踪头(AH-Trace-Id/AH-Thread-Id)的请求上下文,再调用 OpenAI SDK 的流式方法发起请求。这四类用例覆盖了流式场景中"接收—聚合—工具—异常"的完整链路,是验证 AI 网关流式通路是否健壮的经典组合。
二、运行测试:命令与环境准备
2.1 三条核心运行命令
README 给出了从"全部执行"到"定向执行"的三种运行方式:
# 运行本目录全部测试 go test -v # 只运行基础流式对话 go test -v -run TestStreamingChatCompletion # 只运行流式工具调用 go test -v -run TestStreamingWithTools # 只运行长响应流式测试 go test -v -run TestStreamingLongResponse-run后面的正则表达式可以精确圈定单个测试函数,便于在排查问题时只重放某个失败用例。
2.2 前置环境变量
从 config.go 可以看到,测试并非开箱即跑,它依赖以下环境变量(未设置时会通过ValidateConfig直接跳过或报错):
| 环境变量 | 默认值 | 说明 |
|---|---|---|
TEST_AXONHUB_API_KEY | 无(必填) | 访问 AxonHub 网关的 API Key |
TEST_OPENAI_BASE_URL | http://localhost:8090/v1 | AxonHub 网关地址(本地默认 8090 端口) |
TEST_MODEL | deepseek-chat | 被测模型,可改为任意 AxonHub 支持的路由模型 |
TEST_TRACE_ID/TEST_THREAD_ID | 随机生成 | 可选,指定固定追踪 ID |
TEST_DISABLE_TRACE/TEST_DISABLE_THREAD | false | 可选,设为true关闭追踪头注入 |
需要特别说明的是:集成测试通过TEST_OPENAI_BASE_URL指向本地 AxonHub 网关(默认http://localhost:8090/v1),所以运行前需要先启动网关实例;模型名(TEST_MODEL)可以是任意经过 AxonHub 渠道配置、能被路由的模型——这正是 AxonHub "任意 SDK 调任意模型" 的体现:测试代码用 OpenAI SDK,底层实际调用的可以是 DeepSeek、Claude、Gemini 等任何已接入的模型。
2.3 追踪头如何随流式请求注入
TestHelper的CreateStreamingWithHeaders是流式请求的统一入口,源码见 helper.go:
func (h *TestHelper) CreateStreamingWithHeaders(ctx context.Context, params openai.ChatCompletionNewParams) *ssestream.Stream[openai.ChatCompletionChunk] { headerOpts := h.Config.GetHeaderOptions() return h.Client.Chat.Completions.NewStreaming(ctx, params, headerOpts...) }GetHeaderOptions(config.go)会把AH-Trace-Id、AH-Thread-Id转换成option.WithHeader请求选项,随流式请求一起发给网关。这意味着每个流式请求都能在 AxonHub 的追踪体系中留下独立 trace,方便把"chunk 到达节奏、最终聚合结果、错误发生点"与网关侧执行记录对应起来排查——这正是 AxonHub 端到端可观测性设计在集成测试层的直接落地。
三、基础流式对话:分块接收与内容聚合
TestStreamingChatCompletion是理解整个流式模型的最佳起点。它向模型发送一个短问题("Tell me a short story about a robot learning to paint."),然后通过 SDK 的流式迭代器逐块读取。
3.1 请求构造:流式参数其实不需要显式声明
一个反直觉但重要的细节:在 openai-go v3 中,调用NewStreaming方法时不需要在ChatCompletionNewParams里设置Stream: openai.Bool(true)。测试代码注释也明确写了 "no Stream field needed":
params := openai.ChatCompletionNewParams{ Messages: []openai.ChatCompletionMessageParamUnion{ openai.UserMessage(question), }, Model: helper.GetModel(), } stream := helper.CreateStreamingWithHeaders(ctx, params)NewStreaming内部会自动打开流式传输,返回的是*ssestream.Stream[openai.ChatCompletionChunk]。从 AxonHub 网关角度看,openai-api.md 中官方的流式示例也展示了同样模式——只是那里显式设置了Stream: openai.Bool(true)并用client.Chat.Completions.New返回可迭代对象。两种写法都成立,区别在于:显式Stream: true时New返回的也是流式迭代器;用NewStreaming则语义更明确,且方便在调用点附加请求头选项。
3.2 逐块读取:Next / Current / Err 循环
var chunksReceived int var accumulator openai.ChatCompletionAccumulator for stream.Next() { chunk := stream.Current() chunksReceived++ accumulator.AddChunk(chunk) } if err := stream.Err(); err != nil { helper.AssertNoError(t, err, "Stream error occurred") }这段代码是 openai-go 流式读取的固定范式:
stream.Next()每次返回是否有下一个 chunk;stream.Current()取出当前ChatCompletionChunk;accumulator.AddChunk(chunk)把每个增量块喂给ChatCompletionAccumulator,由它完成块级增量内容的拼接聚合;- 循环结束后必须检查
stream.Err(),流式传输中出现的错误(网络中断、服务端异常、SSE 解析失败)都会在这里暴露。
3.3 聚合结果校验
读取结束后,测试通过 accumulator 检查两部分结果:
if len(accumulator.Choices) == 0 { t.Error("Expected at least one choice from streaming") } finalContent := accumulator.Choices[0].Message.Content if chunksReceived == 0 { t.Error("Expected at least one chunk from streaming") } if len(finalContent) == 0 { t.Error("Expected non-empty content from streaming response") } if !testutil.ContainsCaseInsensitive(finalContent, "robot") && !testutil.ContainsCaseInsensitive(finalContent, "paint") { t.Errorf("Expected content to mention robot or paint, got: %s", finalContent) }校验点依次是:至少产生一个choice、至少收到一个 chunk、聚合后的完整内容非空、内容与提问主题相关(出现robot或paint)。其中"chunk 数 > 0"与"聚合内容非空"共同证明了流式链路的两个关键性质:增量可见性与最终一致性——用户既能实时看到逐块输出的内容,网关最终返回的聚合结果也完整无缺。
3.4 网关侧:chunk 如何被聚合成完整响应
客户端用ChatCompletionAccumulator聚合,网关侧同样有对应的聚合实现。AxonHub 的 aggregator.go 中,AggregateStreamChunks函数把收到的全部StreamEvent汇总成一个完整响应,其核心是choiceAggregator(aggregator.go)——它用strings.Builder逐块拼接delta.content、delta.reasoning_content、delta.refusal,并用map[int]*llm.ToolCall按index归并工具调用增量参数。几个值得注意的细节:
- 跳过
[DONE]哨兵:bytes.HasPrefix(chunk.Data, []byte("[DONE]"))时跳过,[DONE]只表示流结束,不承载业务数据; - finish_reason 兜底:若最后一个 chunk 没带
finish_reason,网关会按是否含工具调用补上tool_calls或stop(aggregator.go); - usage 提取:
chunk.Usage非空时被转换为统一用量结构,这也是 AxonHub 成本追踪的数据来源之一。
另外,inbound_stream.go 中的Next()实现展示了网关把上游 provider 流转换为客户端流的另一种方式:当上游源流结束时(source.Next()返回 false),如果上下文未取消、未收到[DONE]且需要 usage,还会通过finalize()补发一个带finish_reason的收尾 chunk 和DoneResponse。这段逻辑对应 README 中 "Chunk-by-chunk content aggregation" 的底层实现,解释了为什么客户端总能收到完整的流式结尾。
四、流式工具调用:边流式边决策
TestStreamingWithTools验证的是生产中最常见的复杂场景:模型在流式回答过程中决定调用工具。它向模型提出一个同时涉及数学计算(25 × 4)和天气查询(东京天气)的组合问题,并引导模型先自我介绍、再调用工具作答。
4.1 工具定义与流式请求构造
两个工具分别定义为calculate(数学计算)和get_current_weather(天气查询),使用shared.FunctionDefinitionParam描述 JSON Schema:
calculatorFunction := shared.FunctionDefinitionParam{ Name: "calculate", Description: openai.String("Perform mathematical calculations"), Parameters: openai.FunctionParameters{ "type": "object", "properties": map[string]any{ "expression": map[string]string{ "type": "string", }, }, "required": []string{"expression"}, }, }然后通过openai.ChatCompletionFunctionTool(...)包装成工具,放入请求的Tools字段:
params := openai.ChatCompletionNewParams{ Messages: []openai.ChatCompletionMessageParamUnion{ openai.UserMessage(question), }, Tools: []openai.ChatCompletionToolUnionParam{calculatorTool, weatherTool}, Model: helper.GetModel(), }4.2 流式工具调用的聚合特性
流式工具调用与普通流式有一个关键差异:工具调用的参数(arguments)是像文本内容一样被分块流式传回的。因此测试同样依赖ChatCompletionAccumulator收集:
if len(accumulator.Choices[0].Message.ToolCalls) > 0 { for i, toolCall := range accumulator.Choices[0].Message.ToolCalls { t.Logf("Tool call %d: %s", i+1, toolCall.Function.Name) switch toolCall.Function.Name { case "calculate": result := simulateCalculatorFunctionFromArgs(toolCall.Function.Arguments) t.Logf("Calculator result: %v", result) case "get_current_weather": result := simulateWeatherFunctionFromArgs(toolCall.Function.Arguments) t.Logf("Weather result: %s", result) } } }Accumulator会把散布在多个 chunk 中的同一工具调用(按index区分)拼合成完整对象,包括函数名和完整的 JSON 参数串。拿到完整参数后,测试用模拟函数执行"工具端逻辑"——simulateCalculatorFunctionFromArgs与simulateWeatherFunctionFromArgs(streaming_test.go)根据参数串匹配返回预设结果。
对照网关源码,AggregateStreamChunks对工具调用的处理如出一辙(aggregator.go):按deltaToolCall.Index初始化工具调用对象,随后把每个 chunk 中Function.Arguments的增量片段追加拼接,函数名与 ID 在非空时更新。因此无论客户端还是网关,工具调用的流式聚合都遵循"按 index 建槽、增量拼接参数"的原则。
4.3 单轮内多次并行工具调用
README 列出的测试列表中虽然没有单列此项,但源码里还有一个更严苛的用例TestStreamingToolMultipleCallsSingleTurn(streaming_test.go),它验证模型在单个 assistant turn 内连续发起多个工具调用(天气 + 计算,两个都要)。请求的关键参数:
params := openai.ChatCompletionNewParams{ Messages: []openai.ChatCompletionMessageParamUnion{ openai.UserMessage(question), }, Tools: []openai.ChatCompletionToolUnionParam{weatherTool, calculatorTool}, ParallelToolCalls: openai.Bool(true), // 允许并行工具调用 ToolChoice: openai.ChatCompletionToolChoiceOptionUnionParam{ OfAuto: openai.String("required"), // 强制要求工具调用 }, Model: helper.GetModel(), }ParallelToolCalls: true开启并行工具调用;ToolChoice设置为required,强制模型必须走工具调用路径、不得用普通文本敷衍。
验证逻辑要求:收集到的工具调用数 ≥ 2,且calculate与get_current_weather都出现。这一用例直接对应 README 中 "Tool calls in streaming mode allow for interactive conversations" 的断言,也说明在流式模式下,AxonHub 网关能把多路工具调用的增量参数按 index 正确拆分、归并、还原成完整的多个调用。
五、长响应流式:token 上限与内容质量校验
TestStreamingLongResponse模拟"长文档生成"类场景——请求一段光合作用(photosynthesis)的详细讲解,覆盖光依赖反应与光不依赖反应,并专门做了参数化配置:
params := openai.ChatCompletionNewParams{ Messages: []openai.ChatCompletionMessageParamUnion{ openai.UserMessage(question), }, Model: helper.GetModel(), MaxCompletionTokens: openai.Int(1000), // 允许更长响应 Temperature: openai.Float(0.7), // 更富创造性 }MaxCompletionTokens: 1000:放宽输出 token 上限,确保模型能生成足够长的内容,从而产生大量 chunk;Temperature: 0.7:适度提高随机性,让内容更有"讲解"的展开度。
5.1 分两个维度验证长响应
if chunksReceived < 5 { t.Errorf("Expected more chunks for long response, got: %d", chunksReceived) } if len(finalContent) < 200 { t.Errorf("Expected longer content, got: %d characters", len(finalContent)) }- chunk 数量维度:要求至少收到 5 个 chunk——证明流式是"持续产出"而非"一次性吐全";
- 内容长度维度:聚合结果至少 200 字符——证明长内容没有在传输或聚合过程中丢失。
5.2 内容语义校验:关键词命中
expectedTerms := []string{"photosynthesis", "light", "chlorophyll", "carbon dioxide", "oxygen"} foundTerms := 0 for _, term := range expectedTerms { if testutil.ContainsCaseInsensitive(finalContent, term) { foundTerms++ } } if foundTerms < 3 { t.Errorf("Expected explanation to contain key photosynthesis terms, found %d/%d", foundTerms, len(expectedTerms)) }关键词校验要求 5 个主题词中至少命中 3 个,防止"模型答非所问、流式通了但内容空洞"这类假阳性。ContainsCaseInsensitive是 helper.go 提供的大小写不敏感子串匹配,兼容模型输出中大小写不一致的情况。
对长响应而言,流式还有一个隐藏价值——首字节延迟感知更优。README 的 Expected Behavior 中 "Streaming should provide faster perceived response times" 正是这个意思:用户不必等 1000 token 全部生成完,而是边生成边看到内容。AxonHub 在网关侧通过 llm/streams/stream.go 定义的统一Stream[T]接口(Next/Current/Err/Close)抽象所有 provider 的流式读取,使得这种"边到边转"的体验在不同模型间保持一致。
六、错误处理:非法参数的优雅失败
TestStreamingErrorHandling是四类用例中最"反常规"的一个:它故意构造非法请求——MaxCompletionTokens: openai.Int(-1),即负数 token 上限——然后验证系统不会 panic、不会挂死,而是优雅地失败:
stream := helper.CreateStreamingWithHeaders(ctx, params) if err := stream.Err(); err == nil { var chunksReceived int var accumulator openai.ChatCompletionAccumulator for stream.Next() { chunk := stream.Current() chunksReceived++ accumulator.AddChunk(chunk) } if err := stream.Err(); err != nil { t.Logf("Stream error (expected): %v", err) // 流中途报错:符合预期 } else { t.Logf("Request accepted despite invalid parameters, received %d chunks", chunksReceived) // 个别 provider 容忍该参数:记录实际行为即可 } } else { t.Logf("Correctly caught error: %v", err) // 请求建立即报错:符合预期 }这段测试体现了流式错误处理的两条路径:
- 建流即失败:
stream.Err()在迭代前就非空(例如参数校验、鉴权失败),直接记录错误; - 流中途失败:前几个 chunk 正常,后续
stream.Err()暴露错误(例如 provider 端拒绝非法参数、网络中断)。
测试对两种结果都放行,只记录实际行为——这种"宽容断言"设计很实用,因为不同模型/网关对max_completion_tokens=-1的容忍度不同。真正要验证的是:无论哪种失败路径,测试进程都不会 panic、不会无限阻塞。
结合网关源码,错误处理链路还有一层保障:llm/transformer/openai/completion_inbound.go 与 inbound 侧转换会把上游 provider 的错误(如 400 参数错误、401 鉴权失败)归一化为统一错误格式返回客户端,而流式循环后的stream.Err()检查则确保传输层异常不会被吞掉。README Notes 中 "Error handling is crucial for production streaming applications" 指的就是:生产环境必须同时处理"建立阶段错误"与"流中错误",并始终检查Err()。
七、进阶场景:客户端中断与状态判定
虽然 README 未展开,但仓库在 Responses API 目录下提供了更深入的流式边界测试 client_disconnect_test.go,它补充了流式场景的另一个关键事实:客户端断开时机决定请求状态是 completed 还是 canceled。
测试要点如下:
- 客户端收到
response.completed(等价[DONE])后立即断开:网关应把请求标记为completed,而不是canceled(对应 GitHub Issue #827 的修复); - 客户端在流中间断开(未收到
[DONE]):请求状态应为canceled; - 还有
TestResponsesStreamingRapidDisconnects模拟多次快速连接/断开,验证服务端在并发断连下的稳定性。
这个用例对生产很有参考价值:Agent(如 Claude Code → AxonHub → Codex 链路)常常在文本输出完毕的瞬间就拆除连接,此时若网关把请求误判为取消,会导致追踪与计费数据错乱。流式应用的开发者应当理解:[DONE]是"完成"的权威信号,收到它之后再断开不应被视为取消。
八、测试设计方法论小结
综合四类(加进阶共六类)用例,可以总结出 AxonHub 流式集成测试的通用方法论,供你在自己的网关或 SDK 项目中复用:
- 统一测试入口:用
TestHelper封装环境变量读取、API Key 校验、追踪头注入,所有用例复用同一套初始化逻辑(见 helper.go 与 config.go); - 固定读取范式:
for stream.Next() { ... }+stream.Err()检查,这是所有流式用例的骨架; - 聚合器统一收口:客户端用
ChatCompletionAccumulator,网关用AggregateStreamChunks/choiceAggregator,两边都遵循"增量拼接、index 归并"原则; - 三重校验:chunk 数量(增量可见性)、聚合内容(最终一致性)、关键词/语义(内容质量),缺一不可;
- 错误容忍设计:对 provider 行为差异宽容(记录实际行为而非硬断言),但对"不 panic、不阻塞"严格把关;
- 边界场景补强:单轮多工具调用、客户端断连时机、快速重复断连,这些才是流式链路真正容易出 bug 的地方。
按此方法论,即使只跑一条go test -v -run TestStreamingChatCompletion,也能快速判断一条 AxonHub 网关流式链路(SDK → 网关 → provider)是否健康;全套用例通过,则说明流式聚合、工具调用、长文本、错误处理与断连语义均已达标,可以放心在生产环境承接流式流量。
- AI 应用
- API网关
- LLM 网关
- 后端
- 前端
【免费下载链接】axonhub
⚡️ Open-source AI Gateway — Use any SDK to call 100+ LLMs. Built-in failover, load balancing, cost control & end-to-end tracing.
相关推荐
AxonHub 集成测试实战:OpenAI Responses API 流式输出(SSE 事件处理、工具调用与异常场景)
AxonHub 集成测试实战:OpenAI Responses API 流式输出(SSE 事件处理、工具调用与异常场景) 导读 本文聚焦 AxonHub 开源仓
AI 应用API网关LLM 网关后端前端AxonHub OpenAI Responses API 内置 `web_search` 工具集成测试实战:从工具调用到引文校验
AxonHub OpenAI Responses API 内置 web_search 工具集成测试实战:从工具调用到引文校验 导读 本文以 AxonHub 仓库
AI 应用API网关LLM 网关后端前端ZCode 开发与打包全指南:桌面、Web 与终端 Agent 三入口的构建、配置与发布
ZCode 开发与打包全指南:桌面、Web 与终端 Agent 三入口的构建、配置与发布 ZCode 是智谱 AI 的开源 AI 编程工作台,覆盖桌面应用、浏览
AI 应用API网关LLM 网关后端前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考