章节08 / 14
  1. 01AI Harness 不是测试脚本,而是 AI 系统的质量控制台
  2. 02先写任务协议,再谈评测指标
  3. 03Golden Dataset:把“感觉不错”变成可回归样例
  4. 04评测不是一个分数:判分器、断言和人工复核怎么组合
  5. 05结构化输出 Harness:先挡住形状错误,再处理业务错误
  6. 06Tool Harness:模型只能提议动作,执行权必须被隔离
  7. 07RAG Harness:先评检索,再评回答
  8. 08Agent Harness:把多步智能体变成可暂停、可恢复、可审计的状态机
  9. 09红队与安全 Harness:把提示注入当成常规回归项
  10. 10观测 Harness:trace 里该看见什么,不该记录什么
  11. 11CI 回归门禁:让 Prompt、模型和检索改动都要过关
  12. 12线上反馈回流:用户反馈怎样变成下一版样例
  13. 13模型路由与发布 Harness:灰度、回滚和成本风险一起看
  14. 14综合项目:交付一个 AI Harness Engineering 蓝图
本文目录12
  1. 传统 Agent 的不可控性与状态化改造思路
  2. 设计一个可暂停与可恢复的 Agent 状态机
  3. 为 Agent 引入执行预算与死信队列
  4. 步骤日志与 Trace 设计:让每一次决策都有迹可循
  5. 在关键节点引入人工审核(Human-in-the-Loop)
  6. 利用 Trace 进行回放调试与回归评测
  7. 运行 Runbook:故障恢复与状态重构实操演练
  8. 故障场景
  9. 故障恢复 Runbook
  10. 验收与自测要求
  11. 技术来源与持续维护规范
  12. AgentHarness:把多步智能体变成可暂停、可恢复:把判断写进 Harness 证据链
08

Agent Harness:把多步智能体变成可暂停、可恢复、可审计的状态机

本指南介绍如何为多步 Agent 设计状态机 Harness,实现执行预算控制、步骤级 Trace 记录、人审节点拦截以及基于状态快照的失败恢复与回放调试。

前置基础
  • 完成前序单元的练习或理解对应 harness 概念
  • 具备基础的 Python 异步编程与面向对象设计能力
学习结果
  • 构建一个支持暂停、恢复与状态导出的 Agent 状态机 Harness
  • 生成包含完整执行路径与 Token 消耗的步骤 Trace 日志
  • 制定一份针对 Agent 中断或执行失败的 Runbook 恢复方案

在传统的单次 Prompt-Response 交互中,测试与评测相对直接。然而,当引入多步智能体(Agent)或 ReAct(Reasoning and Acting)循环时,智能体开始自主选择工具、拆解任务并进行多轮迭代。这种“链式推理”带来了极大的不可预测性:一个在第 5 步出现的幻觉,可能会毁掉前 4 步已经取得的正确结果。如果不加以工程干预,你将面临一个“黑盒”:线上出现报错时无法复现,API 账单因死循环而飙升,且无法在不重跑整个流程的情况下对其中某一步进行隔离评测。

要解决这些痛点,必须为 Agent 构建一个专属的 Harness(测试与运行容器),将其从“任意执行的黑盒代码”改造成“可暂停、可恢复、可审计的显式状态机”。


传统 Agent 的不可控性与状态化改造思路

大多数初期的 Agent 实现直接将 while 循环、LLM 调用和工具执行(Tool Call)硬编码在一个函数中。这种设计存在两个致命缺陷:

  1. 状态无法持久化:如果执行在第 4 步因为网络抖动或工具限流而中断,你必须从头开始运行,这不仅浪费 Token,还可能因为环境变化(例如数据库已写入部分数据)导致副作用叠加。
  2. 无法进行单步评测:你无法单独评估“给定前 3 步的上下文,第 4 步的 Tool Selection 是否正确”,因为你无法直接注入前 3 步的虚拟状态。

状态化改造的核心是将 执行器(Executor)状态存储(State Store) 彻底解耦。Agent 的每一次思考、每一次工具调用,都必须定义为状态机的一次状态转移(Transition)。每一个状态转移都必须产生一份不可变的事件日志。这一思路与 Redux 或 Event Sourcing 类似,但在 AI 场景下,我们还需要额外管理 Token 预算和人类干预节点。


设计一个可暂停与可恢复的 Agent 状态机

我们将 Agent 抽象为一个有限状态机(FSM)。其核心数据结构包含:

  • session_id: 唯一标识一次任务流。
  • current_step: 当前执行步骤的序号。
  • status: 状态,包括 PENDING(等待执行)、RUNNING(执行中)、PAUSED(暂停,如等待人审)、COMPLETED(已完成)、FAILED(失败)。
  • history: 包含之前的思考(Thoughts)、工具调用(Tool Calls)和工具返回结果(Tool Outputs)的列表。
  • context: 共享的任务上下文变量。

