Skip to content

05 - 数据库设计 ​

文档定位 ​

本文档定义「基于开源模块化 Agent 的工业预测性运维系统」的数据持久化层设计,核心是 business-db / ai-db 双库分离 这一安全架构的落地形态。文档给出两张库的完整 DDL、访问权限矩阵、隔离方案演进路线、数据流写入路径与 RAG 知识库构建流程,是 packages/business-api、packages/mcp-gateway、packages/security-plugin 三个后端包的建库依据,也是答辩中「安全管控插件层为什么可信」的数据侧证据。

贯穿全文的唯一硬约束:Agent 既不能直连 business-db,也不能直连 MQTT 服务;Agent 对 business-db 无账户、零权限,所有外部资源访问 100% 经过 MCP 安全网关。

关联文档 ​

  • docs/01-系统总体方案.md —— 系统分层与组件边界
  • docs/02-安全管控插件层设计.md —— 策略引擎、审计钩子(本文档 audit_log 是其落盘层)
  • docs/04-MCP安全网关设计.md —— 工具清单与字段白名单(本文档字段过滤规则的来源)
  • docs/06-Web前端设计.md —— 前端消费本文档定义的 REST 接口
  • docs/adr/ —— 双库分离、SQLite→PostgreSQL 等决策记录
  • packages/business-api/src/、packages/mcp-gateway/src/tools/ —— DDL 与仓储层实现位置

1. 设计目标与隔离原则 ​

1.1 为什么双库分离 ​

单一数据库在 AI 场景下有三个无法回避的问题:提示注入可诱导 Agent 的数据库工具拼出越权 SQL;AI 模块(向量检索、embedding 计算)的内存与 CPU 抖动会拖垮业务查询;AI 侧数据(会话记忆、知识切片)生命周期与业务数据完全不同(天级 vs 年级)。因此拆成两库,收益明确:

收益说明对应威胁场景
边界隔离两库账号体系完全独立,Agent 环境变量里不存在 business-db 连接串,提示注入即使成功也无法拼出可用的凭据提示注入诱导 Agent 越权读取运维工单、用户表
故障域隔离ai-db 崩溃/重建索引/重灌向量库不影响业务库读写;业务库连接池打满也不会阻塞 Agent 会话AI 模块内存泄漏、向量库重建导致进程重启
审计清晰所有跨库访问都必须穿过 MCP 网关,天然形成单一审计点,audit_log 完整可追溯无法解释的越权读取行为难以归因
合规友好业务数据与 AI 派生数据可分别设定备份、保留、销毁策略数据保留期限与删除权要求

1.2 双库职责划分 ​

维度business-db(业务库)ai-db(AI 专用库)
内容传感器时序数据、设备档案、运维工单、用户账号、RBAC、审计日志RAG 知识库文档切片、向量索引、Agent 会话短期记忆
推荐引擎PostgreSQL 16(原型期 SQLite 3.45+)Chroma 0.4.x / Qdrant 1.x;会话记忆用同实例 SQLite 或 PostgreSQL
写入方MQTT 接入层、business-apiRAG 构建脚本、Agent 会话管理模块
读取方business-api(人)、mcp-gateway(Agent 侧,仅经工具暴露)mcp-gateway(唯一读取方,只读检索)
生命周期长期保留,按年归档知识切片随文档版本重建;会话记忆 7 天过期

1.3 访问权限矩阵 ​

权限标记说明:RW 读写、R 只读、W 只写、— 无任何权限、间接R 不可直连、只能经 MCP 网关工具获取被字段白名单过滤后的结果。

角色 / 主体business-dbai-dbMQTT BrokerMCP 网关
Agent 进程(OpenClaw)— 无账户 / 零权限— 无账户 / 零权限(经网关 kb.search 间接获取)— 禁止连接R(仅工具调用,受策略引擎约束)
MCP 安全网关RW(读:白名单视图;写:audit_log / sensor_data / agent_report)R(只读检索,含会话记忆读写代理)订阅 + 受限发布(唯一订阅方)—
business-api 服务RW(业务账号)——R(调用网关做 Agent 触发)
RAG 构建脚本(离线)—RW——
工程师(前端用户)间接R(经 business-api 的 JWT 鉴权接口)间接R(经 business-api 的知识库管理接口)—间接R
系统管理员间接R + 经后台迁移脚本间接R—间接R

关于 MQTT 接入:本系统不设独立的 MQTT 接入服务。网关自身是 Broker 的唯一订阅方,订阅 iot/+/telemetry 并把遥测写入 business-db.sensor_data(见 ADR-0005)。这样"Agent 侧对外数据通路"才真正收敛为网关一个进程。

关于会话记忆:Agent 的会话短期记忆存在 ai-db,但读写同样经网关代理(网关提供会话记忆的读写接口,Agent 侧不持有 ai-db 连接)。这是 ADR-0001 中"Agent 对 ai-db 也是零权限"的直接推论——如果让 Agent 自己连 ai-db 写会话记忆,就会在"所有访问经网关"这条不变量上开出第一个例外,例外一多边界就失效。

关键设计取舍:MCP 网关对 business-db 使用的是独立的只读数据库账号(gw_reader),且只被授予 SELECT 权限、且只在视图 v_sensor_public、v_device_public 等白名单视图上有权限,而非基表。这样即使网关侧存在 SQL 注入,攻击面也被限制在视图列集合内。写入路径(审计、遥测、报告)另用独立的写账号,与读账号分离。


