Agent 跑了 7 天:团队欠下了这 5 笔运维债

作者:袖梨 2026-07-02

我很少在技术文章里聊"团队"这个词。这次要聊。

Agent 跑了 7 天,团队欠下了这 5 笔运维债

不是因为 Agent 技术变难了,而是因为我们意识到:Agent 跑久了,制造的不是 bug,是治理空白。你的代码没写错,模型没出问题,但团队里开始出现一些奇怪的对话——

"这个任务谁批的?"

"Agent 把那个配置改了,是有人让它改的吗?"

"它昨晚干了啥我能看到吗?"

这类问题的共同来源是:当初接入 Agent 的时候,我们把它当成了一个工具——像 CI/CD 一样,跑就跑,挂了重启。但它实际上更接近一个有权限的同事——它能写、能改、能发、能删,而且从不请假。

这篇是我们 openclaw-lab 运行 7 天之后,在团队层面补上的 5 笔治理债。没有宏观方法论,只有实际踩过的坑和补上的机制。


运维债 1:权限矩阵——Agent 该能做什么,你可能没认真想过

Agent 上线前,大多数团队做的权限设计是这样的:

这不是权限设计,这是权限发放。两者的区别:

  • 权限发放:把 Agent 需要用到的权限全给了,然后祈祷它不出事
  • 权限设计:把 Agent 在正常路径上需要的权限给了,并且明确了它不应该能做什么

我们在第 6 天做了一张权限矩阵,这件事花了我们一个小时,但此前五天没人做,因为每个人都以为别人在管这件事。

## Agent 权限矩阵模板(填 //)| 操作 | 是否允许 | 是否需要审批 | 每日上限 | 备注 |
|---|---|---|---|---|
| 读取数据 |  |  | 无限制 | 只读操作,无风险 |
| 生成草稿 |  |  | 20 篇/天 | 草稿不发布,不对外可见 |
| 发布内容 |  |  | 5 篇/天 | cooldown 30min |
| 修改已发布内容 |  |  | 5 次/天 | 需要 Telegram 确认 |
| 删除内容 |  | — | 0 | 永不允许,只能由人操作 |
| 调用外部 Webhook |  |  | 10 次/天 | 需记录 payload |
| 修改系统配置 |  | — | 0 | 高风险,禁止 |
| 发送通知/邮件 |  |  | 50 条/天 | 仅内部通知渠道 |

有几个填表时冒出来的发现让我们意识到之前有多粗心:

Agent 能修改系统配置。我们的 Agent 有调用内部 API 的权限,其中一个接口可以修改推送策略。当时给权限是因为"有时候需要动态调整"——但我们从没想清楚"谁决定什么时候动态调整"。

Agent 能调用外部 Webhook。理由是"有时候需要触发下游任务"。但 Webhook 是最不可逆的操作之一——发出去就发出去了,没有撤回。

填完矩阵之后,"删除"和"修改配置"两列全是 。为什么?因为当你真的坐下来思考"如果 Agent 今晚出 bug 误触发了这个操作,我能接受吗",答案是显然的。没有 Agent 需要以自动方式删除东西。如果它需要,那是流程设计的问题,不是权限的问题。

这张矩阵本身不是代码,是共识。它的价值是让整个团队对"Agent 有什么权力"有一个统一的认知,而不是各自揣测。


运维债 2:失败恢复——"重试 3 次"不是失败处理,是鸵鸟

Agent 的失败分两类,处理方式完全不同,混在一起处理是最常见的错误。

第一类:瞬时失败

网络超时、限流 429、临时服务不可用。这类失败是可重试的,指数退避就够了。

第二类:结构性失败

  • 内容违规被平台拒绝(重试 100 次结果一样)
  • 操作已成功执行但状态未记录(重试会产生副作用)
  • 认证过期(重试会连续产生 401)
  • Agent 的逻辑本身进入了死循环

把这两类都丢给"重试 3 次"的结果,我们第 4 天领教过:一个内容审核失败的任务重试了 5 次,每次都发出了相同的内容,被平台判定为刷量。

我们后来做了一个简单的失败分类器:

