V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
wantDoraemon
V2EX  ›  程序员

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

  •  
  •   wantDoraemon ·
    PGshen · 11 小时 55 分钟前 · 482 次点击

    为什么需要流式 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  
       11 小时 39 分钟前
    试过在前端使用 untruncate-json 流式补全 json
    lrwlf
        2
    lrwlf  
       11 小时 37 分钟前
    有个也能实现流式 json 的项目: https://github.com/josdejong/jsonrepair
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   880 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 21:25 · PVG 05:25 · LAX 13:25 · JFK 16:25
    ♥ Do have faith in what you're doing.