运维AI助手

Administrator 0 阅读 AI 发展史

一个成熟应用的第一步是目录结构清晰,各层职责分明。推荐采用 后端(FastAPI)+ CLI 客户端 + 共享模型 的结构:

text

ops-ai/
├── backend/
│   ├── app/
│   │   ├── main.py              # FastAPI 入口
│   │   ├── config.py            # 环境变量、模型配置
│   │   ├── api/
│   │   │   └── v1/
│   │   │       └── commands.py  # 命令生成/确认/反馈端点
│   │   ├── core/
│   │   │   ├── rag.py           # RAG 检索服务
│   │   │   ├── llm.py           # LLM 调用封装(含结构化输出)
│   │   │   ├── safety.py        # 安全规则引擎
│   │   │   └── env.py           # 环境指纹采集
│   │   ├── models/
│   │   │   └── schemas.py       # Pydantic 请求/响应模型
│   │   └── db/
│   │       ├── audit.py         # 审计日志写入
│   │       └── feedback.py      # 用户反馈存储
│   ├── requirements.txt
│   └── Dockerfile
├── cli/
│   ├── opsai/
│   │   ├── __init__.py
│   │   ├── main.py              # CLI 入口(Typer/Click)
│   │   ├── client.py            # HTTP 客户端封装
│   │   └── env_fingerprint.py   # 本地环境指纹采集
│   └── pyproject.toml
├── knowledge/
│   ├── man_pages/               # 预处理后的 man 页面 / --help 文本
│   └── ingest.py                # 知识库导入脚本
├── docker-compose.yml
└── README.md

CLI 作为入口,后端作为“命令生成与安全审计”的服务端,知识库离线构建、在线检索。这样 CLI 可以轻量分发,后端可以独立升级模型和规则。


后端核心代码:FastAPI + RAG + 安全引擎

数据模型(schemas.py​)

先定义好“生成结果”的结构,后续所有模块围绕它工作:

from pydantic import BaseModel, Field
from typing import Literal, Optional

class CommandRequest(BaseModel):
    query: str = Field(..., description="用户的自然语言描述或报错日志")
    env: dict = Field(default_factory=dict, description="环境指纹,如 os, version")
    session_id: Optional[str] = None

class CommandCandidate(BaseModel):
    command: str
    explanation: str
    risk_level: Literal["readonly", "change", "high_risk"]
    rollback: Optional[str] = None
    retrieved_sources: list[str] = []

class CommandResponse(BaseModel):
    candidates: list[CommandCandidate]
    blocked: bool = False
    block_reason: Optional[str] = None
    audit_id: str

​risk_level​ 是安全层分流的依据:readonly​ 直接返回,change​ 需要用户确认,high_risk​ 直接拦截并给出替代建议。


安全规则引擎(safety.py​)

这是运维 AI 区别于通用 ChatBot 的核心模块,必须独立于 LLM 运行。即使模型输出了“看起来合理”的命令,也要经过这一层:

import re
from dataclasses import dataclass

@dataclass
class SafetyRule:
    pattern: str
    level: str          # "block" 或 "warn"
    message: str
    replacement: str = ""

DEFAULT_RULES = [
    SafetyRule(
        pattern=r"rm\s+-rf\s+/(\s|$)",
        level="block",
        message="禁止递归删除根目录。",
        replacement="请明确指定要删除的子目录路径。",
    ),
    SafetyRule(
        pattern=r"dd\s+.*of=/dev/(sd|nvme|vd)",
        level="block",
        message="禁止直接写入磁盘设备。",
    ),
    SafetyRule(
        pattern=r"mkfs(\.\w+)?\s+/dev/",
        level="block",
        message="禁止对块设备执行 mkfs。",
    ),
    SafetyRule(
        pattern=r"iptables\s+-F",
        level="warn",
        message="清空防火墙规则可能导致连接中断,确认前请先保存规则。",
    ),
    SafetyRule(
        pattern=r"kill\s+-9",
        level="warn",
        message="SIGKILL 无法被进程捕获,可能造成数据丢失。",
    ),
    SafetyRule(
        pattern=r"(shutdown|reboot|poweroff)",
        level="warn",
        message="系统关机/重启命令,请确认影响范围。",
    ),
]