以下是该状态机 Harness 的核心 Python 实现架构:

python
import uuid
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field

class ToolCall(BaseModel):
    tool_name: str
    arguments: Dict[str, Any]
    output: Optional[str] = None
    status: str = "pending"  # pending, success, failed

class AgentStep(BaseModel):
    step_number: int
    thought: str
    tool_calls: List[ToolCall] = []
    timestamp: float

class AgentState(BaseModel):
    session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
    status: str = "PENDING"  # PENDING, RUNNING, PAUSED, COMPLETED, FAILED
    current_step: int = 0
    history: List[AgentStep] = []
    context: Dict[str, Any] = {}
    error_message: Optional[str] = None
    
    # 运行预算控制
    max_steps: int = 10
    accumulated_cost: float = 0.0
    max_cost_usd: float = 0.50

class AgentStateMachineHarness:
    def __init__(self, state: AgentState):
        self.state = state

    def load_state(self, serialized_state: dict):
        self.state = AgentState(**serialized_state)

    def dump_state(self) -> dict:
        return self.state.model_dump()

    def transition_to(self, next_status: str):
        valid_transitions = {
            "PENDING": ["RUNNING"],
            "RUNNING": ["PAUSED", "COMPLETED", "FAILED"],
            "PAUSED": ["RUNNING", "FAILED"],
            "FAILED": ["PENDING", "RUNNING"],  # 允许从失败状态重启或恢复
            "COMPLETED": []
        }
        if next_status not in valid_transitions[self.state.status]:
            raise ValueError(f"Invalid transition from {self.state.status} to {next_status}")
        self.state.status = next_status

通过此 Harness,你可以随时使用 dump_state() 将当前 Agent 的全部记忆、已消耗成本和中间执行结果序列化并保存到 Redis 或数据库中。一旦执行中断,只需从数据库读取数据,调用 load_state(),即可无缝还原现场并继续往下执行。


为 Agent 引入执行预算与死信队列

多步 Agent 最容易出现的问题是“陷入推理死循环”——由于 Prompt 微弱的偏差或工具返回了非预期的格式,Agent 不断重复尝试相同的工具调用,导致 Token 消耗激增。如果 Agent 的单次运行成本或嵌套循环深度超过了预设阈值,必须立即中断执行并将其状态 dump 到持久化存储中,否则失控的 LLM 可能会在几分钟内耗尽你的 API 额度。

我们在 Harness 的每一步执行前加入“预算网关”:

python
class BudgetExceededException(Exception):
    pass

class AgentStateMachineHarness(AgentStateMachineHarness):
    def enforce_budget(self, estimated_step_cost: float):
        # 检查步骤数超限
        if self.state.current_step >= self.state.max_steps:
            self.transition_to("FAILED")
            self.state.error_message = f"Max steps ({self.state.max_steps}) exceeded."
            self.send_to_dead_letter_queue("MAX_STEPS_EXCEEDED")
            raise BudgetExceededException(self.state.error_message)
        
        # 检查资金预算超限
        new_cost = self.state.accumulated_cost + estimated_step_cost
        if new_cost > self.state.max_cost_usd:
            self.transition_to("FAILED")
            self.state.error_message = f"Max cost budget ({self.state.max_cost_usd} USD) exceeded."
            self.send_to_dead_letter_queue("BUDGET_EXCEEDED")
            raise BudgetExceededException(self.state.error_message)
            
        self.state.accumulated_cost = new_cost

    def send_to_dead_letter_queue(self, reason: str):
        # 将当前状态快照序列化后发送至死信队列(DLQ),供开发人员离线分析
        dead_letter_payload = {
            "reason": reason,
            "snapshot": self.dump_state()
        }
        # 实际生产中此处对接消息队列,如 RabbitMQ, AWS SQS 或数据库归档表
        print(f"[DLQ ALERT] Session {self.state.session_id} sent to DLQ. Reason: {reason}")

这种设计不仅防止了资金损失,同时自动收集了宝贵的“边界案例”(Edge Cases)。每一个进入死信队列的 Agent 状态快照,都是最真实的坏案例(Bad Cases),可以直接转化为评测集的数据源。


步骤日志与 Trace 设计:让每一次决策都有迹可循

要对 Agent 进行持续评估,仅仅记录输入和最终输出是远远不够的。根据 LangSmith evaluation concepts 的规范,每一次评测实验(Experiment)都应该基于详尽的运行迹(Trace)进行。一个标准的 Agent Trace 应该包含完整的决策链(Chain of Thought)、工具选择参数及其实际返回。

