高可用实验结果的边界与解读
高可用实验最容易出现两种误读:一次压测通过,就认为系统能够承受同等线上流量;一次 Agent 循环失控,又把所有问题归因于模型不确定。前者忽略了请求分布、依赖和故障注入,后者则掩盖了本应由程序控制的重试、并发与取消边界。
实验结论只覆盖已记录的条件。把变量、样本、拓扑和复现步骤写完整,失败结果才有资格进入架构决策。
先把要验证的假设写成问题
以 Agent 调用内部工具为例,可以分别验证:
- 参数缺失时,系统是否在发出网络请求前拒绝;
- 下游返回不可重试错误时,任务是否立即结束或询问用户;
- 临时错误发生时,重试是否受次数和总时限约束;
- 用户取消后,排队、模型请求和工具调用是否停止;
- 入口流量高于处理能力时,队列是否有上限并能恢复。
这些问题不能用同一条“峰值 QPS”回答。实验应先跑没有故障的基线,再一次注入一种失败。输入分布也要固定,包括任务步骤数、工具类型、参数大小和返回时间。若所有样本都只调用一个快速只读接口,结果不能外推到包含写操作和长任务的工作流。
流量逐级增加,每一级等待系统进入相对稳定状态,观察入口、Agent 执行器和下游队列。负载生成器自身的 CPU、网络与错误也要记录,否则测到的上限可能只是生成端先到瓶颈。
先描述现象,再解释原因
发现内部调用量明显高于入口流量时,先计算每个任务的工具调用分布和步骤数。Agent 正常完成多步骤任务,本来就会产生放大;只有调用数持续增长、同一任务无法结束或失败后仍派生新步骤,才说明控制流可能失去边界。
排查记录把事实、假设和验证分开。事实可以是某类错误增加、队列不再回落、取消后仍有调用;“模型陷入幻觉”只是解释,需要用调用轨迹证明。若发现同一参数连续失败,再查看失败类别和重新规划输入,确认它是否真的在做相同动作。
不要补写没有来源的精确流量、Token 倍数或协程数量。实验日志能支持到什么粒度,就报告到什么粒度。证据不足时写明缺口,比给出一条完整但无法复验的故事更有价值。
Trace 记录控制流,不保存任务正文
Agent Trace 可以在普通 HTTP Span 之外增加运行标识、步骤序号、工具名称、参数校验结果、错误类别、尝试次数和剩余预算。敏感参数、模型 Prompt、工具返回正文不应默认进入 Span 属性,它们容易被导出到多个观测后端。
下面是一段脱敏后的示意结构。args_fingerprint只用于同一次运行内识别重复动作,不能据此还原参数,也不跨用户做全局判断。
{ "name": "agent.tool.execute", "attributes": { "agent.run_id": "run-example", "agent.step": 3, "tool.name": "getUserDetail", "tool.args_valid": false, "tool.args_fingerprint": "sha256:example", "tool.attempt": 1, "error.type": "validation_error", "retryable": false }, "events": [ { "name": "agent.stopped", "reason": "missing_required_argument" } ] }参数缺失属于本地 Schema 校验错误,根本不应发往下游。系统可以让用户补充信息,或者终止当前动作。把相同的 400 响应再次喂给模型,只是消耗预算,并不会创造缺失的业务数据。
三层边界分别解决不同问题
第一层是动作协议。每个工具声明参数 Schema、权限、是否有副作用、幂等要求和可重试错误。模型输出通过校验后,还要由服务端填入当前用户与租户上下文。模型不能提供权限字段,也不能直接决定目标主键。
第二层是单次运行预算。最大步骤、总时限、模型调用、工具调用和并发任务都有上限。任一预算耗尽,状态机进入明确的停止或人工接管节点。运行预算必须随取消信号一起传递到子任务,不能只让最外层请求返回。
第三层是下游保护。连接池、并发许可、队列和断路器保护具体服务,防止所有 Agent 同时重试。它与单个任务的循环检测不同:断路器根据一段时间内的下游失败决定是否暂时停止调用,而循环检测只判断本次运行是否在重复没有进展的动作。
一个按运行隔离的执行示例
下面的 Python 片段演示参数校验之后的异步执行边界。历史只属于一个RunGuard,不会把不同用户的相同查询误判为循环;超时使用单调时钟;相同指纹只有在前一次失败后才累计。
import asyncio import hashlib import json import time from dataclasses import dataclass, field from typing import Any, Awaitable, Callable class ToolExecutionError(Exception): def __init__(self, message: str, *, retryable: bool) -> None: super().__init__(message) self.retryable = retryable @dataclass class RunGuard: max_steps: int deadline: float repeated_failure_limit: int = 2 steps: int = 0 failures: dict[str, int] = field(default_factory=dict) def before_call(self, tool: str, args: dict[str, Any]) -> str: if self.steps >= self.max_steps: raise ToolExecutionError("step budget exhausted", retryable=False) if time.monotonic() >= self.deadline: raise ToolExecutionError("run deadline exceeded", retryable=False) canonical = json.dumps(args, sort_keys=True, separators=(",", ":")) fingerprint = hashlib.sha256(f"{tool}:{canonical}".encode()).hexdigest() if self.failures.get(fingerprint, 0) >= self.repeated_failure_limit: raise ToolExecutionError("repeated failed action", retryable=False) self.steps += 1 return fingerprint def record_failure(self, fingerprint: str) -> None: self.failures[fingerprint] = self.failures.get(fingerprint, 0) + 1 async def execute_tool( guard: RunGuard, tool_name: str, validated_args: dict[str, Any], call: Callable[..., Awaitable[Any]], step_timeout: float, ) -> Any: fingerprint = guard.before_call(tool_name, validated_args) try: return await asyncio.wait_for(call(**validated_args), timeout=step_timeout) except ToolExecutionError as exc: guard.record_failure(fingerprint) if not exc.retryable: raise raise except asyncio.TimeoutError: guard.record_failure(fingerprint) raise ToolExecutionError("tool timeout", retryable=True)示例没有包含 Schema、鉴权、重试退避、幂等键和审计,不能直接作为完整执行器。wait_for发出取消后,底层客户端还必须支持取消;若请求已经到达有副作用的服务,调用方应先查询结果,再决定重试。
相同调用出现两次也不必一律禁止。轮询一个最终一致的只读状态可能是合理行为,关键是协议必须允许,并有等待间隔和总时限。循环规则应结合工具语义,而不是把哈希次数写成全局常量。
降级路径不能依赖失效的 Agent
当模型或 Agent 执行器不可用时,入口仍需根据普通服务状态决定:返回静态信息、进入人工队列,还是拒绝任务。降级逻辑不能再次调用同一模型来判断是否降级,也不能绕过原有权限。
高影响写操作失败时,优先保留当前状态与幂等标识,不要自动换模型重放。只读问答可以回到搜索结果或已有缓存,但要说明结果来源发生变化。恢复后从少量任务开始放量,确认队列、错误和取消都回到预期。
实验结论按范围交付
报告最后应列出环境拓扑、版本、样本分布、负载阶梯、注入故障、指标口径和观测开销。结论写成条件句:在当前步骤预算与队列上限下,某类失败能够收敛;哪些工具和峰值尚未覆盖,则明确留空。
失败实验可以产生三类资产:能稳定回放的样本、阻断同类问题的自动测试,以及故障时可执行的限流与接管说明。它们比一句“Agent 不适合高并发”更有用,也比一张峰值流量图更接近高可用设计真正需要的证据。