def evaluate(command: str) -> dict:
    for rule in DEFAULT_RULES:
        if re.search(rule.pattern, command, re.IGNORECASE):
            return {
                "blocked": rule.level == "block",
                "level": rule.level,
                "reason": rule.message,
                "replacement": rule.replacement,
            }
    return {"blocked": False, "level": "readonly", "reason": "", "replacement": ""}

规则库应该配置化(YAML/数据库),方便后续扩展而不用改代码。高风险命令必须在返回给用户之前被拦截,而不是依赖模型自我审查。


LLM 调用与结构化输出(llm.py​)

不要直接让模型返回自由文本。要求模型输出 JSON,并且强制包含 risk_level​ 和 rollback​:

import json
from openai import OpenAI
from backend.app.models.schemas import CommandCandidate

SYSTEM_PROMPT = """你是一个运维命令生成助手。你的输出必须是 JSON 数组,每个元素包含:
- command: 完整的 shell 命令
- explanation: 一句话解释命令的作用
- risk_level: "readonly" | "change" | "high_risk"
- rollback: 如果命令产生变更,提供回滚命令;只读命令可为 null

规则:
1. 只生成当前环境可用的命令(参考用户提供的 env 和检索到的文档)。
2. 如果不确定,risk_level 至少标为 "change"。
3. 不要编造不存在的参数。
"""

def generate_candidates(query: str, context: str, env: dict) -> list[CommandCandidate]:
    client = OpenAI()
    user_prompt = f"""用户需求:{query}
环境信息:{json.dumps(env, ensure_ascii=False)}
参考文档:
{context}

请生成 1-3 个候选命令。"""
    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_prompt},
        ],
        response_format={"type": "json_object"},
        temperature=0.1,
    )
    raw = json.loads(resp.choices[0].message.content)
    items = raw if isinstance(raw, list) else raw.get("commands", [])
    return [CommandCandidate(**item) for item in items]

关键点:temperature=0.1​ 降低随机性;response_format​ 强制 JSON;系统提示词中明确“不确定就标 change”。


RAG 服务(rag.py​)

使用 LangChain + Chroma 做离线知识库检索。knowledge/ingest.py​ 负责把 man​ 页面和 --help​ 输出切块、向量化并写入 Chroma:

# knowledge/ingest.py
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
import os

def ingest(man_dir: str, persist_dir: str):
    docs = []
    for fname in os.listdir(man_dir):
        if fname.endswith(".txt"):
            loader = TextLoader(os.path.join(man_dir, fname), encoding="utf-8")
            docs.extend(loader.load())
    splitter = RecursiveCharacterTextSplitter(chunk_size=800, chunk_overlap=120)
    chunks = splitter.split_documents(docs)
    Chroma.from_documents(
        chunks,
        OpenAIEmbeddings(),
        persist_directory=persist_dir,
    )

在线检索时,rag.py​ 暴露一个 retrieve(query, k=5)​,返回拼接后的上下文文本和来源列表。RAG 的作用是把模型“锚定”在真实文档上,而不是让它自由发挥。


API 端点(commands.py​)

将 RAG → LLM → 安全层串起来:

from fastapi import APIRouter, HTTPException
from backend.app.core import rag, llm, safety
from backend.app.models.schemas import CommandRequest, CommandResponse, CommandCandidate
from backend.app.db import audit

router = APIRouter()

@router.post("/commands/generate", response_model=CommandResponse)
async def generate(req: CommandRequest):
    context, sources = rag.retrieve(req.query)
    raw_candidates = llm.generate_candidates(req.query, context, req.env)

    final: list[CommandCandidate] = []
    blocked_any = False
    block_reason = None

    for c in raw_candidates:
        verdict = safety.evaluate(c.command)
        if verdict["blocked"]:
            blocked_any = True
            block_reason = verdict["reason"]
            continue
        # 用安全层的判定覆盖模型自评,防止模型低估风险
        c.risk_level = verdict["level"] if verdict["level"] != "readonly" else c.risk_level
        c.retrieved_sources = sources
        final.append(c)

    audit_id = audit.write(req, final, blocked_any)
    return CommandResponse(
        candidates=final,
        blocked=blocked_any,
        block_reason=block_reason,
        audit_id=audit_id,
    )

这里有一个重要的设计决策:安全层的判定结果覆盖模型的 risk_level​。即使模型认为某条 kill -9​ 是只读的,规则引擎也会把它提升为 warn​,触发用户确认。


