从上下文到事实:为运维智能体构建持久化元数据层
运维智能体的瓶颈常常不在推理,而在记忆。本文剖析 opsctl 的设计取舍——为什么向量检索不是这里的答案、结构化的边界该划在哪里、以及如何把专家经验编码成可评审的行为契约。
一、会话即遗忘
把大语言模型接入生产运维,最先撞上的墙通常不是推理能力,而是记忆。
一个典型场景:工程师让智能体检查数据库磁盘水位。它反问主机地址,工程师贴上 IP 和 SSH 用户;它登录、执行、给出结论。这一轮很顺利。第二天,同一个人开了新会话,让它再看一次同一台机器——它又问了一遍主机地址。
连接信息、访问凭据、服务之间的依赖关系,这些运维现场最基本的事实,全部活在对话上下文里。上下文窗口一关,它们随之蒸发。工程师被迫扮演一个人肉数据库,每次会话重新灌入相同的信息。
这不是模型能力的问题。当前一线模型的推理水平足以完成绝大多数排查动作,问题在于系统缺了一层:智能体没有可以信赖的事实来源,只能依赖对话里刚刚被告知的内容。它的判断质量上限,取决于工程师这一轮恰好记得说什么。
opsctl 想补的就是这一层。它不做推理,不接管操作,只回答四个问题:有什么、怎么连、谁依赖谁、什么时候到期。
二、为什么向量检索不是这里的答案
谈到给智能体加记忆,业界的默认反应是检索增强:把文档切片、向量化、按语义相似度召回。这在知识问答场景里行之有效,但在运维场景里存在根本错配。
运维需要精确事实,不是相似片段。「pg-main 的 SSH 登录用户是什么」只有一个正确答案。向量检索返回的是 top-k 近似结果,排序依据是语义距离而非正确性。当召回的第二篇文档恰好提到另一台机器的用户名时,模型无从判断哪个属于当前这台。近似在问答里是可接受的噪声,在「以什么身份登录哪台生产机」上则不是。
事实需要被约束,而检索索引不提供约束。资源名必须唯一;删除一台仍被引用的机器应当被拒绝;依赖关系不能成环。这些都是完整性约束,属于数据库的职责范畴。把它们交给一堆文本切片,等于放弃一致性保证。
事实需要被写入和更新。密码轮换了、机器下线了、依赖变了——这些是频繁的小写入。向量库擅长读多写少的语料,不擅长充当可变状态的权威副本。
所以 opsctl 选了一条朴素的路:一张 SQLite,一套显式 schema,一个命令行工具。智能体要事实就查表,不做相似度匹配。知识可以检索,状态必须存储。
三、四个设计取舍
3.1 万物皆资源
云账号、虚拟机、Redis、MySQL、PostgreSQL、HBase、APISIX 网关、Keycloak、etcd、Docker Swarm、Kubernetes 集群,以及各种自部署服务——它们在物理形态上差异巨大,但从智能体的视角看,需要的信息是同构的:这是什么类型、怎么连上去、有哪些需要盯着的点。
opsctl 把它们统一抽象为 Resource 的子类,遵循「固定字段 + 扩展字段」模式。固定字段由类型声明,扩展字段由使用者随时追加,不需要迁移 schema。
class Resource:
type: ClassVar[str] = ""
standard_attributes: ClassVar[dict[str, dict]] = {}
operations_guide: ClassVar[str] = ""
default_concerns: ClassVar[list[ConcernTemplate]] = []
四个类属性构成整个模型的骨架。前两个回答「是什么、怎么连」,后两个分别承载操作知识与时间维度。
3.2 结构化的边界划在哪里
这是整个设计里最值得琢磨的取舍。
Schema 是一份契约。契约写得越细,校验越强、查询越方便,但覆盖新场景的成本也越高。一个自然的诱惑是把资源的操作能力也建模进去:给 PostgreSQL 加一个 supported_operations 字段,枚举它能做备份、能查慢日志、能看复制延迟。
这条路走不通。一台数据库能做什么,是一个开放集合。你永远列不全,而每次列不全都意味着智能体的能力被 schema 截断了一次。用有限枚举逼近无限集合,是注定失败的建模。
opsctl 的答案是把边界划在「识别与连接」处:
@register_resource
class PostgresResource(Resource):
type = "postgres"
standard_attributes = {
"host": {"type": "str", "required": True},
"port": {"type": "str", "default": "5432"},
"db_name": {"type": "str", "required": True},
"username": {"type": "str", "required": True},
"password": {"type": "secret"},
}
operations_guide = (
"连接: PGPASSWORD=<password> psql -h <host> -p <port> "
"-U <username> -d <db_name>. 具体 SQL 功能由 Agent 自行决定."
)
结构化的部分只有主机、端口、库名、账号、密码——稳定、有限、需要校验。而「连上去之后能干什么」退化成一段自然语言,直接投喂给模型。
这背后是一条更普遍的原则:确定的部分交给 schema,开放的部分交给模型。大模型最不缺的能力就是理解 psql 能做什么。把这件事从数据模型里拿掉,既保住扩展性,也让模型发挥了它真正的长处。
3.3 关系的最小主义
资源之间的关系类型是另一个容易失控的地方。部署于、同属集群、共享网络、主从复制、读写分离——一旦开了口子,很快就会滑向本体论工程,而每增加一种关系,智能体就要多理解一层语义。
opsctl 只保留一种:depends_on。无分组,无标签。
理由很直接:智能体动手之前真正需要回答的问题只有一个——我改动这个,会波及谁。依赖方向足以回答它。其余关系语义对影响分析的边际贡献很小,却要付出模型的理解成本。
约束在写入时强制。添加一条会成环的依赖会被直接拒绝,并把成环路径回报给调用方:
class CycleError(Exception):
def __init__(self, cycle: list[str]) -> None:
super().__init__("拒绝创建: 会形成循环依赖 " + " -> ".join(cycle))
self.cycle = cycle
这个细节值得一提:报错不只说「失败了」,而是给出具体路径。对人来说这是可操作的诊断信息,对智能体来说这是可以据以自我修正的结构化反馈。错误消息也是接口的一部分。
3.4 把时间纳入模型
运维事故里有很大一类不是突发故障,而是可预见的到期:证书过期、云主机欠费停机、磁盘缓慢涨到写不进去、访问密钥长期未轮换。它们的共同点是都带有时间维度,且在事发前很久就可以被发现。
opsctl 用关注点建模这类事项:类别、描述、到期时间、严重级别、状态。
真正有意思的是默认关注点。每种资源类型自带一组模板,在资源被登记的瞬间自动挂上:
default_concerns = [
ConcernTemplate(category="capacity", description="CPU 使用率监控", severity="warning"),
ConcernTemplate(category="capacity", description="内存使用率监控", severity="warning"),
ConcernTemplate(category="capacity", description="磁盘使用率监控", severity="warning"),
ConcernTemplate(category="capacity", description="磁盘 IO 监控", severity="warning"),
ConcernTemplate(category="capacity", description="网络带宽使用率监控", severity="warning"),
ConcernTemplate(category="security", description="SSH 暴力破解监控", severity="critical"),
ConcernTemplate(category="expiry", description="实例过期时间监控", severity="critical"),
ConcernTemplate(category="expiry", description="SSH 密钥到期检查", severity="critical"),
]
登记一台 ECS,八项监控立刻到位;登记一个 PostgreSQL,连接数、复制延迟、归档备份、含预写日志的磁盘占用四项随之就位。
这是把领域专家知识默认值化。新人不需要知道一台机器该盯哪些指标,类型定义已经替他想过了。经验因此从「资深工程师脑子里的清单」变成「代码仓库里可评审、可演进的常量」。
四、实现层面的几个决定
4.1 注册表与扩展点
类型通过装饰器登记进全局注册表,重复注册同名类型直接抛错,避免静默覆盖:
def register_resource(cls: type[Resource]) -> type[Resource]:
type_name = getattr(cls, "type", None)
if not isinstance(type_name, str) or not type_name:
raise ResourceRegistryError(f"{cls.__name__} 缺少有效的 type 字符串属性")
if type_name in _REGISTRY and _REGISTRY[type_name] is not cls:
raise DuplicateResourceTypeError(
f"资源类型 '{type_name}' 已被 {_REGISTRY[type_name].__name__} 注册"
)
_REGISTRY[type_name] = cls
return cls
新增一种资源类型的成本因此降到最低:定义子类、加装饰器、确保模块被导入,CLI 立刻识别。不改路由、不改命令、不动数据库迁移。
4.2 一条路径,两类消费者
opsctl 同时是命令行工具和智能体插件。插件没有直接导入内部的仓储层,而是通过子进程调用 CLI 并取回 JSON。
这个决定看起来绕,收益却很明确:人和模型走的是同一条路径。工程师在终端里验证过的命令,智能体调用时行为完全一致;反过来,智能体出了问题,工程师可以在终端里逐字复现。接口边界清晰,CLI 也因此可以独立测试。
代价是每次调用一次进程启动开销。对于每天几十到几百次的运维查询,这个代价可以忽略。
4.3 数据隔离
插件模式下数据库路径跟随运行时的 profile 目录,不同 profile 的数据彼此独立,也不落在仓库目录里。开发环境与生产环境的资源清单因此不会互相污染。
五、行为契约:把专家经验写成可评审的文档
光有事实还不够。给了智能体完整的资源元数据之后,它仍然可能干出这种事:
发现 pg-main 磁盘 92%,请处理。
信息准确,但毫无价值——它只是把问题原样转交回人。
这是大模型的一种默认行为倾向:倾向于在回答「看起来完整」的时刻停止。报告了现象,任务在形式上就闭合了。要打破这个倾向,必须显式定义什么才算完成。
opsctl 的插件里附带一份上下文文档,规定了工作范式:发现问题 → 深入排查 → 定位根因 → 尝试解决 → 报告。并给出正面示范:
巡检发现 pg-main 磁盘使用率 92%。SSH 登录后确认 /data 分区已满;逐层排查发现预写日志堆积;检查归档状态,发现归档进程已经挂掉;重启归档、清理过期日志段,磁盘回落到 65%。报告根因与修复结果。
两段对照放在一起,传达的不是「要更努力」,而是一个可判定的终止条件:没查到根因,任务就没结束。
文档同时划出安全边界:不在未确认身份的资源上执行命令,不在修复流程之外执行破坏性操作,不擅自修改元数据本身。
值得强调的是这份文档的形态。它不是藏在代码里的提示词字符串,而是仓库中一份独立的 Markdown,跟着版本走,可以被评审、被 diff、被讨论。当智能体的行为规范变成工程产物,它才可能被持续改进。
六、报告:让数据层决定顺序,模型只负责表达
巡检技能定义了统一的输出格式:查询窗口内到期的关注点,分三组呈现——需立即处理、需关注、其余折叠。
🕐 运维巡检 2026-08-07 · 共 12 项到期: critical=1, warning=4, info=7, other=0
🔴 需立即处理 (2)
- pg-main — 磁盘水位 92% — 到期: 2026-08-07T12:00:00+00:00
- web-prod-1 — SSL 证书到期 — 到期: 2026-08-07T07:00:00+00:00
🟡 需关注 (3)
- redis-cache — 内存水位 85% — 到期: 2026-08-09T16:00:00+00:00
- k8s-prod — Pod 重启次数偏高 — 到期: 2026-08-11T16:00:00+00:00
- keycloak-1 — 会话清理 — 到期: 2026-08-12T16:00:00+00:00
🔵 其余 7 项 (折叠)
格式本身不稀奇,真正的工程考量藏在技能定义的几条约束里:
- 禁止逐资源遍历——窗口过滤与排序由数据层一次性完成,不让模型对每个资源轮询一遍。
- 组内顺序由数据层排定,不得重排——模型拿到的已经是排好序的列表,它的职责只剩渲染。
- 统计与分组必须自洽——头部计数与各组条目数要对得上,critical 项永远不落进折叠段。
这三条指向同一个原则:凡是能由确定性代码决定的,就不要交给模型。过滤、排序、分组、计数都有唯一正确答案,交给 SQL 既快又不会错;模型只做它擅长的部分——把结构化数据转成人读得懂的叙述。
格式规范刻意与推送渠道解耦。飞书、钉钉、企业微信还是邮件,收到的都是同一套分组结构。渠道是投递方式,不该反过来影响信息的组织方式。
至于定时,用自然语言设置即可:「每天早上 9 点执行巡检。」智能体自己把定时任务记下来,不需要人去编辑配置文件或手写 cron 表达式。
七、局限与已知取舍
凭据以明文存储在 SQLite 中。secret 标记只用于显示时脱敏,不提供加密。这适用于单人或小团队自持的运维环境,不适合多租户或合规要求严格的场景;要上生产级别,需要接入外部密钥管理。
不对接任何云厂商 API。资源清单靠人工登记或智能体自行同步,元数据层不主动拉取。好处是避开了适配器地狱——不必为每家云、每个版本维护一套 SDK 封装;代价是清单的新鲜度依赖使用纪律。
单一关系类型。无法表达「部署于」「同属集群」这类语义。当前判断是它们对影响分析贡献有限,但这个判断可能随场景变化。
SQLite 单机存储。并发写入能力有限。对单个运维智能体的使用强度足够,多实例共享时需要更换存储。
八、结语
过去两年,提升智能体能力的讨论大多集中在模型本身:更强的推理、更长的上下文、更好的工具调用。但在运维这类高度依赖环境事实的场景里,能力的天花板往往不由模型决定,而由它能否触及可靠的事实决定。
opsctl 只做一件很小的事:把运维现场的四类事实——有什么、怎么连、谁依赖谁、什么时候到期——从对话里搬进结构化存储。它不会让模型变得更聪明,但它让模型的聪明有处着力。
项目以 Apache-2.0 协议开源,代码在 GitHub 上的 EmbraceTeam/DevOps-Agentc。