进阶 📋 6 个步骤 第 576 / 578 篇

用 Flightdeck 给 Agent 机队装上仪表盘与急停开关:传感器接入、令牌预算与 MCP 策略实操

Agent 上了生产之后,总有三个问题绕不开:它在干什么、烧了多少钱、怎么立刻停。网关会多出一个故障点,日志只能事后翻。Flightdeck 用传感器加控制面的方式回答这三个问题。本教程六步落地。

2026-10-29· 12 分钟上手· 约 3843 字· 📡 Flightdeck / 📊 机队仪表盘

Agent 上了生产之后,最常听到的三个问题是:它现在在干什么、它烧了多少钱、以及怎么让它立刻停下来。日志能回答一部分,但日志是事后翻的;网关能限一部分,但把网关塞进链路就多了一个故障点。Flightdeck 走的是第三条路:它不是代理,而是一套传感器加控制面。传感器在 Agent 自己的进程里包住 SDK 的客户端类,模型调用仍然直连厂商,观测数据走带外 HTTP 上报,控制指令则从 HTTP 响应信封里带回来。本教程六步把它立起来:起控制面、接两行传感器、给每个 Agent 一个稳定身份、配令牌预算、用急停与自定义指令接管运行中的 Agent,最后用 MCP 策略管住它能碰哪些服务。

📡 本教程适合:已经在用 LangChain、LangGraph、CrewAI、LlamaIndex,或者直接调 Anthropic 与 OpenAI SDK 的 Python Agent,并且需要回答「谁在跑、跑掉多少钱、怎么停」这三个问题的人。你需要 Docker Engine 28 及以上与 Compose v2;生产部署另需一个域名与 TLS 证书。项目为 Apache-2.0,仅自托管,没有多租户 SaaS 版本。

Step 1:先把控制面立起来,并确认它不是代理

1 控制面起在本机,传感器跑在 Agent 进程里

先把整套栈起起来看形态。克隆仓库后一条命令会拉起四个服务:nginx 在 4000 端口做入口,把 /ingest 前缀转到 8080 的摄取服务、/api 转到 8081 的查询服务、根路径转到 3000 的仪表盘;事件总线用 NATS JetStream,状态落在 Postgres 里。开发栈会自动播种一个测试令牌,仪表盘就在 4000 端口。

git clone https://github.com/flightdeckhq/flightdeck
cd flightdeck
make dev
# 仪表盘:http://localhost:4000     开发栈自动播种的测试令牌:tok_dev

# 生产(需要域名与 TLS):
docker compose \
  -f docker/docker-compose.yml \
  -f docker/docker-compose.prod.yml up -d
# 生产叠加层在 nginx 上终止 TLS、拒绝开发用令牌、给所有服务加 restart: unless-stopped

动手之前先把「它不是什么」记牢,否则很容易用错地方。它不是代理:调用直连厂商,没有任何流量穿过控制面,所以执行路径上不会多出一个故障点。它不是编排器:只观测,从不决定 Agent 下一步做什么。它不是计费系统:成本按公开目录价估算,是对账时的参考而不是发票。它也不是通知平台:没有 Slack、邮件或 PagerDuty 集成,告警得接在你自己的监控里。另外它只自托管,一次部署一个租户。

💡 只想先看效果的话,先跑 make dev 再往下走两节。生产部署多出来的成本几乎全在证书与令牌管理上,与控制面本身无关。

Step 2:两行代码,但不能放错位置

2 init 加 patch,顺序错了会静默失效

Python Agent 的接入就是两行:init 建立上报通道,patch 把 SDK 的客户端类包一层。patch 是绝大多数场景的正确选择,它会拦住几乎所有客户端,包括框架在内部自己构造的那些;只有在想手工包住某一个实例时才用 wrap,而且用了 wrap 就不要再调 patch。

pip install flightdeck-sensor