# failure_classifier.pyfrom enum import Enumclass FailureType(Enum):
    TRANSIENT = "transient"      # 可重试
    STRUCTURAL = "structural"    # 不可重试,需人工介入
    SIDE_EFFECT = "side_effect"  # 操作可能已成功,幂等检查后再决定def classify_failure(error_code: int, error_msg: str, attempts: int) -> FailureType:
    # 认证/授权失败 → 结构性,别重试
    if error_code in (401, 403):
        return FailureType.STRUCTURAL    # 内容审核/业务逻辑拒绝 → 结构性
    if error_code in (422, 451) or "content_policy" in error_msg:
        return FailureType.STRUCTURAL    # 已执行但未确认 → 先查幂等再决定
    if error_code == 0 and "timeout" in error_msg:
        return FailureType.SIDE_EFFECT    # 限流/服务不可用 → 可重试,读 Retry-After
    if error_code in (429, 503):
        return FailureType.TRANSIENT    # 超过重试次数 → 升级为结构性
    if attempts >= 3:
        return FailureType.STRUCTURAL    return FailureType.TRANSIENT# 在 task runner 里使用
async def handle_failure(task, error_code, error_msg):
    failure_type = classify_failure(error_code, error_msg, task.attempts)    if failure_type == FailureType.TRANSIENT:
        delay = 2 ** task.attempts * 60  # 2m, 4m, 8m
        await queue.retry_after(task, delay_seconds=delay)    elif failure_type == FailureType.SIDE_EFFECT:
        # 先检查操作是否已在目标侧生效
        already_done = await check_idempotency(task)
        if already_done:
            await queue.complete(task)  # 标记为完成,不重试
        else:
            await queue.retry_after(task, delay_seconds=60)    elif failure_type == FailureType.STRUCTURAL:
        await queue.fail_permanently(task)
        await notify_on_call(f"Task {task.id} hit structural failure: {error_msg}")

这段代码不长,但它显式地区分了"应该重试"和"不应该重试"——这个区分本来应该在 Agent 系统设计的第一天就做,而不是出了事故才做。

还有一个比较反直觉的发现:"操作已成功但状态未记录"的失败,比显式错误更危险

显式错误(422 Content Rejected)你能看到。但 Agent 发完请求、服务端已执行、但响应在网络中丢失了——这种情况 Agent 以为操作失败了,会重试,而目标侧已经执行了两次。解法是 Outbox Pattern:先写"我要做 X",执行完再写"X 完成了",两步写到不同存储,进程重启后先查 Outbox。这块在上篇(Agent 跑 24 小时后,我补上的 6 个运维护栏)有完整实现,这里不重复。


运维债 3:人工接管机制——"暂停"不等于"关掉"

这笔债我们欠得最晚,但付出的代价最直接。

第 5 天凌晨,Agent 在执行一个批量任务时行为开始异常——不是报错,而是开始生成质量极差的内容(上下文丢失导致,后来确认是 context window 溢出)。我们想"暂停"它,结果没有暂停入口,只能 kill

kill 掉进程有三个问题:

  1. 丢失 in-progress 任务的状态,重启后可能重跑
  2. 不知道它当前做到哪一步了
  3. 重启后 Agent 从头开始,之前的错误状态(比如错误的 context)还在

我们后来实现了一个轻量的分级接管协议,核心就是 4 个端点:

# Level 1 - 软暂停:完成当前任务后不接新任务
curl -X POST  
  -H "Authorization: Bearer $ADMIN_TOKEN" 
  -d '{"reason": "人工检查", "operator": "ethan"}'# Level 2 - 检查点停止:跑完当前步骤就停
curl -X POST # Level 3 - 状态快照后立即停止
curl -X POST  
  -d '{"save_context": true}'# 恢复(Level 1/2/3 均通用)
curl -X POST  
  -d '{"operator": "ethan"}'

四个端点,核心是这样一张状态机:

RUNNING ──pause──→ PAUSING ──task_done──→ PAUSED
   │                                         │
   ├──freeze──→ FROZEN                   resume│
   │                                         │
   └──terminate──→ TERMINATED         RUNNING ←─┘

这套协议的关键不是技术细节,而是让"暂停"成为 Agent 内置行为,而不是外部强杀。Agent 代码里每完成一个步骤都会检查一次控制状态——如果是 PAUSING,下一个任务不取了;如果是 FROZEN,立刻保存状态并退出。

有了这个机制之后,我们遭遇的 2 次异常都在 2 分钟内完成了接管,没有额外的状态损坏。


运维债 4:记忆污染——长跑 Agent 的 context 会越跑越"脏"

这是最难察觉的一笔债。

当 Agent 第一次跑的时候,它的上下文是干净的——只有任务指令和当次的工具返回结果。但随着任务轮次累积,很多团队的 Agent 会把历史任务的摘要追加进 context(为了让它"记住"之前做了什么)。