我们在 Harness 中定义以下 Trace 输出格式。当 Agent 执行每一个 Step 时,它必须向日志收集器或 APM 系统广播符合该格式的结构化数据:

json
{
  "trace_id": "trace_882a8f3b_901c",
  "session_id": "session_abc123",
  "step_number": 3,
  "inputs": {
    "query": "查询用户 2026 年 Q1 的消费总额,并计算折合美元"
  },
  "thought": "我已经获取了人民币消费总额为 7000 元。现在我需要调用货币转换工具将其转换为美元。当前汇率需要通过实时 API 获取。",
  "tool_calls": [
    {
      "tool_name": "get_exchange_rate",
      "arguments": {
        "from_currency": "CNY",
        "to_currency": "USD"
      },
      "output": "{\"rate\": 0.14}",
      "status": "success"
    }
  ],
  "outputs": {
    "intermediate_result": "980 USD"
  },
  "metrics": {
    "step_latency_seconds": 1.45,
    "prompt_tokens": 850,
    "completion_tokens": 120,
    "step_cost_usd": 0.015
  }
}

在评估环节,这些 Trace 数据可以直接对接类似 Azure Foundry 评测(Evaluate generative AI apps)的内置质量 evaluator。Azure Foundry 能够解析这种结构化的 Trace,自动评估其中工具调用的准确率(Tool Call Accuracy)以及检索步骤的 Groundedness。如果 Trace 格式不标准,评测平台就只能把整个 Agent 当作黑盒,从而无法定位具体是“检索不准”还是“模型推理能力不足”导致了最终失败。


在关键节点引入人工审核(Human-in-the-Loop)

在设计人审节点时,如果步骤涉及外部资金划拨或核心数据库写操作,应该将状态机挂起并向审批队列发送阻断式事件,除非获得了显式的人工授权,否则不允许 Agent 自动尝试绕过。

OpenAI 在其 Evaluation best practices 中强调,人工审核(Human Review)和减少 Judge 偏差对于复杂生成式应用的安全性至关重要。因此,我们在 Harness 中将“人审”设计为一个一等公民(First-class Citizen)状态。状态转移到 PAUSED,并暴露一个回调接口供人工判定:

python
class AgentStateMachineHarness(AgentStateMachineHarness):
    def request_human_approval(self, action_details: dict) -> str:
        # 将状态挂起
        self.transition_to("PAUSED")
        # 保存需要审核的动作详情到上下文
        self.state.context["pending_approval_action"] = action_details
        # 实际生产中,这里会向 Web 前端或 Slack 发送一条通知
        return f"Session {self.state.session_id} paused. Awaiting approval for: {action_details['tool_name']}"

    def resume_with_approval(self, approved: bool, feedback: Optional[str] = None):
        if self.state.status != "PAUSED":
            raise ValueError("Can only resume from PAUSED status")
        
        if approved:
            # 清理审核挂载点,恢复运行状态
            self.state.context.pop("pending_approval_action", None)
            self.transition_to("RUNNING")
            print("Approval granted. Resuming Agent execution...")
        else:
            self.transition_to("FAILED")
            self.state.error_message = f"Rejected by human auditor. Feedback: {feedback}"
            print(f"Execution rejected. Feedback: {feedback}")

通过此 Harness 控制结构,人审不仅仅是一个“开关”,人审产生的 feedback 和人工修改后的“正确 Tool 参数”还可以被捕获并保存下来。根据 OpenAI 的指导原则,这些被纠正的数据是构建**高保真黄金数据集(Golden Dataset)**的无价之源,可用于后续的 Agent 微调(Fine-tuning)和断言测试。


利用 Trace 进行回放调试与回归评测

在传统的软件测试中,我们习惯于编写单元测试。但在 Agent 开发中,Prompt 的微调经常导致不可预测的侧面效应(Side Effects)。Anthropic Evaluation Tool 允许开发者在修改 Prompt 之后,一键重跑整个测试集(Eval Suite),在控制台中直接对比前后的表现变化。为了支撑这种能力,Agent Harness 必须提供回放能力(Replay)

如果我们在 Trace 日志中发现 Agent 在第 N 步出现了幻觉或调用了错误的 Tool,可以提取该步骤的快照并利用 LangSmith 等平台重新跑评测,因为只有隔离出错误的上下文,才能准确判定是 Prompt 缺陷还是 Model 升级导致的退化。

例如,你可以编写一个 Replay 脚本,它不需要真的去调用前 N-1 步那些耗时且可能产生外部副作用的工具,而是直接“快照注入”:

