Day 54 / 共 68 天 · 阶段 10 可信可观测

失败闸门 & 护栏:出错但不闯祸

昨天(Day 53)的 Critic 防的是"内容编造"。但真实系统的风险远不止此:工具会调失败、模型会宕机、Agent 可能想执行危险操作(删库、大额转账)、token 会烧超预算。今天学一整套失败闸门与护栏:降级不崩、危险动作先审批、预算闸自动跳闸——让系统在出错时依然"稳、安全、不烧钱"。明天(Day 55)学怎么把这一切看得见(可观测)。

📍 你在阶段 10(可信可观测 D53-56)的位置
D53 防幻觉 Critic D54 失败闸门&护栏 D55 可观测 D56 成本治理&缓存
💡 用一个类比兜住今天(今天全程沿用「汽车安全系统」的世界观) 给 Agent 装的这套东西,就像一辆车的安全系统降级=备胎:轮胎爆了不趴窝,换备胎慢慢开到修理厂;熔断=保险丝:某处一直打火,先断电保住整车别烧了;危险动作审批=倒车影像+安全带提醒:真要做危险操作前,先让人确认一下;预算闸=油表报警:快没油(钱)了自动提醒/限速,别把油烧干抛锚在高速上。今天的核心信念:好系统不是不出错,而是出错时依然安全、可控、不闯大祸。
L01

核心心法:出错但不闯祸

🤔 痛点新手总想"把 Agent 做到永不出错"。但大模型有随机性、外部 API 会抖、网络会断——零故障是幻想。只追求"不出错",一出错就是灾难性的崩溃或闯祸。
💡 本质专业系统的目标不是"不出错",而是"优雅地出错":故障发生时,系统能兜住、能降级、能报警、能不做危险的事。就像汽车不能保证不爆胎,但能保证爆胎时不翻车。可靠性 = 对故障有预案,而不是祈祷别出故障。
📝 举个例子:同一个故障,两种系统 调用翻译 API 时它超时了。
· 脆弱系统:直接抛异常,整个 Agent 崩溃,用户看到一片红色报错。
· 健壮系统:捕获超时→重试一次→还失败就降级用备用翻译源→再不行就回一句"翻译服务暂时不可用,请稍后再试",同时记日志报警。用户体验从"崩了"变成"慢一点但有交代"。
👶 这不就是 Day 05 学的 try/except 吗?是它的"升级版、系统版"。try/except 是单点接住一个错;今天讲的是把接错组织成体系:接住之后怎么重试、怎么降级、什么时候熔断、危险操作要不要拦、钱烧超了怎么办。从"点"到"面",这就是护栏(guardrail)。
L02

降级:不崩,就是胜利

🤔 痛点你的 Agent 依赖好几个外部服务(大模型、向量库、各种工具 API)。只要有一个挂了,整个 Agent 就跟着趴窝,用户什么都得不到。
💡 本质降级(graceful degradation)=主方案不行时,退而求其次给一个"次好但可用"的结果,而不是直接罢工。像爆胎换备胎——备胎跑不快,但能让你安全开到修理厂,总比困在路边强。
降级链:一层不行,退下一层,始终有兜底 主方案 最强模型/工具 备用方案 便宜模型/缓存 兜底话术 "稍后再试/转人工" 绝不 整体崩 挂了 也挂了
图注:设计时永远留一条"最末兜底"——哪怕所有花哨方案都挂了,也能给用户一句人话,而不是一片报错。
# 降级链:主模型挂了退备用模型,再挂就给兜底话术。绝不让异常冒到用户面前
def ask_with_fallback(question):
    try:
        return call_main_model(question)      # ① 主方案:最强的模型
    except Exception:
        pass
    try:
        return call_cheap_model(question)     # ② 备用:便宜/更稳的小模型
    except Exception:
        pass
    return "服务暂时繁忙,请稍后再试,或点此转人工。"   # ③ 兜底:始终有话可回
L03

重试与熔断:该坚持时坚持,该放弃时放弃

