FIELD NOTE
不是三选一:JSON、JSONB、JSONL 在 Agent 数据链路中的位置
JSON 表达一个值,jsonb 决定数据库怎么存和查,JSONL 把多条记录装进流。沿一条 Agent run 数据链路,看三种载体的落点与边界,并给出按问题判断的决策清单。
在 Agent 工程里,tool calling 的请求体、checkpoint 表、trace 文件、训练样本分别出现 JSON、JSONB、JSONL 三个名字。它们常被当成同一个问题的三个候选格式,讨论最后往往落到“该选哪个”。
三个名字回答的是三个不同的问题。JSON 回答“一个值怎么跨系统表达”;jsonb 是 PostgreSQL 的一种列类型,回答“数据库怎么保存和查询一个值”;JSONL 回答“多条记录怎么装进流或文件”。三者不在同一层,不存在三选一。
下面沿一条 Agent run 的数据链路逐个看落点,末尾给决策清单。
它们不在同一层
JSON:跨系统边界的一个值
JSON 是轻量、文本、语言无关的数据交换格式。RFC 8259 把 object 定义为无序的 name/value 集合,并建议名称唯一。语义重点在“无序”:某个实现碰巧保留键序,只算行为,不构成语义保证。跨系统传值时,接收端不能依赖键序。JSON 本身不负责存储、查询或索引,这些由承载它的系统和相邻工具决定。
JSONB:PostgreSQL 的一种列类型
jsonb 是与 json 类型并列的 PostgreSQL 列类型,不是与 JSON 并列的通用文件格式。PostgreSQL JSON 类型文档 把两者的差异说得很清楚:json 保存输入文本的精确副本,处理时重新解析;jsonb 保存分解后的二进制表示,写入时转换略慢,处理更快,且可建索引。作为代价,jsonb 不保留无意义空白、对象键顺序和重复键。
本节只交代行为差异;关系列与 JSONB 的分工,按链路里的查询需求再定。
JSONL:多条记录的行流
JSONL(JSON Lines,也称 NDJSON,惯例用 .jsonl 扩展名)的规则有三项:UTF-8 编码;每行是一个合法 JSON value;行分隔符为 \n。除行分隔符外,行与行之间没有其他耦合,契约止于逐行,不承诺文件级别的解析。它适合逐条处理、日志和进程间消息,因为可以逐行追加、逐行恢复;整个多行文件通常不是单个合法 JSON 文档。JSON Lines 不是 IETF 标准,MIME type 也未标准化。
| 轴 | JSON | PostgreSQL JSONB | JSONL |
|---|---|---|---|
| 回答的问题 | 一个值怎么跨系统表达 | 数据库怎么保存和查询 | 多条记录怎么装进流或文件 |
| 本质 | 文本序列化格式 | 列类型,二进制存储 | 行分隔记录流 |
| 关键语义/行为 | object 无序;名称建议唯一 | 不保留空白、键序、重复键 | 每行一个合法 JSON value;多行整体通常不是单个合法 JSON 文档 |
| 标准状态 | IETF RFC 8259 | PostgreSQL 文档定义 | 非 IETF 标准;MIME type 未标准化 |
| 事务与索引 | 不适用(载体属性) | 可建索引,依赖所在表的事务 | 无事务、无原生索引(文件属性) |
表里“事务与索引”一行是承载层属性:JSON 文本和 JSONL 文件本身没有事务与索引,这两样由数据库或上层系统提供。正因为三个问题不同,三种载体会在同一条链路里同时出现。
一条 Agent run 数据链路
HTTP 收到任务 → 模型 tool call → PostgreSQL checkpoint/tool_calls → trace.jsonl → eval / 训练 / 分析
下面按数据经过的顺序看每一站用哪种载体、为什么。所有示例均为构造示意。

