跳转至

第二章:Harness 架构全景

本章从架构设计的高度,对 Harness 系统进行全面的剖析。

首先,我们提出一个通用的参考架构(三层 + 横切关注点),这个架构在不同的 Harness 实现中都能找到对应的概念——虽然名称和细节可能有所不同,但核心的分层思想是一致的。

接着,我们深入讨论每一层的设计原则、职责范围和接口规范。特别是,我们将对比三个业界的参考实现——OpenAI Codex 的性能型架构、Claude Code 的任务型架构和 OpenClaw 的自驱型架构——展示不同应用场景下的架构取舍。

在随后的章节中,我们特别关注两个横切的关注点——安全性和可观测性——它们虽然不是独立的层,但贯穿整个系统的各个角落,对系统的可靠性至关重要。

最后,本章通过 MiniHarness 项目的脚手架搭建,将所有的理论概念转化为可运行的代码。通过这一实践过程,读者不仅能够深化对架构的理解,更能为后续的系统开发奠定坚实的基础。

本章是连接第一章概念认识和后续各章深入设计的桥梁。

1. 通用参考架构

本节介绍 Harness 系统的通用参考架构,包括其设计目标、核心特性、分层结构、关键决策点和实现变体,为后续各章的深入讨论提供架构基础。

1.1 为什么需要参考架构

在第一章中,我们讨论了 Harness 系统的五大核心子系统和两大基础保障。但从架构的角度看,仅仅列举子系统是不够的。我们需要一个能够显示这些子系统如何组织、如何交互的架构模型。

参考架构的作用是:

  • 提供设计指引:新的 Harness 系统可以参照这个架构进行设计
  • 便于比较:不同的实现方案可以在统一的框架下进行对比
  • 支持演进:当系统需要升级或扩展时,清晰的结构指导我们如何进行改动

本节提出的参考架构,是基于行业最佳实践的抽象,覆盖本书主要案例。简单系统可以裁剪,强实时、强合规或嵌入式场景可能需要变体。

1.2 三层 + 横切关注点参考架构

基于第一章的子系统分析,本书提出一个 三层 + 横切关注点 的参考架构。三层自上而下分别是:

  1. 接入层(Access Layer):系统与外部世界的边界,负责接收用户请求、协议转换和响应格式化。CLI、Web API、SDK 都属于接入层的实现形式。
  2. 编排层(Orchestration Layer):负责复杂任务的分解、多智能体的协调和工作流管理。对于简单的单步任务,编排层可以直接透传;对于需要多个智能体协作的复杂任务,编排层负责分配子任务、管理依赖和聚合结果。
  3. 智能体核心层(Agent Core Layer):Harness 的核心执行引擎,包含运行时引擎、工具层、记忆子系统和模型集成与输出治理四个子系统。它们在运行时引擎的同一循环中交替协作,而非分层调用。

三层之外, 安全、可观测性和存储 作为横切关注点贯穿所有层级。

以下架构图展示了三层结构和横切关注点的全景:

flowchart TB
    subgraph ACCESS["<b>接入层</b>"]
        A1["CLI / Web API / SDK"]
    end

    subgraph ORCH["<b>编排层</b>"]
        O1["任务分解 · 多智能体协调 · 工作流管理"]
    end

    subgraph CORE["<b>智能体核心层</b>"]
        RT["<b>运行时引擎</b><br/>智能体循环"]
        TL["<b>工具层</b><br/>注册 · 执行 · 隔离"]
        MM["<b>记忆子系统</b><br/>工作 · 短期 · 长期"]
        MO["<b>模型集成与输出治理</b><br/>LLM调用 · 格式校验 · 安全过滤"]
        RT --- TL
        RT --- MM
        RT --- MO
    end

    subgraph CROSS["<b>横切关注点</b>"]
        direction LR
        SEC["安全"]
        OBS["可观测性"]
        STO["存储"]
    end

    ACCESS --> ORCH
    ORCH --> CORE

    style ACCESS fill:#e3f2fd,stroke:#90caf9,color:#000000
    style ORCH fill:#fff9c4,stroke:#fff176,color:#000000
    style CORE fill:#e8f5e9,stroke:#a5d6a7,color:#000000
    style CROSS fill:#f3e5f5,stroke:#ce93d8,color:#000000

图 2-1:三层 + 横切关注点参考架构全景

下面逐层展开。

1.2.1 接入层

接入层是用户与 Harness 系统交互的入口。它的职责相对简单但至关重要:

  • 协议适配:将不同来源的请求(CLI 命令、HTTP API、WebSocket 消息等)统一转换为内部任务格式
  • 身份认证:验证请求方身份,为后续的权限检查提供基础
  • 响应格式化:将内部执行结果转换为用户期望的输出格式(流式文本、JSON 等)

接入层的设计原则是“薄而稳定”——它不应包含业务逻辑,只做格式转换和路由分发。

1.2.2 编排层

编排层处于接入层和智能体核心层之间,负责任务级别的调度:

  • 任务分解:将复杂任务拆分为可独立执行的子任务
  • 依赖管理:识别子任务间的依赖关系,构建执行 DAG
  • 多智能体协调:为每个子任务分配合适的智能体,管理智能体间的通信
  • 结果聚合:收集各子任务的结果,合并为最终输出

对于简单任务(如单轮问答),编排层可以直接透传请求到核心层,不做额外处理。

1.2.3 智能体核心层

智能体核心层是 Harness 的心脏。第一章中,我们看到运行时引擎、工具层、记忆子系统、输出治理这四个核心子系统形成星型拓扑。

这里有一个关键的架构事实:模型调用、工具执行、记忆更新并非各自独立的层级,而是在运行时引擎的同一个循环中交替发生的。一个典型的智能体步骤如下:

flowchart LR
    A["<b>运行时引擎</b><br/>接收任务"] --> B["<b>组装上下文</b><br/>记忆"]
    B --> C["<b>调用LLM</b><br/>模型集成"]
    C --> D["<b>解析输出</b><br/>输出治理"]
    D --> E["<b>执行工具</b><br/>工具层"]
    E --> F["反馈LLM"]
    F --> G["更新记忆"]
    G --> H{"完成?"}
    H -->|否| B
    H -->|是| I["返回结果"]

图 2-2:运行时引擎的七步执行流程

注意步骤 3-7 中,运行时引擎在 同一个循环内 交替调用模型集成、输出治理、工具层和记忆子系统。这正是将它们放在同一层而非分层的原因。

核心层的四个子系统各司其职。

a) 运行时引擎(Runtime Engine)

运行时引擎是核心层的协调者,驱动上述七步循环。它负责:

  • 管理智能体循环的生命周期(启动、暂停、恢复、终止)
  • 维护消息类型系统和执行状态
  • 协调其他三个子系统的调用顺序
  • 处理错误恢复和漂移检测

b) 模型集成与输出治理(Model Integration & Output Governance)

模型集成负责与 LLM 的交互:

  • 支持多种 LLM(Claude、GPT、开源模型等)
  • 管理提示词和系统消息
  • 处理 token 限制和成本优化
  • 支持流式和批量调用

输出治理负责处理 LLM 输出中最不确定的部分:

  • 格式校正:如果 LLM 没有返回期望的 JSON 格式,尝试修复
  • 语义验证:检查工具调用的参数是否合理(比如,是否调用了不存在的工具)
  • 安全检查:检查输出是否包含有害内容或违反约束
  • 置信度评估:评估 LLM 对其输出的置信度,低置信度时触发重试或人工审批

为什么需要输出治理?因为 LLM 的输出是不可预测的。即使你在提示词中要求“请返回 JSON 格式”,LLM 也可能返回一段 Markdown、多余的解释文字,或者格式不完整的 JSON。输出治理就是应对这种不确定性的防线。

下面的伪代码展示了输出治理的四步防御流程——从格式解析到安全检查,逐层过滤不合格的输出:

# 伪代码示例:输出治理
class OutputGovernance:
    async def validate_and_fix(self, llm_output: str) -> ParsedOutput:
        """对LLM的原始输出进行校验和修复。

        为什么需要这个方法?
        LLM返回的是自然语言文本,即使要求返回JSON,也可能:
        - 格式错误(缺少引号、多余逗号)
        - 调用了不存在的工具
        - 包含不安全的操作指令
        这个方法通过四个步骤,把不可靠的文本转化为可信的结构化数据。
        """
        # 第一步:尝试将文本解析为JSON
        try:
            parsed = self._parse_json(llm_output)
        except JSONDecodeError:
            # 第二步:解析失败——让LLM自己修复格式错误
            # 这是一种常见的"自愈"策略:把出错的输出再发回给LLM,
            # 要求它修正格式问题
            llm_output = await self._ask_llm_to_fix(llm_output)
            parsed = self._parse_json(llm_output)

        # 第三步:语义验证——格式正确不代表内容正确
        # 比如LLM可能调用了一个系统中不存在的工具,
        # 或者传入了明显不合理的参数(如负数的文件大小)
        if not self._is_semantically_valid(parsed):
            raise ValidationError("Semantic validation failed")

        # 第四步:安全检查——即使内容合理,也要确保没有安全隐患
        # 比如LLM是否试图执行 "rm -rf /" 这样的危险命令
        if not await self._security_check(parsed):
            raise SecurityError("Output failed security check")

        return parsed

c) 工具层(Tool Layer)

工具层是智能体与外部世界的桥梁。它管理:

  • 工具的注册和发现
  • 参数验证和类型检查
  • 工具执行的隔离和超时控制
  • 错误处理和结果标准化

d) 记忆子系统(Memory System)

记忆允许智能体跨步骤、跨会话地保持上下文。按生命周期分为三层:

  • 工作记忆(当前对话的上下文窗口,受 LLM 上下文长度限制)
  • 短期记忆(会话级摘要和临时状态,生命周期数小时到数周)
  • 长期记忆(持久化的知识和学习,通常借助向量索引支持语义检索)

1.2.4 横切关注点

横切关注点贯穿所有层级,提供非业务逻辑的技术支撑。

a) 安全(Security)

  • 权限管理和授权
  • 敏感操作的隔离和沙箱
  • 输入验证和防注入
  • 审计日志记录

b) 可观测性(Observability)

  • 分布式追踪,跟踪请求的完整执行路径
  • 结构化日志,便于搜索和分析
  • 性能指标收集
  • 错误告警和趋势分析

c) 存储(Storage)

  • 长期记忆的持久化(如向量数据库)
  • 审计日志的存储和查询
  • 任务状态和执行历史的保存
  • 缓存层,用于加速频繁访问的数据

1.3 层间通信的设计原则

一个清晰的架构不仅要定义每层的职责,更要定义层间的通信方式。

1.3.1 向下的调用

上层调用下层时,应该使用 明确的、类型安全的接口。所谓“类型安全”,是指每个方法的输入参数和返回值都有明确的类型声明,这样在开发阶段就能发现参数传错的问题,而不是等到运行时才报错。

下面定义了两个接口——编排层调用 AgentCore.run(),接入层调用 Orchestrator.execute()。注意它们都接收 Task 类型、返回 Result 类型,保持了统一的调用契约:

# 清晰的接口定义
class AgentCore:
    async def run(self, task: Task) -> Result:
        """执行单个智能体任务。

        这是编排层调用智能体核心层的入口。
        Task 包含任务描述、参数、上下文等信息;
        Result 包含执行结果、状态码、耗时等信息。
        """
        pass  # 具体实现在第4章运行时引擎中展开

class Orchestrator:
    async def execute(self, task: Task) -> Result:
        """编排多智能体任务。

        这是接入层调用编排层的入口。
        对于简单任务,编排层可能直接转发给AgentCore;
        对于复杂任务,会先分解再分配。
        """
        pass  # 具体实现在第8章编排引擎中展开

1.3.2 向上的反馈

