· 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 Space | Agent 能执行的动作集合 | 动作是否覆盖所有必要行为? |
| Observation | Agent 能感知的信息 | 输出是否足够可解析? |
| 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 的核心要点:
- Action Space 要做到穷举、互斥、可组合
- Tool Definition 要 schema-first,输出要包含 next_actions
- Error Recovery 每个错误路径都要有 recovery_hint
- Evaluation 量化四大指标:完成率、重试次数、通过率、成本