python
def replay_agent_at_step(failed_session_state: dict, test_prompt_patch: str) -> dict:
    """
    加载失败的 Agent 状态快照,替换引发错误的 Prompt,并从出错的步骤开始单步重试。
    """
    harness = AgentStateMachineHarness(AgentState(**failed_session_state))
    
    # 1. 验证状态是否可恢复(应该处于 FAILED 或 PAUSED 状态)
    if harness.state.status not in ["FAILED", "PAUSED"]:
        print("Warning: Replaying a running or completed session.")
    
    # 2. 注入修改后的 Prompt/系统指令(模拟 Patch 操作)
    harness.state.context["system_instruction_override"] = test_prompt_patch
    
    # 3. 将状态重置为 RUNNING,准备单步执行
    harness.state.status = "RUNNING"
    harness.state.error_message = None
    
    print(f"Replaying session {harness.state.session_id} from step {harness.state.current_step}...")
    return harness.dump_state()

这种机制允许测试工程师直接针对“第 4 步的 Model 输出”进行局部回归测试。你可以编写断言,确保在注入新 Prompt 之后,Agent 产生的 tool_calls 参数符合预期,而无需重新等待前 3 步的漫长执行。


运行 Runbook:故障恢复与状态重构实操演练

本小节提供一个可操作的 Runbook 样例,用于演示在线上 Agent 因环境或逻辑崩溃时,如何通过本 Harness 提供的机制进行手动状态重构与恢复。

故障场景

Agent 执行一项包含多步的“客户对账与转账”任务。在第 3 步,Agent 试图调用 get_bank_balance 工具,但由于银行网关暂时性服务中断,该工具抛出 HTTP 503 异常。Agent 状态变更为 FAILED。根据流程,此时不能重新运行,因为第 2 步已经完成了一部分数据拉取和临时锁定(Locking)操作,重新运行会导致死锁。

故障恢复 Runbook

第一步:检索故障快照

从死信队列或日志存储中检索 Session ID 为 session_tx_99281 的最新状态快照:

json
{
  "session_id": "session_tx_99281",
  "status": "FAILED",
  "current_step": 3,
  "history": [
    {
      "step_number": 1,
      "thought": "先获取客户 ID",
      "tool_calls": [{"tool_name": "get_user_id", "arguments": {"name": "Alice"}, "output": "usr_001", "status": "success"}],
      "timestamp": 1716900000.0
    },
    {
      "step_number": 2,
      "thought": "锁定该客户的对账账户",
      "tool_calls": [{"tool_name": "lock_account", "arguments": {"account_id": "acc_99"}, "output": "locked_success", "status": "success"}],
      "timestamp": 1716900005.0
    },
    {
      "step_number": 3,
      "thought": "获取当前余额",
      "tool_calls": [{"tool_name": "get_bank_balance", "arguments": {"account_id": "acc_99"}, "output": null, "status": "failed"}],
      "timestamp": 1716900010.0
    }
  ],
  "context": {
    "user_id": "usr_001",
    "account_id": "acc_99"
  },
  "error_message": "HTTP 503 Service Unavailable on get_bank_balance",
  "accumulated_cost": 0.045
}

第二步:状态修复与人工干预

由于银行接口依然不确定是否完全稳定,系统管理员决定通过 Runbook 脚本手动垫付/修正该步骤的值(例如通过备用查询通道查得该账户余额为 50000 元),并将该步骤的 Tool Output 直接改写为成功状态,以此绕过网络故障。这就是“状态重构”。

执行以下修复脚本:

python
# 模拟从 JSON 文件读取故障快照
fault_snapshot = { ... } # 填入上述第一步的 JSON 数据

harness = AgentStateMachineHarness(AgentState(**fault_snapshot))

# 修复第 3 步的输出(人工注入备用渠道获取到的真实结果)
harness.state.history[-1].tool_calls[0].output = "{\"balance\": 50000, \"currency\": \"CNY\"}"
harness.state.history[-1].tool_calls[0].status = "success"

# 清除错误信息,将状态设回等待运行
harness.state.status = "PENDING" # 允许转移到 RUNNING
harness.state.error_message = None

# 将 current_step 指向第 4 步,表明下一步直接基于已修复的第 3 步结果继续执行
harness.state.current_step = 3  # 注:如果 current_step 表示已完成的步骤索引,更新它

# 导出就绪状态
restored_state = harness.dump_state()
print("Restored State successfully. Ready to resume:")
print(restored_state)

第三步:重新调度恢复运行