下层向上层报告时,应该使用 事件或回调机制,而不是异常(异常应该只在真正的错误情况下使用)。

为什么用事件而不是异常?打个比方:异常就像火警警报,只在出了大问题时才响;而事件更像日常的工作汇报——“我开始执行了”、“我完成了第一步”、“我遇到了一个小问题但已经自行解决”。这些信息对于监控和调试系统至关重要,但它们不是“错误”,不适合用异常来表示。

下面的代码展示了如何通过事件机制实现这种“汇报”:

@dataclass  # @dataclass 是Python的装饰器,自动为类生成构造方法和其他常用方法
class ExecutionEvent:
    """表示系统中发生的一个事件。

    所有层都可以发出事件,上层(或横切关注点中的可观测性模块)
    可以订阅这些事件来监控系统运行状态。
    """
    timestamp: datetime    # 事件发生的时间
    level: str             # 严重程度:"info"(信息), "warning"(警告), "error"(错误)
    source: str            # 事件来源,如 "AgentCore", "ToolLayer"
    message: str           # 人类可读的描述
    context: dict          # 附加信息,如 {"task_id": "abc123", "tool_name": "search"}

class AgentCore:
    def __init__(self, event_handler: Callable[[ExecutionEvent], None]):
        """构造方法接收一个事件处理函数(回调函数)。

        Callable[[ExecutionEvent], None] 的含义是:
        一个接收 ExecutionEvent 参数、不返回值的函数。
        这个函数由上层提供——上层决定收到事件后做什么
        (比如写日志、发告警、更新UI等),
        而下层只负责发出事件,不关心上层如何处理。
        """
        self.event_handler = event_handler

    async def run(self, task: Task):
        # 在开始执行时,发出一个"任务开始"事件
        self.event_handler(
            ExecutionEvent(
                timestamp=datetime.now(),
                level="info",
                source="AgentCore",
                message="Task started",
                context={"task_id": task.id}
            )
        )
        # ... 后续的执行逻辑 ...

1.3.3 核心层内部的协作

智能体核心层内部,运行时引擎与其他子系统之间采用星型拓扑通信,正如第一章所述。这种设计意味着:

  • 运行时引擎是唯一的协调者,其他子系统不直接互相调用
  • 每个子系统通过明确定义的接口与运行时引擎交互
  • 数据流始终经过运行时引擎中转,便于追踪和调试

1.4 OpenAI Codex 的性能型架构

OpenAI Codex 以 Rust 为核心语言(具体比例以官方代码库统计为准),围绕性能和系统级安全构建了完整的 Harness 架构。其组件到参考架构的映射如下:

Codex 架构 参考架构映射
CLI / TUI / Headless 三种入口 + MCP Server 接入层
Subagent 层级委派(支持 3+ 层深度) 编排层
codex-core(智能体循环) 智能体核心层(运行时引擎)
skills / core-skills 模块 + MCP 工具集成 智能体核心层(工具层)
SQLite 持久化记忆 + AGENTS.md 项目文档 智能体核心层(记忆子系统)
execpolicy(Starlark 策略引擎) 智能体核心层(输出治理)
平台原生沙箱(Bubblewrap + seccomp,回退自带 bwrap / Seatbelt) 横切关注点(安全)
OpenTelemetry 原生集成 横切关注点(可观测性)

Codex 的特点:

  • 双层安全模型:execpolicy 策略引擎控制智能体“可以尝试什么”,平台原生沙箱控制“能做什么”,两层独立运作形成纵深防御
  • 线性成本的上下文管理:通过提示缓存和异步上下文压缩,将推理成本从对话长度的二次增长降为线性增长
  • 仓库即上下文:以代码仓库本身作为智能体的首要上下文来源,通过 AGENTS.md 和分层目录结构实现确定性、可版本控制的智能体行为
  • 60+ Cargo Crate 的模块化:每个子系统对应独立的 Rust crate,通过 trait 定义清晰的模块边界

1.5 Claude Code 的任务型架构

Claude Code 针对任务型应用进行了优化,其架构可以映射到参考架构:

Claude Code 架构 参考架构映射
API 入口 / CLI 界面 接入层
Coordinator(多智能体编排) 编排层
QueryEngine(异步循环) 智能体核心层(运行时引擎)
24+ 内置工具 智能体核心层(工具层)
系统提示词缓存 智能体核心层(记忆子系统)
输出验证 智能体核心层(输出治理)
权限检查 + 追踪系统 横切关注点(安全 + 可观测性)

Claude Code 的特点:

  • 流式优化:支持流式工具执行,提高响应速度
  • 类型安全:TypeScript 确保工具接口的类型安全
  • 精细控制:40+ 特性门控,提供细粒度的行为控制
  • 核心层紧耦合:QueryEngine 在同一循环中完成模型调用、输出解析、工具执行和上下文管理,体现了智能体核心层的设计理念

1.6 OpenClaw 的自驱型架构

OpenClaw 是一个自驱型智能体系统,与前两者的“用户发起、完成即停”模式不同,OpenClaw 的智能体可以长期自主运行。它的架构在编排和记忆方面有明显不同的侧重点:

OpenClaw 架构 参考架构映射
Gateway(长连接管理) 接入层
Lobster(工作流引擎) 编排层
智能体执行循环 智能体核心层(运行时引擎)
ClawHub(技能注册与发现) 智能体核心层(工具层)
MEMORY.md(记忆持久化) 智能体核心层(记忆子系统)
模型选择与约束 智能体核心层(模型集成与输出治理)
SOUL.md(行为约束)+ 三级权限 横切关注点(安全)
心跳监控 + 日志系统 横切关注点(可观测性)

OpenClaw 的特点:

  • 单一控制平面:官方协议将 Gateway WebSocket 定位为“单一控制平面 + 节点传输”——客户端(CLI、Web UI、应用)由此接入下发指令,能力节点经同一通道接入,与参考架构的接入层/横切关注点对应良好
  • 强大的编排:Lobster 工作流引擎支持 YAML 定义的复杂流程
  • 持久化智能体:通过心跳机制,智能体可以长期运行和学习

1.7 架构在不同场景中的适配

虽然参考架构是通用的,但在不同的应用场景中,各层的重要性和复杂度会有所不同。下面通过三个由简到繁的场景,帮助读者直观理解架构各层在实际运行时是如何参与工作的。

场景 1:简单的单任务智能体

比如,一个“查询天气”的智能体:

flowchart TD
    A["<b>接入层</b><br/>用户输入:北京的天气"] --> B["<b>编排层</b><br/>跳过(不需要分解)"]
    B --> C["智能体核心层"]
    C --> D["运行时引擎 → 调用LLM"]
    D --> E["解析工具调用(输出治理)"]
    E --> F["执行weather_api(工具层)"]
    F --> G["格式化结果 → 返回"]

    style A fill:#e3f2fd
    style B fill:#fff9c4
    style C fill:#e8f5e9

在这个场景中,编排层完全跳过,智能体核心层只需一轮循环。重点应该放在确保工具调用的正确性。

场景 2:复杂的多步工作流

比如,“为客户创建一个完整的财务计划”:

flowchart TD
    A["<b>接入层</b><br/>接收客户信息"] --> B["<b>编排层</b><br/>分解为多个子任务"]
    B --> C["评估风险等级"]
    B --> D["选择投资产品"]
    B --> E["制定行动计划"]
    C --> F["<b>智能体核心层</b><br/>各智能体多轮循环<br/>调用金融API + 积累分析结果"]
    D --> F
    E --> F
    F --> G["<b>编排层</b><br/>聚合各智能体的结果<br/>生成最终方案"]

    style A fill:#e3f2fd
    style B fill:#fff9c4
    style F fill:#e8f5e9
    style G fill:#fff9c4

在这个场景中,编排层变得至关重要——它需要确保各子任务的顺序、依赖关系和结果验证。

场景 3:多智能体长期协作

比如,OpenClaw 的一个典型应用场景:

flowchart TD
    A["<b>接入层</b><br/>用户发出任务,系统持久化运行"] --> B["<b>编排层</b><br/>Lobster工作流,定义长期目标"]
    B --> C["<b>智能体核心层</b><br/>多个智能体,各自维护状态"]
    C --> D["<b>记忆子系统</b><br/>MEMORY.md + 每日记忆文件<br/>跨天积累学习"]
    C --> E["<b>运行时引擎</b><br/>心跳唤醒,恢复上下文,继续执行"]
    C --> F["<b>横切关注点</b><br/>心跳监控、状态持久化、历史重放"]

    style A fill:#e3f2fd
    style B fill:#fff9c4
    style C fill:#e8f5e9
    style F fill:#f3e5f5

在这个场景中,记忆子系统和横切关注点中的存储变得至关重要,因为智能体需要在相当长的时间内维持一致的行为。

1.8 总结

三层 + 横切关注点的参考架构提供了一个通用的、可扩展的 Harness 系统设计框架。相比传统的多层线性模型,这个架构有三个关键优势:

  1. 忠实反映真实执行流:模型调用、工具执行、记忆更新在运行时引擎的同一循环中交替发生,将它们放在同一层(智能体核心层)符合实际
  2. 与第一章子系统模型一致:五大核心子系统和两大基础保障的划分,自然映射到三层结构和横切关注点
  3. 灵活适配不同场景:编排层可选,简单系统可以跳过;横切关注点按需启用,避免过度设计

通过这个框架:

  • 新设计师可以快速理解 Harness 系统的全景
  • 不同的实现可以在相同的抽象层次上进行比较
  • 系统的演进和扩展有了清晰的方向

在接下来的章节中,我们将逐层深入,讨论每一层的具体设计和实现细节。

2. 执行层的详细设计

执行层是 Harness 系统的核心工作区域。它包括三个紧密相关的子系统:运行时引擎、工具层和记忆子系统。本节将深入讨论它们的设计原则、职责边界和接口规范。

2.1 运行时引擎

运行时引擎是执行层的核心,负责驱动智能体的“感知—推理—行动”循环。本小节介绍其核心职责、执行循环的设计、终止条件和接口规范。

2.1.1 核心职责

运行时引擎管理智能体从创建到销毁的完整生命周期。以下状态机展示了这个生命周期的各个阶段:

stateDiagram-v2
    [*] --> Initializing : 智能体创建或恢复
    Initializing --> ReadyToExecute
    ReadyToExecute --> Executing

    state Executing {
        [*] --> 感知环境检索记忆
        感知环境检索记忆 --> 调用LLM推理
        调用LLM推理 --> 解析输出提取工具调用
        解析输出提取工具调用 --> 执行工具
        执行工具 --> 更新状态存储记忆
        更新状态存储记忆 --> 判断是否继续
        判断是否继续 --> 感知环境检索记忆 : 继续
        判断是否继续 --> [*] : 结束
    }

    Executing --> Completed : 任务完成或达到最大步数
    Completed --> Finalizing : 清理资源,保存最终状态
    Finalizing --> Stopped
    Stopped --> [*]

图 2-3:运行时引擎的状态机与执行循环

2.1.2 执行循环的六个步骤

执行循环是运行时引擎的核心。每一轮循环由六个步骤组成,每个步骤都有明确的输入、输出和职责边界。

步骤 1:感知(Perceive) — 收集当前任务需要的所有上下文信息。这包括三个来源:用户的原始输入(显式上下文)、最近几轮的对话历史(短期记忆)、以及与当前任务语义相关的历史经验(长期记忆的向量检索结果)。感知步骤的质量直接决定了后续推理的质量——如果遗漏了关键上下文,LLM 的推理就会偏离方向。

步骤 2:推理(Reasoning) — 将感知阶段收集的上下文组装为 LLM 的输入(系统提示 + 用户消息 + 可用工具列表),然后调用 LLM 获取响应。这一步的关键设计决策是 上下文组装策略——哪些信息放在系统提示中、哪些放在用户消息中、如何在有限的 Token 预算内优先保留最重要的内容。第四章和第六章将深入讨论这些策略。

