JSON vs JSONL:从数据格式到 AI Agent 的工程实践
文章目录

一、JSON:我们熟悉的"完整对象"
JSON(JavaScript Object Notation)是过去二十年里最成功的数据交换格式。一个典型的 JSON 文档长这样:
{
"messages": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮你的?"},
{"role": "user", "content": "今天天气怎么样?"}
],
"model": "claude-fable-5",
"temperature": 0.7
}
JSON 的核心特征:完整、闭合、一次性解析。
- 整个文件是一个合法的 JSON 值(对象、数组、字符串等)
- 解析器必须读取全部内容才能得到完整的数据结构
- 在内存中,它就是一棵完整的树
这对"配置"和"请求-响应"场景是天作之合——配置文件、API 请求体、一次性的结构化数据传递,JSON 都是不二之选。
但当一个 Agent 系统开始 “边跑边想、边说边做” 时,JSON 的"完整性约束"反而成了瓶颈。
二、JSONL:行级独立的流式格式
JSONL(JSON Lines)的规则简单得惊人:一行一个合法的 JSON 对象,行与行之间互不依赖。
{"type": "tool_use", "id": "toolu_01", "name": "read_file", "input": {"path": "config.yaml"}}
{"type": "tool_result", "id": "toolu_01", "content": "server: {port: 8080}"}
{"type": "thinking", "content": "配置文件读取成功,现在我需要解析端口号……"}
{"type": "text", "content": "你的应用监听在 8080 端口。"}
JSON vs JSONL 核心对比
| 维度 | JSON | JSONL |
|---|---|---|
| 结构 | 单个完整的 JSON 值 | 每行一个独立 JSON 对象 |
| 解析方式 | 一次性全文解析 | 逐行解析,可流式处理 |
| 追加写入 | 需要重写整个文件 | append 即可,无需改写已有内容 |
| 部分读取 | 不行——不完整则非法 | 可以随时截断,不破坏前面的行 |
| 内存占用 | 与文件大小成正比 | 只需当前行的内存 |
| 合法性验证 | 整体合法才算合法 | 逐行合法,坏行不影响好行 |
| 典型场景 | API 请求/响应、配置文件 | 流式输出、日志、数据集、Agent 消息流 |
为什么 JSONL 特别适合"流"?
想象你在看直播弹幕——你不会等主播下播后才一次性收到所有弹幕,而是来一条看一条。Agent 的思考过程和工具调用也是如此:用户不应该等 Agent 完全跑完才看到结果,而应该实时看到每一步推理和行动。
JSONL 就是为这种"来一条消费一条"的模式而生的。

