ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JsonWriter 使用指南:把 API 响应流式写入 JSON 的完整配置

JsonWriter 使用指南:把 API 响应流式写入 JSON 的完整配置 1. 为什么流式写 JSON 总在最后一个括号翻车先说清楚 JsonWriter 是什么、能做什么、适合谁。JsonWriter 是 Android/Java 体系里一个轻量的 JSON 序列化工具它不像 Gson 那样把整个对象一次性映射成字符串而是让你手动控制beginObject、name、value、endObject这些调用一个 token 一个 token 地往输出流里写。适合的场景很明确数据量大、内存吃紧、需要边拿数据边吐结果的服务端接口比如从上游 API 拿到分块响应后逐段转写成合法 JSON 返回给前端。问题也恰恰出在“手动控制”这四个字上。我见过太多流式接口在本地跑得好好的一上生产就偶发返回Unexpected end of JSON input前端JSON.parse直接炸。排查半天发现不是数据错而是某个分支提前return了endArray和endObject没配对JSON 结构缺了收尾。JsonWriter 本身不会帮你校验括号平衡它只负责转义和格式化结构合法性完全靠调用方自己保证。这篇就聚焦服务端流式输出这个落地场景从 API 拿到分块响应后怎么用 JsonWriter 逐段写入怎么保证每一段拼起来仍是合法 JSON最后给一段用 curl 触发、再比对输出能否被解析的验证动作。整套流程我会用 TaoToken 的模型接口做上游数据源来演示因为它返回的就是标准的流式分块拿来练手刚好。你跟着做能跑通一个最小示例也能把这套写法直接搬到自己的业务里。核心检索词先摆出来JsonWriter 流式写入、API 分块响应转 JSON、服务端流式输出结构合法性。这三个词贯穿全文你搜的时候也能对上。2. TaoToken 前置准备拿 Key 与选对模型要让 JsonWriter 有东西可写得先有一个能吐分块响应的上游。TaoToken 的接口兼容 OpenAI 风格的流式返回stream: true时服务端会一段段推data:行正好模拟真实业务里“上游分块、下游转写”的链路。这里把前置动作走一遍后面配置片段才有地方填。第一步是拿访问凭证。打开控制台里的 API Keys 页面新建一个 Key复制出来先存好它只在创建时完整显示一次。地址是 https://taotoken.net/api-keys 进去后点新建命名随意比如jsonwriter-demo。第二步是确认 Base URL。TaoToken 的接口入口统一在 https://taotoken.net/api 所有兼容 OpenAI 的路径都挂在这个根下面比如对话补全就是/v1/chat/completions。注意这个地址不带任何多余后缀拼接时别重复加/v1。第三步是选模型。流式场景建议选响应速度快的对话模型模型 ID 在文档的模型列表里能查到比如常见的gpt-4o-mini这类。你不需要纠结哪个最强JsonWriter 演示关心的是“分块到达”这个行为不是模型智商。想先在线试一下返回长什么样可以去模型对话页面发一条消息把流式开关打开肉眼看看分块长啥样https://taotoken.net/model-chat 。如果你后面打算把这套流式转写用在长期跑的编码 Agent 或批量任务上可以顺带了解下 Coding Plan它按周期计费比单次调用更适合持续输出场景https://taotoken.net/coding-plan 。三样东西凑齐Base URL、API Key、Model ID。这三个是后面所有配置的地基缺一个请求都发不出去。我建议你现在就把它们写在一个临时文件里等下直接往配置片段里粘。3. 可复制的 JsonWriter 初始化与写入配置这一节是全文的技术核心给你能直接抄的配置片段。分两块一块是 JsonWriter 的初始化与写入骨架一块是上游请求的配置。两块拼起来才是一个完整的流式转写服务。先看 JsonWriter 的初始化。关键点是输出流用 UTF-8 包装setIndent决定是否美化流式场景我建议关掉缩进省带宽也省解析开销// JsonWriter 初始化包装 UTF-8 输出流关闭缩进 OutputStream rawOut response.getOutputStream(); JsonWriter writer new JsonWriter(new OutputStreamWriter(rawOut, StandardCharsets.UTF_8)); writer.setIndent(null); // 流式场景不美化减少体积 writer.beginObject(); writer.name(items); writer.beginArray();写入骨架的核心是“配对”。每写一个对象beginObject和endObject必须成对每开一个数组beginArray和endArray必须成对。流式场景最容易漏的是异常分支里的收尾所以我把收尾逻辑放进finally保证无论中途出什么错JSON 都能闭合try { // 逐段写入每段是一个 item 对象 for (Chunk chunk : chunks) { writer.beginObject(); writer.name(id).value(chunk.getId()); writer.name(text).value(chunk.getText()); writer.name(ts).value(chunk.getTimestamp()); writer.endObject(); writer.flush(); // 每段刷一次前端能实时收到 } } finally { writer.endArray(); writer.endObject(); writer.flush(); writer.close(); }注意flush的位置。流式输出的意义就是“边写边发”如果你等全部写完再 flush那就退化成一次性返回了。每写完一个 item 就 flush前端才能一段段拿到。但 flush 不等于 closeclose 只能调一次放在 finally 里收尾。再看上游请求配置。用 curl 触发时请求体里stream必须为true模型 ID 填你第二步选的那个curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, stream: true, messages: [{role: user, content: 用三句话介绍流式输出}] }如果你用 Java 的 HttpClient 发这个请求配置片段长这样重点是BodyHandlers.ofLines()它把响应按行拆开正好对应 SSE 的data:行HttpRequest req HttpRequest.newBuilder() .uri(URI.create(https://taotoken.net/api/v1/chat/completions)) .header(Authorization, Bearer apiKey) .header(Content-Type, application/json) .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); HttpResponseStreamString resp client.send(req, HttpResponse.BodyHandlers.ofLines());到这里初始化、写入、请求三块配置都齐了。你可以先把它们拼成一个最小类下一节我们跑起来验证。4. 验证请求curl 触发与 JSON 可解析性比对配置写完不验证等于没写。这一节给你一套可复现的验证动作先用 curl 触发上游确认分块真的在流再把 JsonWriter 的输出落盘用解析器验证它是不是合法 JSON。第一步触发上游看分块。把上一节的 curl 命令存成trigger.shKey 用环境变量传别硬编码export TAOTOKEN_API_KEY你的Key bash trigger.sh正常返回是一串data: {...}行每行一个增量 chunk最后以data: [DONE]收尾。如果你看到的是一整坨 JSON 一次性返回说明stream没生效回去检查请求体里是不是写成了字符串true而不是布尔true。第二步把 JsonWriter 的输出写到文件然后验证。写一个小 main 方法模拟三段 chunk 写入public static void main(String[] args) throws IOException { FileOutputStream fos new FileOutputStream(out.json); JsonWriter writer new JsonWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8)); writer.setIndent(null); writer.beginObject(); writer.name(items); writer.beginArray(); for (int i 0; i 3; i) { writer.beginObject(); writer.name(id).value(chunk- i); writer.name(text).value(第 i 段内容); writer.endObject(); } writer.endArray(); writer.endObject(); writer.close(); System.out.println(written); }跑完后用 Python 验证它能不能被解析这一步是判断结构合法性的硬标准python3 -c import json; djson.load(open(out.json)); print(OK, len(d[items]), items)输出OK 3 items就说明结构闭合正确。如果报json.decoder.JSONDecodeError八成是endArray或endObject漏了回去数括号。第三步做一次“故意破坏”的对照实验加深印象。把上面代码里的writer.endArray()注释掉再跑Python 解析立刻报错。这个对照能让你直观感受到JsonWriter 不会替你兜底结构合法性是你自己的责任。实测下来这个对照实验比看十遍文档都管用。验证通过后你就有了一个能跑通的最小示例。把它接到真实的上游分块循环里就是完整的流式转写服务。5. 常见报错排查401、local proxy failed 与 choices 读取失败流式转写跑不通报错基本集中在几个固定位置。这一节按真实报错对照着排每个都给你定位思路。401 Unauthorized。这个最直接Key 不对或没带上。检查三处请求头里Authorization是不是Bearer开头注意 Bearer 后面有个空格Key 是不是复制时带了换行或空格环境变量有没有真的 export 成功。用echo $TAOTOKEN_API_KEY | head -c 8看前几位对不对。如果 Key 是在控制台新建的确认没被删。local proxy failed / connection refused。这个报错通常出现在你本地起了个转发进程但进程没起来或端口对不上。先确认你的请求是直连https://taotoken.net/api没有经过任何本地中间层。如果你在代码里配了http.proxyHost之类的系统属性把它去掉。这个报错和网络环境无关纯粹是本地配置多了一层。reading choices / cannot read field choices。这个报错说明你拿到的响应不是预期的补全结构。流式场景下每个data:行里的 JSON 是增量 delta字段路径是choices[0].delta.content不是choices[0].message.content。如果你按非流式的路径去读就会读到 null 然后报错。对照一下非流式读message流式读delta。另外[DONE]那一行不是 JSON解析前要先判断跳过。OAuth / token expired。如果你用的是带过期时间的凭证过期后会返回这个。重新去控制台生成一个或者检查你的刷新逻辑。流式长连接场景下凭证过期可能发生在连接中途建议在写入循环里捕获异常触发重新鉴权后重试当前段。JsonWriter 相关IllegalStateException: Nesting problem。这是 JsonWriter 自己的报错意思是你的 begin/end 嵌套乱了。比如在数组里直接name而没先beginObject或者endObject多调了一次。定位方法在每次 begin/end 调用处打日志数一数当前嵌套深度深度为负或收尾时不为零就是这里的问题。把这几类报错对照一遍基本能覆盖流式转写 90% 的翻车场景。剩下的就是业务逻辑本身的问题了。6. 把流式转写接进你的业务从最小示例到可用服务最小示例跑通后离可用服务还差几步工程化处理这几步决定它能不能上生产。第一异常兜底要覆盖“中途断流”。上游分块可能因为网络抖动断在半路这时候你的 JsonWriter 已经写了一半。处理方式是在finally里无论如何都闭合结构哪怕数据不完整至少保证返回的是合法 JSON前端能解析出“部分结果”而不是直接崩。可以在根对象里加一个partial: true标记让前端知道这次不完整。第二背压控制。流式写入如果下游消费慢flush会阻塞进而拖慢上游读取。给输出流套一层带缓冲的包装或者用异步写避免上游连接被拖死。这个在并发量上来后是必踩的坑。第三字段转义别自己来。JsonWriter 的value(String)会自动处理引号、换行、反斜杠的转义你千万别在传进去之前手动 replace否则会双重转义。我见过有人先把文本里的换成\再传给 JsonWriter结果输出里变成\\\前端解析出来多了一层反斜杠。第四模型 ID 和 Base URL 做成配置项别写死在代码里。不同环境用不同模型硬编码改起来痛苦。把https://taotoken.net/api、Key、Model ID 抽到配置文件启动时注入。这套写法我从最小示例一路用到线上服务最大的体会是JsonWriter 的可靠性不来自它自己而来自你对 begin/end 配对的纪律。把收尾逻辑锁死在 finally 里把 flush 放在每段之后把解析验证加进 CI流式 JSON 就没那么容易翻车了。你现在就可以拿第 4 节的验证脚本改一改接上真实的上游分块跑一遍完整链路。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进