Back to Blog
· 6 min read

AI Agent Harness Engineering 学习笔记

AI

Harness(测试台/控制系统)这个概念来自传统软件测试领域 —— 测试需要受控环境、规范的输入输出、以及可量化的评估标准。AI Agent Harness 就是让 AI Agent 在这个受控框架下运行,实现:

  • 可控性 - 约束 Agent 的行为边界
  • 可观测性 - 追踪 Agent 的思考和行动
  • 可评估性 - 量化 Agent 的输出质量

为什么 AI Agent 需要 Harness

直接 prompt AI 的问题在于「黑盒」—— 你不知道它会做什么、怎么做、做得怎么样。Harness 就像给 Agent 套上缰绳,让它在你设计的轨道上运行。

graph LR
    A[用户请求] --> B[Prompt Only<br/>黑盒模式]
    A --> C[Harness Framework<br/>白盒模式]
    B --> D[不可预测输出]
    C --> E[可追踪轨迹]
    C --> F[可量化评估]
    C --> G[可干预控制]

核心约束

因素描述关键问题
Action SpaceAgent 能执行的动作集合动作是否覆盖所有必要行为?
ObservationAgent 能感知的信息输出是否足够可解析?
Recovery错误处理与恢复能力失败后能否自我纠正?
Context Budget上下文资源管理是否有效利用 token?

Action Space Design

Action Space 是 Agent 所有可能动作的集合。每个动作代表 Agent 与世界交互的一种方式。

// Action Space 示例
interface Action {
  name: string;           // 稳定、显式的动作名称
  description: string;     // 清晰的意图描述
  inputSchema: z.ZodType;  // 输入参数校验
  outputSchema: z.ZodType; // 输出格式定义
}

设计原则

1. 穷举性(Exhaustiveness) 覆盖所有 Agent 可能需要执行的行为,没有遗漏。

2. 互斥性(Mutual Exclusiveness) 避免动作之间的语义重叠,减少 Agent 选择的困惑。

3. 可组合性(Composability) 简单动作可以组合成复杂行为。

// ❌ 反模式:语义重叠
const actions_bad = [
  { name: "delete_file", description: "删除文件" },
  { name: "remove_file", description: "移除文件" },  // 与上面重复!
  { name: "rm_file", description: "删除文件" },      // 又一个重复!
];

// ✅ 正确:互斥且清晰
const actions_good = [
  { name: "delete_file", description: "永久删除文件" },
  { name: "move_file", description: "移动文件到指定位置" },
  { name: "read_file", description: "读取文件内容" },
];

粒度规则(Granularity)

根据任务风险和频率选择合适的动作粒度:

粒度适用场景示例
Micro-Tools高风险操作(部署、权限、迁移)execute_deployment, grant_permission
Medium-Tools常见编辑/读取/搜索循环read_file, search_code, edit_block
Macro-Tools往返开销是主要成本时refactor_component, write_tests

Tool Definition

标准接口设计

每个 Tool 都应该有清晰的接口定义:

import { z } from "zod";

// Tool 定义示例
const ReadFileTool = {
  name: "read_file",
  description: "读取指定路径的文件内容",
  inputSchema: z.object({
    path: z.string().describe("文件路径"),
    offset: z.number().optional().describe("读取偏移量"),
    limit: z.number().optional().describe("读取字节数"),
  }),
  outputSchema: z.object({
    content: z.string(),
    bytesRead: z.number(),
    status: z.enum(["success", "partial", "error"]),
  }),
};

观察设计(Observation Design)

每个 Tool 响应应该包含标准化字段,帮助 Agent 理解结果并决定下一步:

interface ToolResponse<T> {
  status: "success" | "warning" | "error";
  summary: string;           // 一句话结果总结
  data: T;                   // 实际数据
  artifacts?: string[];       // 生成的文件路径
  next_actions?: string[];    // 建议的后续动作
  recovery_hint?: string;     // 错误时的恢复提示
}

错误恢复契约

对于每个错误路径,必须提供:

// ❌ 反模式:只返回错误
{
  "error": "File not found"
}

// ✅ 正确:包含恢复信息
{
  "status": "error",
  "summary": "文件不存在",
  "error": {
    "code": "ENOENT",
    "path": "/tmp/nonexistent.txt",
    "root_cause": "路径不存在或无权访问",
    "recovery_hint": "请检查路径是否正确,或使用 search_files 搜索文件位置",
    "safe_retry": true,
    "stop_condition": "重试3次后仍失败则停止"
  }
}

Evaluation Metrics

评估 Agent 的难点

  • 多路径性 - 同一个目标有多种实现方式
  • 主观性 - 某些输出质量难以量化
  • 长依赖 - 早期决策影响后期结果
  • 成本 - 每次评估都可能产生高额 API 费用

关键指标

指标描述计算方式
Completion Rate任务完成率成功任务数 / 总任务数
Retries Per Task平均重试次数总重试数 / 任务数
Pass@1一次通过率首次成功数 / 总任务数
Pass@3三次尝试通过率三次内成功数 / 总任务数
Cost Per Success每次成功成本总花费 / 成功任务数