步骤 3:决策(Decision) — 解析 LLM 的输出,提取其中的工具调用请求或最终答案。LLM 的输出可能包含多个工具调用(并行或顺序),也可能是一段文本回复。决策步骤需要验证每个工具调用的合法性:工具是否存在、参数格式是否正确、参数值是否在合理范围内。

步骤 4:执行(Execution) — 将验证通过的工具调用交给工具层执行。每个工具调用都设有超时限制,执行过程中的异常不会终止整个循环,而是被捕获并记录为错误结果。这种“容错继续”的策略确保单个工具的失败不会导致整个任务中断。

步骤 5:学习(Learning) — 将本轮循环的完整记录(输入上下文、LLM 推理、工具调用、执行结果)写入记忆系统。短期记忆总是写入;如果这一步具有较高的学习价值(例如遇到了新类型的错误),还会同步写入长期记忆。

步骤 6:判断(Judgment) — 决定是否继续下一轮循环。终止条件通常包括三种:LLM 已输出最终答案、已达到最大步数限制、已超过总执行时间。这些保护机制防止智能体陷入无限循环或消耗过多资源。

2.1.3 接口设计

运行时引擎对外暴露的接口应当简洁明确,核心方法包括:

  • initialize(agent, task):初始化执行环境,加载智能体配置和任务定义
  • step():执行单步循环,返回本步骤的结果——这是最重要的方法,也是测试和调试的主要入口
  • run(task, max_steps, timeout):完整地执行一个任务,内部反复调用 step() 直到满足终止条件
  • pause() / resume():暂停和恢复执行,支持长时任务的中断和续行
  • get_state():获取当前执行状态,供可观测性系统使用

其中 step() 和 run() 的分离是一个重要的设计决策。step() 暴露了单步粒度的控制,使得外部系统可以在每一步之间插入检查点、审批流程或日志记录;run() 则提供了更简洁的“一键执行”接口,适用于不需要细粒度控制的场景。

2.2 工具层

工具层是智能体与外部世界交互的桥梁。LLM 本身只能生成文本,而工具层将文本形式的“意图”转化为真实的系统操作——执行代码、查询数据库、调用 API 等。本小节介绍工具的生命周期、注册表设计、执行流水线和隔离策略。

2.2.1 工具的生命周期

一个工具从被系统感知到执行完毕,经历以下阶段:

flowchart LR
    A["注册"] --> B["发现"] --> C["选择"] --> D["调用"] --> E["隔离执行"] --> F["结果处理"] --> G["反馈"]

图 2-4:工具从注册到执行的完整生命周期

注册 是工具进入系统的入口。每个工具需要提供一份结构化的“自我介绍”:名称、功能描述、输入参数的 JSON Schema、所需权限、超时限制等。这些元数据不仅供系统内部使用,更重要的是 供 LLM 理解工具的能力和使用方式——LLM 根据工具描述来决定何时调用哪个工具、传入什么参数。因此,工具描述的质量直接影响 LLM 的工具选择准确率。

发现和选择 发生在运行时引擎调用 LLM 之前。运行时引擎从注册表中检索当前任务可用的工具列表,将其以 LLM 能理解的格式(通常是 JSON Schema)注入到提示词中。对于工具数量较多的系统,可能需要动态筛选——只向 LLM 暴露与当前任务相关的工具,避免工具列表过长导致 LLM 选择困难。

隔离执行 是工具层最关键的环节。工具的执行可能涉及文件操作、网络请求、shell 命令等高风险操作,因此必须在受控的环境中运行。隔离的粒度从低到高包括:异常捕获(最轻量)、进程隔离(通过 subprocess)、容器隔离(通过 Docker)、系统级沙箱(如 Linux 的 seccomp 或 Bubblewrap)。第十二章将详细讨论各级隔离方案的权衡。

2.2.2 注册表设计

工具注册表的核心是一个“名称 → 定义 + 执行器”的映射。设计上有两个关键决策:

定义与执行器分离。工具的元数据描述(ToolDefinition)和实际执行逻辑(executor)分开存储。这种分离使得同一个工具定义可以对应不同环境下的执行器——例如测试时使用 mock 执行器,生产时使用真实的 API 调用。

LLM 友好的导出。注册表需要提供一个 list_for_llm() 方法,将工具定义转换为 LLM API 所要求的格式。不同的 LLM 提供商对工具描述的格式要求略有不同(如 Anthropic 的 tool_use 和 OpenAI 的 function_calling),注册表应封装这些差异。

2.2.3 执行流水线

工具执行器的职责不只是“调用函数”,而是一条完整的流水线,每一步都有明确的目的:

  1. 查找工具:从注册表中获取工具定义和执行器。找不到则立即返回错误,避免后续无效操作。
  2. 权限检查:验证当前智能体是否有权使用该工具。这是安全层的第一道防线——即使 LLM 输出了工具调用,如果智能体没有对应权限,调用也会被拒绝。
  3. 参数验证:根据工具定义中的 JSON Schema 验证参数的类型和取值范围。LLM 生成的参数并不总是正确的,验证可以在执行前发现明显的格式错误。
  4. 隔离执行:在设定的超时限制内执行工具逻辑。超时保护防止工具长时间阻塞运行时引擎。
  5. 结果标准化:无论工具执行成功还是失败,都返回统一格式的 ToolResult(包含状态码、输出内容、错误信息),使得运行时引擎可以用一致的方式处理所有工具的执行结果。

第五章将深入探讨每个环节的工程实现细节,包括参数校正、重试策略和结果缓存等高级特性。

2.3 记忆子系统

本小节讨论记忆子系统的设计动机、分层策略、检索机制和统一接口设计。

2.3.1 为什么需要分层记忆

人类的记忆天然是分层的:工作记忆容量有限但响应极快,长期记忆容量几乎无限但检索较慢。智能体的记忆系统采用了类似的分层策略,但动机更直接—— LLM 的上下文窗口是有限的。即使最新的模型支持数十万 Token 的上下文,也无法将智能体所有的执行历史、学习经验和领域知识一次性塞入上下文。因此,记忆子系统的核心任务是:在有限的上下文预算内,为当前任务提供最相关的信息。

这个问题可以类比数据库的存储层级:CPU 缓存(快但小)→ 内存(中等)→ 磁盘(慢但大)。每一层在容量、延迟和访问模式上有不同的特点,系统需要智能地决定什么数据放在哪一层。

2.3.2 记忆的分层结构

按生命周期和访问模式划分,从存储实现的视角看,记忆系统通常包含三层(1.3 节概念分层中的“工作记忆”在此视角下并入会话级短期记忆,“向量检索层”则是长期记忆的语义索引):

层级 生命周期 容量 延迟 典型内容 实现方式
短期记忆 当前会话 小(受上下文窗口限制) 极低 最近的对话轮次、临时观察、已完成步骤摘要 内存中的双端队列
长期记忆 跨会话持久化 大 中等 成功的执行模式、用户偏好、学到的规则 数据库或文件系统
向量检索层 随长期记忆同步 大 较高 长期记忆的语义索引 向量数据库

短期记忆 是智能体的“工作台”。它保存当前会话中最近的执行步骤,供运行时引擎在每一轮循环中组装上下文。短期记忆的关键设计决策是 容量上限——当记忆超出上下文窗口的预算时,需要丢弃或压缩旧的条目。常见策略包括滑动窗口(丢弃最早的条目)和摘要压缩(将多条记录浓缩为一段摘要)。

长期记忆 让智能体能够跨会话积累经验。并非所有执行步骤都值得长期保存——一个关键的设计决策是 重要性过滤:只有满足一定重要性阈值的记录才会写入长期存储。判断重要性的常见信号包括:任务是否成功、是否遇到过异常、用户是否给出了反馈等。

向量检索层 为长期记忆提供语义搜索能力。传统的按时间或关键词检索在面对大量记忆时效果有限——智能体需要的是“找到与当前任务最相关的历史经验”,这正是向量检索擅长的。它将每条记忆编码为高维向量,通过余弦相似度等度量找到语义上最接近的记忆条目。

为避免概念混淆,本书区分三类状态:运行时状态 / 事件日志 是当前执行的权威记录;记忆 保存可检索的事实、摘要、偏好和经验;上下文 是运行时在每次模型调用前从状态与记忆中投影出的输入片段。三者可以互相引用,但不能互相替代。

2.3.3 检索策略

记忆管理器需要根据不同的场景选择合适的检索策略:

  • 时间优先:获取最近 N 条记录。适用于需要了解“刚才发生了什么”的场景,例如在多步任务中回顾上一步的结果。
  • 语义优先:按查询与记忆条目的语义相似度排序。适用于“以前遇到过类似问题吗”的场景,例如智能体在执行一个新任务时搜索历史中的类似案例。
  • 混合检索:先通过时间窗口缩小候选集,再按语义排序。这种方式在实践中最为常用,因为它既保证了时效性,又利用了语义匹配的精确度。

2.3.4 统一接口设计

尽管底层有多层存储,记忆管理器应对外暴露统一的接口,让运行时引擎无需关心记忆存储在哪一层。核心接口只需两个操作:

class MemoryManager:
    """统一的记忆接口"""

    async def store_step(self, record: StepRecord, importance: float = 0.5) -> None:
        """存储一步执行记录。短期记忆总是写入;超过重要性阈值时同步写入长期记忆和向量索引。"""

    async def retrieve(self, query: str = None, recent_only: bool = False, top_k: int = 5) -> list[StepRecord]:
        """检索记忆。recent_only=True 走短期记忆;有 query 走向量语义检索;否则走长期记忆时间排序。"""

这种“写入时分层、读取时统一”的设计,使得运行时引擎只需调用 store_step 和 retrieve,而分层逻辑、重要性判断、向量编码等复杂性全部封装在记忆管理器内部。第六章将深入探讨每一层的工程实现细节。

2.4 执行层各子系统的协作

前面分别讨论了运行时引擎、工具层和记忆子系统各自的设计。但在实际执行中,这三个子系统并非独立运行,而是在每一步循环中紧密协作。理解它们的协作方式,是理解执行层整体设计的关键。

2.4.1 协作的核心原则

执行层采用 星型拓扑:运行时引擎是唯一的协调者,工具层和记忆子系统不直接相互调用。这个设计看似增加了间接性,实则带来了三个重要的工程优势:

  • 可追踪性:所有数据流都经过运行时引擎,便于构建完整的执行轨迹
  • 可测试性:每个子系统可以独立 mock,无需搭建完整的系统环境
  • 可替换性:更换记忆后端(如从内存切换到 Redis)不会影响工具层的实现

因此,工具层只返回标准化 ToolResult 和执行元数据;是否写入事件日志、短期记忆或长期记忆,由运行时引擎根据审计、重要性和隐私策略决定。

2.4.2 一步执行的协作流程

在每一步执行中,三个子系统按以下顺序协作:

  1. 记忆 → 运行时引擎:运行时引擎从记忆管理器检索当前任务相关的上下文(最近的对话历史、语义相似的历史经验),组装成 LLM 的输入
  2. 运行时引擎 → LLM:运行时引擎将组装好的上下文发送给 LLM,获取推理结果
  3. 运行时引擎 → 工具层:如果 LLM 输出了工具调用请求,运行时引擎将其转交给工具执行器,由工具层完成参数验证、权限检查和隔离执行
  4. 工具层 → 运行时引擎 → 记忆:工具执行结果返回给运行时引擎,引擎将本步骤的完整记录(输入、推理、动作、结果)写入记忆管理器

这个循环不断重复,直到 LLM 输出最终答案或达到步数上限。

2.4.3 重要性评估

