「一个人公司的DevOps」——给Agent上全套运维监控

你的 Agent 昨晚凌晨 3 点挂了。没有告警,没有日志,没有任何人知道。它安静地死在那一轮 API 调用里,错误信息被重试循环吞没。直到今天早上你打开界面——「工具未响应」。这不是段子,这是每个把 Agent 部署到生产环境的人早晚会遇到的噩梦。 AI Agent 领域的尴尬现实是:大家都在造 Agent,没人在意 Agent 活着的时候怎么样,死了更没人管。 前两篇文章我们聊了 Loop Engineering 的架构基座和持久记忆系统。Agent 确实「能干活」了——但你能放心让它 7×24 跑在服务器上吗?它跑着跑着卡住了你知道不?它偷偷烧掉了你 200 美元的 API 额度你心疼不? 如果你还没想过这些问题,恭喜你——你正处于「做一个能跑的 Agent」和「做一个能一直跑的 Agent」之间的那条鸿沟边上。 今天这一篇,我们就来填这条沟。

  1. 前提条件

开始之前,请确保你的环境满足以下要求:

依赖项版本要求安装方式
Ubuntu22.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 statusRUNNING进程崩溃
API 网关端口200 OK路由不可达
数据库连接查询成功状态无法持久化
消息队列生产-消费测试正常流转任务积压或丢失
磁盘空间使用率 < 85%日志写入失败
内存使用可用 > 500MBOOM 杀死进程

实现方式:

#!/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 schemaAgent 产生了不合预期的输出格式
数据质量偏差检查输出数据中空值/异常值比例采集链路可能出现静默失败
状态机状态分布长期处于「执行中」的任务占比死锁或无限循环
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 的使用原则

  1. 每个告警级联一个 Runbook——P0/P1 告警必须配对 Runbook
  2. Runbook 放在 Agent 也要能读的地方——我用 ERROR_LOG.md 的「修复」字段记录关键步骤,Agent 自己也能参照执行
  3. 每次故障后更新 Runbook——如果发现「Runbook 第 2 步已经过时了」,当天就改
  4. 定期演练——每月一次模拟告警,让值班的人(或者 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]
OpenAILLM 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/

posted @ 2026-08-05 11:42  魏无记  阅读(1)  评论(0)    收藏  举报