# 入口文件最顶部,必须在任何客户端被构造之前
import flightdeck_sensor

flightdeck_sensor.init(
    server="http://localhost:4000/ingest",
    token="tok_dev",
)
flightdeck_sensor.patch()

# 下面是你原来的代码,一行都不用改
import anthropic
client = anthropic.Anthropic()
client.messages.create(model="claude-sonnet-4-6", ...)

位置错了是这类接入最常见的失败原因,而且失败得很安静:不上报、不报错,仪表盘上就是没有这个 Agent。三条具体规则值得抄进代码评审清单。其一,patch 必须跑在 OpenAI 客户端构造之前,在它之前碰过 .chat、.responses 或 .embeddings 的实例会保留原始资源缓存,不被拦截。其二,Anthropic 客户端无论何时构造都会从下一次调用开始被拦,但要求 anthropic 不低于 0.86;更旧的 SDK 只在导入时报一条警告,调用完全不计入。其三,在 patch 之前被碰过的 with_raw_response 与 with_streaming_response 视图会绑到未打补丁的方法上,同样不计入。

有两处调用是不计量的:Anthropic 的 Message Batches(messages.batches.*)与 count_tokens 没有被打补丁,因此批量任务的花费既不受预算约束,也不受急停影响。如果你的 Agent 主要靠 Batch API 跑离线任务,请把这两条当成已知缺口单独记账,而不是假设它已经被管住了。

Step 3:身份、提示词捕获与会话重挂

3 flavor 是后面所有配置的键

每个会话带两个标识:一个是持久的 flavor,一个是临时的会话 id。flavor 用来把 Agent 归成一类,也是后面配预算与策略的单位;不设的话会退化成「用户@主机名」,好处是这样连没按规范部署的 Agent 也会露出来,坏处是它们聚不成一类。

# 生产环境推荐由编排清单注入
AGENT_FLAVOR=research-agent
FLIGHTDECK_SERVER=https://flightdeck.example.com/ingest
FLIGHTDECK_TOKEN=ftd_...

# FLIGHTDECK_AGENT_NAME 是 AGENT_FLAVOR 的别名,两者取其一即可

# 提示词捕获:生产 Agent 默认关闭,只有显式打开才上传完整载荷
FLIGHTDECK_CAPTURE_PROMPTS=true     # 传感器默认 false

提示词捕获这一项要单独拿来讲。生产 Agent 的传感器默认是关的,关着的时候事件里只有 token 数、模型名、延迟、框架名与工具名,没有正文。Claude Code 插件是例外:它观察的是你自己机器上的会话,默认会捕获提示词与脱敏后的工具输入,设成 false 就关掉了。还有一条容易被忽略——开了捕获之后,经 Bedrock、Vertex、Foundry、AWS 与 Mantle 客户端发出的提示词会到达控制面,而这些平台往往正是出于数据驻留才被选中的,因此这类会话要么保持关闭,要么把控制面放进同一个驻留边界。

# 编排器重跑时想要「续上」而不是「新建」会话:传一个稳定 UUID
import uuid
import flightdeck_sensor as fd

NS = uuid.UUID("00000000-0000-0000-0000-000000000001")   # 固定命名空间
fd.init(
    server="http://flightdeck.internal/ingest",
    token="ftd_...",
    session_id=str(uuid.uuid5(NS, workflow_id)),          # 同输入同 UUID
)

这一手对 Temporal 工作流或 Airflow DAG 很有用:任务重跑时在时间线上是连续的一段,而不是每跑一次多出一条新会话。会话 id 必须是合法 UUID,用 uuid5 配固定命名空间就能让同一个工作流每次算出同一个值。另有两条硬限制:每个进程只能有一次 init,第二次是空操作并带一条警告;多智能体框架在单次 init 与共享 flavor 下能正常工作,但按线程隔离会话目前还不支持。

