
这个项目最早源于一次凌晨的重启事故。当时值班的同事在巡检群里说了一句我重启一下数据库备节点然后 SSH 连上去执行了重启命令命令刚发出去他的网络就断了。等第二天早上过来看节点确实重启了但服务进程根本没有拉起来整整几个小时对外服务是降级的。问题不是他没有执行命令而是命令执行之后没有任何人、任何机制在盯着结果。这件事让我意识到运维自动化的核心痛点不是能不能执行命令而是执行完之后如何确认状态真的恢复了。那之后我花了两三个月时间做了一个对话式 Linux 运维 Agent内部代号就叫 X Agent。它的核心能力只有三个通过对话方式下发运维指令高危操作强制人工确认绝不自动执行重启、断线这类场景下自动守候、自动复检。这篇文章把这个项目的设计思路、踩坑过程和最终沉淀下来的机制完整讲一遍。如果你也在做运维自动化、对话式 Agent 或者内部运维中台可以参考这套方案。1. 一次重启没复检事故催生的项目目标与边界1.1 复盘那场事故不是人的问题是工具链断了那台数据库备节点其实只是一个小问题机器负载高计划重启让系统恢复干净。同事的命令本身没有错重启操作也正常执行了真正出问题的环节是命令执行之后。他在断线前看到的是reboot 命令已下发但 reboot 之后系统会断开所有会话他本地网络又恰好抖动后续就再也连不回去了。等第二天人工发现的时候节点已经起来几个小时了服务却一直没有恢复。过程复盘下来大家都清楚问题出在哪里我们现有的自动化工具只能做到执行命令做不到命令执行之后再确认一次。这不是人的责任心问题。凌晨三四点人的注意力本来就差工具链又没有提供任何兜底出一两次错几乎是必然的。当时我们内部已经有一套基于定时任务的巡检系统也有一个简单的运维机器人能执行预设脚本但这些都是单向指令发出去就结束了没有状态回传没有闭环确认。缺的正是执行—守候—复检—告警这个闭环链路。1.2 对话式 Agent 要解决的三件事会聊、会干、会盯做这个项目之前我把需求拆成了三件事。第一件事是会聊也就是对话式交互。用户不写复杂的命令直接说帮我看看 Web 服务的状态把日志目录里三天前的文件清理掉Agent 负责理解意图并把自然语言翻译成可执行的动作。第二件事是会干也就是真正执行。这包含命令的生成、校验、分发以及执行进度的跟踪。这里有个关键点执行必须有边界不能模型说什么就执行什么。第三件事是会盯也就是守候与复检。命令执行完成不代表任务完成特别是重启、重载、扩容这类操作Agent 必须持续观察系统状态直到服务确认恢复正常或者确认失败并触发告警。这三件事里最初我以为会聊是最难的大模型出来之后对话理解已经不是瓶颈。真正难的是后面两件事——怎么在保证安全的前提下执行怎么在断线、重启这类极端情况下还能盯住结果。1.3 明确不做什么边界感很重要做工具最容易犯的错误是什么都想做最后什么都做不好。我在设计 X Agent 时就给自己划了几条边界。不做无条件接管。任何自动化系统都不应该完全接管生产环境Agent 只负责执行有明确授权的操作所有高危动作必须有人工确认。 不做故障自愈。Agent 不会在检测到异常后自动尝试料伤操作这类操作风险极高必须人工决策。 不做复杂业务编排。对话式执行最适合的是单机操作、常规变更、状态巡检、日志排查这类场景复杂的多系统依赖变更还是应该走正式的变更流程和发布系统。划清楚边界之后后面的设计就简单很多核心不是堆功能而是把安全确认和断线补盯这两件事做到极致。2. 四层架构与关键设计取舍会话、意图、执行、守候各管一段2.1 四层分层带来的好处某一层挂了不影响其他整个系统的架构我最终设计成了四个层级简单说就是会话层、意图层、执行层、守候层。四层各自独立互不阻塞。会话层负责和用户对话接收自然语言指令把执行结果用自然语言回传给用户。这一层不碰任何真实命令只做语义转换和展示。意图层负责听明白用户想干什么把自然语言翻译成结构化的任务描述。比如重启一下 Web 服务被翻译成一条任务记录操作类型restart_service目标web风险等级high确认人当前用户。这一步还会附加风险分级结果。执行层负责真正在目标机器上执行动作。它只接受意图层转过来的结构化任务自己不做任何决策。执行层还会做一次最终的命令级校验比如命令里出现了 rm -rf / 这种明显危险的指令直接拒绝。守候层是这次项目里最特别的部分。它负责盯着已经下发的任务尤其处理重启、断线这类场景。即使执行命令的 SSH 会话断了守候进程也会继续运行在目标机器恢复后自动复检。四层分离最大的好处是会话层断连不影响执行层执行层超时不影响守候层每一层挂掉都有迹可循。2.2 技术选型为什么不用一条龙同步调用一开始我觉得架构不用搞复杂用户提问后直接同步执行命令、返回结果不就完了吗后来发现不行。原因是重启、断线这类操作天然是长耗时的而且执行过程可能经历多次网络断开、节点重启用同步调用的方式根本等不到结果。最终我选了异步任务队列 独立守护进程的组合。用户发出请求后意图层生成任务并写入任务队列执行层消费任务并把命令推送到目标机器执行。执行结果不是直接返回给用户的会话而是写入任务状态表。用户随时可以查询任务状态Agent 也会主动推送进度。守护进程独立于用户会话运行。哪怕用户的 Web 页面关了手机 App 切后台了守护进程依然在跑。这个设计模仿的是值班人员盯着命令跑完的过程只是把它自动化了。2.3 一次正常对话的完整流转过程我用一个实际的例子说明整个流转过程。用户说帮我重启 Web 服务然后确认一下服务起来了没有。会话层把这句话传给意图层。意图层解析出三个要素操作是重启对象是 Web 服务还有一个补充要求是确认服务恢复。系统自动把这条任务标记为高风险写入任务队列。执行层消费任务后先做命令校验然后生成一条包装后的命令先执行 systemctl restart 对应的 Web 服务再附带一个状态采集脚本输出服务进程、监听端口、健康检查接口状态三项数据。因为这是高风险操作系统不会直接执行而是给用户推送一条确认卡片卡片上写清楚要执行的完整命令、影响范围、预计耗时。用户点确认后执行层才真正下发命令。命令下发后用户的 SSH 会话可能在重启瞬间断开这时候守候层接管每一分钟探测一次目标机器是否恢复直到任务超时或确认成功。这个流程每一步都有记录每一步的状态都能在会话里查到。用户完全不需要操心命令细节只需要做最关键的那个决策这个命令要不要执行。3. 高危操作强制人工确认从风险识别到双击确认3.1 风险分级三层判断不能让模型背锅高风险操作自动执行是运维事故的温床。我在设计时做了一个三层判断机制确保风险识别不会只依赖某一个环节。第一层是关键词规则。命令或任务描述中如果包含删除清空重启格式化改权限执行脚本这类高危词语直接标记为高风险。这一层虽然粗糙但足够快适合过滤绝大多数常见危险操作。第二层是命令解析。对最终生成的真实 shell 命令做静态分析检查是否包含破坏性指令、是否涉及系统目录、是否包含管道组合。比如 rm -rf /、dd 写设备节点、 /dev/sda 这类命令在这一层直接拦截。即便用户原本想说的是清理临时文件生成的命令如果越界也会被拦下来。第三层是白名单。对于已经经过验证的低风险常规操作比如查看磁盘空间、查看进程状态、读取日志文件可以加入白名单跳过确认。白名单不是用来弱化安全的而是减少对用户的打扰。每个人都不希望每次看个内存占用都要弹确认框。这三层的优先级是任何一个模型生成的行为如果落进了白名单就直接执行如果没落进白名单就按危险程度分档低风险直接执行中风险记录并通知高风险强制等待人工确认。下面这张表是我实际使用的风险等级策略风险等级典型操作执行策略低风险查看状态、读取日志、磁盘查询自动执行并记录中风险清理临时文件、修改非关键配置执行后通知用户高风险重启服务、清空日志、删除文件、改权限强制人工确认后才执行禁止级格式化、删除系统目录、向块设备写入直接拒绝仅允许人工在服务器上操作3.2 确认交互细节唯一令牌、会话锁定、超时默认拒绝强制人工确认听起来简单实现细节里全是坑。我踩过的第一个坑就是确认了但执行的是另一条命令。解决方法是给每个高风险任务生成一个唯一的确认令牌令牌和任务 ID、命令哈希绑定。用户确认的不是一句空洞的我同意执行任务而是确认一串具体的命令哈希。执行层在真正跑命令之前会再比对一次哈希防止在确认之后到执行期间命令被意外篡改。第二个细节是会话锁定。当高风险任务等待确认时同一个用户的其他高风险任务必须排队避免用户连续收到多个确认框手滑点错。这里我用了简单的互斥锁同一用户在同一个时间只能有一个待确认的高风险任务。第三个细节是超时机制。确认卡片发出后默认等待 30 秒超时未点击一律视为拒绝。30 秒是个合理值足够用户读完命令又不至于让操作卡太久。目标机器上如果处于一个半执行状态宁可不执行也不能等一个永远不出现的确认。下面是一段简化后的伪代码展示等待确认的核心逻辑import asyncio import uuid class GuardedTask: def __init__(self, cmd, risk_level, user): self.task_id uuid.uuid4().hex[:8] self.cmd cmd self.risk_level risk_level self.user user # 唯一确认令牌和命令哈希绑定 self.confirm_token uuid.uuid4().hex[:16] self.cmd_hash hash(cmd) self.status pending async def run_guarded_task(task, executor, notifier): # 检查是否命中白名单命中则可直接执行 if task.risk_level low and whitelist.match(task.cmd): return await executor.execute(task) if task.risk_level high: notifier.send_confirm_card( task_idtask.task_id, cmdtask.cmd, tokentask.confirm_token, wait_seconds30 ) try: confirm await asyncio.wait_for( wait_for_confirm(task.confirm_token), timeout30 ) except asyncio.TimeoutError: # 超时默认拒绝 task.status rejected_timeout notifier.send_result(task) return if confirm.direction ! approve or confirm.cmd_hash ! task.cmd_hash: task.status rejected notifier.send_result(task) return # 通过校验后正常执行 result await executor.execute(task) task.status completed if result.ok else failed notifier.send_result(task)注意这里有个容易被忽略的点确认接口传回来的必须是命令哈希的比对结果不能只传一个同意的布尔值。否则你在确认界面看到的是一行命令实际执行的是另一行这个漏洞比不用 Agent 还危险。3.3 高危操作完整流转示例重启 Web 服务我挑一个最典型的高危操作来讲完整流转用户说重启 Web 服务。第一步意图层解析出重启 web两个关键要素风险分级模块直接判定高风险。第二步执行层生成将要执行的完整命令比如 systemctl restart nginx具体服务名以资产库为准同时生成命令哈希。第三步用户手机上弹出确认卡片卡片上显示目标服务器 IP、要执行的完整命令、影响范围说明该服务中断约 10 秒、任务等待超时时间。第四步用户点击确认。系统把确认结果里的哈希和待执行命令的哈希做比对确认一致后才放行。第五步命令通过一个持久的执行通道下发目标机器执行。第六步守候层开始工作检测 Nginx 的进程、端口和健康检查接口。这套流程走下来我个人的感受是真正的安全感不是来自风险识别算法多先进而是来自最终那条强制确认链路是否滴水不漏。只要人没有确认任何代码都不能碰目标机器。3.4 误报与漏报的平衡我踩过的调优记录最早版本的风险识别误报特别多。比如运维同学说清理一下日志目录里三天前的旧文件关键词规则匹配到清理立刻判定为高风险必须确认。这种误报用了一段时间后大家就开始烦躁甚至有人故意点确认不去看内容。所以我做了两个调整一个是把常见的安全清理操作整理成白名单模板比如清理指定路径下 N 天前的 .log 文件属于受控操作可以自动执行另一个是调整了高风险命中逻辑不再单独看清理这个词而是看命令解析后的完整行为是否涉及系统目录、是否使用了通配符删除、是否带 -rf 参数。漏报的问题同样存在。早期版本对systemctl disable xxx这种操作没有判定为高风险后来有一次差点把核心服务设为开机不启动我才把涉及服务启停、开机自启、iptables 规则变更的操作全部拉进高风险列表里。误报和漏报的平衡没有一劳永逸的解法只能靠在实际使用中不断积累规则。我建议做类似系统的人一定要给风险规则库留出可配置的空间而不是把规则硬编码在代码里。4. 重启断线自动守候复检把发完命令就当甩手掌柜变成过去式4.1 断线为什么会丢结果TCP、SSH、进程三者的关系前面提到的那起事故本质上是三个环节同时断裂。第一环是 TCP 连接。用户和服务器之间的 SSH 会话基于 TCP网络抖动或者服务器重启都会让连接中断命令执行的进度就传不回来。第二环是 SSH 会话。执行远程命令时通常依赖 SSH 会话一旦会话断开和会话绑定的远程进程可能收到 SIGHUP 信号被杀掉命令就变成了半执行状态。这也是为什么直接在 SSH 里跑长任务经常不靠谱。第三环是 Agent 进程自身。如果 Agent 进程跑在用户本地或者跳板机上用户设备断网、关机Agent 也一起消失了自然没人继续盯任务。所以要解决重启断线自动守候复检核心不是提高网络稳定性而是把任务从用户会话和Agent 前台进程中彻底剥离出来。4.2 守候机制把任务搬出对话进程我的做法是所有任务在正式执行前会被包装成一个独立单元由系统守护进程接管。具体执行的时候我用类似 systemd-run 的方式把命令放进一个独立的运行单元。这样即使发送命令的 SSH 会话断开、用户的浏览器关闭、甚至 Agent 的主进程重启任务单元都还在系统里跑着守候进程也能继续查询它的状态。下面是一个简化的示例用 systemd-run 创建一个独立的临时任务单元# 将任务包装成独立 systemd unit并脱离当前会话运行 systemd-run --unitagent-task-12345 --scope \ /usr/local/bin/agent-exec --task-id 12345 --action restart-web如果目标机器本身支持 systemd这种方式很干净。任务的标准输出和错误输出会被记录到 journald故障排查时直接 journalctl -u agent-task-12345 就能看到完整日志。对于没有 systemd 的老系统退而求其次可以用 nohupsetsid 方式脱开会话但可靠性和可观测性都会差一些。我个人建议尽量统一用 systemd运维工具本身就必须建立在可靠的初始化系统之上。除了命令脱钩任务状态也必须持久化。我在实现的架构里把任务状态写进了一个本地队列和一张状态表包含任务 ID、期望动作、当前状态、创建时间、最后心跳时间。这样哪怕 Agent 主进程重启只要信息表还在守候进程就能恢复对任务的跟踪。4.3 复检机制端口活着不代表服务好了守候任务最核心的部分是复检逻辑。很多人理解复检就是ping 一下目标机器通不通这是完全不够的。拿重启 Web 服务举例我的复检逻辑分三层第一层是网络探测。TCP 端口能不能连上连不上直接判定失败。这一层最基础但能筛掉大多数还没起来的情况。 第二层是进程检查。目标进程是否存活、运行状态是否 active。进程在但端口没开说明服务可能卡在启动阶段。 第三层是业务探测。访问健康检查接口不仅看返回码还要看返回体里的业务状态字段。服务可能 HTTP 200但内部依赖连不上数据库这种假活状态只有业务探测能发现。这里我总结了一张复检参考表复检项探测方式通过条件网络层TCP 连接目标端口连接建立成功进程层检查服务进程 status进程存在且为 active接口层GET 健康检查 URLHTTP 2xx 且返回体状态正常业务层执行轻量业务查询查询能返回预期数据顺序上有个经验不要并行探测要按层级递进。先确认网络通再确认进程状态最后才做业务探测。否则网络还没稳定就急着查业务接口会得到一堆误导性的失败告警。复检不是只探一次而是需要多次探测、逐步退避。我的默认策略是前 30 秒每 5 秒探测一次之后每分钟探测一次持续 10 分钟。只要有一次全部通过任务标记为已恢复。如果 10 分钟内始终有一项不通过任务标记为复检失败触发告警通知值班人。因为重启过程本身可能需要几十秒甚至几分钟有些服务启动慢前几次探测失败是正常的没必要一见失败就告警。退避重试的核心是给服务恢复留出足够时间同时不让失败状态无限静默。4.4 告警与重试策略不是所有失败都要立刻报警复检失败后的告警策略同样需要仔细设计。一开始我采用的是失败一次告警一次结果半夜被假告警炸醒好几次。后来改成分级告警连续三次探测失败进入可疑状态按严重程度该任务会启动备用探测路径持续 3 分钟依然失败才发送紧急告警持续 10 分钟未恢复则直接通知一线负责人。告警消息里必须带三个信息任务 ID、目标服务、最近一次探测的具体结果。只有状态信息没有结果明细值班同学依然需要登录机器排查那就失去了告警的意义。我还给守候任务加了一个止损开关。如果确认服务起不来任务允许人工触发取消守候并保留现场日志方便后续排查。这个开关必须人工触发守候进程不能自己取消自己。5. 落地过程中我踩过的大坑与最终调优5.1 模型幻觉差点造成事故命令校验层不能少对话式 Agent 绕不开大模型大模型就可能有幻觉。我遇到过最危险的一次用户说查看一下 Nginx 的错误日志模型生成的命令里居然带了一个错误的路径本来应该打开的日志文件不存在如果这条命令直接执行虽然不至于删掉东西但用户会拿到一个文件不存在的错误返回。比这更危险的是一次模拟测试用户说清理 /tmp 下临时文件模型生成的命令因为引号转义问题路径解析变成了根目录下的文件。这一下如果没人拦事情就大了。好在那时命令校验层已经上线命令解析发现了路径越界并直接拒绝执行。所以我的建议是对话式运维 Agent 里大模型只负责理解意图和生成命令草稿绝不能直接执行原版命令。真实执行之前必须经过命令校验层校验层做三件事解析命令结构、检查路径合法性、比对风险规则。这条底线守住了AI 幻觉最多只造成误报不会造成事故。5.2 绕过人工确认的三条岔路sudo、代理登机、批量脚本对话式 Agent 上线之后我一度以为确认机制很完善了结果还是被运维同学绕过了三次虽然是无意绕过但足以说明规则漏洞。第一条岔路是 sudo 嵌套。模型生成的命令如果是 systemctl restart nginx确认链会触发但如果生成的是 sudo systemctl restart nginx同样的命令因为多了 sudo 两个字早期版本里竟然没被识别成同一条高危命令。修复方式是命令哈希生成前先做归一化去掉 sudo、去掉多余空格、统一参数顺序再做风险判断和哈希比对。第二条岔路是代理登录。用户在对话里说帮我在跳板机的另外一台机器上执行一下如果我的执行层只识别当前目标机器而用户通过交互把命令传到了另一台机器确认机制就形同虚设。修复方式是所有跨机器执行必须显式声明目标资产任何未登记的资产都不能执行批量命令。第三条岔路是脚本批处理。用户写了一个脚本上传然后执行脚本内部调了一堆危险命令但对话层只看到执行脚本 xxx.sh。这种场景光是检查命令字符串不够必须对脚本内容做同样风险扫描。如果脚本内包含高风险操作同样要走人工确认流程。这个问题的教训是确认机制不是钉在某个环节上就能生效的必须覆盖所有可能的执行路径包括间接执行。5.3 上下文丢失问题会话 ID 和任务 ID 要绑定用户和 Agent 的对话是持续的但每一条任务执行完后Agent 要能清楚地知道刚才用户想干什么、现在进行到哪一步了。我早期版本架构里任务状态和会话上下文是分离存储的结果用户问刚才那个重启任务怎么样了Agent 答不上来因为它不知道当前会话关联了哪个任务。后来我把会话 ID 和任务 ID 做了强绑定每个会话维护当前活跃任务列表每个任务维护所属会话。用户发消息时如果消息里没有任何与当前任务相关的关键词Agent 默认新开任务如果提到刚才那个继续盯一下这类指代就自动匹配当前会话最近的任务。还有一点上下文不能只放在内存里。Agent 主进程一重启内存里的任务上下文全丢。必须持久化哪怕是一张简单的 SQLite 表也比全放内存稳得多。5.4 权限模型演进观察者、执行者、审批者三种角色最后是权限模型。最初我以为能做运维的人都是可信的确认机制只是防误触不需要区分角色。后来发现权限粒度不够会导致一个尴尬情况一个刚入职的初级运维也能发起删库级别的命令让高级审批人去确认这显然不对。我把角色分成三类观察者、执行者、审批者。观察者只有读权限能看状态、看日志、看任务结果不能发起任何变更操作。 执行者能发起常规变更和中低风险操作高风险操作提出申请后必须由审批者确认。 审批者能审批高风险操作也能发起操作但他的操作同样要记录在案。这套模型上线之后很多流程变得清爽了初级运维发现问题后可以在对话里发指令Agent 自动识别需要审批的任务推送给高级审批人审批通过后自动执行并复检。整个链条里没有人在跳板机上手工敲危险命令所有高危操作都有据可查。我建议做同类系统的人一定要把权限模型前置不要等上线之后再补。权限后面补的时候经常会遇到历史任务无法追溯、审批记录不完整、责任界线模糊的问题非常麻烦。最后分享一点我的真实体会整个项目做下来我最大的感受是对话式 Agent 的难点从来不在对话本身而在对话之外的安全闭环和执行闭环。你做出来的东西不是一个聊天机器人而是一个带着手和眼睛的自动化系统。手要稳眼睛要准手没经过允许不能乱动眼睛要一直盯着结果直到确认完成。最后分享一个实际调优的小技巧复检时不要只看端口一定要加一道业务接口的探测。端口活着只能说明进程在监听说明不了服务真的可用。比如某个服务依赖的数据库连接池满了端口照常开放但业务已经半瘫痪。这种状态用端口探测永远发现不了只有业务探测才能抓住。把这一层加上之后X Agent 处理重启类任务的可靠率提升了一个档次。如果你也在做运维 Agent我的建议是先聚焦一个场景比如重启服务并自动复检把人工确认和断线守候的闭环做扎实然后再往外扩展。这个场景做好了你会获得一套可以复用的安全机制和执行框架之后再加巡检、加变更、加日志分析都会轻松很多。