实现一个流式 Json 解析器,解决既要求结构化数据又需要实时流式输出的一种思路 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
wantDoraemon
V2EX    程序员

实现一个流式 Json 解析器,解决既要求结构化数据又需要实时流式输出的一种思路

  •  
  •   wantDoraemon
    PGshen 6 小时 53 分钟前 416 次点击

    为什么需要流式 JSON 解析?

    AI 模型的输出通常是逐字生成的流式数据,尤其是在实时对话交互的场景中,用户希望能即时看到 AI 输出内容的过程。而另一方面,我们在很多场景下需要 AI 模型输出结构化的内容,方便我们后续处理和展示。我们可以通过 markdown 的标题格式来组织 AI 的输出,但是 markdown 的无法做到强约束。如果需要保证强结构化,那么我们一般会采用 JSON 格式,对此很多模型都有强制约束输出 Json 的参数。 这其中有两大问题:

    1. 高延迟 :如果使用 Json 格式,传统的 Json.Parse 方法必须等待整个字符串接收完毕才能解析,这会导致高延迟问题。用户无法实时看到 AI 的思考过程,体验大打折扣。
    2. 交互不稳定 :如果使用 Markdown 格式,AI 输出可能不符合预期,导致前端展示异常。

    因此,我们需要一种能够边接收数据边解析的方案,确保用户实时看到 AI 的输出,同时保证解析的健壮性。


    场景

    例如我们在理解用户问题这个场景时,既想要结构化的数据,又想要实时的将结果反馈到前端 preview

    设计目标

    为了解决上述问题,我们设计了一款流式 JSON 解析器,目标包括:

    1. 实时性:支持逐字符解析,边接收边触发回调。
    2. 路径订阅:允许用户按需订阅 JSON 中的特定路径(如 $.nodes[*].title),减少无效数据处理。
    3. 增量输出:针对字符串值,仅发送新增部分,避免重复传递完整值。
    4. 健壮性:即使 JSON 格式不完整或后续部分有错误,已解析的数据也能正常使用。

    核心实现

    我们的解析器基于手写的有限状态机( FSM ),逐字符处理流式数据。以下是实现的关键组件和流程:

    1. StreamingJsonParser (流式解析器)

    • 状态机设计:解析器通过状态机维护当前解析上下文,支持对象、数组、字符串、数字等 JSON 元素的逐字符解析。
    • 路径维护:通过栈结构记录当前解析路径(如 ["nodes", 0, "title"]),用于路径匹配和回调触发。
    • 增量输出:针对字符串值,记录上次发送的位置,仅发送新增部分。

    状态机的核心逻辑如下: 状态机

    2. SimplePathMatcher (路径匹配器)

    • 路径解析:支持将路径模式(如 $.nodes[*].title)解析为数组形式(如 ["nodes", "*", "title"])。
    • 通配符匹配:支持 * 通配符,用于匹配数组中的任意索引。
    • 回调触发:当解析器识别到匹配路径的数据时,立即触发用户注册的回调函数。

    增量与实时模式

    解析器支持两种模式:

    1. 实时模式( realtime=true ):在值尚未最终确定时,依据当前缓冲区内容触发回调,适合逐字生成的场景。
    2. 增量模式( incremental=true ):针对字符串值,仅发送新增部分,避免重复传递完整值。

    以下是增量解析的示例:

    matcher := utils.NewSimplePathMatcher() matcher.On("$.choices[0].delta", func(value interface{}, path []interface{}) { fmt.Printf("path=%v, value=%v\n", path, value) }) parser := utils.NewStreamingJsonParser(matcher, true, true) _ = parser.Write("{\"choices\":[{\"delta\":\"") _ = parser.Write("Hel") // 增量触发回调:"Hel" _ = parser.Write("lo\"}]}\n") // 增量触发回调:"lo",结束后不再发送整串 _ = parser.End() 

    性能与内存优化

    • 轻量高效:手写状态机避免引入完整 JSON 库,适合流式场景。
    • 增量缓存:通过记录上次发送位置,减少重复回调数据。
    • 路径匹配优化:使用切片维护路径,避免过度复制。

    常见问题与改进方向

    1. 字符串转义:支持常见转义 \n, \t, \r, \\, \", \/, \b, \f;当前未支持 \uXXXX Unicode 转义序列,可按需扩展。
    2. 路径匹配:目前为精确匹配,可考虑支持前缀匹配或更完整的 JsonPath 语法。
    3. 增量支持:目前仅支持字符串值的增量输出,未来可扩展至对象和数组。

    文档&代码实现

    代码使用 Golang 实现,如果需要使用其他语言,可以让 AI 翻译一下即可。

    通过自研流式 JSON 解析器,成功解决了 AI 应用中实时性和结构化输出的难题。希望这次分享能为有类似需求的开发者提供参考。

    2 条回复    2025-12-16 17:47:13 +08:00
    freeman12
        1
    freeman12  
       6 小时 37 分钟前
    试过在前端使用 untruncate-json 流式补全 json
    lrwlf
        2
    lrwlf  
       6 小时 35 分钟前
    有个也能实现流式 json 的项目: https://github.com/josdejong/jsonrepair
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1570 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 16:22 PVG 00:22 LAX 08:22 JFK 11:22
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86