导语:官方文档写着“建议 2GB 内存”,而你的 VPS 只剩 154MB 可用——这时候把 AI Agent 塞进去,是鲁莽还是工程?本文记录了一次真实的低配落地:Hermes 最终跑起来了,但真正卡住我们的不是内存,而是 systemd 的资源边界、sudo 的隐形后门、以及一条被忽略的 PHP 版本依赖链。这不是一篇“成功教程”,而是一份“踩坑地图”,给同样在资源夹缝里做架构决策的人。

一、为什么要在“不够用”的机器上做这件事
这台 VPS 上已经跑着 Akkoma(SNS)与 PostgreSQL、Nextcloud、Cloudflare Tunnel、Tailscale 与 fail2ban。新增一个 AI Agent,本质是一次资源竞争实验:我想知道在真实负载下,新服务的引入会把现有服务推到什么程度。选择在余量极小的环境里试,是为了提前暴露问题,而不是等上线后再被动救火。
事后复盘,这种“在准生产机上做压力试验”的做法本身有风险。更稳妥的路径是:先在隔离的临时实例上验证资源画像,再回到生产机做受控变更。本文记录的是真实过程,但不建议照搬到承载关键业务的机器上。
二、动手前的基线:先学会正确读内存
2.1 初始资源画像
| 指标 | 初始值 | 判读要点 |
|---|---|---|
| 总内存 | 891MB | 远低于官方建议的 2GB |
| available | 约 154MB | 唯一可信的余量指标 |
| swap 使用 | 约 1GB | 已大量换出,说明内存长期紧张 |
2.2 三个容易读错的地方
- 看 available,不看 used / buff/cache。buff/cache 在需要时会被回收,属于可支配余量;used 高不代表没救。
- swap 被使用 ≠ 有余量。数据被换到磁盘上,换来的是延迟上升,不是容量增加。
- “够不够”要看峰值,不看均值。Agent 类服务有明显的突发内存特征,必须按峰值规划。
低配环境的第一原则:先腾出确定性余量,再谈部署。不要指望 swap 兜底,它只是把 OOM 变成卡顿。
三、腾内存:停掉最大的消费者,但要先做依赖排查
占用最高的是 DNS 过滤服务 AdGuardHome,约 135MB。停它之前,必须确认没有其他组件依赖它作为 DNS 解析入口,否则一停就是全站通信中断。
3.1 停服前的依赖核查清单
- VPS 自身的
/etc/resolv.conf是否指向 AdGuardHome - Docker 守护进程的 DNS 配置是否引用它
- Tailscale 的 DNS 设置是否指向本机
- 是否有其他终端把本机当作 DNS 服务器
确认无依赖后停止,available 从 154MB 提升到约 295MB。这一步是整场实验的“启动资金”。
3.2 备份:没验证过的备份等于没有备份
删除配置前做了退避,但过程中遇到 cp 中断、退避目录被 root 占有导致内容不可读等问题。最终用 diff -rq 逐项比对,确认与原目录一致后才继续。
“我备份过了”是运维事故里最常见的一句遗言。备份的可信度 = 恢复验证的可信度,没有做过 diff 或试恢复的备份,不要当作安全网。
3.3 清理未使用组件
snap:移除 lxd 与 core22,退避数据仅 34kB,容量收益有限,但减少了常驻进程。multipathd:确认未被使用后停止。“应该没用到”必须变成“验证过没用到”。
四、用 systemd 给 Agent 划出硬边界
这里纠正了一个关键误判:最初以为 Hermes 必须用 Docker 跑,实际上官方以 CLI 为主,Docker 并非必需。省掉容器层,本身就能减少一部分内存与复杂度开销。
真正的隔离靠 systemd unit 的 cgroup 限制实现:
[Service] MemoryHigh=300M MemoryMax=400M MemorySwapMax=0 Restart=always OOMScoreAdjust=800
| 指令 | 作用 | 架构意图 |
|---|---|---|
| MemoryMax=400M | 硬上限,超限触发 cgroup OOM | 只杀 Agent,不拖垮整机 |
| MemoryHigh=300M | 软上限,超限先节流回收 | 给突发留缓冲,避免直接击杀 |
| MemorySwapMax=0 | 禁止使用 swap | 宁可重启,不要卡顿拖慢全局 |
| Restart=always | 异常退出自动拉起 | 把 OOM 变成可自愈事件 |
| OOMScoreAdjust=800 | 提高被内核选中的优先级 | 内存紧张时优先牺牲 Agent |
启动后稳定占用约 279MB,落在 MemoryHigh 之下,说明预算设定合理。这套组合的本质是:把“不可控的全局 OOM”转化为“可控的局部重启”。
低配机上跑任何非核心服务,都应默认加上 MemoryMax + OOMScoreAdjust。让边缘服务成为内存压力的“泄压阀”,而不是让数据库或网关先倒下。
五、比内存更危险的事:sudo 的隐形后门
排查过程中发现,当前用户可以通过 sudo 免密执行任意命令——cloud-init 与云厂商 agent 写入了 NOPASSWD: ALL。而 AI Agent 以运行者身份执行操作,这意味着 Hermes 天然继承了完整的 root 能力。
5.1 处置步骤
- 通过管理控制台重设密码,确保有带外恢复通道
- 移除该用户的 NOPASSWD 规则
- 用
sudo -l验证规则已生效
变更 sudo 期间密码一度失效。之所以没有锁死,是因为提前保留了另一个已登录会话。任何涉及认证与权限的变更,必须先准备好带外通道与并行会话,这是硬性纪律。
给所有准备引入 AI Agent 的团队一条铁律:先审计 sudo,再谈接入。Agent 的权限边界,等于它运行账户的权限边界,不多不少。
六、内存之外的三道墙
6.1 墙一:Nextcloud 升级触发 500
为启用 Talk 需要升级 Nextcloud,过程中出现 500。根因是 .htaccess 中残留了服务器未安装的 ModPagespeed 指令。这类“配置文件与运行时能力不匹配”的问题,在跨版本升级中非常典型。
6.2 墙二:PHP 版本依赖链
Nextcloud 35 要求 PHP 8.3 以上,而系统默认 PHP 版本偏低。升级 PHP 又会牵动扩展、FPM 配置与既有应用的兼容性——这是典型的依赖连锁反应,必须在变更前画出影响面。
6.3 墙三:对外暴露与防护
Agent 一旦对外提供接口,就进入攻击面管理范畴:发送量上限、模型输入长度限制、外部访问的鉴权与限流,都会先于内存成为瓶颈。资源规划只是入场券,流量与安全边界才是长期成本。
| 瓶颈类型 | 表现 | 应对策略 |
|---|---|---|
| 内存 | 可用仅 154MB | systemd 硬隔离 + 停用非核心服务 |
| 权限 | NOPASSWD: ALL | 移除免密、保留带外通道 |
| 依赖 | PHP 版本 / .htaccess 指令 | 变更前画影响面,逐项验证 |
| 流量与安全 | 发送量上限、输入限制、外部暴露 | 限流、鉴权、最小暴露面 |
七、可复用的低配部署检查清单
- 用
available而非used判断余量,按峰值而非均值规划 - 停服前完成 DNS / 依赖 / 调用方三重核查
- 备份必须经过
diff -rq或试恢复验证 - 所有非核心服务配置
MemoryMax、MemorySwapMax=0、OOMScoreAdjust - 接入 Agent 前审计
sudo -l,消除 NOPASSWD - 权限变更前保留并行会话与带外恢复通道
- 升级前确认运行时版本与配置文件指令的一致性
点击展开:内存与权限自查命令
# 查看真实可用内存 free -m # 查看 systemd 服务实际内存占用 systemctl status hermes.service systemd-cgtop # 审计 sudo 免密规则 sudo -l # 备份一致性校验 diff -rq /path/to/orig /path/to/backup
低配部署的胜负手不在“能不能跑起来”,而在“跑起来之后系统是否仍然可控”。资源隔离、权限收口、依赖治理三件事做到位,891MB 的机器也能稳定承载一个 AI Agent;任何一项缺失,2GB 也只是延迟爆炸的时间问题。