Benchmark 设计

interface Benchmark {
  name: string;
  tasks: Task[];
  metrics: Metric[];
  constraints: {
    maxTurns: number;        // 最大交互轮次
    maxTokens: number;        // 最大 token 消耗
    timeoutMs: number;        // 超时时间
  };
}

interface Task {
  id: string;
  prompt: string;
  expectedOutcome: string;
  evaluationCriteria: EvaluationCriterion[];
}

构建一个 Mini Agent Harness Demo

架构概览

graph TD
    subgraph "Agent Core"
        P[Planner<br/>ReAct Loop]
        A[Action Selector]
    end

    subgraph "Harness Layer"
        AS[Action Space]
        TR[Tool Registry]
        ER[Evaluation Runner]
    end

    subgraph "Execution Env"
        FS[File System]
        WS[Web Search]
        SE[Shell]
    end

    P --> A
    A --> AS
    AS --> TR
    TR --> FS
    TR --> WS
    TR --> SE

    ER --> AS
    ER --> TR

核心代码

// 1. 定义 Action Space
const FileActionSpace = {
  actions: [
    {
      name: "read_file",
      description: "读取文件内容",
      inputSchema: z.object({
        path: z.string(),
        encoding: z.enum(["utf-8", "base64"]).default("utf-8"),
      }),
    },
    {
      name: "write_file",
      description: "写入文件内容",
      inputSchema: z.object({
        path: z.string(),
        content: z.string(),
        mode: z.enum(["overwrite", "append"]).default("overwrite"),
      }),
    },
    {
      name: "list_directory",
      description: "列出目录内容",
      inputSchema: z.object({
        path: z.string(),
        recursive: z.boolean().default(false),
      }),
    },
  ],

  // 验证动作空间完整性
  validate(): boolean {
    const names = this.actions.map((a) => a.name);
    return new Set(names).size === names.length; // 检查互斥性
  },
};

// 2. Tool Registry
class ToolRegistry {
  private tools: Map<string, Tool> = new Map();

  register(tool: Tool): void {
    if (this.tools.has(tool.name)) {
      throw new Error(`Tool ${tool.name} already registered`);
    }
    this.tools.set(tool.name, tool);
  }

  get(name: string): Tool | undefined {
    return this.tools.get(name);
  }

  list(): Tool[] {
    return Array.from(this.tools.values());
  }
}

// 3. Evaluation Runner
class EvaluationRunner {
  constructor(
    private registry: ToolRegistry,
    private actionSpace: ActionSpace
  ) {}

  async run(task: Task): Promise<EvaluationResult> {
    const startTime = Date.now();
    const trace: Action[] = [];
    let turns = 0;

    while (turns < task.constraints.maxTurns) {
      const context = await this.buildContext(trace);
      const action = await this.selectAction(task, context);

      if (!action) break; // 无可用动作

      const result = await this.executeAction(action);
      trace.push({ action, result, timestamp: Date.now() });

      if (this.isComplete(task, trace)) {
        return this.evaluate(trace, task, startTime);
      }

      turns++;
    }

    return this.evaluate(trace, task, startTime, { reason: "max_turns" });
  }
}

运行示例

// 创建 Harness
const registry = new ToolRegistry();
registry.register(ReadFileTool);
registry.register(WriteFileTool);
registry.register(ListDirectoryTool);

const harness = new EvaluationRunner(registry, FileActionSpace);

// 定义任务
const task: Task = {
  id: "read-config-01",
  prompt: "读取 /app/config.json 并告诉我数据库连接字符串",
  expectedOutcome: "返回 config.json 中的 db.connection 字段值",
  constraints: {
    maxTurns: 5,
    maxTokens: 4000,
    timeoutMs: 30000,
  },
  evaluationCriteria: [
    { type: "contains", value: "connection" },
    { type: "no_sensitive_data", pattern: /password|secret|key/i },
  ],
};

// 运行评估
const result = await harness.run(task);
console.log(result);
// {
//   passed: true,
//   metrics: {
//     completionRate: 1,
//     turns: 1,
//     tokensUsed: 512,
//     latencyMs: 234,
//   },
//   trace: [...]
// }

架构选择

模式适用场景优点缺点
ReAct探索性任务,路径不确定灵活、可解释轮次多、成本高
Function Calling结构化确定性流程精准、可控灵活性低
Hybrid(推荐)ReAct 规划 + typed 执行平衡实现复杂度中等

常见反模式

反模式问题解决方案
动作语义重叠Agent 困惑、选择困难互斥性检查、重构
工具输出不透明无法恢复、无法追踪标准化响应格式
只返回错误Agent 不知道如何修复提供 recovery_hint
上下文过载无关引用淹没关键信息精简 system prompt、引用文件而非内联

总结

AI Agent Harness 的核心要点:

  1. Action Space 要做到穷举、互斥、可组合
  2. Tool Definition 要 schema-first,输出要包含 next_actions
  3. Error Recovery 每个错误路径都要有 recovery_hint
  4. Evaluation 量化四大指标:完成率、重试次数、通过率、成本

参考