💡 把 flavor 的命名当成本钱来管。它是后面所有预算、策略与分组统计的键,名字散了,仪表盘上就只剩一堆「用户@主机名」,什么都聚不起来。

Step 4:令牌预算:警告、降档、抛错三档

4 策略集中配,判定在进程内执行

预算按 flavor 集中配,Agent 在会话开始时把策略拉下来,之后在进程内自己执行,不需要改代码,也不需要每次调用去问服务端。三档行为是固定的:到预算的某个比例发一条事件但调用照常继续;再往上发降档事件并换到更便宜的模型;到顶抛异常。每一次判定都会作为结构化事件落在时间线上,事后能复盘是哪一次调用撞的线。

warn     默认 80%   -> policy_warn      事件记录,调用继续
degrade  默认 90%   -> policy_degrade   自动换到更便宜的模型
block    默认 100%  -> policy_block     抛 BudgetExceededError

try:
    resp = client.messages.create(...)
except flightdeck.BudgetExceededError:
    # 预算用尽:这里要决定是降级返回、排队还是转人工
    return degrade_to_cached_answer()

降档这一档最容易被误读成「自动省钱」。它真正的价值是给系统一个缓冲区:在彻底不可用之前,服务还能用一个更便宜的模型把请求接住。前提是你在代码里真的处理了那个异常——不处理的话,它就是一个普通异常,会以你最不希望的形式冒到用户面前。另一个边界:传感器只执行 Agent 自己已知的预算,它不做模型替换之外的兜底,也没有网关那样的缓存与重试。

预算是本地执行的,不是服务端拦截的:没有接传感器的 Agent,或者绕开被拦截客户端的调用路径,都不会被计入。把预算当成全部的成本防线之前,请在厂商侧也设一层硬限额。

Step 5:急停与自定义指令

5 停止信号在下一次调用时送达

控制面的另一半是运行时干预。仪表盘上可以停掉某一个 Agent,也可以按 flavor 停掉一整类;停止信号会在 Agent 的下一次 LLM 调用时送达。注意是「下一次调用」——一个正在长思考里的 Agent 不会立刻断掉,这是传感器式设计的代价,也是它不引入故障点的原因。自定义指令则让你把任意 Python 函数挂到仪表盘上调用,返回值会作为结果事件落回时间线。

import flightdeck_sensor

@flightdeck_sensor.directive(
    name="clear_cache",
    description="清空提示词缓存",
    parameters=[
        flightdeck_sensor.Parameter(
            name="cache_type", type="string",
            options=["all", "prompt"], default="all",
        ),
    ],
)
def clear_cache(context, cache_type="all"):
    return {"cleared": my_cache.clear(cache_type)}

参数模式只用来生成仪表盘上的表单和指令指纹,运行时刻并不会替你校验——入参验证是自己的责任,别指望装饰器挡住越界的取值。还有一处能力差要说清楚:Claude Code 走的是钩子插件,它上报每一次钩子事件并按 flavor 的 MCP 策略拒绝工具调用,但它没有急停也没有令牌预算,因为钩子无法在调用中途采取行动,仪表盘上对这类会话直接隐藏了停止按钮。

别把急停当成安全边界:它是给运维用的「立刻止损」按钮,送达时机取决于 Agent 何时发起下一次调用。真正需要强隔离的场景,请在执行层用沙箱或容器,而不是依赖控制面发信号。

Step 6:MCP 策略,以及必须知道的已知限制

6 默认全放行,收紧是主动选择

最后一件事是管住 Agent 能碰哪些 MCP 服务。策略分两层:一个全局策略,带允许名单或拒绝名单模式与条目列表;若干按 flavor 的策略,带相对全局的放行或拒绝增量。安装时会自动创建一个空的全局策略,模式是拒绝名单、条目为零,也就是说默认完全放行。

解析规则:按服务器逐条判定,最具体的那一层优先