在步骤 4 中,运行时引擎需要为每条记录评估“重要性”,以决定是否写入长期记忆。常见的评估信号包括:工具调用是否失败(失败的经验往往更值得记住)、用户是否给出了明确的反馈、当前任务是否是新类型等。这个评估逻辑虽然简单,却直接影响智能体的长期学习效果——第六章将对此做深入讨论。

2.5 托管 Agent 虚拟化:Brain/Hands 解耦架构

传统的 Harness 架构将“大脑”(推理引擎)和“手”(执行环境)紧密耦合在一起。这种单体设计在小规模场景中运作良好,但在长期运行的生产环境中面临两个核心问题:

  1. 假设快速老化:Harness 中编码的关于“模型能力的假设”会随着模型升级而过时。例如,Claude Sonnet 4.5 表现出的“上下文焦虑”(在接近上下文限制时过早结束任务)在升级到 Claude Opus 4.6 后消失了,使得针对此行为的特殊处理变成了无效的开销。
  2. 容错性不足:容器作为“宠物”(手工维护的单个实例)需要精心照顾。任何容器故障都导致整个 Agent 停机,而无法通过重试恢复。

托管 Agent 虚拟化 解决这些问题的关键在于将 Agent 的生命周期拆分为三个独立的抽象:

2.5.1 三层虚拟化架构

会话层 (Session) — 追加式的事件日志

  • 记录 Agent 整个生命周期中发生的每一件事
  • 不关心事件的存储位置或格式,仅追加
  • 提供 getEvents() 接口,支持选择性检索:按范围取切片、按时间戳倒回、按关键词搜索
  • 外部状态管理:与 Harness 实现无关,持久化到外部数据库或文件系统
class Session:
    """会话:持久化的事件日志"""

    def __init__(self, session_id: str, storage_backend):
        self.session_id = session_id
        self.storage = storage_backend  # S3, PostgreSQL, 文件系统等
        self.event_stream = []

    async def append_event(self, event: AgentEvent):
        """追加事件,不可删除"""
        event.timestamp = time.time()
        event.session_id = self.session_id
        await self.storage.append(event)

    async def get_events(self,
                        start_index: int = None,
                        start_timestamp: float = None,
                        limit: int = None) -> list[AgentEvent]:
        """灵活检索事件,支持从中间继续、倒回到特定时刻"""
        return await self.storage.query_events(
            session_id=self.session_id,
            start_index=start_index,
            start_timestamp=start_timestamp,
            limit=limit
        )

Harness 层 — 循环逻辑的无状态实现

  • 读取 Session 中的事件来恢复上下文,而非依赖内存状态
  • 实现 Agent 的核心循环逻辑(感知→推理→执行)
  • 对 Session 中的新事件完全无知:只负责生成新事件并写回 Session
  • 允许多个 Harness 实例共同处理同一个 Session(例如从另一个实例恢复执行)
class Harness:
    """Harness:无状态的循环引擎"""

    async def step(self, session: Session, resumption_point: int = None):
        """执行一步,从恢复点继续"""
        # 1. 从Session恢复上下文
        events = await session.get_events(start_index=resumption_point or 0)
        context = self._reconstruct_context(events)

        # 2. 执行单步循环
        perception = self._perceive(context)
        reasoning = await self.llm.infer(perception)
        decision = self._parse_decision(reasoning)

        # 3. 执行工具调用(可能失败)
        results = []
        for tool_call in decision.tool_calls:
            result = await self.tools.execute(tool_call)
            results.append(result)  # 记录执行结果,即使失败也继续

        # 4. 追加事件到Session
        await session.append_event(AgentStepCompleted(
            step_number=len(events),
            perception=perception,
            reasoning=reasoning,
            actions=decision.tool_calls,
            results=results
        ))

    def _reconstruct_context(self, events: list[AgentEvent]) -> dict:
        """从事件流重建当前上下文"""
        context = {"history": [], "state": {}}
        for event in events:
            if isinstance(event, AgentStepCompleted):
                context["history"].append({
                    "input": event.perception,
                    "reasoning": event.reasoning,
                    "actions": event.actions
                })
            elif isinstance(event, StateChanged):
                context["state"].update(event.changes)
        return context

沙箱层 (Sandbox) — 执行环境的可替换实现

  • 独立管理代码执行、文件操作、API 调用等环境
  • 与 Harness 和 Session 完全解耦
  • 容器故障不影响 Session,因为失败的工具调用会记录到 Session 中,Harness 可以重试
  • 支持热切换:同一个 Session 可以在不同的容器中继续执行
class Sandbox:
    """沙箱:可替换的执行环境"""

    async def execute_tool(self, tool_call: ToolCall) -> ToolResult:
        """在隔离环境中执行工具"""
        # 真实实现可能涉及:
        # - Docker容器内代码执行
        # - API网关代理外部服务调用
        # - 文件系统隔离
        try:
            result = await self._run_isolated(tool_call)
            return ToolResult(status="success", output=result)
        except Exception as e:
            return ToolResult(status="error", error=str(e))
            # 注意:错误被返回给Harness,不会导致Session中断

2.5.2 长期任务的上下文管理

对于超过 LLM 上下文窗口的长期任务,Session 作为“外部上下文对象”解决了这个问题:

async def run_long_horizon_task(task: Task, max_total_time: int):
    """长期任务执行,跨越多个上下文窗口"""
    session = Session(task.id, storage_backend=postgresql)
    harness = Harness(model="claude-opus-4-7")
    sandbox = Sandbox(docker_config)

    start_time = time.time()
    step_count = 0

    while time.time() - start_time < max_total_time:
        # 1. 恢复:从Session中取最近1000个token的事件
        # (即使任务已运行10000步,也只加载最近的上下文)
        recent_events = await session.get_events(
            start_index=max(0, step_count - 50),  # 最近50步
            limit=1000  # 最多1000个token
        )

        # 2. 循环:Harness执行一步
        await harness.step(session, resumption_point=step_count)
        step_count += 1

        # 3. 检查是否完成
        latest_event = await session.get_events(start_index=step_count-1, limit=1)
        if latest_event[0].type == "task_completed":
            break

    # 4. 完整恢复(用于事后分析)
    full_history = await session.get_events()
    return analyze_execution(full_history)

2.5.3 托管 Agent 的优势

问题 单体设计 虚拟化设计
容器故障 Agent 中断 在不同容器继续执行
模型升级 旧假设变成死代码 Harness 可灵活适配新模型能力
上下文溢出 需要复杂的压缩策略 Session 外部管理,Harness 读取切片
可观测性 依赖日志系统 事件流天然提供完整轨迹
多实例并行 互相干扰 完全隔离,可实现分布式执行

这种虚拟化设计遵循操作系统的哲学——将具体实现细节抽象成稳定的接口(Session 的事件 API、Harness 的循环契约、Sandbox 的工具执行接口),使得系统能够长期演进而无需破坏性改动。

2.6 总结

执行层通过三个紧密协作的子系统:

  • 运行时引擎:实现智能体的执行循环,从感知、推理、决策到执行的完整流程
  • 工具层:提供安全、标准化的工具调用机制,包括注册、验证、隔离执行
  • 记忆系统:支持工作、短期、长期三层记忆与向量语义检索,让智能体能够学习和改进

这三个子系统的清晰接口设计,使得它们可以独立演进,也可以灵活组合以适应不同的场景需求。

在生产级别的托管 Agent 场景中,进一步的虚拟化(Session/Harness/Sandbox 三层解耦)提供了更强的容错性和适应性,使得系统能够处理长期运行任务并适应模型能力的演进。

3. 安全层与可观测性层

本节介绍安全层和可观测性层的设计原则、核心组件、实现策略和最佳实践,这两层贯穿整个 Harness 系统,为所有子系统提供基础保障。

3.1 基础保障的重要性

安全层和可观测性层虽然不是独立的执行层,但它们贯穿整个 Harness 系统的每个角落。它们的设计质量直接决定了系统的安全性、可维护性和生产就绪程度。

3.2 安全层

安全层的核心理念是:假设智能体的任何决策都可能存在风险,需要进行多层验证。

3.2.1 安全的层次化模型

根据操作的风险程度,我们定义了六个信任等级,每个等级对应不同的权限配置:

信任等级 权限配置 典型应用
Manual Only 完全人工操作 极高风险
Approve Always 每步都需要审批 高风险操作
Approve Once 整个流程开始前审批 生产环境
Ask First 关键操作前请求审批 开发/测试环境
Auto with Notification 自动执行+发送通知 低风险日常操作
Full Trust 完全自主执行,无需通知 玩具应用

3.2.2 权限管理系统的设计

权限管理系统通过定义权限等级来实现细粒度的访问控制:

from enum import Enum
from typing import Protocol, Optional
from pydantic import BaseModel
from datetime import datetime

class PermissionLevel(str, Enum):
    """权限等级定义(6级信任模型)"""
    MANUAL_ONLY = "manual_only"           # 完全人工操作
    APPROVE_ALWAYS = "approve_always"     # 每步审批
    APPROVE_ONCE = "approve_once"         # 整个任务审批一次
    ASK_FIRST = "ask_first"               # 事前询问
    AUTO_WITH_NOTIFICATION = "auto_with_notification"  # 自动+通知
    FULL_TRUST = "full_trust"             # 完全信任

class ResourcePermission(BaseModel):
    """对某个资源的权限定义"""
    resource_id: str          # 资源标识(如"file://config.yaml")
    resource_type: str        # 资源类型(如"file", "api", "database")
    permission_level: PermissionLevel
    actions: list[str]        # 允许的操作(如"read", "write", "delete")
    conditions: dict = {}     # 额外条件(如"max_size_mb": 100)

class AgentPermissions(BaseModel):
    """智能体的权限配置"""
    agent_id: str
    permissions: list[ResourcePermission]
    default_level: PermissionLevel = PermissionLevel.ASK_FIRST  # 默认询问级别
    created_at: datetime
    expires_at: Optional[datetime] = None  # 权限过期时间

class PermissionManager:
    """权限管理器"""

    def __init__(self, policy_storage: PolicyStorage):
        self.policy_storage = policy_storage
        self.cache = {}  # 权限缓存,加速查询

    async def check_permission(
        self,
        agent_id: str,
        resource_id: str,
        action: str
    ) -> tuple[bool, Optional[str]]:
        """
        检查Agent是否有权进行操作。

        返回:(是否有权, 理由或None)
        """
        # 从缓存中获取权限
        permissions = self.cache.get(agent_id)
        if permissions is None:
            # 从存储中加载
            permissions = await self.policy_storage.load(agent_id)
            self.cache[agent_id] = permissions

        # 检查权限是否过期
        if permissions.expires_at and datetime.now() > permissions.expires_at:
            return False, "Agent permissions have expired"

        # 查找匹配的权限
        for perm in permissions.permissions:
            if self._resource_matches(perm.resource_id, resource_id):
                if action in perm.actions:
                    return True, None

        # 如果没有找到,使用默认权限
        if permissions.default_level == PermissionLevel.FULL_TRUST:
            return True, None
        elif permissions.default_level == PermissionLevel.ASK_FIRST:
            return False, "Request needs human approval"

        return False, "Permission denied"

    async def request_approval(
        self,
        agent_id: str,
        resource_id: str,
        action: str,
        reason: str
    ) -> ApprovalRequest:
        """提交审批请求"""
        return ApprovalRequest(
            agent_id=agent_id,
            resource_id=resource_id,
            action=action,
            reason=reason,
            created_at=datetime.now(),
            status="pending"
        )

    def _resource_matches(self, pattern: str, resource: str) -> bool:
        """检查资源是否匹配权限模式"""
        # 支持通配符匹配
        # 如 "file://*.log" 匹配所有.log文件
        from fnmatch import fnmatch
        return fnmatch(resource, pattern)

3.2.3 沙箱隔离的设计

对于高风险的操作(如执行系统命令、修改文件),Harness 需要在隔离的环境中执行,以防止其影响整个系统。

