Agent 上了生产之后,最常听到的三个问题是:它现在在干什么、它烧了多少钱、以及怎么让它立刻停下来。日志能回答一部分,但日志是事后翻的;网关能限一部分,但把网关塞进链路就多了一个故障点。Flightdeck 走的是第三条路:它不是代理,而是一套传感器加控制面。传感器在 Agent 自己的进程里包住 SDK 的客户端类,模型调用仍然直连厂商,观测数据走带外 HTTP 上报,控制指令则从 HTTP 响应信封里带回来。本教程六步把它立起来:起控制面、接两行传感器、给每个 Agent 一个稳定身份、配令牌预算、用急停与自定义指令接管运行中的 Agent,最后用 MCP 策略管住它能碰哪些服务。
Step 1:先把控制面立起来,并确认它不是代理
先把整套栈起起来看形态。克隆仓库后一条命令会拉起四个服务: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 集成,告警得接在你自己的监控里。另外它只自托管,一次部署一个租户。
Step 2:两行代码,但不能放错位置
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:身份、提示词捕获与会话重挂
每个会话带两个标识:一个是持久的 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 下能正常工作,但按线程隔离会话目前还不支持。
Step 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:急停与自定义指令
控制面的另一半是运行时干预。仪表盘上可以停掉某一个 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 策略,以及必须知道的已知限制
最后一件事是管住 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 线上会改名,升级前先看路线图。
常见问题速查
| 现象 | 原因与处理 |
|---|---|
| 仪表盘上看不到 Agent | init 与 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 可用 |