「一个人公司的DevOps」——给Agent上全套运维监控
你的 Agent 昨晚凌晨 3 点挂了。没有告警,没有日志,没有任何人知道。它安静地死在那一轮 API 调用里,错误信息被重试循环吞没。直到今天早上你打开界面——「工具未响应」。这不是段子,这是每个把 Agent 部署到生产环境的人早晚会遇到的噩梦。 AI Agent 领域的尴尬现实是:大家都在造 Agent,没人在意 Agent 活着的时候怎么样,死了更没人管。 前两篇文章我们聊了 Loop Engineering 的架构基座和持久记忆系统。Agent 确实「能干活」了——但你能放心让它 7×24 跑在服务器上吗?它跑着跑着卡住了你知道不?它偷偷烧掉了你 200 美元的 API 额度你心疼不? 如果你还没想过这些问题,恭喜你——你正处于「做一个能跑的 Agent」和「做一个能一直跑的 Agent」之间的那条鸿沟边上。 今天这一篇,我们就来填这条沟。
- 前提条件
开始之前,请确保你的环境满足以下要求:
| 依赖项 | 版本要求 | 安装方式 |
|---|---|---|
| Ubuntu | 22.04 LTS | 系统安装 |
| Python | ≥ 3.10 | |
| pip 包 | requests | |
| Docker | ≥ 24.x | 见 §6.1 安装步骤 |
环境说明:本文所有命令和脚本在 Ubuntu 22.04 环境下验证通过。其他 Linux 发行版(Debian 12、CentOS Stream 9)需自行调整包管理器命令。macOS 用户可使用 Homebrew 替代 apt。 一、为什么Agent也需要运维——跑着跑着挂了没人知道 先讲一个真实事故。 我的 Agent「元宝」负责一个数据采集任务——每天凌晨抓取 91 所大学的录取数据,清洗、比对、出报告。上线第一周,一切正常。第二周开始,我每天早上收到的报告都缺失了 3-5 所大学的数据。 排查过程极度痛苦: 检查代码逻辑——没问题
检查 API 限制——正常
检查网络连接——稳定
直到我查了 Agent 的完整执行日志,才发现真相:第 37 所大学的网站改了接口格式,Agent 解析失败后进入了无限重试。重试到第 8 次时,token 预算耗尽策略触发了——但「触发耗尽」本身被我设定为「静默跳过」。于是 Agent 默默地跳过了后续所有大学,继续执行「报告生成」步骤,生成了一份看起来完整但实际上缺了 3 所大学的报告。 这里有几个致命问题:
| 问题 | 后果 |
|---|---|
| 没有实时告警 | 我在第二天早上才发现,错过了 8 小时的修复窗口 |
| 重试机制没有上限 | 资源被无限消耗 |
| 异常被静默吞没 | Agent 以为一切正常 |
| 没有执行进度追踪 | 不知道哪一步出了问题 |
更可怕的是,这不是偶发事件。当你的 Agent 系统超过 3 个独立任务之后,「某些东西在某个地方不正常」就成了常态,而不是异常。 如果你的运维体系还停留在「我隔几个小时看一眼」,那你一定会在某个深夜里收到用户的质问——或者更糟,连用户都没发现,但你的 API 账单已经悄然翻了三倍。 这就是为什么我们要给 Agent 上全套运维监控——不是因为你闲,而是因为你迟早会被迫做这件事。主动做,痛苦可控。被动做,损失不可控。 二、健康检查体系——端口/API/DB三层次 运维最核心的一件事只有一件:知道你的系统现在是不是活的。 在传统 DevOps 里,健康检查是标配——Nginx 有
/health
端点,PostgreSQL 有
pg_isready
,K8s 有 Liveness 和 Readiness Probe。但大多数 AI Agent 项目直接把这一层跳过了。 他们认为 Agent 不是 Web 服务,不需要健康检查。大错特错。 Agent 也是服务——只不过它的「接口」不是 HTTP,而是工具调用链、LLM 返回质量、状态机流转的正确性。如果这些都不可检查,你就是在盲飞。 我把 Agent 的健康检查拆为三个层次,每一层检查不同的死亡方式。 第一层:端口与基础设施健康(Liveness) 这是最底层的检查。不管你的 Agent 代码多优雅,底层基础设施死了,一切都白搭。 检查项:
| 检查目标 | 方法 | 预期状态 | 异常后果 |
|---|---|---|---|
| Agent 进程存活 | / systemd status | RUNNING | 进程崩溃 |
| API 网关端口 | 200 OK | 路由不可达 | |
| 数据库连接 | 查询成功 | 状态无法持久化 | |
| 消息队列 | 生产-消费测试 | 正常流转 | 任务积压或丢失 |
| 磁盘空间 | 使用率 < 85% | 日志写入失败 | |
| 内存使用 | 可用 > 500MB | OOM 杀死进程 |
实现方式:
#!/bin/bash
# layer1_liveness.sh — 基础设施存活检查
set -euo pipefail
check_port() {
local port="$1"
local name="$2"
curl -sf --max-time 5 "http://localhost:${port}/health" > /dev/null 2>&1
local ret=$?
if [ $ret -eq 0 ]; then
echo "[OK] $name (port $port) is alive"
else
echo "[FAIL] $name (port $port) is DOWN"
fi
return $ret
}
check_db() {
local name="${1:-PostgreSQL}"
timeout 5 psql -c "SELECT 1" > /dev/null 2>&1
local ret=$?
if [ $ret -eq 0 ]; then
echo "[OK] $name is reachable"
else
echo "[FAIL] $name is unreachable"
fi
return $ret
}
check_disk() {
local mount="${1:-/}"
local threshold="${2:-85}"
local usage
usage=$(df -h "$mount" | awk 'NR==2 {print $5}' | tr -d '%')
if [ "$usage" -lt "$threshold" ]; then
echo "[OK] Disk $mount usage: ${usage}% (threshold: ${threshold}%)"
return 0
else
echo "[FAIL] Disk $mount usage: ${usage}% exceeds ${threshold}%"
return 1
fi
}
check_memory() {
local threshold_mb="${1:-500}"
local available
available=$(free -m | awk 'NR==2 {print $7}')
if [ "$available" -gt "$threshold_mb" ]; then
echo "[OK] Available memory: ${available}MB (threshold: ${threshold_mb}MB)"
return 0
else
echo "[FAIL] Available memory: ${available}MB below ${threshold_mb}MB"
return 1
fi
}
failed=0
check_port 8080 "Agent API" || ((failed++))
check_port 6379 "Redis" || ((failed++))
check_db "PostgreSQL" || ((failed++))
check_disk "/" 85 || ((failed++))
check_memory 500 || ((failed++))
echo "-----------------------------------"
echo "Total checks failed: $failed"
exit $failed
关键原则:第一层检查必须在 Agent 代码之外独立运行。如果你的 Agent 进程自己检查自己的端口活没活——它死了,检查也死了。用 cron、systemd timer 或独立的外部监控服务来做。 第二层:API 与依赖健康(Readiness) 这一层检查 Agent 依赖的外部服务和 API 是否可达、返回是否符合预期。这是 Agent 运维最容易被忽视的盲区。 Agent 不像传统 Web 服务——它的依赖不仅是一个数据库,还包括: LLM API(OpenAI / Claude / 本地模型)
搜索引擎 API
第三方数据源 API
内部工具调用的微服务
文件存储系统
任何一个依赖出问题,Agent 就在「看似正常运转、实际做无用功」的状态。 检查策略:
#!/usr/bin/env python3
# layer2_readiness.py — API与依赖健康检查
import os
import sys
import json
import requests
from datetime import datetime
# 从环境变量读取敏感信息
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
checks = {
"llm_api": {
"url": "https://api.openai.com/v1/models",
"method": "GET",
"headers": {"Authorization": f"Bearer {OPENAI_API_KEY}"},
"expected_status": 200,
"timeout": 10,
},
"vector_db": {
"url": "http://localhost:6333/health",
"method": "GET",
"expected_status": 200,
"timeout": 5,
},
"data_source": {
"url": "https://api.university-data.org/ping",
"method": "GET",
"expected_status": [200, 204],
"timeout": 15,
}
}
results = {}
overall_pass = True
for name, config in checks.items():
try:
resp = requests.request(
method=config["method"],
url=config["url"],
headers=config.get("headers", {}),
timeout=config["timeout"]
)
expected = config.get("expected_status", 200)
if isinstance(expected, list):
passed = resp.status_code in expected
else:
passed = resp.status_code == expected
results[name] = {
"status": "PASS" if passed else "FAIL",
"http_code": resp.status_code,
"checked_at": datetime.now().isoformat()
}
if not passed:
overall_pass = False
except Exception as e:
results[name] = {
"status": "FAIL",
"error": str(e),
"checked_at": datetime.now().isoformat()
}
overall_pass = False
output = {
"overall": "PASS" if overall_pass else "FAIL",
"checks": results
}
print(json.dumps(output, indent=2, ensure_ascii=False))
sys.exit(0 if overall_pass else 1)
实战建议:把第二层检查频率设为每 30 秒一次。LLM API 的可用性波动很大,你越早发现它挂了(或降速了),就能越早切换备用模型或降级策略。 第三层:业务逻辑与数据质量健康(Deep Health) 这是 Agent 特有的健康检查层——传统 Web 服务没有这个维度。它检查的是 Agent 是否在「正确」地工作,而不仅仅是「活着」。 检查项:
| 检查 | 方法 | 异常含义 |
|---|---|---|
| 任务完成率 | 对比「计划执行数」和「实际完成数」 | Agent 可能在悄悄跳过任务 |
| 输出格式合规性 | 每次 LLM 返回后校验 JSON schema | Agent 产生了不合预期的输出格式 |
| 数据质量偏差 | 检查输出数据中空值/异常值比例 | 采集链路可能出现静默失败 |
| 状态机状态分布 | 长期处于「执行中」的任务占比 | 死锁或无限循环 |
| Token 消耗速率 | 与历史基线对比是否异常 | 可能在无限重试或上下文膨胀 |
实现方式:在 Agent 的状态机每次状态变更时,向一个健康指标存储(Prometheus / InfluxDB / 甚至只是一个 JSON 文件)写入一条记录。独立的 Deep Health Check 进程定期查询这些指标,计算健康评分。
#!/usr/bin/env python3
# layer3_deep_health.py — 业务健康评分
import json
import statistics
from datetime import datetime, timedelta
def detect_token_spikes(state_history, window_minutes=5, z_threshold=3.0):
"""检测 token 消耗异常尖峰"""
now = datetime.now()
cutoff = now - timedelta(minutes=30)
recent_tokens = []
for s in state_history:
ts = datetime.fromisoformat(s["timestamp"])
if ts > cutoff:
recent_tokens.append(s.get("tokens_consumed", 0))
if len(recent_tokens) < 6:
return 0
mean = statistics.mean(recent_tokens)
stdev = statistics.stdev(recent_tokens) if len(recent_tokens) > 1 else 1.0
recent = recent_tokens[-window_minutes:] if len(recent_tokens) >= window_minutes else recent_tokens
spikes = sum(1 for t in recent if (t - mean) > (z_threshold * stdev))
return spikes
def compute_health_score(state_history):
"""基于状态机历史计算业务健康评分 0-100"""
score = 100
total_tasks = len(state_history)
if total_tasks == 0:
return 100 # 无任务即为健康
stuck_tasks = sum(
1 for s in state_history
if s.get("state") == "executing" and s.get("duration_min", 0) > 30
)
failed_tasks = sum(1 for s in state_history if s.get("state") == "failed")
# 卡住任务每1个扣5分
score -= stuck_tasks * 5
# 失败任务每1个扣10分
score -= failed_tasks * 10
# Token 消耗异常检测(连续5分钟消耗 > 3σ 偏离基线)
token_spikes = detect_token_spikes(state_history)
score -= token_spikes * 5
return max(0, min(100, score))
def main():
"""主入口:读取状态历史,计算并输出健康评分"""
import sys
state_file = sys.argv[1] if len(sys.argv) > 1 else "/opt/agent-monitor/state_history.jsonl"
state_history = []
try:
with open(state_file, "r") as f:
for line in f:
if line.strip():
state_history.append(json.loads(line))
except FileNotFoundError:
print(json.dumps({"health_score": -1, "error": "State file not found"}))
sys.exit(2)
score = compute_health_score(state_history)
result = {
"health_score": score,
"total_tasks": len(state_history),
"checked_at": datetime.now().isoformat(),
"status": "HEALTHY" if score >= 70 else "DEGRADED" if score >= 40 else "CRITICAL"
}
print(json.dumps(result, indent=2, ensure_ascii=False))
if score >= 70:
sys.exit(0)
elif score >= 40:
sys.exit(1)
else:
sys.exit(2)
if __name__ == "__main__":
main()
核心洞察:第三层健康检查是 Agent 运维区别于传统运维的本质所在。传统运维关心「服务通不通」,Agent 运维还需要关心「任务做得对不对」。而后者往往才是真正的灾难——因为 Agent 可能在「正常地做错误的事」。 三、告警级别——P0致命/P1严重/P2警告/P3通知 健康检查只是「发现」,告警才是「让人知道」。但最糟糕的告警体系是什么?——对所有问题一视同仁。 如果你把「数据库磁盘快满了」和「某个菜谱网站返回了 503」放在同一个告警通道里,你的团队要么被告警淹没变成「告警疲劳」,要么无视所有告警直到真的出大事。 我参考传统 SRE 的告警分级体系,针对 Agent 运维的特点做了调整: P0 — 致命(立即响应,打断当前工作) 定义:系统核心功能完全不可用,或正在造成实际损失(资金流失、数据丢失)。 典型场景: Agent 进程完全退出,systemd 重启失败
核心 LLM API 连续 5 分钟不可用(无备用模型)
数据库无法写入,导致状态无法持久化
API 账单当前小时消耗超过日预算的 30%
无限循环导致 token 消耗速率超过基线 10 倍
响应要求:立即响应,需要人工介入。无论凌晨几点。 告警通道:电话 / 钉钉/飞书电话通知 / 短信 P1 — 严重(30分钟内响应) 定义:系统功能部分受损,但核心链路仍在运行。不处理可能在 1 小时内升级为 P0。 典型场景: 单个外部 API 依赖不可用(有备用方案但尚未自动切换)
Agent 状态机中有任务卡住超过 30 分钟
健康检查连续 3 次失败
磁盘使用率超过 85%
Token 消耗速率超过基线 3 倍
响应要求:30 分钟内确认,2 小时内修复或降级。 告警通道:飞书/钉钉消息 @提及 + 音视频呼叫(非电话) P2 — 警告(4小时内响应) 定义:系统正常运行,但存在潜在风险或已出现轻微异常。 典型场景: 单个健康检查偶发失败(1 小时内自我恢复)
数据质量检测发现异常值比例超过阈值
任务队列积压但仍在处理中
内存使用超过 70%
SSL 证书将在 7 天内过期
响应要求:下一个工作日内处理。 告警通道:飞书/钉钉普通消息 + 仪表盘标记 P3 — 通知(关注但不处理) 定义:信息性通知,不需要立即行动,但值得记录和关注。 典型场景: Agent 每日任务报告(完成了 X 个任务)
Token 消耗日报(比昨天多/少了多少)
新版本部署成功通知
数据库备份完成确认
低优先级的日志模式检测发现
响应要求:阅读即可,无需处理。 告警通道:飞书/钉钉普通消息(不 @) 告警疲劳预防 我的铁律只有一条:如果一个告警被触发但不需要任何人行动,它就是噪声。噪声必须被消除,而不是被习惯。 每月做一次「告警审计」——拉出过去 30 天所有告警,检查每条触发的告警是否真的产生了行动。如果某条告警触发 10 次但 10 次都无人响应——要么降级它的优先级,要么修复它的误报率。 四、故障响应Runbook——从发现到恢复的标准化流程 告警响了,然后呢? 大多数团队在这步就乱了——告警来了但没人知道该做什么,或者每个人都觉得自己在做但做的是重复工作。 Runbook(故障响应手册)就是解决这个问题的:把「惊慌失措的排查」变成「按流程执行的标准化操作」。 我的 Runbook 模板
# Runbook: [故障名称]
版本: v1.0 | 最后更新: 2026-07-01
## 1. 故障识别
- **告警规则**: [关联的告警名称]
- **触发条件**: [什么条件下告警]
- **影响范围**: [哪些功能/用户受影响]
- **严重级别**: [P0/P1/P2/P3]
## 2. 快速恢复(5分钟内)
> 先恢复服务,再排查根因
- [ ] 第一步: 重启 Agent 进程 `systemctl restart agent`
- [ ] 第二步: 检查状态机恢复 `cat STATE.md | head -20`
- [ ] 第三步: 确认健康检查通过 `curl localhost:8080/health`
- [ ] 第四步: 检查关键依赖可用性(API/DB/消息队列)
## 3. 根因排查(恢复后执行)
### 检查清单
- [ ] 检查最近一次部署的变更
- [ ] 检查 ERROR_LOG 是否有同类历史错误
- [ ] 检查 LLM API 近期可用性状态页
- [ ] 检查 token 消耗是否异常
- [ ] 检查外部依赖是否有版本变更
### 常用诊断命令
查看最近日志
journalctl -u agent --since "30 min ago" -n 100
检查进程资源
ps aux | grep agent | grep -v grep
Token消耗审计
tail -100 agent.log | grep "token_usage" | jq .
4. 恢复验证
Agent 返回正常任务处理状态
所有 P0/P1 告警已恢复
数据一致性验证通过
用户在 15 分钟内未再报告同类问题
5. 事后复盘
故障时间线:
发现时间:
响应时间:
恢复时间:
修复时间:
根因总结:
改进措施:
是否更新 ERROR_LOG?
三个真实的 Agent 故障 Runbook
Runbook #1: LLM API 超时风暴
场景: OpenAI API 连续返回 5xx 或超时超过 30 秒 P级别: P1(有备用模型 → P1, 无备用模型 → P0) 快速恢复:
检查 OpenAI 状态页 https://status.openai.com[1]
如果已确认故障 → 切换备用模型(切换命令: agent config set model claude-sonnet-4)
如果本地有失败缓存 → 启动降级模式(只处理缓存任务,不发起新调用) 根因排查:
检查近期 API key 是否超限
检查是否有批量重试导致限流
检查历史日志,是否在固定时段出现
Runbook #2: 状态机卡死故障
场景: Agent 状态机中某个任务连续 30 分钟处于「执行中」状态 P级别: P2(单任务卡住)→ P1(连续3个以上任务卡住) 快速恢复:
手动强制标记任务为失败: agent task force-fail
检查是否产生了孤儿进程
如果多个任务同时卡住 → 重启 Agent 进程 根因排查:
检查任务涉及的工具调用是否全部正常返回
检查 LLM 是否输出了不合预期的格式(导致工具解析卡住)
检查是否有死锁(两个任务互相等待资源)
Runbook #3: Token 消耗异常飙高
场景: Token 消耗速率超过历史基线 5 倍,持续 5 分钟 P级别: P1(可能产生显著费用) 快速恢复:
暂停所有非关键任务: agent task pause --priority=2,3
检查最近 100 条 LLM 调用日志,定位 token 大头
如果是无限循环 → 强制终止任务 根因排查:
检查是否上下文窗口膨胀(每次调用把历史全塞进去)
检查是否有无效重试(同一个错误重复调用 LLM 试图理解)
检查模型参数是否被误改(max_tokens 被设得巨大)
Runbook 的使用原则
- 每个告警级联一个 Runbook——P0/P1 告警必须配对 Runbook
- Runbook 放在 Agent 也要能读的地方——我用
ERROR_LOG.md的「修复」字段记录关键步骤,Agent 自己也能参照执行 - 每次故障后更新 Runbook——如果发现「Runbook 第 2 步已经过时了」,当天就改
- 定期演练——每月一次模拟告警,让值班的人(或者 Agent 自己)走一遍 Runbook
<img src="../images/article04-devops.png" alt="架构图" style="max-width:100%;border-radius:8px;margin:16px 0;">
五、成本控制——Token/费用追踪
聊完「怎么不死」,聊一个更现实的问题——你烧得起吗?
Agent 运维有一个传统 DevOps 不具备的核心维度:成本运维。 你的 Agent 不会像传统服务器一样每月交固定费用——它的消耗和它的工作量成正比,而工作量可能在没有人类干预的情况下翻 100 倍。
我见过最惨的案例:一个人用 AutoGPT 处理数据,晚上睡觉前启动了,第二天醒来发现花了 800 美元——因为 Agent 陷入了「发现问题→调用 LLM 试图理解→问题没有解决→再次调用 LLM」的死循环。
成本监控的四个层次
第一层:总费用追踪(最基础)
# cost_tracker.py — 简单的费用追踪脚本
import json
import os
from datetime import datetime, timedelta
COST_LOG = os.environ.get("COST_LOG_PATH", "cost_log.jsonl")
DAILY_BUDGET = float(os.environ.get("DAILY_BUDGET", "10")) # 每日预算上限(美元)
def log_token_usage(model, prompt_tokens, completion_tokens, cost_per_1k=0.01):
"""记录每次 LLM 调用的费用"""
cost = (prompt_tokens + completion_tokens) / 1000 * cost_per_1k
entry = {
"timestamp": datetime.now().isoformat(),
"model": model,
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"cost": round(cost, 6)
}
with open(COST_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
return cost
def check_daily_budget():
"""检查当日是否已超预算"""
today = datetime.now().strftime("%Y-%m-%d")
total = 0.0
try:
with open(COST_LOG, "r") as f:
for line in f:
entry = json.loads(line)
if entry["timestamp"].startswith(today):
total += entry["cost"]
except FileNotFoundError:
pass
return total, total > DAILY_BUDGET
第二层:按任务/项目归因 只看总费用不够——你需要知道钱都花在哪了。每个 LLM 调用都附带一个
task_id
或
project_id
,按此聚合:
-- 伪SQL风格的聚合查询
SELECT
project_id,
SUM(cost) as total_cost,
COUNT(*) as api_calls,
AVG(prompt_tokens + completion_tokens) as avg_tokens
FROM cost_log
WHERE timestamp > NOW() - INTERVAL '7 days'
GROUP BY project_id
ORDER BY total_cost DESC
第三层:费用异常检测 建立历史基线,实时检测异常消耗:
def detect_cost_anomaly(window_minutes=5, z_threshold=3.0):
"""检测近5分钟的费用消耗是否异常"""
recent = get_cost_in_window(window_minutes)
baseline = get_historical_baseline(window_minutes)
# 计算 Z-score
z_score = (recent - baseline['mean']) / (baseline['std'] + 0.001)
if z_score > z_threshold:
# P2 告警:费用异常升高
alert("P2", f"Token消耗异常: {z_score:.1f}σ 偏离基线")
elif z_score < -0.5:
# P3 通知:费用异常降低(可能任务未正常执行)
notify("P3", f"Token消耗显著低于基线,检查任务是否正常执行")
第四层:自动预算阀值 不止检测,还要自动止损:
# cost_governor.py — 自动预算控制
class CostGovernor:
def __init__(self, daily_budget=10.0, hourly_budget=2.0):
self.daily_budget = daily_budget
self.hourly_budget = hourly_budget
def get_today_spent(self):
"""查询当日累计 API 消费(美元)"""
import datetime
today = datetime.date.today().isoformat()
# 从成本数据库/日志中汇总今日所有 API 调用费用
# 实际落地可接入 SQLite/Redis 或解析 Prometheus 指标
return self._query_spent(since=today)
def get_last_hour_spent(self):
"""查询最近一小时的 API 消费(美元)"""
import datetime
one_hour_ago = (
datetime.datetime.utcnow() - datetime.timedelta(hours=1)
).isoformat()
return self._query_spent(since=one_hour_ago)
def is_critical_path(self):
"""判断当前调用是否属于关键任务(不可跳过)"""
# 可依据调用上下文打标:critical / normal / low
# 例如:健康检查、告警发送、数据持久化走关键路径
return getattr(self, '_critical', False)
def _query_spent(self, since):
"""内部方法:从持久化存储中聚合指定时段的消费总额"""
# TODO: 接入实际的成本存储(Prometheus counter 或 SQLite)
# 示例:SELECT SUM(cost) FROM api_calls WHERE timestamp >= since
return 0.0
def should_proceed(self, estimated_cost=0.0):
"""决定是否允许本次LLM调用"""
daily_spent = self.get_today_spent()
hourly_spent = self.get_last_hour_spent()
if daily_spent + estimated_cost > self.daily_budget * 0.8:
# 超过日预算80% → 只允许关键任务
if not self.is_critical_path():
return False, "daily_budget_threshold_reached"
if hourly_spent > self.hourly_budget:
# 超过小时预算 → 暂停非关键任务
return False, "hourly_budget_exceeded"
if daily_spent > self.daily_budget:
# 超过日预算 → 全部暂停
return False, "daily_budget_exhausted"
return True, "ok"
成本控制的实际经验
| 策略 | 效果 | 实现成本 |
|---|---|---|
| 设置每日硬上限 | 防止账单失控 | 低(一行配置) |
| 任务级别预算分配 | 高优先级任务不受低优先级浪费 | 中(需要任务分类) |
| 缓存相同请求的 LLM 输出 | 减少 20-40% 重复调用 | 中(引入缓存层) |
| 动态 max_tokens 控制 | 减少不必要的 tokens | 低(根据任务类型预设) |
| 备用模型降级 | 日预算剩余 > 30% 用 GPT-4,< 30% 用 GPT-4o-mini | 中(两套模型切换) |
我个人的原则:不要让 Agent 比你自己还能花钱。每月月底看 API 账单时的那种肉疼感,应该是可控的、预期的。 六、环境依赖安装——Docker + Prometheus + Grafana 在真正部署监控栈之前,先确保服务器具备所需环境。以下适用于 Ubuntu 22.04 / Debian 12,其他发行版请自行调整包管理器。 工具来源 本文涉及的各个工具均来自其官方 GitHub 仓库,建议直接从源头获取:
| 工具 | 用途 | GitHub / 官方地址 |
|---|---|---|
| Docker | 容器化运行环境 | https://github.com/docker/docker-ce[2] |
| Prometheus | 指标采集与监控 | https://github.com/prometheus/prometheus[3] |
| Grafana | 监控面板与可视化 | https://github.com/grafana/grafana[4] |
| OpenAI | LLM API(成本追踪对象) | https://github.com/openai/openai-python[5] |
6.1 安装 Docker
#!/bin/bash
# install_docker.sh — 一键安装 Docker(Ubuntu/Debian)
set -euo pipefail
echo "=== 安装 Docker ==="
# 卸载旧版本(如果存在)
sudo apt-get remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true
# 安装依赖
sudo apt-get update
sudo apt-get install -y \
ca-certificates \
curl \
gnupg \
lsb-release
# 添加 Docker 官方 GPG 密钥
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# 添加 Docker 仓库
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 安装 Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# 将当前用户加入 docker 组(免 sudo)
sudo usermod -aG docker "$USER"
# 启动并设置开机自启
sudo systemctl enable docker
sudo systemctl start docker
# 验证安装
docker --version
docker run --rm hello-world
echo "=== Docker 安装完成,请重新登录以使用免 sudo docker 命令 ==="
6.2 安装 Prometheus(通过 Docker 或二进制) 方式一:Docker(推荐,最简单)
# 拉取 Prometheus 镜像
docker pull prom/prometheus:latest
# 创建配置和数据目录
sudo mkdir -p /etc/prometheus /var/lib/prometheus
# 测试启动(Ctrl+C 停止)
docker run --rm -p 9090:9090 \
-v /etc/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml \
prom/prometheus:latest
方式二:二进制安装(无需 Docker)
#!/bin/bash
# install_prometheus_binary.sh — 二进制安装 Prometheus
set -euo pipefail
PROMETHEUS_VERSION="2.52.0"
INSTALL_DIR="/usr/local/bin"
CONFIG_DIR="/etc/prometheus"
DATA_DIR="/var/lib/prometheus"
# 下载并解压
cd /tmp
curl -LO "https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz"
tar xzf "prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz"
# 安装二进制
sudo cp "prometheus-${PROMETHEUS_VERSION}.linux-amd64/prometheus" "$INSTALL_DIR/"
sudo cp "prometheus-${PROMETHEUS_VERSION}.linux-amd64/promtool" "$INSTALL_DIR/"
# 创建目录和配置
sudo mkdir -p "$CONFIG_DIR" "$DATA_DIR"
sudo cp "prometheus-${PROMETHEUS_VERSION}.linux-amd64/prometheus.yml" "$CONFIG_DIR/"
# 创建 systemd 服务
sudo tee /etc/systemd/system/prometheus.service > /dev/null << 'EOF'
[Unit]
Description=Prometheus Monitoring
After=network.target
[Service]
ExecStart=/usr/local/bin/prometheus \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/var/lib/prometheus/ \
--web.listen-address=0.0.0.0:9090
Restart=always
User=root
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable prometheus
sudo systemctl start prometheus
# 清理
rm -rf "/tmp/prometheus-${PROMETHEUS_VERSION}.linux-amd64"*
echo "Prometheus 安装完成: http://localhost:9090"
6.3 安装 Grafana 方式一:Docker(推荐)
# 拉取 Grafana 镜像
docker pull grafana/grafana:latest
# 创建数据目录
sudo mkdir -p /var/lib/grafana
# 测试启动
docker run --rm -p 3000:3000 \
-v /var/lib/grafana:/var/lib/grafana \
grafana/grafana:latest
方式二:APT 安装(Ubuntu/Debian)
#!/bin/bash
# install_grafana_apt.sh — APT 安装 Grafana
set -euo pipefail
# 添加 Grafana APT 仓库
sudo apt-get install -y software-properties-common wget
sudo mkdir -p /etc/apt/keyrings
wget -q -O - https://apt.grafana.com/gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/grafana.gpg
echo "deb [signed-by=/etc/apt/keyrings/grafana.gpg] https://apt.grafana.com stable main" | \
sudo tee /etc/apt/sources.list.d/grafana.list > /dev/null
# 安装
sudo apt-get update
sudo apt-get install -y grafana
# 启动
sudo systemctl daemon-reload
sudo systemctl enable grafana-server
sudo systemctl start grafana-server
echo "Grafana 安装完成: http://localhost:3000 (默认账号: admin/admin)"
七、实操:一键部署全套监控 理论讲完了,环境准备好了,现在上干的。以下是一套可以直接拿来用的监控部署方案——全部开源工具,零成本起步。 架构总览
┌─────────────────────────────────────────────────┐
│ Monitoring Stack │
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ Agent │────▶│ Prometheus │ │
│ │ Metrics │ │ (指标存储) │ │
│ └────────────┘ └──────┬─────┘ │
│ │ │
│ ┌────────────────────┐ │ ┌────────────┐ │
│ │ Health Checks │───┼──▶│ Grafana │ │
│ │ (3-layer script) │ │ │ (仪表盘) │ │
│ └────────────────────┘ │ └────────────┘ │
│ │ │
│ ┌────────────┐ │ ┌────────────┐ │
│ │ Alert │──────────┼──▶│ Feishu/ │ │
│ │ Manager │ │ │ DingTalk │ │
│ └────────────┘ │ │ Webhook │ │
│ │ └────────────┘ │
│ ┌────────────┐ │ │
│ │ Cost │──────────┘ │
│ │ Tracker │ │
│ └────────────┘ │
└─────────────────────────────────────────────────┘
一键部署脚本 以下是一个完整的部署脚本,包含 Prometheus + Grafana + 三层次 Health Checks + Cost Exporter + Alert Manager。修复了所有 bash 语法错误并补齐了被截断的部分。
#!/bin/bash
# deploy_monitoring.sh — 一键部署全套Agent监控
# 用法: bash deploy_monitoring.sh [agent_name]
# 前置条件: Docker 已安装(参见第六节),curl/jq 可用
set -euo pipefail
AGENT_NAME="${1:-my-agent}"
MONITOR_DIR="/opt/agent-monitor/${AGENT_NAME}"
PROMETHEUS_PORT=9090
GRAFANA_PORT=3000
COST_EXPORTER_PORT=9091
echo "====================================="
echo " 部署 Agent 监控系统: ${AGENT_NAME}"
echo "====================================="
# 0. 环境前置检查
echo ""
echo "[0/8] 检查前置依赖..."
if ! command -v docker &> /dev/null; then
echo "[ERROR] Docker 未安装,请先运行 install_docker.sh"
exit 1
fi
if ! command -v curl &> /dev/null; then
echo "[ERROR] curl 未安装"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "[WARN] jq 未安装,正在安装..."
sudo apt-get update -qq && sudo apt-get install -y -qq jq
fi
# 确保端口未被占用
for port in $PROMETHEUS_PORT $GRAFANA_PORT $COST_EXPORTER_PORT; do
if ss -tln | grep -q ":${port} "; then
echo "[WARN] 端口 ${port} 已被占用,请手动释放后重试"
fi
done
echo "[OK] 前置依赖检查通过"
# 1. 创建目录结构
echo ""
echo "[1/8] 创建目录结构..."
mkdir -p "${MONITOR_DIR}"/{prometheus,grafana,scripts,alerts,dashboards,data}
echo "[OK] 目录已创建: ${MONITOR_DIR}"
# 2. 创建 Prometheus 配置
echo ""
echo "[2/8] 创建 Prometheus 配置..."
cat > "${MONITOR_DIR}/prometheus/prometheus.yml" << 'PROMEOF'
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
monitor: agent-monitor
rule_files:
- "/etc/prometheus/alerts/*.yml"
scrape_configs:
- job_name: 'agent-health'
scrape_interval: 10s
static_configs:
- targets: ['host.docker.internal:8080']
labels:
agent: 'my-agent'
env: 'production'
- job_name: 'agent-cost-metrics'
scrape_interval: 30s
static_configs:
- targets: ['host.docker.internal:9091']
labels:
agent: 'my-agent'
- job_name: 'prometheus-self'
scrape_interval: 30s
static_configs:
- targets: ['localhost:9090']
PROMEOF
# 替换 agent name 占位符
sed -i "s/my-agent/${AGENT_NAME}/g" "${MONITOR_DIR}/prometheus/prometheus.yml"
echo "[OK] Prometheus 配置完成"
# 3. 创建告警规则(P0-P3)
echo ""
echo "[3/8] 创建告警规则..."
cat > "${MONITOR_DIR}/alerts/agent_alerts.yml" << 'ALERTEOF'
groups:
- name: agent_alerts
interval: 30s
rules:
# P0 - Agent 进程死亡
- alert: AgentProcessDown
expr: up{job="agent-health"} == 0
for: 1m
labels:
severity: critical
priority: P0
channel: phone
annotations:
summary: "Agent进程 {{ $labels.agent }} 已停止"
description: "Agent {{ $labels.agent }} 在 {{ $labels.instance }} 上已停止响应超过1分钟。请立即检查。"
runbook_url: "https://wiki.example.com/runbooks/agent-process-down"
# P0 - 数据库不可用
- alert: DatabaseUnreachable
expr: agent_db_up == 0
for: 1m
labels:
severity: critical
priority: P0
annotations:
summary: "{{ $labels.agent }} 数据库不可达"
# P1 - 健康检查失败
- alert: HealthCheckFailed
expr: agent_health_check{check="liveness"} < 1
for: 3m
labels:
severity: high
priority: P1
channel: feishu_mention
annotations:
summary: "{{ $labels.agent }} 健康检查连续失败"
description: "健康检查 {{ $labels.check }} 连续3分钟失败,请排查。"
# P1 - 磁盘使用率过高
- alert: DiskUsageHigh
expr: agent_disk_usage_percent > 85
for: 5m
labels:
severity: high
priority: P1
annotations:
summary: "{{ $labels.agent }} 磁盘使用率 {{ $value }}%"
# P2 - Token消耗异常
- alert: TokenCostAnomaly
expr: rate(agent_token_cost_total[5m]) > 3 * avg(rate(agent_token_cost_total[30m]))
for: 5m
labels:
severity: warning
priority: P2
annotations:
summary: "Token消耗速率异常升高"
description: "近5分钟消耗速率是30分钟均值的3倍以上"
# P3 - 每日费用报告
- alert: DailyCostReport
expr: agent_daily_cost > 0
for: 24h
labels:
severity: info
priority: P3
annotations:
summary: "Agent {{ $labels.agent }} 本日费用: ${{ $value }}"
ALERTEOF
echo "[OK] 告警规则创建完成(4条P0-P3规则)"
# 4. 创建健康检查脚本(三层次合一)
echo ""
echo "[4/8] 创建健康检查脚本..."
cat > "${MONITOR_DIR}/scripts/health_check.sh" << 'HEALTHEOF'
#!/bin/bash
# health_check.sh — Agent 三层次健康检查
# 输出 Prometheus textfile 格式指标,供 node_exporter textfile collector 采集
# 或通过 cron + pushgateway 推送
set -euo pipefail
METRICS_DIR="${METRICS_DIR:-/tmp/agent-metrics}"
METRICS_FILE="${METRICS_DIR}/agent_health.prom"
AGENT_NAME="${AGENT_NAME:-my-agent}"
mkdir -p "${METRICS_DIR}"
# 辅助函数:输出 Prometheus 指标
emit_metric() {
local name="$1"
local value="$2"
local labels="${3:-}"
local help="${4:-}"
if [ -n "$help" ]; then
echo "# HELP ${name} ${help}" >> "${METRICS_FILE}.tmp"
echo "# TYPE ${name} gauge" >> "${METRICS_FILE}.tmp"
fi
if [ -n "$labels" ]; then
echo "${name}{${labels}} ${value}" >> "${METRICS_FILE}.tmp"
else
echo "${name} ${value}" >> "${METRICS_FILE}.tmp"
fi
}
# 清空临时文件
> "${METRICS_FILE}.tmp"
# ========== 第一层:基础设施健康 ==========
# Agent 进程检查
if pgrep -f "agent" > /dev/null 2>&1; then
emit_metric "agent_up" 1 "check=\"process\",agent=\"${AGENT_NAME}\"" "Agent process liveness"
else
emit_metric "agent_up" 0 "check=\"process\",agent=\"${AGENT_NAME}\"" "Agent process liveness"
fi
# 端口检查
for port_info in "8080:agent-api" "6379:redis" "5432:postgresql"; do
port="${port_info%%:*}"
service="${port_info##*:}"
if ss -tln 2>/dev/null | grep -q ":${port} " || netstat -tln 2>/dev/null | grep -q ":${port} "; then
emit_metric "agent_port_up" 1 "port=\"${port}\",service=\"${service}\",agent=\"${AGENT_NAME}\"" "Port ${port} (${service}) reachability"
else
emit_metric "agent_port_up" 0 "port=\"${port}\",service=\"${service}\",agent=\"${AGENT_NAME}\"" "Port ${port} (${service}) reachability"
fi
done
# 磁盘使用率
disk_usage=$(df -h / 2>/dev/null | awk 'NR==2 {print $5}' | tr -d '%')
if [ -n "$disk_usage" ]; then
emit_metric "agent_disk_usage_percent" "${disk_usage}" "mount=\"/\",agent=\"${AGENT_NAME}\"" "Disk usage percentage for /"
fi
# 可用内存 (MB)
avail_mem=$(free -m 2>/dev/null | awk 'NR==2 {print $7}')
if [ -n "$avail_mem" ]; then
emit_metric "agent_memory_available_mb" "${avail_mem}" "agent=\"${AGENT_NAME}\"" "Available memory in MB"
fi
# 数据库连接检查
if timeout 5 psql -c "SELECT 1" > /dev/null 2>&1; then
emit_metric "agent_db_up" 1 "check=\"postgresql\",agent=\"${AGENT_NAME}\"" "Database connectivity"
else
emit_metric "agent_db_up" 0 "check=\"postgresql\",agent=\"${AGENT_NAME}\"" "Database connectivity"
fi
# ========== 第二层:API 依赖健康 ==========
# LLM API 可达性
if curl -sf --max-time 10 "https://api.openai.com/v1/models" \
-H "Authorization: Bearer ${OPENAI_API_KEY:-}" > /dev/null 2>&1; then
emit_metric "agent_api_reachable" 1 "target=\"openai\",agent=\"${AGENT_NAME}\"" "External API reachability"
else
emit_metric "agent_api_reachable" 0 "target=\"openai\",agent=\"${AGENT_NAME}\"" "External API reachability"
fi
# ========== 第三层:业务健康(从状态文件读取) ==========
STATE_FILE="${STATE_FILE:-/opt/agent-monitor/${AGENT_NAME}/data/state_history.jsonl}"
if [ -f "${STATE_FILE}" ]; then
# 统计卡住任务数(>30分钟仍在执行中)
stuck_count=$(grep '"state":"executing"' "${STATE_FILE}" 2>/dev/null | wc -l || echo 0)
emit_metric "agent_stuck_tasks" "${stuck_count}" "agent=\"${AGENT_NAME}\"" "Number of stuck tasks (>30min)"
# 统计失败任务数
failed_count=$(grep '"state":"failed"' "${STATE_FILE}" 2>/dev/null | wc -l || echo 0)
emit_metric "agent_failed_tasks" "${failed_count}" "agent=\"${AGENT_NAME}\"" "Number of failed tasks"
# 今日任务完成数
today=$(date +%Y-%m-%d)
completed_today=$(grep "\"${today}\"" "${STATE_FILE}" 2>/dev/null | grep '"state":"completed"' | wc -l || echo 0)
emit_metric "agent_tasks_completed_today" "${completed_today}" "agent=\"${AGENT_NAME}\"" "Tasks completed today"
fi
# 原子替换指标文件
mv "${METRICS_FILE}.tmp" "${METRICS_FILE}"
echo "Metrics written to ${METRICS_FILE}"
HEALTHEOF
chmod +x "${MONITOR_DIR}/scripts/health_check.sh"
echo "[OK] 健康检查脚本创建完成"
# 5. 创建费用追踪 Exporter
echo ""
echo "[5/8] 创建费用追踪 Exporter..."
cat > "${MONITOR_DIR}/scripts/cost_exporter.py" << 'COSTEOF'
#!/usr/bin/env python3
"""
Prometheus exporter for Agent token costs.
Exposes daily cost and API call count at :9091/metrics.
"""
import json
import os
import sys
from datetime import datetime
from http.server import HTTPServer, BaseHTTPRequestHandler
COST_LOG = os.environ.get("COST_LOG_PATH", "/opt/agent-monitor/cost_log.jsonl")
LISTEN_PORT = int(os.environ.get("COST_EXPORTER_PORT", "9091"))
class CostExporter(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/metrics":
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
metrics = self.generate_metrics()
self.wfile.write(metrics.encode("utf-8"))
elif self.path == "/health":
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
self.wfile.write(b"OK")
else:
self.send_response(404)
self.end_headers()
def log_message(self, format, *args):
"""Suppress default logging to stderr."""
pass
def generate_metrics(self):
today = datetime.now().strftime("%Y-%m-%d")
total_cost = 0.0
total_calls = 0
cost_by_model = {}
try:
with open(COST_LOG, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
entry = json.loads(line)
except json.JSONDecodeError:
continue
if entry.get("timestamp", "").startswith(today):
cost = entry.get("cost", 0)
total_cost += cost
total_calls += 1
model = entry.get("model", "unknown")
cost_by_model[model] = cost_by_model.get(model, 0.0) + cost
except FileNotFoundError:
pass
lines = []
lines.append("# HELP agent_daily_cost Total daily cost for Agent (USD)")
lines.append("# TYPE agent_daily_cost gauge")
lines.append(f"agent_daily_cost {total_cost:.4f}")
lines.append("# HELP agent_daily_api_calls Total daily API calls")
lines.append("# TYPE agent_daily_api_calls gauge")
lines.append(f"agent_daily_api_calls {total_calls}")
lines.append("# HELP agent_cost_by_model Daily cost breakdown by model")
lines.append("# TYPE agent_cost_by_model gauge")
for model, cost in cost_by_model.items():
safe_model = model.replace('"', '_').replace("'", "_")
lines.append(f'agent_cost_by_model{{model="{safe_model}"}} {cost:.4f}')
return "\n".join(lines) + "\n"
if __name__ == "__main__":
print(f"Starting Cost Exporter on 0.0.0.0:{LISTEN_PORT}")
print(f"Reading cost log from: {COST_LOG}")
server = HTTPServer(("0.0.0.0", LISTEN_PORT), CostExporter)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nShutting down Cost Exporter...")
server.shutdown()
COSTEOF
chmod +x "${MONITOR_DIR}/scripts/cost_exporter.py"
echo "[OK] 费用追踪 Exporter 创建完成"
# 6. 创建 systemd 服务(健康检查定时任务 + Cost Exporter)
echo ""
echo "[6/8] 创建 systemd 服务..."
# 健康检查定时器
cat > "/etc/systemd/system/agent-healthcheck-${AGENT_NAME}.service" << SERVICEEOF
[Unit]
Description=Agent Health Check - ${AGENT_NAME}
After=network.target
[Service]
Type=oneshot
ExecStart=${MONITOR_DIR}/scripts/health_check.sh
Environment="AGENT_NAME=${AGENT_NAME}"
Environment="STATE_FILE=${MONITOR_DIR}/data/state_history.jsonl"
User=root
SERVICEEOF
cat > "/etc/systemd/system/agent-healthcheck-${AGENT_NAME}.timer" << TIMEREOF
[Unit]
Description=Agent Health Check Timer - ${AGENT_NAME}
[Timer]
OnCalendar=*:0/1
Persistent=true
[Install]
WantedBy=timers.target
TIMEREOF
# Cost Exporter 常驻服务
cat > "/etc/systemd/system/agent-cost-exporter-${AGENT_NAME}.service" << COSTSVCEOF
[Unit]
Description=Agent Cost Exporter - ${AGENT_NAME}
After=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 ${MONITOR_DIR}/scripts/cost_exporter.py
Environment="COST_LOG_PATH=${MONITOR_DIR}/data/cost_log.jsonl"
Environment="COST_EXPORTER_PORT=${COST_EXPORTER_PORT}"
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
COSTSVCEOF
systemctl daemon-reload
systemctl enable "agent-healthcheck-${AGENT_NAME}.timer"
systemctl start "agent-healthcheck-${AGENT_NAME}.timer"
systemctl enable "agent-cost-exporter-${AGENT_NAME}.service"
systemctl start "agent-cost-exporter-${AGENT_NAME}.service"
echo "[OK] systemd 服务创建并启动"
# 7. 启动 Prometheus(Docker)
echo ""
echo "[7/8] 启动 Prometheus..."
# 先停止旧容器(如果存在)
docker rm -f "prometheus-${AGENT_NAME}" 2>/dev/null || true
docker run -d \
--name "prometheus-${AGENT_NAME}" \
--restart always \
--network host \
-p "${PROMETHEUS_PORT}:9090" \
-v "${MONITOR_DIR}/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro" \
-v "${MONITOR_DIR}/alerts:/etc/prometheus/alerts:ro" \
-v "${MONITOR_DIR}/data/prometheus:/prometheus" \
prom/prometheus:latest \
--config.file=/etc/prometheus/prometheus.yml \
--storage.tsdb.path=/prometheus \
--web.enable-lifecycle
# 等待 Prometheus 就绪
echo "等待 Prometheus 启动..."
for i in $(seq 1 30); do
if curl -sf "http://localhost:${PROMETHEUS_PORT}/-/ready" > /dev/null 2>&1; then
echo "[OK] Prometheus 已就绪: http://localhost:${PROMETHEUS_PORT}"
break
fi
sleep 2
done
echo "[OK] Prometheus 启动完成"
# 8. 启动 Grafana
echo ""
echo "[8/8] 启动 Grafana..."
docker rm -f "grafana-${AGENT_NAME}" 2>/dev/null || true
docker run -d \
--name "grafana-${AGENT_NAME}" \
--restart always \
--network host \
-p "${GRAFANA_PORT}:3000" \
-v "${MONITOR_DIR}/grafana:/var/lib/grafana" \
-v "${MONITOR_DIR}/dashboards:/etc/grafana/provisioning/dashboards" \
-e "GF_SECURITY_ADMIN_USER=admin" \
-e "GF_SECURITY_ADMIN_PASSWORD=admin" \
-e "GF_USERS_ALLOW_SIGN_UP=false" \
grafana/grafana:latest
# 等待 Grafana 就绪
echo "等待 Grafana 启动..."
for i in $(seq 1 30); do
if curl -sf "http://localhost:${GRAFANA_PORT}/api/health" > /dev/null 2>&1; then
echo "[OK] Grafana 已就绪: http://localhost:${GRAFANA_PORT}"
break
fi
sleep 2
done
echo ""
echo "====================================="
echo " 部署完成!"
echo "====================================="
echo ""
echo "Prometheus: http://localhost:${PROMETHEUS_PORT}"
echo "Grafana: http://localhost:${GRAFANA_PORT} (admin/admin)"
echo "健康检查: ${MONITOR_DIR}/scripts/health_check.sh"
echo "费用追踪: http://localhost:${COST_EXPORTER_PORT}/metrics"
echo "告警规则: ${MONITOR_DIR}/alerts/agent_alerts.yml"
echo "指标目录: /tmp/agent-metrics/agent_health.prom"
echo ""
echo "下一步: 在 Grafana 中添加 Prometheus 数据源 (http://localhost:${PROMETHEUS_PORT})"
echo " 然后导入仪表盘或手动创建面板"
Grafana 仪表盘配置 部署完成后,在 Grafana 中导入以下四个核心仪表盘面板: Agent 概览面板:当前状态(Up/Down)、任务完成率、平均响应时间
健康检查面板:三层健康检查状态、失败次数趋势、依赖可用性
告警面板:P0-P3 告警数量、响应时间、MTTR(平均恢复时长)
成本面板:费用趋势、按任务拆分、预算使用率、费用异常事件
添加数据源步骤: 访问
http://localhost:3000
,登录 admin/admin
左侧菜单 → Connections → Data sources → Add data source
选择 Prometheus,URL 填入
http://localhost:9090
点击 Save & Test,显示绿色 "Successfully queried the Prometheus API" 即成功
最小可行方案(如果你不想用 Docker) 如果你觉得上面这套太重了,最小方案只需要一个文件:
#!/bin/bash
# minimal_monitor.sh — 最小化监控方案
# 用法: 放入 crontab: */5 * * * * /path/to/minimal_monitor.sh >> /var/log/agent-monitor.log 2>&1
LOG_FILE="${LOG_FILE:-/var/log/agent-monitor.log}"
# 1. 检查 Agent 是否活着
if ! pgrep -f "agent" > /dev/null 2>&1; then
echo "[ALERT] P0: Agent进程已停止! $(date)"
fi
# 2. 检查关键 API 是否可达
if curl -sf --max-time 10 "https://api.openai.com/v1/models" \
-H "Authorization: Bearer ${OPENAI_API_KEY:-}" > /dev/null 2>&1; then
echo "[OK] OpenAI API 可达 - $(date)"
else
echo "[WARN] P1: OpenAI API 不可达 - $(date)"
fi
# 3. 检查今日费用
COST_FILE="${COST_FILE:-cost_log.jsonl}"
if [ -f "$COST_FILE" ]; then
today=$(date +%Y-%m-%d)
total=$(grep "$today" "$COST_FILE" 2>/dev/null | \
awk -F'"cost":' '{sum+=$2} END {printf "%.2f", sum}')
echo "[INFO] 今日费用: \$${total:-0.00} - $(date)"
else
echo "[INFO] 费用日志文件不存在: $COST_FILE - $(date)"
fi
# 4. 磁盘空间检查
usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [ "${usage:-0}" -gt 85 ]; then
echo "[WARN] P2: 磁盘使用率 ${usage}% - $(date)"
fi
把这个文件放进 cron,每 5 分钟跑一次:
# 编辑 crontab
crontab -e
# 添加以下行
*/5 * * * * /opt/agent-monitor/minimal_monitor.sh >> /var/log/agent-monitor.log 2>&1
全过程不到 5 分钟,但你获得的是 Agent 生命周期的可见性。之前你是在盲飞,现在你至少有了一个高度表。 八、验证方法——确保监控真的在工作 部署完成不代表监控生效。以下是分层次的验证步骤,每一步都必须在部署后执行。 8.1 基础设施层验证
# 1. 确认 Docker 容器正常运行
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep -E "prometheus|grafana"
# 2. 确认 systemd 服务运行正常
systemctl status "agent-healthcheck-${AGENT_NAME}.timer"
systemctl status "agent-cost-exporter-${AGENT_NAME}.service"
# 3. 确认端口监听
ss -tln | grep -E "9090|9091|3000"
# 4. 手动执行一次健康检查
bash /opt/agent-monitor/my-agent/scripts/health_check.sh
cat /tmp/agent-metrics/agent_health.prom
8.2 监控链路验证
# 1. 验证 Prometheus 可访问
curl -s http://localhost:9090/-/ready
# 期望输出: Prometheus Server is Ready.
# 2. 验证 Prometheus 正在采集指标
curl -s http://localhost:9090/api/v1/targets | jq '.data.activeTargets[] | {job: .labels.job, health: .health}'
# 期望: 所有 target 的 health 为 "up"
# 3. 验证 Cost Exporter 指标
curl -s http://localhost:9091/metrics
# 期望: 看到 agent_daily_cost 和 agent_daily_api_calls 指标
# 4. 验证 Grafana 可访问
curl -s http://localhost:3000/api/health
# 期望: {"database": "ok"}
8.3 告警链路验证
# 1. 检查 Prometheus 告警规则是否加载
curl -s http://localhost:9090/api/v1/rules | jq '.data.groups[] | .name'
# 2. 手动触发一条测试告警(停止 Agent 进程)
systemctl stop your-agent-service
# 等待 1-2 分钟后检查告警状态
curl -s http://localhost:9090/api/v1/alerts | jq '.data.alerts[] | select(.labels.severity=="critical")'
# 3. 恢复后确认告警自动消除
systemctl start your-agent-service
8.4 端到端验证清单
| 验证项 | 命令/方法 | 期望结果 |
|---|---|---|
| Docker 运行 | prometheus/grafana 容器 Status=Up | |
| Prometheus UI | 浏览器访问 :9090 | 可查看 targets 和 graph |
| Grafana UI | 浏览器访问 :3000 | 可登录并添加数据源 |
| 健康检查指标 | 包含 agent_up, agent_disk_usage_percent 等 | |
| 费用指标 | 包含 agent_daily_cost | |
| Cron 定时执行 | 看到每分钟触发的日志 |
九、常见错误与排障指南 部署过程中最常见的问题,我都帮你踩过坑了。 错误1:
docker: command not found
症状: deploy_monitoring.sh 报错 "Docker 未安装"
原因: Docker 未安装或当前用户不在 docker 组
解决:
1. 运行第六节的 install_docker.sh
2. 重新登录或执行 newgrp docker
3. 验证: docker ps
错误2:端口被占用
症状: docker run 报错 "port is already allocated"
原因: 3000/9090/9091 端口已被其他进程占用
排查:
sudo ss -tlnp | grep -E "3000|9090|9091"
解决:
# 释放端口
sudo kill -9 <PID>
# 或者修改脚本中的端口变量
PROMETHEUS_PORT=9091 GRAFANA_PORT=3001 bash deploy_monitoring.sh
错误3:Prometheus target 显示 DOWN
症状: http://localhost:9090/targets 中 target 状态为 DOWN
原因:
- agent-health target: host.docker.internal 在 Linux 上不可用
- cost-metrics target: cost_exporter.py 未启动
解决:
# Linux 上替换 host.docker.internal 为 localhost
sed -i 's/host.docker.internal/localhost/g' /opt/agent-monitor/*/prometheus/prometheus.yml
# 重启 Prometheus
docker restart prometheus-my-agent
# 确认 cost_exporter 运行
systemctl status agent-cost-exporter-my-agent
错误4:健康检查脚本权限不足
症状: systemctl status 显示 "Permission denied" 或 exit code 126
原因: 脚本无执行权限
解决:
chmod +x /opt/agent-monitor/*/scripts/health_check.sh
chmod +x /opt/agent-monitor/*/scripts/cost_exporter.py
错误5:curl: (7) Failed to connect 或 Connection refused
症状: 健康检查脚本中 curl 检测失败
原因:
- Agent 服务未启动
- Agent 未暴露 /health 端点
- 防火墙阻拦
解决:
# 确认 Agent 在监听
ss -tln | grep 8080
# 如果没有 /health 端点,修改健康检查脚本使用 pgrep 替代
# 检查防火墙
sudo ufw status
错误6:
awk: cmd. line:1: runaway string constant
症状: minimal_monitor.sh 中 awk 命令报错
原因: awk 的 -F 参数中的引号未正确转义
原错误写法:
awk -F'"cost":' '{sum+=$2} END{...}'
正确写法:
awk -F'"cost":' '{sum+=$2} END {printf "%.2f\n", sum}'
错误7:Prometheus 规则文件路径不匹配
症状: Prometheus 日志显示 "rule files not found"
原因: Docker 容器内路径与宿主机不一致
解决:
# 确认 volume 挂载正确
docker inspect prometheus-my-agent | jq '.[0].Mounts'
# 告警规则应挂在容器的 /etc/prometheus/alerts/
# 对应宿主机的 /opt/agent-monitor/my-agent/alerts/
错误8:Grafana 登录后看不到数据
症状: Grafana 面板显示 "No data"
原因:
- 未添加 Prometheus 数据源
- 数据源 URL 写的是 localhost 但 Docker 网络隔离
解决:
# 如果用 host 网络模式,数据源 URL 应为 http://localhost:9090
# 如果用 bridge 网络模式,数据源 URL 应为 http://host.docker.internal:9090
# 确认在 Grafana → Data Sources → 点击 Save & Test 显示绿色
十、运维成熟度模型——你的Agent在第几级? 最后,用一个简单的成熟度模型帮你定位自己在哪:
| 级别 | 特征 | 标志性动作 |
|---|---|---|
| L0 - 裸奔 | Agent 跑在终端,死了没人知道 | 看到「工具未响应」才去排查 |
| L1 - 基础探测 | 有 cron 检查进程是否存活 | 写了个 ping 脚本放在 crontab |
| L2 - 分层检查 | 三层健康检查 + 日志采集 | 每天看一下日志有没有异常 |
| L3 - 告警体系 | P0-P3 分级告警 + 通知通道 | 凌晨被飞书告警叫醒过 |
| L4 - 自动响应 | Runbook 自动化 + 自愈能力 | Agent 挂了自动重启并记录 ERROR_LOG |
| L5 - 预测运维 | 趋势分析 + 容量规划 + 成本优化 | 在故障发生前就收到预警 |
我目前的 Agent 系统稳定在 L3,部分场景到了 L4。从 L0 到 L2,一个月可以搞定。从 L2 到 L4,需要的是制度——不是技术。 因为到了 L3 之后,瓶颈不再是「怎么实现」,而是「发现了问题之后,有没有人去修」。而这恰恰是「一个人公司」最困难的部分——当你没有值班团队时,每一行告警规则、每一个 Runbook、每一条成本预算,都是你提前为自己铺的路。 行动号召:今天下午做三件事——1)在你的 Agent 启动脚本里加一个存活检测;2)打开你的 API 提供商后台,设置预算上限;3)创建一个 ERROR_LOG.md,写上「2026-07-01: 给 Agent 上了监控」。三件事加在一起不超过 15 分钟,但今晚睡觉时你会安心很多。 下一篇预告:当「技术选型不是技术题」——一人公司年入50万的基础设施账单 欢迎在评论区分享你的 Agent「裸奔」经历,或者你已经用上的骚操作运维方案。 关于作者:魏无记,AI 和数智化实践者。专注 Agent 工程化与 Loop Engineering 研究以及数智化转型。公众号持续更新 Agent 工程化实战系列和数智化转型相关知识实践——每篇都是保姆级教程照做就行。 引用链接 [1] https://status.openai.com[6] [2] https://prometheus.io/docs/[7] [3] https://grafana.com/docs/[8] 引用链接 [1]https://status.openai.com [2]https://github.com/docker/docker-ce [3]https://github.com/prometheus/prometheus [4]https://github.com/grafana/grafana [5]https://github.com/openai/openai-python [6]https://status.openai.com [7]https://prometheus.io/docs/ [8]https://grafana.com/docs/

浙公网安备 33010602011771号