CLI 客户端:让运维真正用起来

后端 API 准备好后,CLI 是最高效的交付形态。使用 Typer 实现:

# cli/opsai/main.py
import typer
import httpx
from rich.console import Console
from rich.prompt import Confirm
from cli.opsai.env_fingerprint import collect

app = typer.Typer()
console = Console()
API_BASE = "http://127.0.0.1:8000/api/v1"

@app.command()
def ask(query: str, confirm: bool = False):
    env = collect()
    with httpx.Client(timeout=60) as client:
        resp = client.post(f"{API_BASE}/commands/generate", json={
            "query": query,
            "env": env,
        })
        data = resp.json()

    if data.get("blocked") and not data["candidates"]:
        console.print(f"[red]已拦截:[/red]{data['block_reason']}")
        raise typer.Exit(1)

    for c in data["candidates"]:
        console.print(f"\n[bold]命令:[/bold]{c['command']}")
        console.print(f"[dim]{c['explanation']}[/dim]")
        console.print(f"风险等级:{c['risk_level']}")
        if c.get("rollback"):
            console.print(f"回滚:{c['rollback']}")

    if confirm and data["candidates"]:
        cmd = data["candidates"][0]["command"]
        if data["candidates"][0]["risk_level"] != "readonly":
            if not Confirm.ask(f"确认执行 `{cmd}`?"):
                raise typer.Exit()
        import subprocess
        subprocess.run(cmd, shell=True)

if __name__ == "__main__":
    app()

​env_fingerprint.py​ 采集 platform.system()​、platform.version()​、当前用户、是否 root 等信息,作为上下文发给后端。CLI 只负责“问”和“展示/执行”,所有生成和安全逻辑集中在后端,便于统一升级。


部署与“成熟”所需的工程配置

​docker-compose.yml​ 把后端和向量库编排在一起,开箱即用:

services:
  api:
    build: ./backend
    ports:
      - "8000:8000"
    environment:
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - CHROMA_PERSIST_DIR=/data/chroma
    volumes:
      - chroma_data:/data/chroma

  chroma:
    image: chromadb/chroma:latest
    ports:
      - "8001:8000"
    volumes:
      - chroma_data:/chroma/chroma

volumes:
  chroma_data:

​backend/Dockerfile​:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

审计日志建议写入 SQLite(轻量)或 PostgreSQL(多人使用),记录 audit_id​、原始 query​、候选命令、用户是否采纳、是否被拦截。这是后续做 Bad Case 分析和模型迭代的数据基础。


让应用真正“成熟”的三个关键点

上面的代码是一个可运行的起点,但要达到生产可用,需要在三个地方做持续加固:

第一,规则引擎的覆盖率和可维护性。 把 safety.py​ 中的规则迁移到 YAML 或数据库,增加版本管理和生效范围(比如区分“生产环境规则”和“测试环境规则”)。可以参考 shellfirm​ 的规则分类思路,但必须自己维护一套面向你团队环境的规则集。

第二,RAG 知识库的质量决定生成准确性的上限。 只导入 man​ 页面是不够的。你还需要导入:不同 OS 发行版的命令差异表、内部 SOP 脚本、常见故障的“现象→诊断命令→修复命令”案例库。ingest.py​ 应该支持按来源打标签,检索时根据 env​ 过滤(例如 Ubuntu 环境只检索 Ubuntu 相关的文档块)。

第三,反馈闭环。 CLI 的 ask​ 命令可以增加一个 --feedback​ 参数,把用户对候选命令的评价(采纳/修改/拒绝)回传到后端。后端定期导出这些数据,分析被拒绝或修改的命令,补充到规则库或 RAG 知识库中。没有反馈数据,模型和规则都不会自己变好。


启动顺序

# 1. 准备知识库
python knowledge/ingest.py --man-dir ./knowledge/man_pages --persist-dir ./chroma

# 2. 启动后端
docker-compose up -d

# 3. 安装并运行 CLI
pip install -e cli/
opsai ask "磁盘满了,帮我看看哪个目录占用最大"

这套代码骨架的价值在于:它已经把“生成—校验—拦截—审计”的主链路完整跑通了。你可以先替换成自己的模型 API key 和知识库,跑起来看到效果,再逐步加固规则、扩充文档、接入反馈数据。成熟度是在这个骨架上迭代出来的,而不是一开始就设计出来的。