class SandboxExecutor:
    """在沙箱中执行操作"""

    async def execute(
        self,
        tool_call: ToolCall,
        isolation_level: IsolationLevel = IsolationLevel.PROCESS
    ) -> ToolResult:
        """
        在隔离环境中执行工具调用。

        isolation_level:
          - NONE: 直接执行,无隔离
          - PROCESS: 进程级隔离
          - CONTAINER: 容器级隔离(Docker)
          - VM: 虚拟机级隔离
        """
        if isolation_level == IsolationLevel.NONE:
            return await self._execute_directly(tool_call)

        elif isolation_level == IsolationLevel.PROCESS:
            return await self._execute_in_subprocess(tool_call)

        elif isolation_level == IsolationLevel.CONTAINER:
            return await self._execute_in_container(tool_call)

        elif isolation_level == IsolationLevel.VM:
            return await self._execute_in_vm(tool_call)

    async def _execute_in_subprocess(self, tool_call: ToolCall) -> ToolResult:
        """在子进程中执行,隔离系统调用"""
        import subprocess

        try:
            # 构造执行命令
            cmd = self._build_command(tool_call)

            # 在子进程中执行,限制资源
            result = subprocess.run(
                cmd,
                timeout=30,
                capture_output=True,
                text=True,
                # 限制内存和CPU
                # cgroups配置需要在系统层面
            )

            if result.returncode == 0:
                return ToolResult(
                    status="success",
                    output=result.stdout
                )
            else:
                return ToolResult(
                    status="error",
                    error=result.stderr
                )

        except subprocess.TimeoutExpired:
            return ToolResult(
                status="timeout",
                error="Execution exceeded 30s timeout"
            )

    async def _execute_in_container(self, tool_call: ToolCall) -> ToolResult:
        """在Docker容器中执行,完整的资源隔离"""
        import docker

        client = docker.from_env()

        try:
            # 在临时容器中执行
            # containers.run 本身不支持超时参数,超时控制交给 asyncio
            output = await asyncio.wait_for(
                asyncio.to_thread(
                    client.containers.run,
                    image="python:3.11-slim",
                    command=self._build_command(tool_call),
                    remove=True,
                    mem_limit="512m",  # 512MB内存限制
                    memswap_limit="1g",  # 包括swap
                    nano_cpus=500_000_000,  # 限制到50%CPU(单位为1e-9核)
                    network_disabled=True  # 禁用网络
                ),
                timeout=30,
            )

            return ToolResult(
                status="success",
                output=output.decode()
            )

        except docker.errors.ContainerError as e:
            return ToolResult(
                status="error",
                error=str(e)
            )

3.2.4 审计日志的设计

审计日志记录所有安全相关事件,便于事后追踪和分析:

class AuditLog(BaseModel):
    """审计日志记录"""
    timestamp: datetime
    agent_id: str
    action_type: str  # "permission_check", "tool_execution", "approval_request"等
    resource: str
    operation: str
    status: str  # "allowed", "denied", "executed", "failed"
    details: dict = {}  # 额外信息
    ip_address: Optional[str] = None
    user_id: Optional[str] = None

class AuditLogger:
    """审计日志系统"""

    def __init__(self, storage: AuditLogStorage):
        self.storage = storage
        self.buffer = []  # 日志缓冲,批量写入以提高性能
        self.buffer_size = 100

    async def log(self, audit_log: AuditLog) -> None:
        """记录一条审计日志"""
        self.buffer.append(audit_log)

        if len(self.buffer) >= self.buffer_size:
            await self._flush()

    async def _flush(self) -> None:
        """将缓冲的日志写入存储"""
        if not self.buffer:
            return

        await self.storage.batch_insert(self.buffer)
        self.buffer.clear()

    async def query(
        self,
        agent_id: str = None,
        action_type: str = None,
        status: str = None,
        time_range: tuple[datetime, datetime] = None,
        limit: int = 100
    ) -> list[AuditLog]:
        """查询审计日志"""
        filters = {}
        if agent_id:
            filters["agent_id"] = agent_id
        if action_type:
            filters["action_type"] = action_type
        if status:
            filters["status"] = status

        return await self.storage.query(
            filters=filters,
            time_range=time_range,
            limit=limit
        )

    async def export(
        self,
        file_path: str,
        format: str = "json"
    ) -> None:
        """导出审计日志"""
        logs = await self.storage.query(filters={})

        if format == "json":
            with open(file_path, "w") as f:
                json.dump(
                    [log.model_dump() for log in logs],
                    f,
                    default=str
                )
        elif format == "csv":
            import csv
            with open(file_path, "w", newline="") as f:
                writer = csv.DictWriter(f, fieldnames=AuditLog.model_fields)
                writer.writeheader()
                for log in logs:
                    writer.writerow(log.model_dump())

3.3 可观测性层

可观测性的核心目标是:当系统出现问题时,能够快速定位根本原因。

可观测性的三个支柱:日志、追踪、指标。

3.3.1 结构化日志

结构化日志使用 JSON 格式记录事件,便于机器解析和检索:

from enum import Enum
import logging
import json
from datetime import datetime

class LogLevel(str, Enum):
    DEBUG = "DEBUG"
    INFO = "INFO"
    WARNING = "WARNING"
    ERROR = "ERROR"
    CRITICAL = "CRITICAL"

class StructuredLogger:
    """结构化日志系统"""

    def __init__(self, name: str, storage: LogStorage):
        self.name = name
        self.storage = storage
        self.context = {}  # 当前的上下文信息

    def set_context(self, **kwargs) -> None:
        """设置日志上下文"""
        self.context.update(kwargs)

    async def log(
        self,
        level: LogLevel,
        message: str,
        **kwargs
    ) -> None:
        """记录一条日志"""
        log_record = {
            "timestamp": datetime.now().isoformat(),
            "logger": self.name,
            "level": level.value,
            "message": message,
            "context": self.context,
            "extra": kwargs
        }

        # 输出到控制台
        print(json.dumps(log_record))

        # 存储
        await self.storage.save(log_record)

    async def search(
        self,
        query: str,
        level: LogLevel = None,
        time_range: tuple[datetime, datetime] = None,
        limit: int = 100
    ) -> list[dict]:
        """搜索日志"""
        return await self.storage.search(
            query=query,
            level=level,
            time_range=time_range,
            limit=limit
        )

# 结构化日志使用示例
logger = StructuredLogger("agent.runtime", storage)
logger.set_context(agent_id="agent-001", task_id="task-123")

await logger.log(
    level=LogLevel.INFO,
    message="Tool execution started",
    tool_name="weather_api",
    params={"city": "Beijing"}
)

3.3.2 分布式追踪

分布式追踪通过跟踪请求跨越多个系统组件的执行路径,来诊断性能问题:

from contextlib import asynccontextmanager
from dataclasses import dataclass, field
import uuid
from datetime import datetime
from typing import Optional

@dataclass
class TraceSpan:
    """追踪单元"""
    trace_id: str          # 追踪的全局ID
    span_id: str           # 这个span的ID
    parent_span_id: Optional[str]  # 父span的ID
    operation_name: str    # 操作名称
    start_time: datetime
    end_time: Optional[datetime]
    duration_ms: Optional[float]
    status: str            # "success", "error", "timeout"
    error: Optional[str]
    attributes: dict = field(default_factory=dict)  # 属性(如参数、结果等)

class Tracer:
    """分布式追踪系统"""

    def __init__(self, service_name: str, storage: TraceStorage):
        self.service_name = service_name
        self.storage = storage
        self.current_trace_id: Optional[str] = None
        self.span_stack: list[TraceSpan] = []

    def start_trace(self, trace_id: str = None) -> str:
        """开始一个新的追踪"""
        if trace_id is None:
            trace_id = str(uuid.uuid4())

        self.current_trace_id = trace_id
        self.span_stack = []
        return trace_id

    def start_span(
        self,
        operation_name: str,
        attributes: dict = None
    ) -> TraceSpan:
        """开始一个新的span"""
        parent_span = self.span_stack[-1] if self.span_stack else None

        span = TraceSpan(
            trace_id=self.current_trace_id,
            span_id=str(uuid.uuid4()),
            parent_span_id=parent_span.span_id if parent_span else None,
            operation_name=operation_name,
            start_time=datetime.now(),
            end_time=None,
            duration_ms=None,
            status="pending",
            error=None,
            attributes=attributes or {}
        )

        self.span_stack.append(span)
        return span

    async def end_span(
        self,
        span: TraceSpan,
        status: str = "success",
        error: str = None
    ) -> None:
        """结束一个span"""
        span.end_time = datetime.now()
        span.duration_ms = (
            span.end_time - span.start_time
        ).total_seconds() * 1000
        span.status = status
        span.error = error

        # 弹出栈
        if self.span_stack and self.span_stack[-1] == span:
            self.span_stack.pop()

        # 存储
        await self.storage.save(span)

    @asynccontextmanager
    async def context_manager(
        self,
        operation_name: str,
        attributes: dict = None
    ):
        """上下文管理器,简化span的使用"""
        span = self.start_span(operation_name, attributes)
        try:
            yield span
            await self.end_span(span, status="success")
        except Exception as e:
            await self.end_span(span, status="error", error=str(e))
            raise

# 分布式追踪使用示例
async with tracer.context_manager("tool_execution", {"tool": "weather_api"}):
    # 执行工具
    result = await weather_api.get(city="Beijing")

3.3.3 性能指标收集

性能指标收集系统记录和汇总关键性能指标,支持实时监控和告警:

from collections import defaultdict
from statistics import mean, stdev, quantiles
from datetime import datetime
import time

class MetricsCollector:
    """性能指标收集"""

    def __init__(self):
        self.metrics: dict[str, list[float]] = defaultdict(list)
        self.counters: dict[str, int] = defaultdict(int)

    def record_duration(
        self,
        metric_name: str,
        duration_ms: float
    ) -> None:
        """记录一个时间指标"""
        self.metrics[metric_name].append(duration_ms)

    def increment_counter(
        self,
        counter_name: str,
        value: int = 1
    ) -> None:
        """增加计数"""
        self.counters[counter_name] += value

    def get_statistics(self, metric_name: str) -> dict:
        """获取指标的统计信息"""
        if metric_name not in self.metrics or not self.metrics[metric_name]:
            return {}

        values = self.metrics[metric_name]
        return {
            "count": len(values),
            "min": min(values),
            "max": max(values),
            "mean": mean(values),
            "stdev": stdev(values) if len(values) > 1 else 0,
            "p50": quantiles(values, n=4)[1],  # 50th percentile (median)
            "p99": quantiles(values, n=100)[-1]  # 99th percentile
        }

    async def export_metrics(self, time_interval: int = 60) -> dict:
        """定期导出指标"""
        return {
            "timestamp": datetime.now().isoformat(),
            "metrics": {
                name: self.get_statistics(name)
                for name in self.metrics
            },
            "counters": dict(self.counters)
        }

# 性能指标收集使用示例
metrics = MetricsCollector()

# 记录工具执行时间
start = time.time()
result = await tool.execute()
duration = (time.time() - start) * 1000
metrics.record_duration("tool_execution_time", duration)

# 记录错误计数
if result.status != "success":
    metrics.increment_counter("tool_execution_errors")

# 导出指标
stats = metrics.get_statistics("tool_execution_time")
print(f"Tool execution: mean={stats['mean']:.1f}ms, p99={stats['p99']:.1f}ms")

3.3.4 可观测性的集成

可观测性的三个支柱(日志、追踪、指标)需要有机集成,通过共同的上下文实现关联:

