面向 LLM 流式输出的 JSON 增量解析 + 事件化输出 Demo
项目展示 · 它解决什么问题 · 难点与思路(精简版) · 快速开始 · 性能概览
GIF 1:展示“原始 LLM 一行一个 chunk”与“解析后给客户端的一行一个 chunk”对比。
Streaming Output:左侧原始流,右侧解析后事件流。
GIF 2:展示 Report Preview 对比:左边是客户端直接按收到的信息输出,右边是按固定时间间隔输出。
Report Preview:左侧即时输出,右侧定时批量输出。
难点主要出现在:LLM 同时开启 stream 和 response_format。
response_format 要求模型生成结构化 JSON,但 stream 返回的是被切碎的字节流,任意 chunk 都可能是不完整 JSON 片段,甚至截断 UTF-8 字符。
这个项目的目标是:在 JSON 尚未闭合时,也能稳定输出可消费的增量事件,同时避免乱码、字段串台和非完整字段误发。
一句话:边收、边解析、边输出,而不是等完整 JSON 再一次性处理。
- chunk 边界不对齐:可能切在字符串中间、甚至 UTF-8 字符中间。
- 业务字段语义不一致:有的字段可以实时发,有的必须完整发(如 ID)。
- 性能波动大:chunk 越碎,状态切换和内存分配越重。
- 字节级状态机:按字节推进 JSON 语法状态。
- 路径映射:把 JSON 路径映射为业务事件类型(如
topic[0].name)。 - 输出策略分层:普通字段增量发,close-only 字段闭合后发。
- UTF-8 尾巴缓存:半个字符先缓存,下一 chunk 补齐再输出。
项目需要以下配置:
api_base(必填)api_key(必填)model(必填)summary_lang(可选,默认zh,可设en)
api_base=https://your-api-base/v1
api_key=your_api_key
model=your_model_name
summary_lang=zh
go run web_server.go
默认地址:http://127.0.0.1:8080
自定义端口示例:
WEB_ADDR=:8081 go run web_server.go
本地样例数据(命令:go test -bench . -benchmem ./sdk/lexer)。
环境:darwin/arm64, Apple M4 Pro
Tip(列含义)
ns/op:每次迭代平均耗时(越小越好)MB/s:吞吐(越大越好)B/op:每次迭代平均分配字节(越小越好)allocs/op:每次迭代平均分配次数(越小越好)
| Case | ns/op | MB/s | B/op | allocs/op |
|---|---|---|---|---|
| ReplayLike_MixedRunes | 101,872 | 9.93 | 224,559 | 1,959 |
| ReplayLike_TinyRunes | 202,833 | 4.99 | 359,785 | 3,102 |
| Baseline_Fixed64Bytes | 21,545 | 46.97 | 20,107 | 257 |
| CloseOnly_ReplayLike | 1,677,961 | 12.91 | 3,239,081 | 24,269 |
| CloseOnly_Baseline128Bytes | 130,438 | 166.11 | 260,104 | 890 |
如何理解这 5 组结果
- chunk 粒度是第一影响因子:同一常规 payload 下,切得越碎,性能越差。
- case 4/5 是极端压力测试:用于观察 close-only 最坏上限,不代表常规业务分布。
- 大 chunk 更友好:吞吐更高、分配更低,这在常规和极端场景都成立。

