手头管着十几台服务器,日常无非是“看监控、查日志、重启服务、被同事叫醒处理告警”。直到某天凌晨,我发现自己在重复三年前刚入行时就在做的事——SSH登录、敲top、翻journalctl、甩systemctl restart。那一刻我意识到,与其继续当一个“带键盘的人形监控脚本”,不如把这件事彻底自动化。
这年头正好赶上 MCP(Model Context Protocol,模型上下文协议)概念爆发——它把大模型(LLM)和外部工具用一种标准方式连起来,让 AI 不再只会聊天,而是能真正动手干活。我用 Python 把这个协议和开源 LLM 组合到一起,手搓了一个服务器运维助手:让它自己看负载、自己查日志、自己判断故障、自己执行修复,干完活还要跟我汇报“为什么这么干”。这篇博文就是完整的折腾记录,从架构设计、代码实现到排坑实录,照着做你也能搭一套出来。
1. 为什么是 Python + MCP + LLM?先想清楚再动手
1.1 从 Function Calling 到 MCP:一次痛苦的协议演进
一年前我就在尝试用函数调用(Function Calling)给 LLM 扩展能力,思路很简单:告诉模型“你可以调用这些函数,参数长这样”,模型输出 JSON,代码解析后执行。听起来很美好,实际维护起来全是坑:OpenAI 有自己的一套 schema,Claude 有另一套,国产模型又各不相同,每个模型升级一次,我的配置就废一次。更麻烦的是团队里如果有两个服务想同时接入 LLM,工具代码根本无法共享,只能复制粘贴再各改各的。
MCP 解决的就是这个“重复造轮子”的问题。它的核心模型可以理解成给 AI 世界定了一套标准的“USB 接口协议”:工具提供方(MCP Server)把能力包装成标准化的工具列表和参数定义,客户端(MCP Client)负责和 LLM 交互并路由调用请求,两边只认协议不认厂商。我写一个 MCP Server 暴露几个运维工具,任何支持 MCP 的客户端——Claude Desktop、VS Code 插件,甚至我自己用 Python 写的控制台——都能直接复用,再不用为一个模型写一套适配代码。
1.2 整体架构:没有大脑的服务器只是一堆零件
这套助手的架构可以用一条链路说清楚:你 → MCP Client → LLM → MCP Server → 工具集 → 服务器。真正干活的部分是 MCP Server,它里面挂着一批运维工具:系统状态读取、日志检索、服务管理、命令执行、告警收敛。LLM 是“决策大脑”,它根据你的自然语言请求和当前工具返回的数据,决定下一步调哪个工具、怎么调;MCP Client 是“信使”,专门负责在 LLM 和 Server 之间传话。
这个设计的关键在于“把决定权交给模型,但把执行边界焊死”。LLM 只负责决策是否调用某个工具、生成调用参数,真正吃吐数据的还是代码——我不会让模型直接生成 Shell 命令去碰生产环境,所有命令都经过白名单校验和参数过滤,超时、审计、熔断全都有兜底。这样既享受了 LLM 的推理能力,又不至于被模型的幻觉带进沟里。
1.3 它能帮我解决哪些问题
在完全没有“智能”之前,我的运维日常是流程化的苦力活;有了这套助手之后,它的价值主要体现在这四件事上:
- 告警处理提速:凌晨收到 CPU 过高告警,不用爬起来,先让助手自己登录排查,看是负载均衡问题还是代码 bug,再决定要不要重启。
- 日志问答化:从“给我 grep 一下这个时间段的报错”变成“这个时间段服务为什么重启了”,LLM 会自己组合日志检索工具去翻证据链。
- 巡检报告自动生成:每天早晨让助手把各组件的健康度、资源趋势、潜在风险汇总成一份简报,不用再手动筛监控面板。
- 知识沉淀:每次故障处理完,让助手把排查过程整理成 Markdown 文档归档,后辈接手时能少走一大截弯路。
这套系统适合谁?像我一样的个人开发者、小团队运维、SRE 新手,手里有零散服务器的场景最合适。它不是一个能直接拿来卖钱的产品,但能实打实帮你从重复劳动里解放出来。
2. 核心组件选型:参数背后的真实考量
2.1 LLM 选型对比:按场景选脑子
LLM 是这套系统里唯一带“智力”的部分,选型直接决定了助手是靠谱还是人工智障。我实际测过几类方案,把关键对比整理成了下面这张表:
| 方案 | 推理能力 | 函数调用稳定度 | 数据私密性 | 成本 | 适合场景 |
|---|---|---|---|---|---|
| Claude 3.5/4 系列 | 最强 | 高 | 低(数据出域) | 较高 | 复杂故障推理,可接受数据外送 |
| GPT-4o / o3 系列 | 较强 | 高 | 低 | 较高 | 同上 |
| DeepSeek-V3 / Qwen-Max API | 强 | 中高 | 低(但国内链路) | 低 | 高性价比默认方案 |
| Qwen-72B / DeepSeek-R1 本地部署 | 中上 | 中 | 高 | 硬件成本 | 隐私敏感场景 |
| Qwen-7B / Llama-3.1-8B 本地小模型 | 中等 | 中 | 高 | 极低 | 测试开发环境,或可接受较弱势场景 |
我个人最终的选择是:开发调试用 Qwen-7B 本地小模型(免费、随便折腾),生产巡检用 DeepSeek-V3 API(性价比高),复杂故障复盘的“智囊”用 Claude 3.5 Sonnet(推理稳,但只在数据脱敏后送出去)。如果你没有数据私密的硬约束,直接用 DeepSeek 或 Qwen 的 API 就足够跑起来。
2.2 MCP SDK:官方的和社区的你都要知道
Python 生态里接入 MCP 有两条路:一是官方提供的mcpPython SDK,二是社区封装的各种框架。我一开始用的是原生的mcp.server.fastmcp,它是官方推出的一套高层 API,用装饰器就能把函数暴露成工具,写起来很爽:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("ops-agent") @mcp.tool() def check_cpu_load(threshold: float = 0.8) -> str: """读取当前系统 CPU 平均负载,返回是否超过阈值。""" import psutil load = psutil.getloadavg()[0] return f"当前 1 分钟负载: {load}, 阈值: {threshold}, 状态: {'超载' if load > threshold else '正常'}"写完之后反射出来的 JSON Schema 会自动带进 MCP 协议里,LLM 读到描述就会知道自己能用什么、参数怎么传。除了这条官方路线,社区还有fastmcp之外的一些封装(比如基于 pydantic 的代码生成工具、基于 WebSocket 的 MCP 网关),但它们本质上没有跳出官方协议,我建议新人直接基于官方 SDK 上手,坑少、文档全、遇到问题还能翻源码。
2.3 服务器端工具链:psutil 够用,但别止于此
工具集的实现可以纯靠 Python 标准库 + psutil,但真实运维场景远远不止看 CPU。我把工具分成了四个层级:
- 系统巡检类:
psutil封装 CPU、内存、磁盘、网络 IO、进程列表;uptime查看系统负载。 - 日志检索类:封装
journalctl、grep和tail,带时间范围和关键字过滤,返回最近 N 条。 - 服务管理类:封装
systemctl和supervisorctl的状态查询、启停、重启操作。 - 命令执行类:通用 Shell 执行,但做了命令白名单校验,只允许预先登记过的安全指令。
这里必须强调一点:不要把通用 shell 任意执行暴露给 LLM。我在第一版就踩过坑——让模型用pkill关掉一个服务,结果正则匹配太宽,差点把另一个核心进程带走。后来所有命令执行都改成“先查询白名单中匹配项,再让用户确认参数”,整体稳定性提升了一个量级。
3. 手搓全过程:从零到能跑,每一步都要能抄
3.1 30 分钟搭好 MCP Server 骨架
先把环境准备好。我是在 Ubuntu 22.04 + Python 3.10 上做的,依赖只有几个,一套命令搞定:
python3 -m venv .venv source .venv/bin/activate pip install mcp psutil然后用fastmcp建一个最简 Server。这一步你就已经拥有一个“能被 LLM 驱动”的工具服务了。
# ops_server.py import json import shlex import psutil from mcp.server.fastmcp import FastMCP mcp = FastMCP("ops-agent") @mcp.tool() def get_system_summary() -> str: """获取系统整体状态:CPU、内存、磁盘、负载、开机时间。""" cpu_percent = psutil.cpu_percent(interval=1) mem = psutil.virtual_memory() disk = psutil.disk_usage("/") load = psutil.getloadavg() return json.dumps({ "cpu_percent": cpu_percent, "memory_percent": mem.percent, "disk_percent": disk.percent, "load_avg": {"1min": load[0], "5min": load[1], "15min": load[2]}, }, ensure_ascii=False) @mcp.tool() def get_process_top(n: int = 10) -> str: """获取当前 CPU 占用最高的 n 个进程,默认 10 个。""" procs = [] for p in psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]): try: procs.append(p.info) except (psutil.NoSuchProcess, psutil.AccessDenied): continue procs.sort(key=lambda x: x["cpu_percent"], reverse=True) return json.dumps(procs[:n], ensure_ascii=False) @mcp.tool() def run_systemctl(service: str, action: str) -> str: """执行 systemctl 管理命令。action 只能为 status/start/stop/restart。 示例: run_systemctl(service="nginx", action="restart") """ allowed = {"status", "start", "stop", "restart"} if action not in allowed: return f"不允许的 action: {action},只能使用 {allowed}" cmd = f"systemctl {action} {shlex.quote(service)}" return cmd注意run_systemctl这一步我没有真的执行,只返回了命令字符串。为什么?因为在开发阶段你需要先看模型的“意图”是否正确——它经常写错服务名或 action,直接执行很容易出事故。把“决策”和“执行”分开,是调试期的关键策略。
最后跑起来:
python ops_server.py mcp.run(transport="stdio")3.2 工具研发:四条链路,一个都不能少
纯看状态的工具没有太多含金量,真正有用的是“查询到问题 → 定位原因 → 给出结论”的组合链路。我一套一套拆给你们看。
链路一:状态快照与趋势判断
只读当前的 CPU 数字意义不大,关键是判断趋势。这里我加了一个“历史采样”机制:每隔 5 分钟抓一次负载快照,存到 SQLite 或本地 JSON 文件里。LLM 请求时,返回的不只是一瞬间的值,而是最近 6 小时的走势,它才能判断“现在 80% 是在涨还是在跌”。实现上很简单,用apscheduler定时抓,或者干脆用cron跑一个collect_metrics.py,写入一张时间序列表:
CREATE TABLE metrics ( ts TEXT PRIMARY KEY, cpu REAL, mem REAL, load1 REAL, disk_used REAL );链路二:日志检索闭环
日志是排查故障的第一现场。我用两个工具:一个负责搜关键词(支持时间范围),一个负责抓取某个服务的最近journalctl -u xxx -n 100。这两个工具返回的都是原始文本,但 LLM 真正厉害的一点是能从中提炼因果链——比如“13:41 报 OOM,13:42 服务退出,13:43 自动重启”,它会把这三行自动串联成“内存耗尽导致进程崩溃被 systemd 拉起”。
链路三:服务生命周期管理
前面的run_systemctl只返回命令,这是安全的。但为了让整个流程自动化闭环,我还加了一个“确认执行”接口:LLM 先把要执行的命令和理由写在请求里,只有我们本地 MCP Server 判断命令在白名单内,才真实调用subprocess执行,并把 stdout/stderr 回给模型。如果你想让整个闭环更自动化,还可以接一个“人工确认”钩子,比如通过企业微信或钉钉推一条审批消息。
链路四:告警收敛与自愈
最让人头疼的是告警风暴——同一个服务挂了,监控系统五分钟发一次消息。我在 Server 里写了一个简单的“故障聚合器”:相同服务、相似报错在 30 分钟内只发一次告警,并自动触发一次 LLM 排查。如果排查结果明确是内存泄漏,且服务名在白名单内,LLM 会主动给出重启建议;如果重启后五分钟内再次告警,系统会自动“熔断”——不再自动重启,转而升级给人处理。这个“自愈但永远保留逃生舱”的思路,是 AI 运维最容易被低估的工程实践。
3.3 MCP Client 接入:让 LLM 真正“看见”工具
MCP Server 建好了,但 LLM 要怎么连上来?我推荐三套方案,由易到难。
方案一:Claude Desktop 直接接入
在 Claude Desktop 的配置文件claude_desktop_config.json里加一行:
{ "mcpServers": { "ops-agent": { "command": "python", "args": ["/绝对路径/ops_server.py"] } } }重开客户端,你就能在对话里直接输入“帮我看看刚才 1 分钟负载多少”,Claude 会自动调用get_system_summary和get_process_top,整个推理过程还会显示“正在使用工具”。这是体验最丝滑的一条路,适合快速验证工具描述写得好不好。
方案二:自写 Python Client 走 MCP 协议
如果你不想依赖闭源客户端,可以用官方的mcp客户端库写一个极简 REPL:
import anyio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): server_params = StdioServerParameters( command="python", args=["ops_server.py"], ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() for t in tools.tools: print(f"工具: {t.name} -> {t.description}") res = await session.call_tool("get_system_summary", {}) print("结果:", res) anyio.run(main)这个客户端的意义在于:你可以完全不用商业大模型的客户端,而是把你选的任何 LLM(包括本地小模型)接入 MCP。它把你的系统从“依赖 Claude Desktop”解放成了“依赖任何 LLM API”,自由度一下子高了很多。
方案三:完整对话式封装
这是最接近真实产品形态的一种。我写了一个ops_chat.py,先启动本地 LLM(或调用 DeepSeek API),每次用户输入先让 LLM 输出工具调用 JSON:
{ "tool": "get_process_top", "args": {"n": 5} }然后由代码调用 MCP 工具,拿到结果再喂给 LLM 循环推演。这个过程中需要维护一个“会话上下文窗口”,记录每次工具调用和结果,LLM 才能在前序结果之上继续推演。比起前面两种方案,这个封装能完全控制推理次数、工具调用上限和超时策略,是真正适合生产级别的结构。
4. 避坑实录:我在真实环境里踩过的那些雷
4.1 工具描述写得差,LLM 再强也白搭
这是我第一周最深刻的教训。最初我写的工具描述只有一句话“获取系统状态”,然后 LLM 各种误解参数:过 threshold 传字符串、把服务名写成 IP 地址。后来我老老实实按照“This tool does X;Parameters:xxx means yyy;Returns:zzz format”的规范重写了一遍,调用成功率从不到 50% 直接拉到 90% 以上。工具描述就是给 LLM 看的 API 文档,你糊弄它,它就糊弄你。
4.2 命令执行失控,差点带走半个集群
前面已经提到过一次,但这是我真正最恐慌的一刻,必须单独拎出来讲:一开始我以为 LLM 足够聪明,直接允许它调subprocess跑任意命令。某次模拟演练,我故意让它“重启所有内存占用超过 1G 的进程”,它生成的pkill -f正则把java、nginx、mysql全部匹配了进去,如果不是测试环境,那就是一场生产事故。从那以后,我的每一条命令都必须满足以下三个条件之一才允许执行:在白名单内、参数必须通过shlex.quote、服务名必须存在于 systemd 单元列表。宁可让流程多一步人工确认,绝不把裸命令执行权交给模型。
4.3 LLM 的上下文窗口是“死亡陷阱”
日志工具返回的是大文本,动辄几万字符,LLM 的上下文窗口有限,一旦塞满,模型就会“遗忘”前面的工具结果,开始自说自话。我在这里做了三层处理:一层是裁剪日志行数,工具只返回最重要的前 20 行;一层是摘要优先,如果 LLM 需要看详细日志,先让它链式调一个summarize_lines工具;最后一层是上下文策略,超过多少 token 就强制开启新会话。你可以用 token 计数库(如tiktoken)实时监控,也可以用最笨的“字数切分”在测试环境先用起来。
4.4 模型幻觉:它可能会编造“看起来合理”的命令
幻觉问题无解,只能缓解。我见过它编造一个根本不存在的 systemd 服务名,也见过它把--force参数拼在systemctl restart后面(实际上 systemctl 根本没有这个参数)。缓解办法是:给工具加上严格的参数枚举验证,服务名传进来先查一次 systemd 列表,action 必须匹配status/start/stop/restart四选一。一旦验证失败,MCP Server 必须返回明显错误信息,而不是静默忽略。
4.5 常见问题速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| LLM 不调用任何工具,答非所问 | 工具描述太抽象,模型不知道能用 | 重写描述,加入具体示例参数 |
| 调用总是报参数错误 | Pydantic Schema 与实际参数不匹配 | 用tools.schema现场检查 JSON Schema |
| 本地模型输出 JSON 格式错乱 | 小模型函数调用能力弱 | 升级到 70B 级模型,或加输出格式校验后重试 |
| 工具执行超时导致会话中断 | 日志检索文本太大了 | 加timeout=10,并在工具内做截断 |
| MCP Server 启动后客户端连不上 | 路径或 Python 环境不对 | 先在命令行手动跑一遍 Server,确认能正常启动 |
| 重启服务后仍然异常 | systemd 服务依赖外部资源未就绪 | 让 LLM 查看服务状态后二次诊断,不要盲目重启 |
5. 实战演练:一次完整的 CPU 飙升自动排查
为了让你更直观地理解整个闭环,我写了一段模拟对话。这是我在测试环境真实跑过 20 多次之后的最佳效果。
用户输入:帮我看下为什么刚才 1 分钟负载特别高
MCP 链路实际发生的事:
- 我先手动把一台测试机压了一个高负载进程,然后触发对话。
- 我的自写 Client 把用户输入交给 LLM。
- LLM 发现用户要“看负载”,自动调用
get_system_summary。 - 工具返回:
{"cpu_percent": 97, "load_avg": {"1min": 12.3, "5min": 8.5}, ...}。 - LLM 看到 1 分钟负载远高于 5 分钟,判断“短时尖峰”,于是调用
get_process_top(n=5)。 - 工具返回:
[{"pid": 3201, "name": "python3 stress.py", "cpu_percent": 400, ...}]。 - LLM 给出结论并附上建议:
CPU 尖峰由 PID 3201(stress.py)导致,该进程占用 4 核。建议观察 5 分钟,如仍持续可终止该进程。 - 用户点头确认,LLM 才继续调用
run_systemctl(或者一个专门的kill_process白名单工具)去清理。
这一步的体验真的“上头”——从“发现故障”到“定位原因”只需要一轮对话,过去我要自己敲至少五条命令。更妙的是,整个过程里 LLM 还会主动拉住你:“不建议立即 restart,因为进程可能是压测任务,建议先确认业务影响。”这种“能动手但不乱动”的克制,恰好是 AI 运维最重要的工程素养。
6. 进阶玩法与经验谈:这玩意儿还能怎么浪
6.1 引入“LLM as Judge”做自动化巡检
“模型当评委”是个很香的进阶玩法。做法是:让一个专门的评审 LLM 每天审视运维助手的排查日志和行动记录,给它的推理质量打分,并挑出错判、漏判和过度自信的地方。这样相当于给你的 AI 运维系统装了一个“AI 质检员”。我在prompt里写了一组评分细则:是否依据前一环数据推理、是否给出可回溯证据、是否在操作前提示风险、是否在未知情况时主动求助。这些评分会定期回流成微调样本或规则补充,让助手越用越准。
6.2 多服务器纳管:它能不能管 100 台?
单机版跑通后,自然想扩展。我的做法是让 MCP Server 不再只连本机,而是通过 SSH 或 agent 节点管理多台机器。你可以给 MCP 加一个“节点选择”参数:node="web-01"时,工具内部登到对应主机执行命令。这里有个大坑:SSH 会话延迟高,LLM 很容易超时。我的优化是给每个节点加本地缓存队列,状态查询走缓存,命令执行才实时拉取;同时把get_system_summary的响应压缩成一行短格式,减少上下文开销。扩展到 100 台时,最核心的不是你会不会写工具,而是“工具返回的数据如何被 LLM 高效消费”——信息密度和格式统一,比工具数量重要得多。
6.3 容错与自愈:AI 运维最后一道防线
有一件事我印象特别深刻:某次调试中,我故意让 MCP Server 在调用中途崩溃(文件处置不当抛异常),结果 LLM 自动嗅探到“工具调用失败”信号,居然编造了一个不存在的“重试成功”结果反馈给我。这件事让我明白,LLM 会本能地“圆谎”,因为它被训练成要博取认同。从此我在工具抛错时一律返回结构化错误码,并强制要求 LLM 必须描述错误码含义后重新决策。这套容错设计的原则很简单:异常一旦出现,先阻断执行,再让模型说清楚“我看到什么、我要做什么、依据是什么”,不满足这三条就不放行下一步。
6.4 最后分享几个真实经验
如果你要复刻这套系统,我最大的建议是:别想着一步到位。先跑通“人工发消息 → 看工具描述 → 手动调用工具 → 把结果贴给模型”的最初形态,再慢慢加自动化。我在测试期写过一个“傻瓜模式”,就是不真正执行任何命令,只打印 LLM 认为应该执行的命令,跑了整整三天,把所有可能触发危险的情况都暴露出来,才开始让它在测试环境放行真实命令。靠着一级一级放宽权限,现在它已经能独立完成绝大部分巡检和基础故障自愈。
回过头来看,这件事最有价值的地方也许不是“运维自动化”本身,而是它让你真正摸清了 MCP 和 LLM 的能力边界。以前我会怀疑“AI 能不能取代运维”,经过这一年的实践,我的答案是:它能取代那些“重复的、有规律的、可描述的”工作,但它既需要充分的工具支持,也需要人的工程兜底。把这套 MCP 思路从运维挪到别的领域——数据库管理、CI/CD 流水线、甚至客服工单自动分类——你会发现,过去所有依赖 API 调用的场景,都能用同样一套架构套进去,让 LLM 变成真正的“数字员工”而不是“聊天机器人”。这就是我这一整版折腾下来最核心的收获:先把工具交给模型,再给世界加上一点永不关闭的保险丝。