class ObservabilityManager:
    """统一的可观测性管理"""

    def __init__(
        self,
        logger: StructuredLogger,
        tracer: Tracer,
        metrics: MetricsCollector
    ):
        self.logger = logger
        self.tracer = tracer
        self.metrics = metrics

    async def record_tool_execution(
        self,
        tool_name: str,
        params: dict,
        result: ToolResult,
        duration_ms: float
    ) -> None:
        """记录工具执行的完整可观测性"""
        # 1. 记录日志
        await self.logger.log(
            level=LogLevel.INFO if result.status == "success" else LogLevel.ERROR,
            message=f"Tool execution completed: {tool_name}",
            tool_name=tool_name,
            status=result.status,
            duration_ms=duration_ms
        )

        # 2. 记录指标
        self.metrics.record_duration(
            f"tool_execution_time:{tool_name}",
            duration_ms
        )
        if result.status != "success":
            self.metrics.increment_counter(f"tool_execution_errors:{tool_name}")

        # 3. 追踪信息已通过span记录

3.4 安全与可观测性的协同

安全检查和可观测性监控需要紧密协作,通过统一的上下文追踪和记录所有安全相关事件:

class SecureObservableHarness:
    """整合安全性和可观测性的Harness"""

    def __init__(
        self,
        permission_manager: PermissionManager,
        audit_logger: AuditLogger,
        observability: ObservabilityManager,
        tool_registry: ToolRegistry
    ):
        self.permission_manager = permission_manager
        self.audit_logger = audit_logger
        self.observability = observability
        self.tool_registry = tool_registry

    async def execute_tool_safely(
        self,
        agent_id: str,
        tool_name: str,
        params: dict
    ) -> ToolResult:
        """安全且可观测的工具执行"""
        # 1. 权限检查
        allowed, reason = await self.permission_manager.check_permission(
            agent_id=agent_id,
            resource_id=f"tool://{tool_name}",
            action="execute"
        )

        if not allowed:
            # 记录被拒绝的尝试
            await self.audit_logger.log(AuditLog(
                timestamp=datetime.now(),
                agent_id=agent_id,
                action_type="permission_check",
                resource=f"tool://{tool_name}",
                operation="execute",
                status="denied",
                details={"reason": reason}
            ))

            return ToolResult(
                status="permission_denied",
                error=reason
            )

        # 2. 记录尝试
        await self.audit_logger.log(AuditLog(
            timestamp=datetime.now(),
            agent_id=agent_id,
            action_type="tool_execution_start",
            resource=f"tool://{tool_name}",
            operation="execute",
            status="started"
        ))

        # 3. 执行工具
        tool = self.tool_registry.get(tool_name)
        start_time = time.time()
        try:
            result = await tool.execute(**params)
            duration_ms = (time.time() - start_time) * 1000

            # 4. 记录成功
            await self.audit_logger.log(AuditLog(
                timestamp=datetime.now(),
                agent_id=agent_id,
                action_type="tool_execution_complete",
                resource=f"tool://{tool_name}",
                operation="execute",
                status="success"
            ))

            # 5. 记录可观测性数据
            await self.observability.record_tool_execution(
                tool_name=tool_name,
                params=params,
                result=result,
                duration_ms=duration_ms
            )

            return result

        except Exception as e:
            duration_ms = (time.time() - start_time) * 1000

            # 记录错误
            await self.audit_logger.log(AuditLog(
                timestamp=datetime.now(),
                agent_id=agent_id,
                action_type="tool_execution_error",
                resource=f"tool://{tool_name}",
                operation="execute",
                status="error",
                details={"error": str(e)}
            ))

            await self.observability.record_tool_execution(
                tool_name=tool_name,
                params=params,
                result=ToolResult(status="error", error=str(e)),
                duration_ms=duration_ms
            )

            raise

3.5 总结

安全层和可观测性层虽然横切于整个系统,但它们的实现:

  • 安全层:通过权限管理、沙箱隔离、审计日志,确保系统的每一个操作都在控制范围内
  • 可观测性层:通过日志、追踪、指标,确保当出现问题时能够快速定位和诊断

这两层的良好设计是生产级 Harness 系统的必要条件。

4. 层间接口设计

良好的接口设计是模块化系统的基础。本节讨论 Harness 系统中各层之间的通信接口,包括消息格式、工具调用协议、事件流规范等。

4.1 统一的消息格式

Harness 系统中流动的核心数据对象是消息(Message)。无论是智能体与 LLM 的通信,还是运行时引擎与工具层的通信,都通过结构化的消息传递。

4.1.1 消息的通用结构

消息对象是系统内部通信的基本单位,其定义如下:

from typing import Optional, Any, Literal
from datetime import datetime
from enum import Enum
import uuid

class MessageRole(str, Enum):
    """消息的角色"""
    USER = "user"
    ASSISTANT = "assistant"
    TOOL = "tool"
    SYSTEM = "system"

class MessageType(str, Enum):
    """消息的类型"""
    TEXT = "text"
    TOOL_CALL = "tool_call"
    TOOL_RESULT = "tool_result"
    FUNCTION_CALL = "function_call"
    EVENT = "event"

class Message:
    """通用消息基类"""

    def __init__(
        self,
        role: MessageRole,
        type: MessageType,
        content: str,
        message_id: str = None,
        parent_id: Optional[str] = None,
        metadata: dict = None,
        timestamp: datetime = None
    ):
        self.message_id = message_id or str(uuid.uuid4())
        self.role = role
        self.type = type
        self.content = content
        self.parent_id = parent_id
        self.metadata = metadata or {}
        self.timestamp = timestamp or datetime.now()

    def to_dict(self) -> dict:
        """转换为字典,便于序列化"""
        return {
            "message_id": self.message_id,
            "role": self.role.value,
            "type": self.type.value,
            "content": self.content,
            "parent_id": self.parent_id,
            "metadata": self.metadata,
            "timestamp": self.timestamp.isoformat()
        }

    @classmethod
    def from_dict(cls, data: dict) -> "Message":
        """从字典反序列化"""
        return cls(
            role=MessageRole(data["role"]),
            type=MessageType(data["type"]),
            content=data["content"],
            message_id=data.get("message_id"),
            parent_id=data.get("parent_id"),
            metadata=data.get("metadata", {}),
            timestamp=datetime.fromisoformat(data["timestamp"])
        )

4.1.2 工具调用消息

当智能体决定调用一个工具时,它发送一个TOOL_CALL类型的消息:

class ToolCallMessage(Message):
    """工具调用消息"""

    def __init__(
        self,
        tool_name: str,
        tool_params: dict,
        agent_id: str,
        **kwargs
    ):
        super().__init__(
            role=MessageRole.ASSISTANT,
            type=MessageType.TOOL_CALL,
            content=f"Calling tool: {tool_name}",
            metadata={
                "tool_name": tool_name,
                "tool_params": tool_params,
                "agent_id": agent_id
            },
            **kwargs
        )

    @property
    def tool_name(self) -> str:
        return self.metadata["tool_name"]

    @property
    def tool_params(self) -> dict:
        return self.metadata["tool_params"]

4.1.3 工具结果消息

当工具执行完成后,返回一个TOOL_RESULT类型的消息:

class ToolResultMessage(Message):
    """工具执行结果消息"""

    def __init__(
        self,
        tool_call_id: str,
        tool_name: str,
        status: str,
        output: Any = None,
        error: str = None,
        **kwargs
    ):
        super().__init__(
            role=MessageRole.TOOL,
            type=MessageType.TOOL_RESULT,
            content=output if isinstance(output, str) else str(output),
            parent_id=tool_call_id,
            metadata={
                "tool_name": tool_name,
                "status": status,  # "success", "error", "timeout"
                "output": output,
                "error": error
            },
            **kwargs
        )

    @property
    def status(self) -> str:
        return self.metadata["status"]

    @property
    def is_success(self) -> bool:
        return self.status == "success"

4.2 工具调用协议

工具调用协议定义了模型工具意图、运行时调度和工具执行器之间如何通信。Agent 不直接绕过运行时调用工具;运行时负责验证、权限、审计和结果回传。

4.2.1 调用请求的标准格式

Agent 输出工具调用意图时使用的请求格式定义如下:

import uuid
import json

class ToolCallRequest:
    """工具调用请求"""

    def __init__(
        self,
        tool_id: str,                 # 工具的唯一标识
        parameters: dict,              # 工具参数
        call_id: str = None,          # 调用的唯一标识
        timeout_seconds: int = 30,
        retry_count: int = 0,
        metadata: dict = None
    ):
        self.tool_id = tool_id
        self.parameters = parameters
        self.call_id = call_id or str(uuid.uuid4())
        self.timeout_seconds = timeout_seconds
        self.retry_count = retry_count
        self.metadata = metadata or {}

    def to_json(self) -> str:
        """转换为JSON格式"""
        return json.dumps({
            "tool_id": self.tool_id,
            "parameters": self.parameters,
            "call_id": self.call_id,
            "timeout_seconds": self.timeout_seconds,
            "retry_count": self.retry_count,
            "metadata": self.metadata
        })

    @classmethod
    def from_json(cls, json_str: str) -> "ToolCallRequest":
        """从JSON反序列化"""
        data = json.loads(json_str)
        return cls(
            tool_id=data["tool_id"],
            parameters=data["parameters"],
            call_id=data.get("call_id"),
            timeout_seconds=data.get("timeout_seconds", 30),
            retry_count=data.get("retry_count", 0)
        )

4.2.2 调用响应的标准格式

工具执行完毕后返回的响应格式定义如下:

from typing import Any, Literal
import json

class ToolCallResponse:
    """工具调用响应"""

    def __init__(
        self,
        call_id: str,
        status: Literal["success", "error", "timeout", "permission_denied"],
        result: Any = None,
        error_message: str = None,
        error_type: str = None,
        execution_time_ms: float = None,
        metadata: dict = None
    ):
        self.call_id = call_id
        self.status = status
        self.result = result
        self.error_message = error_message
        self.error_type = error_type
        self.execution_time_ms = execution_time_ms
        self.metadata = metadata or {}

    def to_json(self) -> str:
        """转换为JSON格式"""
        return json.dumps({
            "call_id": self.call_id,
            "status": self.status,
            "result": self.result,
            "error_message": self.error_message,
            "error_type": self.error_type,
            "execution_time_ms": self.execution_time_ms,
            "metadata": self.metadata
        }, default=str)

    @classmethod
    def from_json(cls, json_str: str) -> "ToolCallResponse":
        """从JSON反序列化"""
        data = json.loads(json_str)
        return cls(
            call_id=data["call_id"],
            status=data["status"],
            result=data.get("result"),
            error_message=data.get("error_message"),
            error_type=data.get("error_type"),
            execution_time_ms=data.get("execution_time_ms")
        )

4.3 事件流规范

Harness 系统内部通过事件流进行异步通信。这允许不同的子系统在解耦的状态下协作。

4.3.1 事件的通用结构

事件是系统内部异步通信的基本单位,其结构定义如下:

import uuid
from datetime import datetime

class Event:
    """事件基类"""

    def __init__(
        self,
        event_type: str,
        source: str,  # 事件来源,如"runtime_engine", "tool_executor"
        data: dict,
        event_id: str = None,
        timestamp: datetime = None
    ):
        self.event_id = event_id or str(uuid.uuid4())
        self.event_type = event_type
        self.source = source
        self.data = data
        self.timestamp = timestamp or datetime.now()

    def to_dict(self) -> dict:
        return {
            "event_id": self.event_id,
            "event_type": self.event_type,
            "source": self.source,
            "data": self.data,
            "timestamp": self.timestamp.isoformat()
        }

