ByteNoteByteNote
数据库怎么设计:10 张表与 JSON 扩展列
字

字节笔记本

2026年10月24日 · 约 11 分钟读完

数据库怎么设计:10 张表与 JSON 扩展列

API中转
¥120

Agent SDK 概览

本文是《从 0 写 Agent》系列第 35 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「数据库设计:10 张表与 JSON 扩展列」。

agents-server 用 SQLite 存所有配置和数据,Agent 定义、会话、MCP server、沙箱配置、guardrail、trace。10 张表,怎么设计才灵活可扩展?

这一章拆 agents-server 的数据库设计,核心是"JSON 列承载扩展性 + 敏感字段隔离 + 泛型 CrudStore"。


10 张表

agents-server 的 schema(store/schema.go)有 10 张表:

表存什么
sessions会话(id/name/pinned/agent_config_id)
messages会话消息(含原始 SDK item JSON)
agent_configsAgent 定义(最肥的表)
mcp_serversMCP server 配置
memories持久记忆
settingsKV 配置
provider_routes模型路由(前缀→api_key/base_url)
sandbox_configs沙箱配置
trace_eventstrace/span
guardrails护栏定义

Claude Code

核心设计一:JSON 列承载扩展性

这是最重要的设计。很多表的"配置"字段是 JSON 列(json.RawMessage),不是展开的关系字段:

go
// store/models.go(概念)
type McpServerConfig struct {
    ID            string          `bun:"id"`
    TransportType string          `bun:"transport_type"`  // stdio / streamable_http
    Config        json.RawMessage `bun:"config"`          // ← JSON 列!
}

type SandboxConfig struct {
    Type    string          `bun:"type"`    // local / docker / ssh
    Config  json.RawMessage `bun:"config"`  // ← JSON 列!
}

为什么用 JSON 列存配置?

想象 MCP server 有两种 transport(stdio / streamable_http),每种配置字段不同:

  • stdio:{command, args, env}
  • streamable_http:{url, oauth_config}

如果用关系字段,要么加一堆可空列(混乱),要么拆两张表(复杂)。JSON 列让"不同 transport 的不同配置"都存在一个字段里,新增 transport 不用改表结构。

go
// bridge 层根据 TransportType 解析 Config
switch cfg.TransportType {
case "stdio":
    var c StdioConfig
    json.Unmarshal(cfg.Config, &c)
case "streamable_http":
    var c HTTPConfig
    json.Unmarshal(cfg.Config, &c)
}

新增一种 transport?加一个 case + 新 struct,表结构不动。 这是"schema 稳定 + 格式灵活"的经典权衡。


核心设计二:敏感字段隔离

有些字段是敏感的,ChatGPT token、MCP OAuth token。agents-server 把它们独立成列,CRUD 时特殊保护:

go
// store/models.go
type AgentConfig struct {
    // ...
    ChatGPTToken string `bun:"chatgpt_token" json:"-"`  // JSON 输出忽略
}

json:"-",这个字段不参与 JSON 序列化,不会通过 API 返回给前端。

更重要的,Update 时不覆盖 token(agent_config_store.go:22):

go
// 重写 Update,排除 chatgpt_token 列
func (s *AgentConfigStore) Update(ctx, cfg) error {
    _, err := s.db.NewUpdate().Model(cfg).
        ExcludeColumn("chatgpt_token").  // 不更新 token 列
        WherePK().Exec(ctx)
}

为什么? 用户编辑 Agent 配置(改模型名、改指令)时,前端传回的表单不含 token(因为 json:"-")。如果 Update 不排除 token 列,会用空值覆盖掉已存的 token。ExcludeColumn 保证编辑配置不会擦掉 token。


核心设计三:泛型 CrudStore

13 个 handler 大部分是 CRUD(增删改查)。为避免每个资源写一遍 CRUD,agents-server 用泛型 CrudStore(store/crud_store.go:45):

go
// 概念示意
type CrudStore[T any] struct {
    db *bun.DB
}

func (s *CrudStore[T]) Create(ctx, entity *T) error { /* 通用 insert */ }
func (s *CrudStore[T]) List(ctx) ([]T, error)       { /* 通用 select all */ }
func (s *CrudStore[T]) Get(ctx, id string) (*T, error) { /* 通用 select by id */ }
func (s *CrudStore[T]) Update(ctx, entity *T) error { /* 通用 update */ }
func (s *CrudStore[T]) Delete(ctx, id string) error { /* 通用 delete */ }

所有"string 主键 + created_at/updated_at"的实体共享这五个方法。 各资源 store 只需 embed 它,再加自己的特化查询:

go
type SessionStore struct {
    *CrudStore[Session]  // 通用 CRUD
}

// 特化查询(session 独有的)
func (s *SessionStore) ListPinned(ctx) ([]Session, error) {
    // 只查 pinned 的
}

Go 泛型让 CRUD 不重复。 这是泛型在业务代码里的实用场景,不是炫技,是真正减少重复。


索引设计

两条索引(schema.go:31,39):

go
// messages 的复合索引
Index("idx_messages_session_id").Column("session_id", "id")

// trace_events 的复合索引
Index("idx_trace_events_session_run").Column("session_id", "run_id")

和第 10 章 SQLSession 的索引设计同理,给查询模式建索引。messages 的查询是"按 session 取消息按 id 排序",trace_events 是"按 session+run 取 trace"。


WAL 模式

go
// DSN: file:data.db?cache=shared&_journal_mode=WAL

WAL(Write-Ahead Logging)模式提升并发读,写操作不阻塞读。对 Web 应用(多用户同时读 + 偶尔写)很重要。

注意 SetMaxOpenConns(1)(db.go:19),因为 SQLite 写是串行的(单写者),限制连接数避免锁竞争。


小结

读完这一章,你应该理解 agents-server 数据库设计的三个核心:

  1. JSON 列承载扩展性,不同 transport/backend 的配置存一个 JSON 字段,新增类型不改表
  2. 敏感字段隔离,token 独立列 + json:"-" + Update ExcludeColumn,防泄露防覆盖
  3. 泛型 CrudStore,CRUD 不重复,各 store embed + 特化查询

这是"schema 稳定 + 格式灵活 + 代码不重复"的数据库设计范式。 适合配置驱动的应用。


下一篇

第 36 章,WebSocket 流式:三层架构。agents-server 怎么把 SDK 的流式事件推给前端?连接层(鉴权+写锁)→ 分发层(HITL pending 状态机)→ 运行层(SDK 事件→protocol 事件)。


本文是《从 0 写 Agent》系列第 35 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

相关文章

分享: