从 ReAct 循环到 Agent Harness:把模型行为变成可恢复的产品能力

2026-07-24 29 预计阅读时间: 1 分钟
来源: my.oschina.net AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:14 分钟

ReAct 解决了一个关键问题:让模型在“思考、行动、观察”的循环中调用工具并逐步完成任务。但当 Agent 进入真实产品,团队面对的不再只是模型能否完成任务,而是用户能否看见进度、暂停执行、确认高风险操作、从故障中恢复,并追溯每一步发生了什么。

这时,工程重心会从模型循环转向 Agent Harness。Harness 不是另一套推理方法,而是包围模型循环的运行框架:它定义状态、保存事实、执行工具、处理审批,并向前端提供稳定的交互边界。

ReAct 只描述循环,产品还需要运行契约

一个最小 ReAct 循环通常可以概括为:

  1. 将用户目标和当前上下文发送给模型。
  2. 模型决定回答用户或调用工具。
  3. 系统执行工具,把结果作为 observation 返回模型。
  4. 重复以上过程,直到完成或达到限制。

这个循环适合验证 Agent 是否“能做事”,但直接用于产品会留下几个缺口:

  • 状态只存在于内存或消息历史里:进程重启后难以继续。
  • 模型输出被当成运行事实:模型说“文件已写入”,不等于文件系统确认写入成功。
  • 前端依赖自然语言猜测进度:UI 很难稳定展示“等待审批”或“工具执行失败”。
  • 副作用缺少边界:发邮件、付款、删除资源等操作不能只依赖模型自行判断。
  • 历史不可审计:只有一串聊天消息,无法解释某次工具调用的参数、结果和审批人。

Agent Harness 要补上的正是这些工程契约。模型可以提出下一步,但运行时必须决定这一步是否允许、如何执行、如何记录,以及执行后状态如何变化。

共享 State Schema:让前后端围绕同一份状态工作

聊天消息不应该是 Agent 状态的唯一载体。更稳定的做法是定义结构化 State Schema,并让后端、持久化层和前端共享它。

下面是一份可以直接保存为 agent-state.schema.json 的最小 JSON Schema。示例假设 Agent 支持暂停、审批、失败恢复和事件追踪;实际项目可以按业务增加租户、权限和任务产物字段。

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://example.com/schemas/agent-state.schema.json",
  "title": "AgentRunState",
  "type": "object",
  "required": ["run_id", "status", "goal", "version", "events"],
  "properties": {
    "run_id": { "type": "string", "minLength": 1 },
    "status": {
      "type": "string",
      "enum": ["queued", "running", "waiting_approval", "paused", "completed", "failed"]
    },
    "goal": { "type": "string" },
    "version": { "type": "integer", "minimum": 0 },
    "pending_action": {
      "type": ["object", "null"],
      "required": ["action_id", "tool", "arguments", "risk"],
      "properties": {
        "action_id": { "type": "string" },
        "tool": { "type": "string" },
        "arguments": { "type": "object" },
        "risk": { "type": "string", "enum": ["low", "high"] }
      },
      "additionalProperties": false
    },
    "events": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["seq", "type", "payload"],
        "properties": {
          "seq": { "type": "integer", "minimum": 1 },
          "type": { "type": "string" },
          "payload": { "type": "object" }
        },
        "additionalProperties": false
      }
    }
  },
  "additionalProperties": false
}

前端不需要解析“我准备发送邮件,请确认”这句话来判断是否展示按钮。它只需要检查:

const needsApproval = state.status === "waiting_approval";
const canResume = state.status === "paused" || state.status === "failed";
const pendingTool = state.pending_action?.tool;

这条边界非常重要:自然语言负责解释,结构化状态负责控制 UI。即使模型更换、提示词调整或输出措辞变化,产品交互仍然稳定。

Schema 还应带版本。生产系统可能需要采用乐观并发控制:前端提交审批时携带当前 version,后端只在版本匹配时更新状态,避免用户批准了已经被替换的旧操作。

运行事实不能由模型宣布

Agent 系统里至少存在三类信息:

  • 模型提议:模型建议调用什么工具、使用哪些参数。
  • 运行事实:工具是否真正启动、返回什么、是否超时、产生了哪些资源。
  • 产品状态:当前运行是执行中、等待审批、失败还是完成。

其中只有 Harness 可以写入运行事实。模型生成的工具调用只是提案,不能直接成为“邮件已发送”或“部署已完成”的证据。

可以把每次变化记录成追加事件:

{"seq":1,"type":"run.started","payload":{"goal":"生成并发送周报"}}
{"seq":2,"type":"action.proposed","payload":{"action_id":"a-17","tool":"send_email"}}
{"seq":3,"type":"approval.requested","payload":{"action_id":"a-17"}}
{"seq":4,"type":"approval.granted","payload":{"action_id":"a-17","actor":"user-42"}}
{"seq":5,"type":"tool.succeeded","payload":{"action_id":"a-17","message_id":"m-903"}}
{"seq":6,"type":"run.completed","payload":{}}

这种事件序列同时服务于四个目标:恢复运行、刷新 UI、审计副作用,以及定位失败。需要注意,日志里不应无条件保存访问令牌、完整邮件正文或用户隐私数据;工具参数和结果应经过字段级脱敏,并设置保留周期。

一个可运行的最小 Harness

下面的 Python 示例没有绑定具体模型 SDK,而是集中展示 Harness 的职责。它会拦截高风险工具、等待人工确认、记录事件,并在进程重启后从 JSON 文件恢复。

将代码保存为 harness.py,使用 Python 3.10 及以上版本运行。首次运行会停在审批状态,再次加上 --approve 即可继续。

from __future__ import annotations

import argparse
import json
import uuid
from pathlib import Path
from typing import Any

STATE_FILE = Path("run-state.json")


def save(state: dict[str, Any]) -> None:
    tmp = STATE_FILE.with_suffix(".tmp")
    tmp.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
    tmp.replace(STATE_FILE)


def emit(state: dict[str, Any], event_type: str, payload: dict[str, Any]) -> None:
    state["events"].append({
        "seq": len(state["events"]) + 1,
        "type": event_type,
        "payload": payload,
    })
    state["version"] += 1
    save(state)


def load_or_create() -> dict[str, Any]:
    if STATE_FILE.exists():
        return json.loads(STATE_FILE.read_text(encoding="utf-8"))

    state = {
        "run_id": str(uuid.uuid4()),
        "status": "running",
        "goal": "生成周报并发送给团队",
        "version": 0,
        "pending_action": None,
        "events": [],
    }
    emit(state, "run.started", {"goal": state["goal"]})
    return state


def propose_next_action(state: dict[str, Any]) -> dict[str, Any]:
    # 实际项目可在这里调用模型,并校验其结构化输出。
    return {
        "action_id": str(uuid.uuid4()),
        "tool": "send_email",
        "arguments": {
            "to": "team@example.com",
            "subject": "Weekly report",
            "body": "Build is green. Two issues remain open.",
        },
        "risk": "high",
    }


def execute_tool(action: dict[str, Any]) -> dict[str, Any]:
    # 示例不发送真实邮件;接入邮件 API 时应使用 action_id 作为幂等键。
    return {"message_id": f"demo-{action['action_id']}", "accepted": True}


def run(approved: bool) -> None:
    state = load_or_create()

    if state["status"] == "completed":
        print("Run already completed.")
        return

    action = state["pending_action"]
    if action is None:
        action = propose_next_action(state)
        state["pending_action"] = action
        emit(state, "action.proposed", {
            "action_id": action["action_id"],
            "tool": action["tool"],
            "risk": action["risk"],
        })

    if action["risk"] == "high" and not approved:
        state["status"] = "waiting_approval"
        emit(state, "approval.requested", {
            "action_id": action["action_id"],
            "tool": action["tool"],
            "arguments": action["arguments"],
        })
        print("Approval required. Re-run with --approve.")
        return

    if approved:
        emit(state, "approval.granted", {"action_id": action["action_id"]})

    state["status"] = "running"
    save(state)

    try:
        result = execute_tool(action)
        emit(state, "tool.succeeded", {
            "action_id": action["action_id"],
            "result": result,
        })
        state["pending_action"] = None
        state["status"] = "completed"
        emit(state, "run.completed", {})
        print(json.dumps(result, indent=2))
    except Exception as exc:
        state["status"] = "failed"
        emit(state, "tool.failed", {
            "action_id": action["action_id"],
            "error": str(exc),
        })
        raise


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("--approve", action="store_true")
    args = parser.parse_args()
    run(args.approve)

运行方式:

python harness.py
python harness.py --approve
cat run-state.json

这个示例仍是最小实现。接入真实工具时,还应补充参数校验、超时、重试策略和幂等键。尤其是支付、发信、创建工单等副作用操作,重试不能简单地再次执行;Harness 应先查询外部系统,确认相同 action_id 是否已经成功。

UI 边界决定控制权在哪里

一个成熟的 Agent UI 不只是展示 token 流。它需要把 Harness 暴露的状态映射成明确动作:

  • running:展示当前步骤,并允许暂停。
  • waiting_approval:展示工具、关键参数和影响范围,提供批准与拒绝操作。
  • failed:展示可公开的错误信息,并区分“重试当前工具”和“重新规划”。
  • paused:保留运行上下文,允许用户修改目标后继续。
  • completed:展示经过工具确认的结果和产物,而不是只展示模型总结。

前端也不应直接修改完整状态对象。更稳妥的 API 是提交命令,例如 POST /runs/{id}/approvePOST /runs/{id}/pausePOST /runs/{id}/resume,由后端验证当前状态是否允许该转换。这样可以避免客户端把 failed 任意改成 completed

实时更新可以使用 Server-Sent Events 或 WebSocket,但事件流只是通知通道,数据库中的状态快照和事件记录才是恢复依据。客户端断线重连后,应先读取最新快照,再从最后一个事件序号继续订阅。

落地时先守住五条边界

从 ReAct 原型演进到 Agent Harness,不必一次建设复杂平台。可以按风险逐步推进:

  1. 先定义状态机和 Schema:明确允许的状态与转换,并给 Schema 设置版本。
  2. 区分提议与事实:模型只提出动作,Harness 执行工具并记录结果。
  3. 给副作用加审批和幂等性:高风险操作必须有显式策略,不能只靠提示词约束。
  4. 保存可恢复的检查点:至少持久化运行状态、待执行动作和事件序号。
  5. 让 UI 消费结构化状态:自然语言用于解释,按钮可用性和流程控制来自后端状态。

Harness 的价值不在于让 Agent 显得更自主,而在于限制自主行为的范围,并把执行过程变成产品可以管理的对象。模型能力会变化,工具也会持续增加;只要状态契约、运行事实和 UI 边界保持清晰,Agent 才能从一次性的演示循环演进为可交互、可恢复、可控制、可追溯的系统。


相关推荐