# 定义不同的事件类型
class EventType:
    """事件类型常量"""

    # 运行时引擎事件
    TASK_STARTED = "task_started"
    STEP_STARTED = "step_started"
    STEP_COMPLETED = "step_completed"
    TASK_COMPLETED = "task_completed"
    TASK_FAILED = "task_failed"

    # 工具执行事件
    TOOL_CALL_REQUESTED = "tool_call_requested"
    TOOL_CALL_APPROVED = "tool_call_approved"
    TOOL_CALL_REJECTED = "tool_call_rejected"
    TOOL_EXECUTION_STARTED = "tool_execution_started"
    TOOL_EXECUTION_SUCCEEDED = "tool_execution_succeeded"
    TOOL_EXECUTION_FAILED = "tool_execution_failed"

    # 权限事件
    PERMISSION_CHECK_PASSED = "permission_check_passed"
    PERMISSION_CHECK_FAILED = "permission_check_failed"
    APPROVAL_REQUEST_CREATED = "approval_request_created"
    APPROVAL_REQUEST_APPROVED = "approval_request_approved"
    APPROVAL_REQUEST_REJECTED = "approval_request_rejected"

    # 记忆事件
    MEMORY_UPDATED = "memory_updated"

4.3.2 事件订阅系统

事件总线实现发布-订阅模式,允许多个订阅者监听特定事件:

from typing import Callable
import asyncio

class EventBus:
    """事件总线,支持发布-订阅模式"""

    def __init__(self):
        self.subscribers: dict[str, list[Callable]] = {}
        self.event_history: list[Event] = []

    def subscribe(
        self,
        event_type: str,
        handler: Callable[[Event], None]
    ) -> str:
        """
        订阅某个事件类型。

        返回subscription_id,可用于后续取消订阅。
        """
        if event_type not in self.subscribers:
            self.subscribers[event_type] = []

        self.subscribers[event_type].append(handler)
        return f"sub_{event_type}_{len(self.subscribers[event_type])}"

    def unsubscribe(self, event_type: str, handler: Callable) -> None:
        """取消订阅"""
        if event_type in self.subscribers:
            self.subscribers[event_type].remove(handler)

    async def publish(self, event: Event) -> None:
        """发布事件"""
        # 存储到事件历史
        self.event_history.append(event)

        # 调用所有订阅者
        handlers = self.subscribers.get(event.event_type, [])
        for handler in handlers:
            try:
                if asyncio.iscoroutinefunction(handler):
                    await handler(event)
                else:
                    handler(event)
            except Exception as e:
                # 记录处理器的异常,但不中断其他处理器
                print(f"Error in event handler: {e}")

    def get_event_history(
        self,
        event_type: str = None,
        source: str = None,
        limit: int = 100
    ) -> list[Event]:
        """查询事件历史"""
        results = self.event_history
        if event_type:
            results = [e for e in results if e.event_type == event_type]
        if source:
            results = [e for e in results if e.source == source]
        return results[-limit:]

4.3.3 事件驱动的架构示例

以下是一个完整的事件驱动架构使用示例,展示了多个子系统如何通过事件进行协作:

class EventDrivenRuntime:
    """事件驱动的运行时引擎"""

    def __init__(self, event_bus: EventBus):
        self.event_bus = event_bus

        # 订阅权限事件
        self.event_bus.subscribe(
            EventType.APPROVAL_REQUEST_APPROVED,
            self._on_approval_approved
        )

        # 订阅工具执行事件
        self.event_bus.subscribe(
            EventType.TOOL_EXECUTION_SUCCEEDED,
            self._on_tool_succeeded
        )

    async def execute_task(self, task: Task) -> None:
        """执行任务"""
        # 发布任务开始事件
        await self.event_bus.publish(Event(
            event_type=EventType.TASK_STARTED,
            source="runtime_engine",
            data={"task_id": task.id, "task_description": task.description}
        ))

        # ... 执行任务 ...

        # 发布任务完成事件
        await self.event_bus.publish(Event(
            event_type=EventType.TASK_COMPLETED,
            source="runtime_engine",
            data={"task_id": task.id, "result": "success"}
        ))

    async def _on_approval_approved(self, event: Event) -> None:
        """处理审批批准事件"""
        tool_call_id = event.data["tool_call_id"]
        # 继续执行被暂停的工具调用
        ...

    async def _on_tool_succeeded(self, event: Event) -> None:
        """处理工具执行成功事件"""
        # 更新内部状态
        ...

4.4 三大参考系统的接口对比

三个参考系统在接口设计上体现了不同语言和架构范式的取舍。

4.4.1 OpenAI Codex 的接口设计

Codex 使用 Rust 的 trait 和 enum 定义接口,通过类型系统在编译期保证安全性:

/// 执行策略的决策结果
enum Decision {
    Allow,                    // 直接执行
    Prompt(String),           // 需要用户审批,附带理由
    Forbidden(String),        // 禁止执行
}

/// 技能(Skill)的标准元数据
struct SkillMetadata {
    name: String,
    description: String,
    instructions: String,
    dependencies: Vec<String>,
}

/// 沙箱执行的统一接口
trait SandboxExecutor {
    fn execute(&self, command: &str, working_dir: &Path) -> Result<Output>;
    fn is_available(&self) -> bool;
}

特点:

  • Rust 的所有权系统和 Result 类型保证内存安全和错误处理的完备性
  • execpolicy 通过 Starlark DSL 定义规则,与 Rust 核心解耦
  • MCP 工具和内置技能共享统一的调用接口

4.4.2 Claude Code 的接口设计

Claude Code 使用 TypeScript 的类型系统定义接口,确保编译时的类型安全:

interface SDKMessage {
  id: string;
  role: "user" | "assistant" | "tool";
  content: string | ContentBlock[];
  metadata?: Record<string, any>;
}

interface ToolUseBlock {
  type: "tool_use";
  id: string;
  name: string;
  input: Record<string, any>;
}

interface ToolResultBlock {
  type: "tool_result";
  tool_use_id: string;
  content: string | Record<string, any>;
  is_error?: boolean;
}

特点:

  • 类型安全的接口定义
  • 支持流式处理(streaming)
  • 灵活的内容块(ContentBlock)设计

4.4.3 OpenClaw 的接口设计

OpenClaw 使用 WebSocket 协议进行通信,消息格式基于帧(Frame)。官方协议只定义了三种 RPC 风格的帧类型:

WebSocket 帧结构(官方协议):

帧类型 结构 用途
Request {type:"req", id, method, params} 客户端发起操作
Response {type:"res", id, ok, payload\|error} 服务端确认请求结果
Event {type:"event", event, payload, seq?, stateVersion?} 服务端异步推送(心跳即属于事件族)

特点:

  • 长连接保证高实时性
  • 极简的 req/res/event 三类帧覆盖所有通信场景,具体语义由 method/event 字段表达
  • Heartbeat 以事件形式推送,支持长期运行的 Agent

4.5 版本控制和向后兼容性

当 Harness 系统演进时,接口的向后兼容性变得重要。

class VersionedMessage:
    """带版本的消息,支持向后兼容"""

    VERSION = "1.0"

    def __init__(
        self,
        role: MessageRole,
        type: MessageType,
        content: str,
        version: str = None,
        **kwargs
    ):
        self.version = version or self.VERSION
        # ... 其他初始化 ...

    @classmethod
    def from_dict(cls, data: dict) -> "VersionedMessage":
        """从字典反序列化,处理版本兼容性"""
        version = data.get("version", "1.0")

        if version == "1.0":
            return cls._from_v1_0(data)
        elif version == "2.0":
            return cls._from_v2_0(data)
        else:
            raise ValueError(f"Unsupported message version: {version}")

    @classmethod
    def _from_v1_0(cls, data: dict) -> "VersionedMessage":
        """从v1.0格式反序列化"""
        # 处理v1.0特定的字段和格式
        return cls(
            role=MessageRole(data.get("role")),
            type=MessageType(data.get("type")),
            content=data.get("content"),
            version="1.0"
        )

    @classmethod
    def _from_v2_0(cls, data: dict) -> "VersionedMessage":
        """从v2.0格式反序列化"""
        # 处理v2.0特定的字段和格式
        return cls(
            role=MessageRole(data.get("role")),
            type=MessageType(data.get("type")),
            content=data.get("content"),
            version="2.0"
        )

4.6 总结

良好的接口设计包括:

  1. 统一的消息格式:所有层间通信都使用结构化的消息
  2. 工具调用协议:标准化的请求和响应格式
  3. 事件流规范:异步通信的统一事件定义
  4. 版本控制:为系统演进预留向后兼容的机制

这些接口的清晰定义,使得 Harness 系统的各个组件可以独立开发和测试,同时保持整体的一致性。

5. MiniHarness 脚手架搭建

本节将前两节的架构设计落地为可运行的代码骨架。完成后,MiniHarness 项目将具备完整的目录结构、依赖配置和核心接口定义,为后续章节的增量开发打下基础。

完整代码见 lab/ 目录,可直接运行。本节聚焦设计决策和关键接口,不重复贴出全部源码。

5.1 项目结构设计

MiniHarness 的目录结构遵循“按子系统分包”的原则,每个包对应一个 Harness 子系统:

目录 对应子系统 职责 首次实现章节
core/ 公共基础 消息、工具、智能体、事件的接口定义 本章
runtime/ 运行时引擎 智能体循环、流式处理、事件驱动 第 4 章
tools/ 工具层 工具注册、执行流水线、内置工具 第 5 章
memory/ 记忆子系统 存储、上下文组装、记忆整合 第 6 章
models/ 模型集成 Provider 抽象、输出解析、质量门控 第 7 章
orchestration/ 编排引擎 任务分解、状态机、多智能体协调 第 8 章
mcp/ MCP 集成 MCP 客户端、工具发现、协议适配 第 9 章
reliability/ 可靠性与可观测性 日志、追踪、监控指标、容错机制 第 11 章
security/ 安全防护 权限管理、路径校验、护栏、安全执行 第 12 章
utils/ 工具类 配置管理等辅助模块 本章

项目还包含 tests/(按 unit/integration 分层)和 examples/(使用示例)。

5.2 依赖配置

MiniHarness 使用 pyproject.toml 管理项目元数据和依赖(见 lab/pyproject.toml)。当前核心依赖保持在六个小型库:

依赖 用途
anthropic Claude API 客户端
openai OpenAI API 客户端
pydantic 数据校验与序列化
httpx 异步 HTTP 客户端(MCP 传输等)
python-dotenv 环境变量管理
pyyaml YAML 配置解析

开发依赖包括 pytest(测试)、black/isort(格式化)、mypy(类型检查)、pylint(代码分析)。

这种最小依赖策略是有意为之的——作为教学项目,MiniHarness 避免引入重型框架(如 LangChain、LlamaIndex),让读者能看清 Harness 的每一层是如何从零构建的。

5.3 核心接口定义

core/ 包定义了四组基础接口,它们是整个系统的公共语言。

消息系统(core/message.py)

消息是 Harness 中所有组件之间通信的载体。设计要点:

  • 角色枚举 (MessageRole):user / assistant / tool / system,与 LLM API 的角色模型对齐
  • 类型枚举 (MessageType):text / tool_call / tool_result / event,区分不同用途的消息
  • 通用消息类 (Message):包含 role、type、content 三个核心字段,加上 message_id(唯一标识)、parent_id(消息链追踪)、metadata(扩展信息)
  • 特化子类:ToolCallMessage 和 ToolResultMessage 通过 metadata 字段提供便捷属性访问
# 核心消息结构(完整代码见 lab/mini_harness/core/message.py)
@dataclass
class Message:
    role: MessageRole
    type: MessageType
    content: str
    message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    parent_id: Optional[str] = None
    metadata: Dict[str, Any] = field(default_factory=dict)
    timestamp: datetime = field(default_factory=datetime.now)

选择 dataclass 而非 Pydantic 的 BaseModel,是因为核心消息需要轻量和灵活——在 智能体循环中每秒可能创建数百个 Message 实例,dataclass 的开销更小。

工具接口(core/tool.py)

工具层的接口设计遵循“定义与实现分离”的原则:

  • ToolInputSchema:描述工具参数的 JSON Schema,与 LLM 的 function calling 格式对齐
  • ToolDefinition:工具的元数据(名称、描述、参数 schema、权限要求、超时时间)
  • Tool(ABC):抽象基类,子类只需实现 execute() 方法

