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-api | RAG 构建脚本、Agent 会话管理模块 |
| 读取方 | business-api(人)、mcp-gateway(Agent 侧,仅经工具暴露) | mcp-gateway(唯一读取方,只读检索) |
| 生命周期 | 长期保留,按年归档 | 知识切片随文档版本重建;会话记忆 7 天过期 |
1.3 访问权限矩阵
权限标记说明:RW 读写、R 只读、W 只写、— 无任何权限、间接R 不可直连、只能经 MCP 网关工具获取被字段白名单过滤后的结果。
| 角色 / 主体 | business-db | ai-db | MQTT Broker | MCP 网关 |
|---|---|---|---|---|
| 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_report | Agent 分析报告落库 | 100 / 50 万 |
audit_log | 安全审计日志 | 5 千 / 2 亿 |
schema_migration | 迁移版本记录 | 10 / 200 |
2.2 用户与 RBAC 四表
-- ---------- 用户账号 ----------
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'))
);| 字段 | 类型 | 约束 | 注释 |
|---|---|---|---|
| id | INTEGER | PK, AUTOINCREMENT | 主键 |
| username | TEXT | NOT NULL, UNIQUE | 登录名 |
| password_hash | TEXT | NOT NULL | 密码哈希,禁止存明文/可逆加密 |
| password_algo | TEXT | NOT NULL | argon2id 或 bcrypt,为将来换算法留升级位 |
| status | TEXT | CHECK | 账号状态,锁定态由 failed_count 触发 |
| failed_count / locked_until | INTEGER / TEXT | — | 暴力破解防护 |
取舍:选 argon2id 是因为它对 GPU 破解的抵抗力优于 bcrypt;原型期若 Node 侧
argon2原生模块编译困难,降级为 bcrypt(cost=12),字段password_algo保证两种哈希可共存、可渐进迁移。
-- ---------- 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);| 表 | 关键字段 | 说明 |
|---|---|---|
| role | code | 角色唯一标识,前端按钮级权限 v-permission 依据 permission.code |
| permission | code / resource / action | 权限点粒度到「资源:动作」,避免粗粒度导致授权过度 |
| user_role | (user_id, role_id) 复合主键 | 一人可多角色,取并集 |
| role_permission | (role_id, permission_id) 复合主键 | 角色-权限多对多 |
注意:Agent 在本表中没有任何账号记录。 AGENT_SVC 角色仅为将来可能的服务账号占位,原型期不创建、不授予,Agent 身份仅存在于 MCP 网关的会话上下文中。
2.3 设备档案与传感器时序数据
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);| 字段 | 类型 | 约束 | 注释 |
|---|---|---|---|
| metric | TEXT | CHECK 枚举 | 温度 / 振动两类,新增指标需改 CHECK 或迁移为字典表 |
| value | REAL | NOT NULL | 采样值 |
| quality | INTEGER | DEFAULT 1 | 用于过滤丢包补值,避免污染曲线与阈值判断 |
| ts | TEXT | NOT NULL | 采样时刻,received_at 为入库时刻,两者分离便于排查链路延迟 |
| trace_id | TEXT | 可空 | 与 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 告警与工单
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
↓
CLOSED2.5 Agent 分析报告
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,前端可点击跳转知识库管理页 |
| status | CONVERTED 表示已转为工单,防止同一报告重复建单 |
安全提示:
agent_report由 business-api 写入(网关把结果回传给业务服务落库),Agent 自身没有该表的写权限,避免 Agent 自造报告污染运维决策。
2.6 安全审计日志(核心表)
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_id | TEXT | NOT NULL | 贯穿 MQTT→网关→Agent→前端;同一 traceId 下会有 PLUGIN 与 GATEWAY 两条记录,前端审计页按它串联展示 |
| layer | TEXT | CHECK | PLUGIN = 安全管控插件层的策略判定;GATEWAY = MCP 网关的独立复核与执行。纵深防御的两层证据链 |
| caller_type | TEXT | CHECK | 区分 Agent 调用与人类调用,是「谁在访问」的第一判据 |
| agent_id / session_id | TEXT | 可空 | Agent 身份与会话,支撑「同一会话是否反复试探」的分析 |
| tool_name | TEXT | NOT NULL | MCP 工具名,与 docs/04-MCP安全网关设计.md 工具清单一一对应 |
| tool_args_digest | TEXT | 可空 | 只存摘要不存全量入参,防止审计表本身成为敏感数据泄露源 |
| result_fields | TEXT | 可空 | 记录字段白名单实际生效结果,证明最小必要原则被落实 |
| redacted_fields | TEXT | 可空 | 被剥离的字段名。这是**「裁剪确实发生了」的直接证据**,答辩时可与原始行对照 |
| result_digest | TEXT | 可空 | 行数 + 内容哈希,可验证但不可反推业务数据 |
| policy_result | TEXT | CHECK | ALLOW / DENY / REWRITE(参数被收窄而非拒绝)/ MASK(字段脱敏)/ RATE_LIMIT(限流拦截) |
| policy_rule_id | TEXT | 可空 | 命中规则编号(插件层 R3、网关 G4),便于逐条演示策略 |
| duration_ms | INTEGER | 可空 | 网关侧耗时,用于性能与异常检测 |
设计取舍:审计表刻意不存原始业务数据。备选方案是存完整 request/response 便于复盘,但那样审计库就成了 business-db 的副本,一旦被读取则边界隔离失效。折中为「摘要 + 哈希 + 字段白名单记录」,兼顾可追溯与最小暴露。
保留策略:原型期不清理;落地期按月分区,热数据保留 6 个月在线,之后归档到对象存储并只保留 trace_id + policy_result + ts 索引行。
2.7 迁移版本表
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_id | string | 是 | 全局唯一,形如 doc12-c035,用于 citations 引用 |
doc_id | string | 是 | 来源文档 id,关联 kb_document 表 |
doc_title | string | 否 | 来源文档标题,检索结果直接展示 |
doc_type | string | 是 | manual(故障手册)/ case(检修案例)/ sop(作业规范) |
device_model | string | 是 | 适用设备型号,检索时按当前设备型号过滤,避免跨型号误引 |
chapter | string | 否 | 章节标题,用于结果定位 |
page | int | 否 | 页码,前端可提示「手册 P.42」 |
section_path | string | 否 | 形如 第3章/3.2 振动异常,层级面包屑 |
version | string | 是 | 文档版本,重建时按版本批量删除旧切片 |
effective_date | string | 否 | 文档生效日期 |
content_hash | string | 否 | 切片内容哈希,用于增量重建去重 |
text | string | 否 | 原始切片文本(Chroma 的 documents 字段) |
3.3 知识库文档登记表(ai-db 内 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 会话短期记忆
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-db | ai-db | 隔离强度 | 适用场景 |
|---|---|---|---|---|
| 原型期(本毕设) | 独立 SQLite 文件 data/business.db | 独立 SQLite data/ai-memory.db + Chroma 持久化目录 data/chroma/ | 逻辑隔离:文件独立、账号独立、进程独立、无跨库 JOIN | 单机演示、答辩验证 |
| 落地期 | PostgreSQL 实例 A(独立主机/容器) | Qdrant 实例 B + PostgreSQL 实例 C(会话记忆) | 物理隔离:不同主机、不同网络段、独立凭据与备份链路 | 生产环境 |
原型期逻辑隔离的四条硬性要求(缺一不可):
- 文件系统分离:两库文件位于不同目录,
data/business/与data/ai/,权限位不同。 - 账号分离:即便都是 SQLite 无账号概念,也用不同 OS 进程 + 不同工作目录 + 不同文件权限模拟;若切换到 PostgreSQL 则必须建两套账号。
- 进程分离:business-api 进程只持有 business-db 句柄;Agent 进程只持有 ai-db 句柄。二者不共享任何 DB 连接对象。
- 无跨库查询:禁止在两库之间做 JOIN 或外键。需要跨域数据时,唯一通路是 MCP 网关工具调用。
4.2 连接串与账号分离示例
.env.business-api(业务接口服务,仅此进程可见):
# 原型期
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(网关,只有业务库只读账号):
# 原型期:只读打开,禁止写
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 变量):
# 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 安全网关,唯一持有全部数据源凭据的进程):
# 读账号(只授白名单视图 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 collection6.2 增量重建策略
| 场景 | 判定依据 | 动作 |
|---|---|---|
| 新增文档 | kb_document 无该 doc_code | 全量解析 → 切片 → 入库 |
| 文档更新 | file_hash 变化 | 按 doc_id 删除旧切片,重新入库,version +1 |
| 仅换 embedding 模型 | embed_model 与配置不一致 | 全量重建(维度变化必须重建,不可混存) |
| 切片策略调整 | chunk_size/overlap 配置变更 | 全量重建,并在 kb_document 记录新参数 |
6.3 检索参数建议
| 参数 | 原型期取值 | 落地期建议 | 说明 |
|---|---|---|---|
| top-k | 5 | 8~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 | 部署拓扑与环境变量清单 |