🤔 痛点API 偶尔抖一下(临时网络问题),重试一下就好——但如果它彻底挂了,你还傻乎乎一直重试,只会拖垮自己、还把对方越打越死。
💡 本质两个配套机制:重试(retry)=偶发失败就再试几次(像打电话占线,隔一会儿再拨),通常配"越等越久"的退避;熔断(circuit breaker)=如果某服务连续大量失败,就暂时不再打它、直接走降级,像保险丝跳闸——先断开保护整体,过阵子再试探性恢复。
机制解决关键细节汽车类比
重试 retry偶发、临时的失败限次数(如 3 次)、退避(越等越久)占线了隔会儿再拨
熔断 circuit breaker持续性故障连续失败到阈值就"跳闸",暂停调用保险丝跳闸保整车
超时 timeout卡死不返回设最长等待,超了当失败处理等太久就放弃这条路

👶 小白:无限重试到成功为止,不是更稳吗?

👨‍🏫 老师:恰恰相反,无限重试是"雪崩加速器"。对方本来就快挂了,你还拼命重试,等于往火上浇油,把它彻底打死,还把你自己的资源耗光、让用户干等。所以要限次数 + 退避 + 熔断三件套:偶发抖动靠重试兜住,真挂了就赶紧熔断、走降级。"知道什么时候该放弃"是可靠性工程的关键智慧——面试聊到高可用,这几个词能说清楚就很加分。

L04

危险动作先审批:人在环把最后一道闸

🤔 痛点你给 Agent 配了工具:能发邮件、能改数据库、能退款。可万一它"想岔了",自作主张删了全库数据、给用户退了 100 万——这可不是重试能挽回的。
💡 本质把工具分级:低风险的(查天气、读文档)让 Agent 自己做;高风险、不可逆的(删数据、大额转账、群发邮件)必须"人在环(human-in-the-loop)"——执行前暂停,交给人确认(呼应 Day 41 的 interrupt)。像开车时系统提醒"确认要倒车吗?",把最危险的一下交给人拍板。
# 危险动作分级:高风险工具执行前必须人工确认
DANGEROUS = {"delete_db", "refund", "send_bulk_email"}   # 高风险工具清单

def run_tool(name, args):
    if name in DANGEROUS:
        # 不可逆/高风险:先暂停,把意图摆给人看,等人点"批准"才继续
        print(f"⚠️ Agent 想执行【{name}】,参数:{args}")
        if input("批准吗?(yes/no) ") != "yes":
            return {"status": "denied", "msg": "人工拒绝,已取消"}   # 拦下,安全
    return really_run(name, args)   # 低风险 或 已获批准,才真正执行
判断"要不要审批"的黄金标准:这个动作可逆吗?出错代价大吗? 可逆又低代价(如发一条查询)放行;不可逆或高代价(删除、花钱、对外发送)一律先审批。宁可多问一句,也别让 Agent 闯不可挽回的祸。
L05

预算闸:别让 Agent 把钱烧穿

🤔 痛点Agent 是循环工作的(想→动手→看→再想)。万一它陷入死循环、或被恶意用户诱导疯狂调用,一夜之间就能烧掉天文数字的 token 费——早上一看账单,心梗。
💡 本质预算闸(budget guard)=给每次任务/每个用户/每天设一个消耗上限(步数、token、钱),到顶就自动跳闸停下,像油表报警+断油保护。呼应 Day 32 的"循环上限"——这里把它扩展成"钱和步数的双保险"。
# 预算闸:给一次任务设步数和 token 双上限,到顶就跳闸
class BudgetGuard:
    def __init__(self, max_steps=10, max_tokens=50_000):
        self.max_steps, self.max_tokens = max_steps, max_tokens
        self.steps = self.tokens = 0
    def charge(self, used_tokens):        # 每走一步/每次调用后记账
        self.steps += 1
        self.tokens += used_tokens
        if self.steps > self.max_steps or self.tokens > self.max_tokens:
            raise RuntimeError("💸 预算超限,跳闸停止")   # 到顶立即刹车

guard = BudgetGuard()
# 在 Agent 主循环里,每步都 guard.charge(本步token);超了就抛错、走降级兜底
📝 举个例子:预算闸救命 某 Agent 因一个 bug 陷入"调工具→失败→再调"的死循环。没有预算闸,它一晚上跑了 8 万次、烧掉一大笔钱才被发现。加了预算闸后,跑到第 10 步就自动跳闸、报警、走兜底——损失从"一大笔"变成"几分钱 + 一条告警"。这类闸门看着不起眼,却是真金白银的保命符。
L06

输入输出护栏:进门安检 + 出门质检