1) 该 flavor 的策略里有这个 URL 的条目   -> 用它
2) 否则全局策略里有这个 URL 的条目       -> 用它
3) 否则按全局模式的默认值:
      允许名单模式 -> 未知服务器被拒绝
      拒绝名单模式 -> 未知服务器被放行

服务器身份 = 规范化 URL(或 stdio 命令),与它自报的名字无关
展示用指纹 = sha256(规范化URL + 0x00 + 名字)

判定结果有两种:警告会发一条事件然后放行;拒绝会发事件、冲刷事件队列并抛出异常,框架会把它当成一次工具调用失败交回给 Agent 的推理循环。策略更新通过响应信封里的指令刷新缓存,但新的策略要到下一次会话开始才生效——正在跑的会话沿用它开始时那份。观测面覆盖六类 MCP 事件:工具列表、工具调用、资源列表、资源读取、提示词列表、提示词获取。

上线前值得抄走的六条已知限制

1) Anthropic 需要 >= 0.86,更旧的 SDK 调用完全不计入
2) Message Batches 与 count_tokens 未打补丁,批量花费在预算与急停之外
3) litellm 的 text_completion 与 responses 入口未打补丁
4) 一个进程只能有一次 init;按线程隔离会话尚未支持
5) mcp 2.x 的 Client 会命中响应缓存,缓存回答不产生 MCP 事件(传 cache=None 可观测每个请求)
6) llama-index-llms-anthropic 至 0.12.3 总是带 temperature,在 anthropic 1.x 上失败

框架覆盖方面,凡是内部构造 Anthropic 或 OpenAI 客户端的框架都会被自动拦住,包括 LangChain、LangGraph、LlamaIndex、CrewAI 1.14+ 与 bifrost;事件的 framework 字段取裸名,并且高层框架优先于传输层——一条走 litellm 的 LangChain 流水线报的是 langchain。分析页可以按 flavor、模型、框架、主机、Agent 类型、团队、厂商、角色与父会话分组。最后一条预期管理:成本是估算值,定价数据在 pricing.yaml 里,把它当成对账参考而不是发票;客户端接口在 2.x 线上会改名,升级前先看路线图。

💡 落地顺序建议:先只接传感器、不配任何策略,跑一周把机队的真实画像看清;再按 flavor 设预算;最后才收紧 MCP 策略。反过来做的话,你会先挡住一堆自己都还没数清的调用,然后花几天排查为什么任务突然失败。

常见问题速查

现象原因与处理
仪表盘上看不到 Agentinit 与 patch 没放在入口最顶部;客户端在 patch 之前就被构造了
接入后毫无事件检查 anthropic 是否不低于 0.86;更旧版本只在导入时报一条警告
批量任务花费没被算进去Message Batches 与 count_tokens 未打补丁,属已知缺口,需单独记账
想让重跑续上同一条时间线给 init 传稳定的会话 UUID,用 uuid5 配固定命名空间
Agent 显示为「用户@主机名」没设 AGENT_FLAVOR 或 FLIGHTDECK_AGENT_NAME
担心提示词被上传生产传感器默认关闭;Claude Code 插件默认开启,设 FLIGHTDECK_CAPTURE_PROMPTS=false
点停止后 Agent 没立刻停信号在下一次 LLM 调用时送达,长思考不会立刻中断
Claude Code 会话没有停止按钮钩子插件无急停与预算,仪表盘对这类会话隐藏该按钮
MCP 调用被拒但配置没错查 flavor 级策略;最具体的一层优先,未知服务器按全局模式处理
改了策略却没生效新策略在下一次会话开始才生效,运行中的会话沿用旧策略
成本数字和账单对不上按公开目录价估算,只作对账参考,不是发票
想接 Slack 或 PagerDuty 告警没有通知集成,请在自己的监控里接告警
多智能体想按线程分开统计按线程隔离会话尚未支持;单次 init 配共享 flavor 可用
← 返回教程中心