restored_state 发送回 Agent 执行队列。执行引擎加载该状态后,由于 history 中已经包含了第 3 步的成功输出,Agent 不会再次请求失效的 get_bank_balance 工具,而是直接以此为上下文,往下执行第 4 步(例如,计算转账比例并调用 transfer_funds 工具)。


验收与自测要求

为了验证你的 Agent Harness 是否已经具备状态机的核心特征,建议使用以下 checklist 进行验收:

  • 状态机解耦测试:能否在不运行任何 LLM 调用和 Tool 调用代码的情况下,通过 load_state 还原一个包含 5 步历史记录的 Agent 实例?
  • 死信队列拦截:在模拟死循环场景(如通过 Mock 工具让 Tool 总是返回错误格式且 Agent 决定不断重试)时,Agent 是否能在达到最大步数或最大 API 费用限制后,准确地转产为 FAILED 并向控制台或 MQ 输出状态快照?
  • 单步回放能力:能否提取一个失败 Trace 中的某一步(例如第 3 步),修改其对应 Prompt 指导词后单独重跑,并产生新的第 4 步输出?
  • 人工拦截点:在执行一个特定的高危工具前,Agent 是否会将 status 变更为 PAUSED,并阻塞等待直至显式调用 resume_with_approval

技术来源与持续维护规范

本 Harness 的设计理念与技术边界遵循以下行业实践:

  1. LangSmith 评测概念 (2026-05-28 访问):关于步骤 Trace、回归评测与比较实验的设计。当 LangSmith 的 Trace 格式标准(Run Tree Structure)发生更新时,应当同步调整本 Harness 导出的 JSON Schema。
  2. OpenAI 评测实践建议 (2026-05-28 访问):关于人审机制引入、防范 LLM 评测 Judge 偏差以及从生产故障快照构建评估数据集的路径。当使用更先进的推理模型(如基于强化学习自我纠错的模型)时,应评估其原生的 Step-by-step 思考过程是否可被 Harness 剥离与干预。
  3. Anthropic 评测工具规范 (2026-05-28 访问):关于重跑测试套件与在 Prompt 变更后进行对比。未来如果 Anthropic 提供更细粒度的 API 级状态缓存(Caching State),应当将缓存 Key 与本 Harness 的 session_id 和步骤哈希结合,以进一步节省重跑成本。
  4. Azure Foundry 评测指南 (2026-05-28 访问):关于内置质量评估器和安全过滤器的使用。在实际云端部署时,本 Harness 产生的 Trace 可直接通过 Pipeline 投递给 Azure AI Evaluation SDK,实现自动化的越狱检测与相关性评分(Relevance Scoring)。

AgentHarness:把多步智能体变成可暂停、可恢复:把判断写进 Harness 证据链

当 Agent 能行动时,harness 的价值就从“评回答”扩展到“评轨迹和副作用”。本课交付物是 一个 Agent 状态机、执行 trace 样例、失败恢复 runbook,它必须能被复跑、复核、追踪和复盘。

如果 AgentHarness:把多步 还没有对应的输入样例,先不要讨论自动化覆盖率,因为没有样例就无法判断 harness 是否真的抓住问题。 如果一次评测失败会影响发布,应该保留失败输入、模型输出、工具轨迹和判分理由,否则团队只能凭记忆争论。 如果你准备把某个结果自动放行,必须先写清人工复核的例外条件,只有例外条件明确时,门禁才不会变成新的风险源。

红队失败样例要保留原始 payload、期望阻断结果和实际绕过路径。围绕 AgentHarness:把多步智能 做排查时,记录步骤、样例、指标、风险、修复动作和复测结果。

LangChain 的《LangSmith evaluation concepts》说明:支撑数据集、experiment、evaluator、比较实验和回归评测。;这意味着 AgentHarness:把多步 要把来源转成可执行断言。OpenAI 的《Evaluation best practices》提醒:支撑目标定义、数据集、指标、连续评测、人审、judge 偏差和 eval harness 设计。;因此本课必须写清自动判断和人工判断的边界。Anthropic 的《Using the Evaluation Tool》提供的证据是:支撑 prompt 变更后重跑 eval suite 和控制台评测流程。;所以当前结论按 2026-05-28 的来源状态使用。

复核触发条件要具体:如果模型 API/SDK、评测工具版本、Agent 框架、RAG 指标、安全规范、合规政策、平台 release/changelog 或关键来源页面变化,需要重新运行本课样例并更新 harness。

练习验收:把 AgentHarness:把多步智能 接入一个最小 AI 应用,提交包含输入样例、评测结果、失败诊断、来源证据、人工复核结论和下一步修复动作的记录。缺少任何一项,都标记为 needs_review。