HTTP 边界与 tool calling:JSON
跨系统传的是需要自包含的完整值。请求体、工具调用的参数和结果在进程与语言边界上交换,接收端无法假定对方的数据结构或内部存储。自包含意味着消息不依赖发送端的上下文,接收端拿文本就能解析出完整调用意图:
{
"run_id": "run_8f3c",
"tool": "search",
"arguments": {
"query": "PostgreSQL jsonb GIN index",
"limit": 5
}
}
JSON 只解决表达,不解决验证:Schema 与载体是两个问题。工具入参出参通常要配 JSON Schema、Pydantic 或 Zod,载体本身不承诺任何结构约束。接收端也不能依赖发送端碰巧保留的键序。
在线状态:普通关系列 + JSONB
中间状态要支持事务、并发、按 run 恢复与查询,这些是数据库的职责,所以进 PostgreSQL。checkpoint 行保存 run 的状态,tool_calls 行记录调用历史——每次调用的参数和结果;恢复一个 run 时按 run_id 把两者查回,事务保证状态迁移不会半途而废。建表按字段分工:run_id、tool_name、status、created_at 承担高频过滤、关联、唯一性、权限、时间和状态;arguments jsonb、result jsonb 存放结构因工具而异的参数和模型返回元数据。
CREATE TABLE tool_calls (
run_id text NOT NULL,
tool_name text NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL,
arguments jsonb NOT NULL,
result jsonb
);
CREATE INDEX idx_tool_calls_arguments
ON tool_calls USING GIN (arguments jsonb_path_ops);
查询时关系列和 JSONB 各司其职:tool_name、status 做等值比较,需要时配 B-tree 索引;arguments @> ... 这类包含表达式由 GIN 索引加速。查失败的 search 调用:
SELECT run_id, created_at, result
FROM tool_calls
WHERE tool_name = 'search'
AND status = 'failed'
AND arguments @> '{"query": "PostgreSQL jsonb GIN index"}'
ORDER BY created_at DESC;
反模式也按同一分工判断:不要把所有 Agent 状态塞进一个巨大 JSONB,稳定且高频参与关系查询的字段留在关系列,结构因工具而异的 payload 才进 JSONB。动态 payload 不等于无 schema:jsonb 列同样需要 schema_version、约束或应用层验证。这里和 HTTP 边界的验证是同一个原则,区别只在验证点从进程边界移到了写入路径。
大对象另有边界:长文本、大型 tool result、二进制内容存对象存储,数据库只保存引用和必要元数据。大 JSONB 值虽由 TOAST 负责压缩与行外存储,但不支持原地部分更新:内容一旦实际修改就整值重写,频繁更新大值会带来写放大和表膨胀;GIN 索引的维护成本随值增大而上升,读写大值还要付出取出与解压的开销。加上 JSON 不能原生表达二进制,只能 base64,二进制内容不适合放进 JSON 或 JSONB 列。
json 类型的典型适用场景是原样存取、从不查询内部字段:需要保留输入文本的精确副本(键序、重复键、空白)时,json 保存的就是原文。默认首选 jsonb;应用同样不应依赖从库里读出的键序。
过程记录:trace.jsonl
链路里的过程数据是 append-only 的事件流:tool call 开始、结束、延迟、token 数、错误。每个事件独立产出、独立消费,不需要跨事件的事务;写入后基本不再修改,也鲜少需要按内部字段在线查询。JSONL 的逐行结构正好匹配,每一行自包含,不依赖前一行:
{"run_id": "run_8f3c", "ts": "2026-08-06T13:59:01+08:00", "event": "tool_call_start", "tool": "search", "arguments": {"query": "PostgreSQL jsonb GIN index", "limit": 5}}
{"run_id": "run_8f3c", "ts": "2026-08-06T13:59:03+08:00", "event": "tool_call_end", "tool": "search", "result": {"matches": 12, "top": [{"title": "PostgreSQL: Documentation", "url": "..."}]}}
JSONL 不提供数据库级的事务和原生索引。多进程写入、事件顺序、幂等、轮转、敏感信息脱敏,都需要在文件之外设计。规模化分析时把 JSONL 转成 Parquet 或导入数据仓库再查询,它是中间载体,不是终点。
评测与训练样本也常以 JSONL 为入口,例如 Fine-tuning API 的消息序列格式——仅举例,不是所有平台的要求。
按问题判断的决策清单
以下问题依次问下去:
- 是否跨系统传一个完整值?——是——JSON,并配 Schema 验证。
- 是否需要事务,且要按内部字段高频过滤、关联、唯一性、权限、时间、状态?——是——稳定字段放关系列,确实动态的 payload 用 JSONB;不要把所有状态塞进一个巨大 JSONB,那等于放弃类型约束、唯一约束和排序索引。链路里的 checkpoint 表与 tool_calls 表就是这种分工。
- 是否主要逐条追加、按行扫描与恢复?——是——JSONL。
- 对象是否很大(长文本、大型 tool result、二进制)?——是——对象存储,数据库只保存引用和必要元数据。
一条工程规则
跨系统边界用 JSON;在线状态用关系列加 JSONB;过程记录用 JSONL。
DISCUSSION
评论
正在加载评论…