这种分离使得工具注册表可以只持有 ToolDefinition(用于向 LLM 描述工具),而不需要加载工具的实际实现——这正是第 5 章将实现的延迟加载机制的基础。

智能体接口(core/agent.py)

智能体接口定义了状态机模型:IDLE → INITIALIZING → EXECUTING → COMPLETED / FAILED,以及 PAUSED 状态用于支持人机协同。ExecutionResult 封装了执行结果,包括状态、输出和错误信息。

事件系统(core/event.py)

事件系统为可观测性提供基础。EventType 枚举覆盖三类事件:任务生命周期(started/completed/failed)、步骤生命周期、工具执行生命周期,以及权限检查事件。

5.4 工具注册表

工具注册表(tools/registry.py)是工具层的核心数据结构,负责管理所有可用工具的注册、查找和 Schema 缓存。

# 注册表核心接口(完整代码见 lab/mini_harness/tools/registry.py)
class ToolRegistry:
    def register(self, tool: Tool): ...      # 注册工具并缓存 Schema
    def get(self, name: str) -> Tool: ...    # 按名称获取工具
    def list_tools(self) -> List[Dict]: ...  # 返回所有工具的 Schema(供 LLM 使用)

注册表在注册时就缓存好工具的 Schema,避免每次 LLM 调用都重新生成——这个看似微小的优化在高频调用场景下非常重要。

5.5 配置管理

配置管理模块(utils/config.py)使用单例模式,从 .env 文件和环境变量加载配置。关键配置项包括 API 密钥、模型选择、最大步数和执行超时时间。

项目根目录的 .env.example 提供了配置模板,开发者只需复制并填入实际值即可开始使用。

5.6 验证脚手架

搭建完成后,运行测试来验证基础设施:

cd lab
pip install -e ".[dev]"
pytest -v

测试用例覆盖消息创建与序列化、工具定义与注册、智能体创建等基础功能(见 lab/tests/unit/test_core.py 和 lab/tests/unit/test_tools.py)。

5.7 下一步

至此,MiniHarness 已有了坚实的骨架:

  • 公共语言 (core/):消息、工具、智能体、事件的统一接口
  • 注册机制 (tools/):工具的注册与发现
  • 配置基础 (utils/):环境变量驱动的配置管理
  • 测试框架 (tests/):pytest + pytest-asyncio 就绪

从第 4 章开始,我们将在这个骨架上逐章添加子系统:运行时引擎 → 工具层 → 记忆子系统 → 输出治理 → 编排引擎 → MCP 集成 → 生产化加固 → 可靠性保障 → 安全层 → 测试验收。

6. 本章小结

本章系统地阐述了 Harness 的整体架构设计,包括三层+横切关注点的参考架构、核心组件的深入设计、层间接口规范和参考实现对比。以下是主要内容的总结。

6.1 核心架构模型

三层 + 横切关注点参考架构

Harness 系统采用三层结构加横切关注点的方式组织,清晰地分离了不同的关注点:

graph TB
    A["<b>接入层</b><br/>(与用户/系统的接口、协议适配)"]
    B["<b>编排层(可选)</b><br/>(多智能体编排和任务分解)"]
    C["<b>智能体核心层</b><br/>(单智能体执行的核心工作区)"]
    D["<b>运行时引擎</b><br/>(循环中枢)"]
    E["模型集成与输出治理"]
    F["工具层"]
    G["记忆子系统"]
    H["横切关注点:安全 / 可观测性 / 存储"]

    A --> B
    B --> C
    C --> D
    C --> E
    C --> F
    C --> G

    D -.-> H
    E -.-> H
    F -.-> H
    G -.-> H

    style C fill:#e8f4f8
    style D fill:#fff4e6
    style E fill:#fff4e6
    style F fill:#fff4e6
    style G fill:#fff4e6
    style H fill:#f0f0f0

图 2-5:Harness 三层+横切关注点架构总览

这个架构与第一章的星型子系统模型一致:模型调用、工具执行、记忆更新在运行时引擎的同一循环中交替发生,因此归属同一层(智能体核心层),而非人为分到不同层级。

6.2 智能体核心层的深入设计

智能体核心层是 Harness 系统的核心,包含四个紧密协作的子系统(运行时引擎、模型集成与输出治理、工具层、记忆子系统):

运行时引擎

实现智能体的执行循环:

  1. 感知:从记忆中检索相关信息
  2. 推理:调用 LLM 进行思考
  3. 决策:提取工具调用意图
  4. 执行:通过工具层执行操作
  5. 学习:更新记忆和状态

关键设计模式:状态机管理、异步协调、超时控制

工具层

提供安全、标准化的外部系统访问:

  • 工具注册表:管理所有可用工具
  • 参数验证:确保格式和类型正确
  • 权限检查:验证执行权限
  • 隔离执行:在沙箱中运行,防止副作用
  • 结果标准化:统一的输出格式

记忆子系统

按生命周期支持分层记忆结构:

  • 工作记忆:当前对话的上下文窗口(LLM 上下文)
  • 短期记忆:会话级的摘要和临时状态(内存或文件)
  • 长期记忆:跨会话的持久化知识(数据库),借助向量索引支持语义检索

这三层记忆的协作,使得智能体能够跨越时间维度学习和改进。

6.3 安全与可观测性

两个横切的关注点贯穿整个系统:

安全层的关键要素

  1. 权限管理:梯度化的权限模型(Free → Ask-first → Approve-once)
  2. 沙箱隔离:进程级、容器级或 VM 级隔离
  3. 审计日志:记录所有关键操作
  4. 输入验证:防止注入攻击

可观测性层的三个支柱

  1. 日志(Logs):结构化、可搜索的事件记录
  2. 追踪(Traces):分布式追踪,追踪请求的完整路径
  3. 指标(Metrics):性能指标和趋势分析

6.4 层间接口规范

清晰的接口是模块化系统的基础:

统一的消息格式

所有层间通信都使用结构化的 Message 对象:

  • 消息基类
  • ToolCallMessage(工具调用)
  • ToolResultMessage(工具结果)
  • TextMessage(文本信息)

工具调用协议

标准化的请求-响应模式:

flowchart TD
    A["ToolCallRequest"] -->|执行| B["ToolCallResponse"]

    style A fill:#e3f2fd
    style B fill:#e8f5e9

图 2-6:工具调用的请求-响应模式

事件流规范

事件总线实现发布-订阅模式,支持异步通信和解耦。

6.5 参考实现的对比

OpenAI Codex

  • 语言:Rust(核心语言,具体比例以官方代码库统计为准),60+ Cargo crate 模块化
  • 安全:execpolicy 策略引擎 + 平台原生沙箱双层防御
  • 上下文:提示缓存 + 异步压缩,线性成本增长
  • 编排:Subagent 层级委派,支持 3+ 层深度

优势:系统级性能和安全保障,适合对延迟和隔离要求高的场景

Claude Code

  • 语言:TypeScript(~50 万行),类型安全
  • 架构:模块化设计,40+ 特性门控
  • 编排:Coordinator 动态多智能体
  • 集成:24+ 内置工具 + MCP 扩展

优势:响应快速,类型安全,易于集成

OpenClaw

  • 通信:WebSocket 长连接 + 心跳机制
  • 架构:Gateway WebSocket 作为单一控制平面 + 节点传输
  • 工作流:Lobster 确定性引擎
  • 记忆:MEMORY.md + 每日记忆文件(SOUL.md 承担行为约束)

优势:支持长期自主运行,适合持久化应用

6.6 MiniHarness 脚手架

本章完成了 MiniHarness 项目的初始化:

文件结构

MiniHarness 项目的完整文件结构如下所示:

  • lab/
  • pyproject.toml - 项目配置和依赖
  • mini_harness/
    • core/ - 核心接口定义
    • message.py
    • tool.py
    • agent.py
    • event.py
    • tools/ - 工具注册表
    • registry.py
    • utils/ - 工具函数
    • config.py
  • tests/unit/ - 测试套件
    • test_core.py
    • test_tools.py

核心接口已定义

  • Message:统一的消息类型
  • Tool/ToolDefinition:工具的抽象
  • Agent:智能体的配置
  • Event:事件定义
  • ToolRegistry:工具管理

可运行的代码

已实现完整的类型定义、序列化、注册表逻辑,并提供了基本的单元测试。

6.7 与其他章节的关系

各个章节之间的联系和依赖关系如下所示:

flowchart TD
    A["第1章(概论)"] -->|定义基本概念| B["第2章(架构全景)← 你在这里"]
    B -->|定义设计原则| C["第3章(设计原则)"]
    C -->|实现权限和验证| D["第4-8章(子系统深入)"]
    D -->|实现每个层的详细功能| E["第9-14章(高级主题)"]
    E -->|生产部署、可靠性、安全加固| F["完成"]

    style A fill:#e8f5e9
    style B fill:#fff9c4,stroke:#ffb74d,stroke-width:2px
    style C fill:#e8f5e9
    style D fill:#e3f2fd
    style E fill:#f3e5f5

图 2-7:Harness 工程指南全书的章节结构与学习路径

6.8 关键学习点总结

概念 说明 实践体现
分层架构 三层 + 横切关注点,明确职责 接入层-编排层-智能体核心层 + 安全/可观测性/存储
执行循环 智能体的核心是感知-推理-决策-执行的循环 RuntimeEngine 的步骤实现
工具抽象 统一各种外部系统的接口 ToolRegistry 和 Tool 基类
三层记忆 工作+短期+长期(向量检索作语义索引),支持学习 MemoryManager 的设计
权限管理 梯度化信任,从 Free 到 Approve-once PermissionManager 和 AuditLog
可观测性 日志+追踪+指标 Logger, Tracer, MetricsCollector
接口规范 清晰的消息格式和协议 Message, ToolCallRequest/Response

6.9 常见设计问题与答案

Q: 为什么要有编排层?

A: 当任务过于复杂,无法由单个 Agent 完成时,编排层负责分解任务、分配给专门的 Agent、汇总结果。即使对于简单系统,编排层也可以很轻薄。

Q: 为什么需要三层记忆?

A: 不同的使用场景对记忆的要求不同。工作记忆快速访问当前上下文,短期记忆保留会话级的摘要和状态,长期记忆持久化跨会话的知识并借助向量索引支持语义检索。三层按生命周期递进,形成完整的记忆体系。

Q: 工具层的权限检查和安全层的权限检查重复了吗?

A: 不应各自维护独立政策。安全层提供统一的策略决策服务;工具层负责本地前置条件和参数能力检查,输出治理负责在执行前消费同一策略决策。这样可以同时保留细粒度控制和系统级一致性。

Q: 为什么需要事件总线?

A: 事件总线实现了不同子系统的解耦。监控系统可以订阅所有事件来进行可观测性,权限系统可以订阅工具执行事件,而不需要运行时引擎直接依赖它们。

6.10 后续学习路径

  1. 第 3 章:深入学习设计原则,特别是“约束优先”和“渐进信任”
  2. 第 4 章:实现完整的运行时引擎,包括错误处理和重试策略
  3. 第 5 章:实现各种工具的适配器,学习如何集成真实系统
  4. 第 6 章:实现记忆系统,包括向量数据库集成

6.11 代码审查清单

完成本章的脚手架搭建后,检查以下内容:

  • 项目目录结构完整
  • pyproject.toml 依赖配置正确
  • 核心接口(message, tool, agent, event)已定义
  • 工具注册表实现正确
  • 所有单元测试通过
  • 代码风格一致(Black 格式化)
  • 类型提示完整(mypy 检查)
  • 文档注释充分

完成这些检查后,MiniHarness 项目将为后续的功能开发提供坚实的基础。