🤔 痛点用户可能输入恶意内容(还记得 Day 20 的提示词注入吗),Agent 也可能输出不该说的(骂人、泄露隐私、给出违规建议)。这些光靠 Critic 查事实还不够。
💡 本质在 Agent 的"入口"和"出口"各加一道护栏(guardrail)输入护栏=进门安检,拦掉注入攻击、违规请求;输出护栏=出门质检,拦掉脏话、隐私泄露、越界建议。像机场"安检进+安检出"两道门,危险品进不来也带不出去。
输入护栏 → Agent → 输出护栏 输入护栏 拦注入/违规 Agent 处理 正常干活 输出护栏 拦脏话/隐私
图注:护栏可以是简单的关键词/正则规则,也可以是一个专门的"安全模型"来判定。两道门叠加,进出都安全。
👶 护栏和昨天的 Critic 有啥区别?都是"检查关卡",但管的东西不同。Critic 查"内容真不真"(有没有编造事实);输出护栏查"内容该不该说"(有没有脏话、隐私、违规)。一个管"真实性",一个管"安全合规性",常常一起上,构成完整的出口把关。
L07

想看工业级的闸门与护栏体系?去深潜

🤔 痛点今天讲的降级、熔断、审批、预算闸、护栏,在真实"可信框架"里是怎么统一编排、和 Critic/评测/可观测拼成一套的?
💡 本质gov-agents 主打"可信/工程化",把失败处理、审批闸门、预算与安全护栏做成了系统化的模块,正是今天这些护栏思想的工业级落地。
🔗 想深入?去看《gov-agents 21 天精讲》→
重点看它的"失败闸门 / 降级 / 审批 / 预算与护栏"相关章节,对照今天学的降级链、危险动作审批、预算闸,看它如何把这些串成一条可靠的执行流水线。看完回来继续 Day 55——学怎么把系统的一举一动都"看得见"(结构化日志、trace、监控告警)。
L08

今日小结 + 动手 10 分钟

🧠 今天你应该能回答

  • 为什么专业系统的目标是"优雅出错"而不是"永不出错"?
  • 降级是什么?为什么一定要留一条"最末兜底"?
  • 重试、熔断、超时各治什么?为什么"无限重试"是坏主意?
  • 哪些动作必须"人在环审批"?判断标准是什么(可逆性+代价)?
  • 预算闸解决什么问题?它和"循环上限"什么关系?
  • 输入/输出护栏各拦什么?和 Critic 的分工是什么?

✋ 动手 10 分钟:给一个假 Agent 装上三道闸

把下面的代码存成 guardrails.py 跑一跑,亲手体验"降级 + 审批 + 预算闸"如何联手让系统出错也不闯祸:

import random
DANGEROUS = {"refund"}
budget = {"steps": 0, "max": 5}          # 预算闸:最多 5 步

def call_model(q):
    if random.random() < 0.5:            # 模拟一半概率主模型挂掉
        raise Exception("主模型超时")
    return f"[主模型答] {q}"

def ask(q):
    budget["steps"] += 1
    if budget["steps"] > budget["max"]:
        return "💸 预算跳闸,停止"          # 预算闸
    try:
        return call_model(q)             # 主方案
    except Exception:
        return f"[备用/兜底] 服务繁忙,已降级回答:{q}"   # 降级不崩

def run_tool(name):
    if name in DANGEROUS:                # 危险动作先审批
        return f"⚠️ 【{name}】需人工批准(此处应暂停等确认)"
    return f"已执行 {name}"

for i in range(7):                        # 故意跑 7 次,看第 6 次起预算跳闸
    print(i, ask("退款要多久"))
print(run_tool("refund"))                 # 看危险动作被拦下要审批

多跑几次,观察三件事:① 主模型挂掉时是否总有兜底(降级);② 跑到第 6 次是否跳闸(预算闸);③ refund 是否被拦去审批。你刚给一个 Agent 装上了生产级的"安全带"。

明日预告 · Day 55:今天让系统"出错不闯祸",但你怎么知道它出了错、降了级、跳了闸?明天学可观测:结构化日志、trace 一次完整运行、监控 token/延迟/错误率/输出漂移,并认识 LangSmith / Langfuse 这类工具——让系统的一举一动都看得见、追得到。
← Day 53 · 防幻觉 Critic Day 55 · 可观测 →