Appearance
OpsPilot:先规定助手可以改变什么
从 ModelDock 值班席接到一个具体问题:申请已经受理,但 Pod 一直 Pending。如果模型认为“重启一下就好”,你愿意给它写集群的权限吗?这一项目先把证据和写入权限分开,再逐步增加工具调用、持久化审批与恢复验证。可以先不配置任何语言模型。
一次查询 LLM 只能得到文字建议;Agent 的关键是根据观测决定下一步工具、执行并更新状态。本项目保留固定状态图作基线,另外增加真实 Kind 的单 Agent。默认 planner 按状态选择只读工具,PLANNER=llm 时用 Chat Completions Function Calling 选择 observe(source)。模型没有返回有效工具调用时,仅重试一次受 JSON Schema 约束的 source 选择。先用 Pod 状态限定可选来源:未调度且未启动容器时,网络端点不是首选证据。source 是剩余工具的枚举,模型不能构造 shell 或任意 patch。模型负责选择下一条证据,最终结论与动作由可检查的确定性策略约束。
stateDiagram-v2; [*] --> Plan; Plan --> Observe; Observe --> Plan; Plan --> Approval; Plan --> ReportOnly; Approval --> Repair: approved; Approval --> Rejected: denied; Repair --> Verify; Verify --> Resolved; Verify --> Failed
Pending 并不唯一指向容量不足:未调度、容器拉取镜像等都可能显示 Pending。先读 pods,若 Pod 未调度则查与其 UID 关联的 Events;FailedScheduling 指向 selector/affinity 时再读 Deployment 模板。这里仅当真实模板与预设故障精确匹配,才提出移除 selector。对资源不足、拉取失败等可能诊断但只输出报告;应用重启退避不是最终业务根因。
系统边界:同一产品的两种运行模式
| 层 | 本地模拟 | 集群实验 |
|---|---|---|
| 入口 | Go HTTP 把 /ops 反代给 Python,页面展示故障 | Vue 控制台经开发代理访问 FastAPI;不是每条请求都经过 Go |
| 运行时 | LangGraph → simulated evidence → 暂停 | 有界的 Plan → Observe 循环,证据不足禁止写 |
| 工具 | MCP stdio → 模型模拟服务 | stdio → 有限 kubectl 查询 / 指标;只有批准节点可打开写角色会话 |
| 存储 | SQLite 保存任务与经验 | SQLite 检查点可换 PostgreSQL;审计和验证过的 memory 仍有本地依赖 |
| Redis / Prometheus | 不要求 Redis | Redis 在 ModelDock 限流;Agent 可以查 Prometheus,并不靠 Redis 保证审批幂等 |
需求如何变成验收标准
给值班人减少跨页面找线索的时间,但自动改错对象比慢更严重。为此分开定义三种成功:诊断分类有证据;建议动作符合策略;执行后工作负载恢复。前两项成功不能推导第三项。只用一次模型 API 可以写总结,无法在进程重启后恢复准确的审批位置;多个 Agent 会增加协调状态,并不提供更严格的写权限。
完整源码:opspilot/cluster/run.py · 第 30–46 行
python
class State(TypedDict,total=False):
id:str
observations:dict
trace:list
status:str
action:str
source:str
approved:bool
verified:dict
diagnosis:str
citations:list
tokens:int
def sufficient(e):
return (e.get("deployment",{}).get("selector")=={"lab.invalid/pool":"absent"}
and any(p["phase"]=="Pending" for p in e.get("pods",{}).get("pods",[]))
and any(x["reason"]=="FailedScheduling" and ("selector" in x["message"] or "affinity" in x["message"]) for x in e.get("events",{}).get("items",[])))开发顺序。 定义 State 与终止条件 → 写 planner → 接一个只读工具 → 存检查点和审批 → 真实 Pending 验收 → 评估和追问。先跑 rules 基线;模型参与的是选择下一条证据,不是执行任意命令。
先写一个用例,再决定要不要调用模型
- 输入只有 Pod Pending,预期下一动作是查询证据,不能返回 repair。先写 diagnose 与 next_source 的测试。
- 补上 FailedScheduling 但事件是 GPU 不足,应产出 report_only;不要复用 selector 的修复动作。
- 只有 Pod、事件和 Deployment 指向预设错误 selector,才生成 proposal。持久保存观察和 thread_id,断开进程,再加载同一任务;下一节点必须仍是审批。
- 加上受控工具与独立 verify,先注入一次版本变化,再模拟响应丢失。比起“LLM 回答正确”,检查拒绝旧计划与重放不产生第二次写更能验证系统。
- 最后更换 planner,保留同一个工具白名单、执行条件和回放样本。才能比较模型选择证据是否减少了无效调用。
具体分类逻辑在这里。策略返回的是诊断和引用来源,不返回可以任意执行的命令:
完整源码:opspilot/cluster/reason.py · 第 5–18 行
python
def diagnose(obs):
pods=obs.get("pods",{}).get("pods",[])
events=obs.get("events",{}).get("items",[])
text=" ".join(e.get("message","") for e in events)
if any(p.get("phase")=="Pending" for p in pods) and any(e.get("reason")=="FailedScheduling" for e in events):
if obs.get("deployment",{}).get("selector")=={"lab.invalid/pool":"absent"} and ("selector" in text or "affinity" in text):
return "selector_mismatch",["pods","events","deployment"]
if "Insufficient nvidia.com/gpu" in text:return "gpu_insufficient",["pods","events"]
for pod in pods:
for c in pod.get("containers",[]):
last=c.get("lastState",{}).get("terminated",{})
if last.get("reason")=="OOMKilled" and last.get("exitCode")==137:return "oom_killed",["pods"]
waiting=c.get("state",{}).get("waiting",{}).get("reason")
if waiting in ("ErrImagePull","ImagePullBackOff"):return "image_pull_failure",["pods"]