2. business-db 详细设计 ​

2.1 建表总览 ​

表名用途预估行数(原型期 / 落地期)
user_account用户账号10 / 500
role角色5 / 30
permission权限点40 / 200
user_role用户-角色关联12 / 800
role_permission角色-权限关联80 / 1500
device设备基础档案6 / 2000
sensor_data温度/振动时序数据5 万 / 10 亿
device_alert设备告警事件200 / 500 万
work_order运维工单30 / 20 万
agent_reportAgent 分析报告落库100 / 50 万
audit_log安全审计日志5 千 / 2 亿
schema_migration迁移版本记录10 / 200

2.2 用户与 RBAC 四表 ​

sql
-- ---------- 用户账号 ----------
CREATE TABLE user_account (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  username      TEXT    NOT NULL UNIQUE,
  display_name  TEXT    NOT NULL,
  password_hash TEXT    NOT NULL,              -- argon2id 优先;bcrypt cost>=12 为备选
  password_algo TEXT    NOT NULL DEFAULT 'argon2id',
  email         TEXT,
  status        TEXT    NOT NULL DEFAULT 'ACTIVE'
                        CHECK (status IN ('ACTIVE','LOCKED','DISABLED')),
  failed_count  INTEGER NOT NULL DEFAULT 0,     -- 连续登录失败次数,达阈值锁定
  locked_until  TEXT,
  last_login_at TEXT,
  created_at    TEXT    NOT NULL DEFAULT (datetime('now')),
  updated_at    TEXT    NOT NULL DEFAULT (datetime('now'))
);
字段类型约束注释
idINTEGERPK, AUTOINCREMENT主键
usernameTEXTNOT NULL, UNIQUE登录名
password_hashTEXTNOT NULL密码哈希,禁止存明文/可逆加密
password_algoTEXTNOT NULLargon2id 或 bcrypt,为将来换算法留升级位
statusTEXTCHECK账号状态,锁定态由 failed_count 触发
failed_count / locked_untilINTEGER / TEXT—暴力破解防护

取舍:选 argon2id 是因为它对 GPU 破解的抵抗力优于 bcrypt;原型期若 Node 侧 argon2 原生模块编译困难,降级为 bcrypt(cost=12),字段 password_algo 保证两种哈希可共存、可渐进迁移。

