让一个自主 Agent 跑起来,三天就能做到。但让一个 Agent 在团队里长跑------权限收口、接管机制、记忆清洗、成本核算------这些债你一定会还,问题只是主动还还是被动还。这是我们第 7 天补的那批账单。
我很少在技术文章里聊"团队"这个词。这次要聊。
不是因为 Agent 技术变难了,而是因为我们意识到:Agent 跑久了,制造的不是 bug,是治理空白。你的代码没写错,模型没出问题,但团队里开始出现一些奇怪的对话------
"这个任务谁批的?"
"Agent 把那个配置改了,是有人让它改的吗?"
"它昨晚干了啥我能看到吗?"
这类问题的共同来源是:当初接入 Agent 的时候,我们把它当成了一个工具------像 CI/CD 一样,跑就跑,挂了重启。但它实际上更接近一个有权限的同事------它能写、能改、能发、能删,而且从不请假。
这篇是我们 openclaw-lab 运行 7 天之后,在团队层面补上的 5 笔治理债。没有宏观方法论,只有实际踩过的坑和补上的机制。
运维债 1:权限矩阵------Agent 该能做什么,你可能没认真想过
Agent 上线前,大多数团队做的权限设计是这样的:
"它需要调 API,那就给它 API Key。它要发帖,那就让它有发帖权限。"
这不是权限设计,这是权限发放。两者的区别:
- 权限发放:把 Agent 需要用到的权限全给了,然后祈祷它不出事
- 权限设计:把 Agent 在正常路径上需要的权限给了,并且明确了它不应该能做什么
我们在第 6 天做了一张权限矩阵,这件事花了我们一个小时,但此前五天没人做,因为每个人都以为别人在管这件事。
markdown
## 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 次,每次都发出了相同的内容,被平台判定为刷量。
我们后来做了一个简单的失败分类器:
python
# failure_classifier.py
from enum import Enum
class 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 掉进程有三个问题:
- 丢失 in-progress 任务的状态,重启后可能重跑
- 不知道它当前做到哪一步了
- 重启后 Agent 从头开始,之前的错误状态(比如错误的 context)还在
我们后来实现了一个轻量的分级接管协议,核心就是 4 个端点:
bash
# Level 1 - 软暂停:完成当前任务后不接新任务
curl -X POST http://localhost:8765/control/pause \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d '{"reason": "人工检查", "operator": "ethan"}'
# Level 2 - 检查点停止:跑完当前步骤就停
curl -X POST http://localhost:8765/control/checkpoint-stop
# Level 3 - 状态快照后立即停止
curl -X POST http://localhost:8765/control/freeze \
-d '{"save_context": true}'
# 恢复(Level 1/2/3 均通用)
curl -X POST http://localhost:8765/control/resume \
-d '{"operator": "ethan"}'
四个端点,核心是这样一张状态机:
sql
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 记忆,而是分清两种信息:
python
# 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}\n\n{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 带得太多,还是工具调用太频繁?
补上成本归因只需要一个中间件层:
python
# token_tracker.py
import 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_topic |
34 | $4.21 | $0.124 | 38% |
write_draft |
19 | $3.87 | $0.204 | 35% |
publish_check |
89 | $1.44 | $0.016 | 13% |
format_review |
56 | $0.98 | $0.018 | 9% |
context_summary |
203 | $0.56 | $0.003 | 5% |
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 项整理成团队可以直接执行的检查列表:
markdown
## 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%),再上实时监控。从离线查询开始,工程成本低,验证了价值再投入。