这件事在短期内看起来很有用——Agent 知道"今天已经发了 3 篇文章",不会重复发。但有一个问题几乎没有人在设计阶段想到:追加进去的历史摘要本身可能是错的

如果某次任务失败了,失败的部分结果也可能被摘要进了 context。下一次任务开始时,Agent 的"起点认知"就带着上次的错误残留。错误会随着轮次传播,直到某次表现异常才被人发现——而此时追溯原因已经很困难了,因为 context 已经被多轮任务的摘要叠加了好几层。

我们叫它记忆污染(Memory Poisoning)。

解法不是不用 context 记忆,而是分清两种信息:

# memory_strategy.py# 类型 A:事实性状态(不会过期、不会出错)
# 存数据库,Agent 每次启动时查询,不放进 context
FACTUAL_STATE = {
    "published_articles_today": "SELECT COUNT(*) FROM audit WHERE action='publish' AND date=today()",
    "pending_tasks": "SELECT COUNT(*) FROM task_queue WHERE status='pending'",
    "last_run_at": "SELECT MAX(completed_at) FROM task_queue WHERE status='completed'"
}# 类型 B:上下文理解(当前任务内有效,跨任务无效)
# 只放进单次任务的 context,任务完成后丢弃
EPHEMERAL_CONTEXT = [
    "当前任务的用户意图",
    "这次调用的中间结果",
    "临时的推理过程"
]# 错误的做法:把 B 类信息持久化到下次任务
def wrong_approach():
    summary = agent.summarize_last_run()  # 包含了错误的中间状态
    next_context = f"上次运行摘要:{summary}nn{new_task}"
    # 错误传播了# 正确的做法:用数据库存 A 类,B 类每轮清空
def right_approach(new_task):
    # 每次启动时,从数据库查询干净的事实状态
    state = db.query_state(FACTUAL_STATE)
    fresh_context = f"""
当前事实状态(来自数据库,不是上轮摘要):
- 今日已发布:{state['published_articles_today']}
- 待处理任务:{state['pending_tasks']}当前任务:{new_task}
"""
    return fresh_context

这个改动之后,我们的 Agent 在连续运行 48 小时后,性能没有出现之前观察到的"越跑越奇怪"现象。不是因为 Agent 变聪明了,而是因为它每次启动时拿到的是干净的事实,而不是上次的推理残留。


运维债 5:成本归因——不知道钱花在哪,就不知道该砍哪里

我们用 Agent 跑了 7 天之后,算了一下 token 账单:比预期高了 2.3 倍。

高在哪里?这才是问题所在——我们说不清楚。

Agent 每天跑很多任务,每个任务都消耗 token,但我们没有按任务类型聚合的账单。结果是一笔大数字,没法决策:是某类任务本来就贵,还是某个步骤在无效循环?是 context 带得太多,还是工具调用太频繁?

补上成本归因只需要一个中间件层:

# token_tracker.pyimport time
from dataclasses import dataclass, field
from typing import Optional
import sqlite3@dataclass
class TokenRecord:
    task_id: str
    task_type: str       # 'write_draft', 'publish_check', 'research', etc.
    model: str
    input_tokens: int
    output_tokens: int
    cost_usd: float
    step: str            # 任务内的步骤名
    timestamp: float = field(default_factory=time.time)# 按任务类型的成本汇总(一周数据)
COST_SUMMARY_QUERY = """
SELECT
    task_type,
    COUNT(*) as task_count,
    SUM(input_tokens) as total_input,
    SUM(output_tokens) as total_output,
    ROUND(SUM(cost_usd), 4) as total_cost_usd,
    ROUND(AVG(cost_usd), 4) as avg_cost_per_task
FROM token_records
WHERE timestamp > strftime('%s', 'now', '-7 days')
GROUP BY task_type
ORDER BY total_cost_usd DESC;
"""

我们跑了这个查询之后,看到的结果:

任务类型任务次数(7天)总成本(USD)单次均价占比
research_topic34$4.21$0.12438%
write_draft19$3.87$0.20435%
publish_check89$1.44$0.01613%
format_review56$0.98$0.0189%
context_summary203$0.56$0.0035%

context_summary 单次很便宜,但运行了 203 次——比所有其他任务加起来都多。往下钻一层才发现:有一段逻辑在每次工具调用前都会重新 summarize 一次全量 context,触发了 203 次 summary 任务。这个无效调用不改代码根本发现不了,因为从日志里看它"正常运行"。