sql
-- ---------- RBAC 四表 ----------
CREATE TABLE role (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  code        TEXT NOT NULL UNIQUE,      -- ADMIN / ENGINEER / VIEWER / AGENT_SVC
  name        TEXT NOT NULL,
  description TEXT,
  is_builtin  INTEGER NOT NULL DEFAULT 0, -- 内置角色不可删除
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE permission (
  id       INTEGER PRIMARY KEY AUTOINCREMENT,
  code     TEXT NOT NULL UNIQUE,   -- 形如 device:read / workorder:write / audit:read
  name     TEXT NOT NULL,
  resource TEXT NOT NULL,          -- device / sensor_data / work_order / audit_log / kb
  action   TEXT NOT NULL           -- read / write / delete / export
);

CREATE TABLE user_role (
  user_id INTEGER NOT NULL REFERENCES user_account(id) ON DELETE CASCADE,
  role_id INTEGER NOT NULL REFERENCES role(id) ON DELETE CASCADE,
  granted_at TEXT NOT NULL DEFAULT (datetime('now')),
  PRIMARY KEY (user_id, role_id)
);

CREATE TABLE role_permission (
  role_id       INTEGER NOT NULL REFERENCES role(id) ON DELETE CASCADE,
  permission_id INTEGER NOT NULL REFERENCES permission(id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, permission_id)
);

CREATE INDEX idx_role_permission_role ON role_permission(role_id);
表关键字段说明
rolecode角色唯一标识,前端按钮级权限 v-permission 依据 permission.code
permissioncode / resource / action权限点粒度到「资源:动作」,避免粗粒度导致授权过度
user_role(user_id, role_id) 复合主键一人可多角色,取并集
role_permission(role_id, permission_id) 复合主键角色-权限多对多

注意:Agent 在本表中没有任何账号记录。 AGENT_SVC 角色仅为将来可能的服务账号占位,原型期不创建、不授予,Agent 身份仅存在于 MCP 网关的会话上下文中。

2.3 设备档案与传感器时序数据 ​

sql
CREATE TABLE device (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  device_code   TEXT NOT NULL UNIQUE,       -- 业务主键,如 DEV-003;MQTT 主题与工具入参均用它
  name          TEXT NOT NULL,
  model         TEXT,                       -- 设备型号,用于知识库元数据匹配
  location      TEXT,
  firmware_ver  TEXT,
  mqtt_client_id TEXT UNIQUE,               -- 与网关的 MQTT 订阅校验
  status        TEXT NOT NULL DEFAULT 'OFFLINE'
                CHECK (status IN ('ONLINE','OFFLINE','MAINTENANCE','FAULT')),
  last_seen_at  TEXT,
  -- ── 以下为「业务敏感字段」:Agent 通过 db.query_device 无法获得(字段白名单剥离)──
  serial_no       TEXT,                     -- 出厂序列号:可用于设备追踪
  asset_no        TEXT,                     -- 资产编号:与财务台账关联
  line_code       TEXT,                     -- 产线编码:可用于推断产线布局
  calib_k         REAL,                     -- 传感器校准系数:可用于反推标定方案
  owner_dept      TEXT,                     -- 归属部门
  purchase_price  REAL,                     -- 采购价
  supplier_contact TEXT,                    -- 供应商联系人
  -- ────────────────────────────────────────────────────────────────
  commissioned_at TEXT,                     -- 投运日期(Agent 可见,用于判断设备寿命阶段)
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE TABLE sensor_data (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  device_id  INTEGER NOT NULL REFERENCES device(id) ON DELETE CASCADE,
  metric     TEXT    NOT NULL CHECK (metric IN ('temperature','vibration')),
  value      REAL    NOT NULL,
  unit       TEXT    NOT NULL,              -- ℃ / mm·s⁻¹
  quality    INTEGER NOT NULL DEFAULT 1,    -- 1=有效 0=异常值(越界或丢包补值)
  ts         TEXT    NOT NULL,              -- ISO8601 UTC,采样时刻
  received_at TEXT   NOT NULL DEFAULT (datetime('now')),
  trace_id   TEXT                           -- 链路追踪,与 audit_log 对齐
);

-- 核心索引:按设备 + 时间范围查询曲线
CREATE INDEX idx_sensor_device_ts ON sensor_data(device_id, ts DESC);
CREATE INDEX idx_sensor_metric_ts ON sensor_data(metric, ts DESC);
CREATE UNIQUE INDEX uq_sensor_dedup ON sensor_data(device_id, metric, ts);
字段类型约束注释
metricTEXTCHECK 枚举温度 / 振动两类,新增指标需改 CHECK 或迁移为字典表
valueREALNOT NULL采样值
qualityINTEGERDEFAULT 1用于过滤丢包补值,避免污染曲线与阈值判断
tsTEXTNOT NULL采样时刻,received_at 为入库时刻,两者分离便于排查链路延迟
trace_idTEXT可空与 audit_log.trace_id 同源,可端到端追溯

索引与容量策略

项目原型期方案落地期方案
主索引(device_id, ts DESC) 复合索引同上,另加 (metric, ts) 支撑跨设备聚合
写入量估算6 设备 × 2 指标 × 1Hz ≈ 103 万行/天(原型期实测降为 0.2Hz,约 20 万行/天)2000 设备 × 1Hz ≈ 1.7 亿行/天
分区不分区,SQLite 单文件 + 定期归档PostgreSQL 按月 PARTITION BY RANGE (ts),旧分区 detach 后转冷存
降采样原型期不做,前端直接查原始点引入连续聚合/物化视图:1min 均值表 sensor_data_1m 供长周期曲线,原始表仅保留 90 天
去重uq_sensor_dedup 唯一索引 + INSERT OR IGNORE 保证 MQTT QoS1 重投不重复入库PostgreSQL 侧用 ON CONFLICT DO NOTHING

取舍说明:原型期刻意不引入 TimescaleDB,因为毕设验证重点是安全管控插件层而非时序引擎性能;但索引设计与分区方案预留了迁移路径,答辩时可说明「当前实现满足原型验证,落地期替换为 TimescaleDB hypertable」。

存储形态 vs 返回形态(重要):sensor_data 采用窄表(metric / value 一行一指标),便于新增指标与跨指标聚合。而 Agent 侧工具 mqtt.read_sensor 返回的是宽表形态 {temp, vibration, time}——由网关在返回前做透视(pivot)与字段重命名。

这个转换正是网关"加工而不只是转发"的体现:数据库里存什么形状是存储设计问题,Agent 看到什么形状是接口契约与最小权限问题,两者解耦。同时它也解释了为什么 Agent 不能直连数据库——直连拿到的是一堆 metric/value 行,还得自己写透视逻辑,而透视逻辑一旦在 Agent 侧,Agent 就能顺带查询其他 metric 甚至其他表。

2.4 告警与工单 ​

sql
CREATE TABLE device_alert (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  device_id   INTEGER NOT NULL REFERENCES device(id) ON DELETE CASCADE,
  metric      TEXT NOT NULL,
  level       TEXT NOT NULL CHECK (level IN ('INFO','WARN','CRITICAL')),
  rule_code   TEXT,                      -- 触发规则标识,如 TEMP_HIGH_80
  observed    REAL NOT NULL,
  threshold   REAL NOT NULL,
  status      TEXT NOT NULL DEFAULT 'OPEN'
              CHECK (status IN ('OPEN','ACKED','CLOSED')),
  occurred_at TEXT NOT NULL,
  acked_by    INTEGER REFERENCES user_account(id),
  created_at  TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_alert_device_time ON device_alert(device_id, occurred_at DESC);
CREATE INDEX idx_alert_status ON device_alert(status);

CREATE TABLE work_order (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  order_no      TEXT NOT NULL UNIQUE,        -- WO-20260915-0001
  device_id     INTEGER NOT NULL REFERENCES device(id),
  alert_id      INTEGER REFERENCES device_alert(id),
  report_id     INTEGER REFERENCES agent_report(id),  -- Agent 报告转工单时回填
  title         TEXT NOT NULL,
  description   TEXT,
  priority      TEXT NOT NULL DEFAULT 'MEDIUM'
                CHECK (priority IN ('LOW','MEDIUM','HIGH','URGENT')),
  status        TEXT NOT NULL DEFAULT 'CREATED'
                CHECK (status IN ('CREATED','ASSIGNED','PROCESSING','RESOLVED','VERIFIED','CLOSED','CANCELLED')),
  assignee_id   INTEGER REFERENCES user_account(id),
  created_by    INTEGER NOT NULL REFERENCES user_account(id),
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  assigned_at   TEXT,
  resolved_at   TEXT,
  closed_at     TEXT,
  resolution    TEXT
);

CREATE INDEX idx_wo_status ON work_order(status, priority);
CREATE INDEX idx_wo_device ON work_order(device_id);

工单状态机(status 字段的合法迁移,由 business-api 服务层校验,非法迁移返回 409):

CREATED ──assign──> ASSIGNED ──start──> PROCESSING ──resolve──> RESOLVED
   │                    │                    │                      │
   └────cancel──────────┴────cancel──────────┘                  verify
                                                                   │
                                                              VERIFIED
                                                                   │
                                                                CLOSE
                                                                   ↓
                                                                CLOSED

2.5 Agent 分析报告 ​

sql
CREATE TABLE agent_report (
  id             INTEGER PRIMARY KEY AUTOINCREMENT,
  report_no      TEXT NOT NULL UNIQUE,
  device_id      INTEGER NOT NULL REFERENCES device(id),
  session_id     TEXT NOT NULL,              -- 关联 Agent 会话
  trace_id       TEXT NOT NULL,              -- 关联 audit_log,可回放推理链路
  title          TEXT NOT NULL,
  conclusion     TEXT NOT NULL,              -- Agent 最终结论
  confidence     REAL,                       -- 0~1,模型自评置信度
  severity       TEXT CHECK (severity IN ('LOW','MEDIUM','HIGH')),
  reasoning_chain TEXT,                      -- JSON:ReAct 步骤 [{thought,action,observation}]
  citations      TEXT,                       -- JSON:引用知识库切片 id 列表
  model_name     TEXT,                       -- 生成该报告的模型标识
  status         TEXT NOT NULL DEFAULT 'DRAFT'
                 CHECK (status IN ('DRAFT','PUBLISHED','CONVERTED','ARCHIVED')),
  created_at     TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_report_device_time ON agent_report(device_id, created_at DESC);
CREATE INDEX idx_report_trace ON agent_report(trace_id);
字段说明
reasoning_chain存 ReAct 循环的 thought/action/observation 序列,前端报告页据此渲染推理链路
citations知识库切片 id,前端可点击跳转知识库管理页
statusCONVERTED 表示已转为工单,防止同一报告重复建单

安全提示:agent_report 由 business-api 写入(网关把结果回传给业务服务落库),Agent 自身没有该表的写权限,避免 Agent 自造报告污染运维决策。

2.6 安全审计日志(核心表) ​

sql
CREATE TABLE audit_log (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  trace_id        TEXT    NOT NULL,        -- 全链路追踪 id,前端展示入口
  span_id         TEXT,                    -- 一次会话内多工具调用的序号
  layer           TEXT    NOT NULL CHECK (layer IN ('PLUGIN','GATEWAY')),
                                           -- PLUGIN = 安全管控插件层判定;GATEWAY = MCP 网关执行
  caller_type     TEXT    NOT NULL CHECK (caller_type IN ('AGENT','USER','SYSTEM')),
  caller_id       TEXT,                    -- Agent 时为 agent_id,用户时为 user_account.id
  agent_id        TEXT,                    -- 如 ops-analyst
  session_id      TEXT,
  tool_name       TEXT    NOT NULL,        -- 与 docs/04 的工具清单一一对应
  tool_args_digest TEXT,                   -- 入参摘要(脱敏后 JSON,超长截断)
  result_fields   TEXT,                    -- 实际返回给 Agent 的字段白名单(逗号分隔)
  redacted_fields TEXT,                    -- 被裁剪剥离的字段名(逗号分隔),证明最小权限生效
  result_digest   TEXT,                    -- 出参摘要(行数 + 哈希,不落原始数据)
  row_count       INTEGER,
  policy_result   TEXT    NOT NULL CHECK (policy_result IN ('ALLOW','DENY','REWRITE','MASK','RATE_LIMIT')),
  policy_rule_id  TEXT,                    -- 命中的策略规则标识,如 R3 / G4
  deny_reason     TEXT,
  retry_after_ms  INTEGER,                 -- 限流时提示模型等待时长
  duration_ms     INTEGER,
  client_ip       TEXT,
  ts              TEXT    NOT NULL,        -- 事件时间
  created_at      TEXT    NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX idx_audit_trace ON audit_log(trace_id);
CREATE INDEX idx_audit_ts ON audit_log(ts DESC);
CREATE INDEX idx_audit_tool_ts ON audit_log(tool_name, ts DESC);
CREATE INDEX idx_audit_session ON audit_log(session_id);
CREATE INDEX idx_audit_policy ON audit_log(policy_result, ts DESC);
CREATE INDEX idx_audit_layer ON audit_log(layer, ts DESC);
字段类型约束注释
trace_idTEXTNOT NULL贯穿 MQTT→网关→Agent→前端;同一 traceId 下会有 PLUGIN 与 GATEWAY 两条记录,前端审计页按它串联展示
layerTEXTCHECKPLUGIN = 安全管控插件层的策略判定;GATEWAY = MCP 网关的独立复核与执行。纵深防御的两层证据链
caller_typeTEXTCHECK区分 Agent 调用与人类调用,是「谁在访问」的第一判据
agent_id / session_idTEXT可空Agent 身份与会话,支撑「同一会话是否反复试探」的分析
tool_nameTEXTNOT NULLMCP 工具名,与 docs/04-MCP安全网关设计.md 工具清单一一对应
tool_args_digestTEXT可空只存摘要不存全量入参,防止审计表本身成为敏感数据泄露源
result_fieldsTEXT可空记录字段白名单实际生效结果,证明最小必要原则被落实
redacted_fieldsTEXT可空被剥离的字段名。这是**「裁剪确实发生了」的直接证据**,答辩时可与原始行对照
result_digestTEXT可空行数 + 内容哈希,可验证但不可反推业务数据
policy_resultTEXTCHECKALLOW / DENY / REWRITE(参数被收窄而非拒绝)/ MASK(字段脱敏)/ RATE_LIMIT(限流拦截)
policy_rule_idTEXT可空命中规则编号(插件层 R3、网关 G4),便于逐条演示策略
duration_msINTEGER可空网关侧耗时,用于性能与异常检测

设计取舍:审计表刻意不存原始业务数据。备选方案是存完整 request/response 便于复盘,但那样审计库就成了 business-db 的副本,一旦被读取则边界隔离失效。折中为「摘要 + 哈希 + 字段白名单记录」,兼顾可追溯与最小暴露。

保留策略:原型期不清理;落地期按月分区,热数据保留 6 个月在线,之后归档到对象存储并只保留 trace_id + policy_result + ts 索引行。

2.7 迁移版本表 ​

sql
CREATE TABLE schema_migration (
  version     TEXT PRIMARY KEY,          -- 20260915_001_init
  name        TEXT NOT NULL,
  applied_at  TEXT NOT NULL DEFAULT (datetime('now')),
  checksum    TEXT NOT NULL
);

3. ai-db 详细设计 ​

3.1 向量集合设计 ​

项目设计值说明
集合名kb_fault_knowledge故障手册 + 检修案例统一集合,用元数据区分
向量维度768原型期用本地 embedding 模型(如 bge-base-zh-v1.5,768 维);落地期若换 1536 维模型需重建集合
距离度量cosine中文语义检索常用;Qdrant 用 Cosine,Chroma 用 hnsw:space=cosine
索引HNSW (M=16, ef_construct=200)原型期数据量小也可用暴力检索,但保留 HNSW 参数便于演示
chunk 大小512 token,overlap 64 token(约 12.5%)工业手册段落较长,512 保证语义完整;64 overlap 防止表格/步骤被截断
每文档切片数上限 2000 片,超出告警防止单文档切片爆炸拖慢重建
存储引擎Chroma 0.4.x(原型期) / Qdrant 1.x(落地期)见 ADR;Chroma 部署简单,Qdrant 支持分布式与更细的 payload 过滤

3.2 切片元数据(payload)字段 ​

字段类型是否过滤索引说明
chunk_idstring是全局唯一,形如 doc12-c035,用于 citations 引用
doc_idstring是来源文档 id,关联 kb_document 表
doc_titlestring否来源文档标题,检索结果直接展示
doc_typestring是manual(故障手册)/ case(检修案例)/ sop(作业规范)
device_modelstring是适用设备型号,检索时按当前设备型号过滤,避免跨型号误引
chapterstring否章节标题,用于结果定位
pageint否页码,前端可提示「手册 P.42」
section_pathstring否形如 第3章/3.2 振动异常,层级面包屑
versionstring是文档版本,重建时按版本批量删除旧切片
effective_datestring否文档生效日期
content_hashstring否切片内容哈希,用于增量重建去重
textstring否原始切片文本(Chroma 的 documents 字段)

3.3 知识库文档登记表(ai-db 内 SQL) ​

向量库只存切片,文档级元信息用一张关系表管理,便于「按文档重建/删除」。

sql
CREATE TABLE kb_document (
  id            INTEGER PRIMARY KEY AUTOINCREMENT,
  doc_code      TEXT NOT NULL UNIQUE,
  title         TEXT NOT NULL,
  doc_type      TEXT NOT NULL CHECK (doc_type IN ('manual','case','sop')),
  device_model  TEXT,
  version       TEXT NOT NULL DEFAULT 'v1',
  source_path   TEXT NOT NULL,          -- 原始文件路径,如 docs/kb/xxx.pdf
  file_hash     TEXT NOT NULL,          -- 文件级哈希,判断是否需要重建
  chunk_count   INTEGER NOT NULL DEFAULT 0,
  embed_model   TEXT,                   -- 切片时所用 embedding 模型,换模型须全量重建
  status        TEXT NOT NULL DEFAULT 'PENDING'
                CHECK (status IN ('PENDING','INDEXING','READY','FAILED')),
  indexed_at    TEXT,
  created_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

3.4 Agent 会话短期记忆 ​

sql
CREATE TABLE agent_session (
  session_id    TEXT PRIMARY KEY,        -- UUID,与 audit_log.session_id 同源
  agent_id      TEXT NOT NULL,
  user_id       INTEGER,                 -- 触发该会话的工程师(可空,定时巡检时为 NULL)
  device_id     INTEGER,                 -- 会话聚焦的设备
  title         TEXT,
  summary       TEXT,                    -- 滚动摘要,压缩早期轮次
  turn_count    INTEGER NOT NULL DEFAULT 0,
  token_used    INTEGER NOT NULL DEFAULT 0,
  status        TEXT NOT NULL DEFAULT 'ACTIVE'
                CHECK (status IN ('ACTIVE','CLOSED','EXPIRED')),
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  last_active_at TEXT NOT NULL DEFAULT (datetime('now')),
  expires_at    TEXT NOT NULL            -- 默认 last_active_at + 7 天
);

CREATE TABLE agent_memory_turn (
  id          INTEGER PRIMARY KEY AUTOINCREMENT,
  session_id  TEXT NOT NULL REFERENCES agent_session(session_id) ON DELETE CASCADE,
  turn        INTEGER NOT NULL,          -- 会话内轮次,从 1 递增
  role        TEXT NOT NULL CHECK (role IN ('system','user','assistant','tool')),
  content     TEXT NOT NULL,             -- 该轮消息内容(tool 轮为工具结果摘要)
  tool_name   TEXT,                      -- role=tool 时的工具名,与 audit_log 对齐
  trace_id    TEXT,                      -- 关联审计记录
  token_count INTEGER,
  created_at  TEXT NOT NULL DEFAULT (datetime('now')),
  UNIQUE (session_id, turn, role)
);

CREATE INDEX idx_memory_session_turn ON agent_memory_turn(session_id, turn);
CREATE INDEX idx_session_expire ON agent_session(expires_at);
策略设计理由
过期expires_at = 最后活动 + 7 天,定时任务软删为 EXPIRED 后 3 天物理删除短期记忆只服务于「同一故障排查连续对话」,长期保留无价值且增加泄露面
压缩turn_count > 20 或 token_used > 8k 时,把前 10 轮压成 agent_session.summary控制上下文长度,避免超出模型窗口
工具结果role='tool' 的 content 只存字段白名单过滤后的摘要,不存网关返回的原始大对象记忆库同样受最小必要原则约束
隔离会话记忆只属于 ai-db,business-db 不含任何会话内容Agent 内部推理过程不污染业务数据

4. 双库物理 / 逻辑隔离方案对比 ​

4.1 阶段演进 ​

阶段business-dbai-db隔离强度适用场景
原型期(本毕设)独立 SQLite 文件 data/business.db独立 SQLite data/ai-memory.db + Chroma 持久化目录 data/chroma/逻辑隔离:文件独立、账号独立、进程独立、无跨库 JOIN单机演示、答辩验证
落地期PostgreSQL 实例 A(独立主机/容器)Qdrant 实例 B + PostgreSQL 实例 C(会话记忆)物理隔离:不同主机、不同网络段、独立凭据与备份链路生产环境

原型期逻辑隔离的四条硬性要求(缺一不可):

  1. 文件系统分离:两库文件位于不同目录,data/business/ 与 data/ai/,权限位不同。
  2. 账号分离:即便都是 SQLite 无账号概念,也用不同 OS 进程 + 不同工作目录 + 不同文件权限模拟;若切换到 PostgreSQL 则必须建两套账号。
  3. 进程分离:business-api 进程只持有 business-db 句柄;Agent 进程只持有 ai-db 句柄。二者不共享任何 DB 连接对象。
  4. 无跨库查询:禁止在两库之间做 JOIN 或外键。需要跨域数据时,唯一通路是 MCP 网关工具调用。

4.2 连接串与账号分离示例 ​

.env.business-api(业务接口服务,仅此进程可见):

bash
# 原型期
BUSINESS_DB_DIALECT=sqlite
BUSINESS_DB_FILE=./data/business/business.db

# 落地期(PostgreSQL,业务账号具备读写权限)
# BUSINESS_DB_URL=postgresql://biz_app:${BIZ_APP_PWD}@10.0.1.10:5432/business
# BUSINESS_DB_POOL_MAX=20

.env.mcp-gateway(网关,只有业务库只读账号):

bash
# 原型期:只读打开,禁止写
GATEWAY_DB_DIALECT=sqlite
GATEWAY_DB_FILE=./data/business/business.db
GATEWAY_DB_READONLY=true

# 落地期:只读账号 + 只授视图权限
# GATEWAY_DB_URL=postgresql://gw_reader:${GW_READER_PWD}@10.0.1.10:5432/business
# GATEWAY_DB_POOL_MAX=5

.env.agent(Agent 进程,注意这里没有任何 business-db 变量):

bash
# Agent 只知道自己能连 ai-db,以及 MCP 网关地址
AI_DB_DIALECT=sqlite
AI_DB_FILE=./data/ai/ai-memory.db
VECTOR_STORE=chroma
CHROMA_DIR=./data/ai/chroma

GATEWAY_MCP_ENDPOINT=http://127.0.0.1:7801
AGENT_ID=ops-analyst

# 不存在 BUSINESS_DB_URL / AI_DB_URL / MQTT_BROKER_URL / AGENT_TOKEN_SECRET

答辩要点:Agent 进程的环境变量清单里根本不存在 business-db 的连接串与 MQTT 地址。这不是「靠代码自觉不连」,而是「凭据层面就没有可连的东西」——即便提示注入完全控制 Agent 的推理输出,也无法构造出可用的数据库连接。这是边界隔离的最终兜底。

.env.mcp-gateway(MCP 安全网关,唯一持有全部数据源凭据的进程):

bash
# 读账号(只授白名单视图 SELECT)
GATEWAY_DB_READ_URL=file:./data/business/business.db
# 写账号(仅 audit_log / sensor_data / agent_report)
GATEWAY_DB_WRITE_URL=file:./data/business/business.db
# ai-db(只读检索 + 会话记忆读写代理)
AI_DB_URL=http://127.0.0.1:8000
# MQTT:网关是 Broker 的唯一订阅方
MQTT_BROKER_URL=mqtt://127.0.0.1:1883
MQTT_TOPIC_TELEMETRY=iot/+/telemetry
MQTT_TOPIC_CMD=iot/+/cmd
# 令牌签发
AGENT_TOKEN_SECRET=<从密钥管理注入>

5. 数据流与写入路径 ​

5.1 上行写入链路(STM32 → MQTT → 网关 → business-db) ​

 ┌────────────────┐   温度/振动采样
 │ STM32 + FreeRTOS│  1Hz / 0.2Hz
 │  (传感器节点)   │
 └───────┬────────┘
         │ MQTT PUBLISH  QoS1
         │ topic: iot/{deviceCode}/telemetry
         v
 ┌────────────────┐
 │  MQTT Broker   │  (Mosquitto)
 └───────┬────────┘
         │ 订阅 iot/+/telemetry   ← 网关是唯一订阅方(ADR-0005)
         v
 ┌──────────────────────────────────────────────┐
 │  MCP 安全网关  mqtt/subscriber.ts + ingest.ts │
 │  1. topic 解析出 deviceCode                   │
 │  2. 载荷 JSON 校验 + 量程合法性校验            │
 │  3. deviceCode 是否已在 device 表登记          │
 │  4. seq 单调性校验(QoS1 重投幂等丢弃)        │
 │  5. 沿用/生成 trace_id                        │
 │  6. 批量写入(1s 或 200 条先到先触发)         │
 └───────┬──────────────────────────────────────┘
         │ 写权限:sensor_data / device.last_seen_at / audit_log
         v
 ┌────────────────┐
 │  business-db   │   sensor_data(device_id, metric, value, ts)
 │  (业务库)      │   device(last_seen_at)
 └────────────────┘

注意:本系统不设独立的 MQTT 接入服务。网关自身订阅 Broker 并把遥测写入业务库。这样"Agent 侧对外数据通路"才真正收敛为网关一个进程——若另起一个 ingest 服务,它就成了第二个持有业务库写凭证的组件,边界论证会变复杂。

关键点:Agent 完全不在写入链路上。Agent 既不订阅 MQTT,也不写业务库,它只能在需要时通过 MCP 网关工具「读」已经落库的数据。

5.2 下行读取链路(Agent → MCP 网关 → business-db → 字段过滤 → 返回) ​

 ┌──────────────────────────────────────────────┐
 │  Agent 进程 (OpenClaw + ReAct)               │
 │  Thought → Action: mqtt.read_sensor          │
 │  参数: {deviceId:"DEV-003",                  │
 │         from:"...", to:"...", limit:200}     │
 └───────────────┬──────────────────────────────┘
                │ ⓪ 安全管控插件层 tool:before(L1 策略判定 + 配额)
                │ ① MCP 协议调用(Agent 唯一外部出口)
                │    环境变量: GATEWAY_MCP_ENDPOINT
                v
 ┌────────────────────────────────────────────────────────────┐
 │  MCP 安全网关 (packages/mcp-gateway)                        │
 │                                                            │
 │  ② 身份识别  session_id / agent_id / Agent 令牌             │
 │  ③ 策略引擎  白名单工具?参数合规?限流?设备在授权范围?    │
 │        ├── DENY  → 写 audit_log(policy_result=DENY) → 拒绝 │
 │        └── ALLOW → 继续                                     │
 │  ④ 参数归一化  强制注入 limit 上限、时间窗上限              │
 │  ⑤ 以 gw_reader 只读账号查询白名单视图,并透视成宽表       │
 │        SELECT ts, metric, value FROM v_sensor_public       │
 │         WHERE device_id=? AND ts BETWEEN ?                 │
 │  ⑥ 字段白名单过滤 + 脱敏 (MASK)                             │
 │  ⑦ 写 audit_log(ALLOW, result_fields, duration_ms)          │
 └───────────────┬────────────────────────────────────────────┘
                 │ ⑧ 返回精简数据(不含运维工单、用户、成本等敏感字段)
                 v
 ┌──────────────────────────────────────────┐
 │  Agent 进程  作为 Observation 进入下一轮  │
 │  ReAct 循环 → 生成分析报告               │
 └──────────────────────────────────────────┘

链路要点对照表:

环节归属安全作用
⓪ 策略判定安全管控插件层(L1,进程内)越权调用在发起前被拦下;配额约束使 ReAct 循环必然收敛
① MCP 协议调用Agent 侧唯一出口Agent 无 DB 驱动、无 MQTT 客户端,物理上无法绕过
②③ 身份与策略复核MCP 安全网关(L2,硬边界)不信任 L1 判定,独立复核;提示注入可改变 Action 内容,但改不了策略判定
⑤ 只读视图网关侧 DB 账号基表权限不外露,攻击面收窄到视图列
⑥ 字段白名单网关实现最小必要原则,运维成本、客户信息等永不出库
⑦ 审计落盘网关(权威落点)每次调用必留痕;L1 与 L2 共享 trace_id,形成可追溯证据链

⓪ 与 ②③ 是纵深防御的两层,不是重复劳动:L1 有会话上下文(能判断"这次调用是否与当前任务相关"),L2 有凭证与裁剪能力(Agent 没有别的路可走)。详见 ADR-0004。


6. RAG 知识库构建流程 ​

6.1 离线构建流水线 ​

 docs/kb/*.pdf|*.md|*.docx
        │
        ▼
 ┌─────────────┐  ① 解析:PDF 用 pdf-parse/pymupdf,保留标题层级与页码
 │   Parser    │     产出: [{page, section_path, text}]
 └──────┬──────┘
        ▼
 ┌─────────────┐  ② 清洗:去页眉页脚、去乱码、合并断行、表格转 Markdown
 │  Cleaner    │
 └──────┬──────┘
        ▼
 ┌─────────────┐  ③ 切片:递归字符切分,chunk=512 token / overlap=64
 │   Chunker   │     优先按 section 边界切,避免跨章节粘连
 └──────┬──────┘     产出: chunk + 元数据(chapter/page/section_path)
        ▼
 ┌─────────────┐  ④ 向量化:bge-base-zh-v1.5 (768 维) 批量 encode
 │  Embedder   │     batch=32,写入前记录 embed_model 与 content_hash
 └──────┬──────┘
        ▼
 ┌─────────────┐  ⑤ 入库:upsert 到 Chroma/Qdrant collection
 │  Indexer    │     payload 写入全部元数据字段;更新 kb_document 状态
 └──────┬──────┘     同一 doc_id 重建时先 delete(where doc_id) 再插入
        ▼
   kb_fault_knowledge collection

6.2 增量重建策略 ​

场景判定依据动作
新增文档kb_document 无该 doc_code全量解析 → 切片 → 入库
文档更新file_hash 变化按 doc_id 删除旧切片,重新入库,version +1
仅换 embedding 模型embed_model 与配置不一致全量重建(维度变化必须重建,不可混存)
切片策略调整chunk_size/overlap 配置变更全量重建,并在 kb_document 记录新参数

6.3 检索参数建议 ​

参数原型期取值落地期建议说明
top-k58~10 + rerank原型期 k=5 足够;引入 reranker 后可先召回 30 再精排到 5
相似度阈值cosine ≥ 0.35≥ 0.45(配合 rerank 分数)低于阈值视为「知识库无覆盖」,Agent 应回答「无依据」而非编造
元数据过滤device_model = 当前设备型号同上 + doc_type 白名单防止跨型号误引手册导致错误维修建议
去重按 content_hash 去重同上 + MMR 多样性避免同一段落多个切片占据全部 top-k
引用输出返回 chunk_id + doc_title + page同上供前端报告页渲染引用角标,可溯源

检索结果同样经过 MCP 网关(工具名 kb.search),网关会校验 topK 上限、强制设备型号过滤,并把调用写入 audit_log。Agent 不直接持有向量库客户端。


7. 迁移与初始化 ​

7.1 目录约定 ​

data/
├── business/
│   └── business.db            # 业务库文件(原型期)
├── ai/
│   ├── ai-memory.db           # 会话记忆(原型期)
│   └── chroma/                # 向量库持久化目录
└── backups/

packages/business-api/
└── migrations/
    ├── 20260915_001_init.sql              # 全量建表
    ├── 20260915_002_seed_rbac.sql         # RBAC 权限点种子
    ├── 20260916_003_seed_device.sql       # 示例设备
    └── 20260918_004_add_audit_index.sql   # 增量迁移示例

packages/business-api/src/db/
├── migrate.ts                 # 迁移执行器:按文件名排序,写入 schema_migration
├── seed.ts                    # 种子数据装载(幂等)
└── repositories/              # 各表仓储层

迁移规则:文件名 YYYYMMDD_NNN_描述.sql;只允许追加、禁止修改已应用文件;执行器校验 checksum,不一致直接报错退出;每个迁移在事务内执行。

7.2 种子数据清单 ​

数据内容幂等策略
内置管理员admin / 初始密码从 SEED_ADMIN_PASSWORD 环境变量读取(不写死在 SQL 里),argon2id 哈希INSERT ... WHERE NOT EXISTS
内置角色ADMIN、ENGINEER、VIEWER按 role.code 幂等
权限点device:read、sensor_data:read、work_order:*、audit_log:read、kb:* 等约 40 条按 permission.code 幂等
示例设备DEV-001~DEV-003,含 model / location / mqtt_client_id按 device_code 幂等
示例故障手册3 份文档登记 + 切片索引(轴承磨损、电机过热、振动异常)按 doc_code + file_hash 幂等
示例告警与工单每条设备 1 条 OPEN 告警 + 1 条 CREATED 工单,便于前端演示按业务编号幂等

seed 脚本严禁做的事:不得创建任何 Agent 数据库账号;不得把 business-db 连接串写入 Agent 相关配置文件。初始化完成后应执行一次自检:断言 grep -r "business.db" packages/agent 配置文件 无结果。

7.3 初始化执行顺序 ​

1. pnpm db:migrate        # 建表 + schema_migration 记录
2. pnpm db:seed           # 角色/权限/管理员/示例设备/示例工单
3. pnpm kb:index          # 解析 docs/kb → 切片 → embedding → Chroma
4. pnpm db:verify         # 自检:表存在性、索引存在性、Agent 无 business-db 配置

第 4 步 db:verify 是答辩演示的加分项:它会打印双库隔离检查结果(Agent 配置文件不含 business-db 与 MQTT 变量、网关账号为只读、审计表可写),把「安全约束」从文档承诺变成可执行断言。


附:本文档与其他文档的接口边界 ​

本文档产出被谁消费消费方式
audit_log 表结构docs/02-安全管控插件层设计.md审计钩子写入字段定义
字段白名单视图docs/04-MCP安全网关设计.md每个 MCP 工具对应的视图与列集合
REST 数据形态docs/06-Web前端设计.md前端类型定义与接口契约
双库连接串分离docs/01-系统总体方案.md部署拓扑与环境变量清单