三、Agent 场景中的 JSONL 实战
目前主流 AI Agent 系统对 JSONL 的使用已经渗透到几乎每一个环节。
3.1 Streaming API:一个请求,多条返回
当你调用 LLM 的 streaming API 时,服务端返回的不是一个完整的 JSON 响应,而是一系列 Server-Sent Events,每个 event 内部的数据就是一行"事实上的 JSONL"(通常是 data: {"choices": [...]} 的形式)。
OpenAI / Anthropic 的流式响应本质上就是 JSONL:
data: {"type": "text", "content": "我来"}
data: {"type": "text", "content": "帮你"}
data: {"type": "text", "content": "查一下。"}
data: {"type": "tool_use", "name": "search", "input": {"query": "..."}}
data: [DONE]
没有 JSONL 会怎样? 那你只能等模型完整生成完毕后才能看到第一个字——用户体验回到拨号上网时代。
3.2 工具调用追踪:Agent 的"黑匣子"
一个多步 Agent 的执行过程会产生大量事件:
type: "thinking" → 模型在思考
type: "tool_use" → 模型决定调用工具
type: "tool_result" → 工具返回结果
type: "thinking" → 模型分析工具结果
type: "tool_use" → 再次调用工具
type: "text" → 最终回复
这些事件天然就是 JSONL——时间有序、独立可读、增量产生。
每个事件带上 timestamp 和 id,整个 Agent 执行过程就有了完整的审计日志:
{"ts": "2026-07-18T10:23:01.001Z", "type": "start", "agent": "research-agent", "task": "调研竞品定价"}
{"ts": "2026-07-18T10:23:01.500Z", "type": "tool_use", "tool": "web_search", "input": {"q": "SaaS 定价策略 2026"}}
{"ts": "2026-07-18T10:23:03.200Z", "type": "tool_result", "tool": "web_search", "output": "3 条结果", "tokens": 450}
{"ts": "2026-07-18T10:23:05.100Z", "type": "tool_use", "tool": "deep_read", "input": {"url": "https://..."}}
{"ts": "2026-07-18T10:23:08.300Z", "type": "tool_result", "tool": "deep_read", "output": "..."}
有了这样的 JSONL 日志,你可以:
- 重放:从任意步骤重新执行 Agent
- 审计:快速 grep 所有
tool_use事件看 Agent 调用了哪些外部服务 - 统计:流水线式分析延迟、Token 消耗、工具成功率——不需要加载整个日志到内存
3.3 微调数据集(Fine-tuning Data)
几乎所有主流 LLM 的微调数据格式都是 JSONL。以 OpenAI 的 fine-tuning 格式为例:
{"messages": [{"role": "system", "content": "你是客服助手"}, {"role": "user", "content": "如何退款?"}, {"role": "assistant", "content": "您可以在订单页面点击..."}]}
{"messages": [{"role": "system", "content": "你是客服助手"}, {"role": "user", "content": "物流多久到?"}, {"role": "assistant", "content": "顺丰发货,通常 2-3 天"}]}
为什么是 JSONL 而不是一个巨大的 JSON 数组?
- 数据量大——一个 fine-tuning 数据集可能有几十万到几百万条,单个 JSON 数组会撑爆内存和 IDE
- 流式上传和验证——逐行校验,一行格式错误不影响其他行
- Shuffle 和分片——可以用
shuf file.jsonl | head -10000一行命令取子集 - 增量追加——新数据直接
>>追加,不需要反序列化→合并→重新序列化
3.4 多 Agent 通信与消息队列
在复杂的多 Agent 系统中(比如 Claude Code 的 workflow 模式),不同 Agent 之间需要传递结构化消息。JSONL 文件作为轻量级消息队列非常实用:
# Agent A 写入任务
{"event": "task_dispatched", "task_id": "abc", "handler": "code-reviewer", "payload": {...}}
# Agent B 边处理边写结果
{"event": "review_started", "task_id": "abc"}
{"event": "finding", "task_id": "abc", "severity": "high", "description": "..."}
{"event": "review_complete", "task_id": "abc", "score": 0.85}
为什么不用数据库? 对于开发调试阶段和轻量级编排,JSONL 零依赖、零配置,tail -f 就是天然的消费者。生产环境当然要上 RabbitMQ / Kafka,但 JSONL 在原型阶段"不引入新基础设施"的价值不可忽视。
3.5 日志聚合与可观测性
AI Agent 系统天然是异步、多步、长时运行的。JSONL 日志让其可视化变得简单:
# 统计今天各 Agent 的工具调用次数
grep '"type":"tool_use"' agent-2026-07-18.jsonl | jq -r '.tool' | sort | uniq -c | sort -rn
# 找出所有报错的工具调用
grep '"type":"tool_error"' agent.jsonl | jq '{tool, error: .error, ts}'
# 统计 token 总消耗
grep '"type":"token_count"' agent.jsonl | jq '.tokens' | paste -sd+ | bc
Unix 管线和 jq 的生态系统就是 JSONL 最好的处理工具——几千行、几万行、甚至百万行,都可以用这套组合拳流式处理。
四、设计取舍:什么时候该用什么?
选 JSON 当:
- API 的请求体和响应体(一次性传递完整信息)
- 配置文件(
package.json、tsconfig.json、claude_settings.json) - 需要在对象内部进行复杂嵌套和交叉引用的数据
- 一次性导出/导入的场景(数据量可控)
- 需要 schema 校验时——整体校验比逐行校验更符合预期(“要么全对,要么全错”)
选 JSONL 当:
- 流式输出——LLM 的 token-by-token 生成、Agent 的事件流
- 大规模数据集——训练数据、评测数据、爬虫结果
- 日志和审计追踪——Agent 执行过程、用户行为日志
- 增量写入——需要频繁追加的时序数据
- 管道处理——需要
grep | jq | awk组合加工的文本流 - 多进程协作——多个进程同时写同一个文件(每行写入是原子的)
一条实用原则
如果你的数据是"一张快照"——写完就不再变了——用 JSON。
如果你的数据是"一条河流"——持续产生、增量消费、可能需要随时截断——用 JSONL。
五、一个小技巧:在项目中同时使用两种格式
很多成熟项目是这样玩的:
project/
├── config.json # 配置:用 JSON,一次性加载
├── prompts/
│ └── system.jsonl # 提示词模板:用 JSONL,逐条复用
├── data/
│ ├── train.jsonl # 训练数据:50万条,JSONL
│ └── test.jsonl # 测试数据:1万条,JSONL
├── logs/
│ └── agent-2026-07-18.jsonl # 运行时日志:JSONL
└── results/
└── eval_report.json # 评估报告:用 JSON,方便阅读
规则就是:输出的消费者是谁,就选适合它的格式。
- 人类阅读的报告 → JSON(或直接 Markdown)
- 机器逐行消费 → JSONL
- 一次性加载的元数据 → JSON
更多推荐

所有评论(0)