修掉这个之后,下一周账单降了 31%。

这就是成本归因的价值——不是让你削减 Agent 能力,而是让你找到那些功能上多余、费用上昂贵的调用。


5 笔债的优先级和实施顺序

不是所有团队都要同时补这 5 项,按影响和成本排一下:

债务不补的最坏后果实施成本建议优先级
权限矩阵Agent 误触不可逆操作(删除/外发)低(2小时填表)P0
失败分类重试产生副作用,被平台标记异常中(1天代码)P0
人工接管异常时只能强杀,状态损坏中(1天代码)P1
记忆清洗长跑后 Agent 行为越来越不可预测中(半天重构)P1
成本归因账单说不清楚,优化无法决策低(几小时加日志)P2

权限矩阵是最快能做的——不需要写代码,只需要团队坐下来填一张表,达成共识。但恰恰是这种"不需要代码"的事,在工程师团队里最容易被推迟。

有个观察可能反直觉:权限矩阵的主要价值不是防止 Agent 乱来,而是防止团队里的人对 "Agent 能做什么" 各执一词。出了事之后你会发现,5 个人里可能有 3 种不同的理解——有人以为 Agent 不能改配置,有人以为只要有 API Key 就可以改。矩阵的价值是消除这种信息差。


我们在第 7 天发现的真正问题

7 天的运维经历之后,我觉得把所有问题归结为"技术债"有点过度简化了。

真正的问题是这样的:团队接入 Agent 的速度,比团队建立 "对 Agent 的共同认知" 的速度快了一个数量级。

每个人都知道"我们有个 Agent 在跑",但没有人能清楚说出:

  • 它今天做了什么(→ 需要审计日志摘要)
  • 它有什么权力(→ 需要权限矩阵)
  • 出问题了谁负责(→ 需要明确值班角色)
  • 长期跑下去成本怎么算(→ 需要成本归因)

这些不是工程问题,是团队信息对齐问题。工程机制(代码、协议、日志)是手段,目标是让团队里每个人对 Agent 的边界有一致的认知。


可复制的治理清单

把上面 5 项整理成团队可以直接执行的检查列表:

## AI Agent 治理清单 v1(openclaw-lab 验证)### 上线前(必须)
- [ ] 权限矩阵填写完成,所有不可逆操作标注  或 (需审批)
- [ ] 失败分类逻辑实现:区分 transient / structural / side_effect
- [ ] 人工接管端点存在:至少支持 pause 和 resume
- [ ] 幂等 key 设计:发布/外发类操作必须有去重机制### 上线后 7 天内(建议)
- [ ] context 记忆策略审查:事实性状态从数据库读,不从上轮摘要读
- [ ] 成本归因上线:按任务类型拆分 token 消耗
- [ ] 审计日志接入:记录所有产生外部副作用的操作### 持续运营(每周)
- [ ] 有人每天花 5 分钟看 Agent 审计摘要
- [ ] 每周一次成本查询,识别异常涨幅
- [ ] 每两周回顾一次权限矩阵,是否有权限需要收紧

这张清单不完整,也不需要完整——它的目标是让团队在 7 天内建立最基础的可见性,而不是一步到位搭建完整的 Agent 治理平台


常见问题

Q:权限矩阵应该多细粒度?操作级还是接口级?

A:从操作语义级别开始,不要从接口级开始。"发布文章"比"调用 /api/v1/posts POST" 更容易达成共识——前者人人能理解,后者只有写代码的人看得懂。接口级权限是实现细节,矩阵是团队共识文档,两个层面都要有,但不要混在一张表里。

Q:记忆污染多久之后会开始明显影响输出质量?

A:根据我们的观察,context 带有历史摘要的 Agent,在第 5-7 个任务周期之后开始出现可察觉的偏差(比如推断任务状态时更依赖"上次说过的话"而非工具实时返回的结果)。这个拐点因 context 窗口大小、摘要质量和任务类型不同而不同。没有通用数字,但"三天不出问题不代表没问题"——改变通常是渐进的,要主动测,不能等到明显异常才排查。

Q:成本归因是实时的还是离线的?

A:先做离线的。用 SQLite 存每次大模型调用的 token 数和模型名,每天跑一次汇总查询。不需要实时 dashboard——对大多数团队来说,离线日报的信息密度已经够做决策了。等到你识别出需要实时告警的指标(比如某类任务成本突然涨了 50%),再上实时监控。从离线查询开始,工程成本低,验证了价值再投入。

相关文章

精彩推荐