跳到主要内容

加护栏与审批门(HITL)

目录

目标

01-最小骨架 上增加生产 Harness 的三项能力:

  1. 工具分级:只读自动 / 写操作需审批(HITL)
  2. 重复调用检测:同 tool+args 连续 2 次则阻断
  3. 输入与步数护栏:消息长度、maxSteps 触顶时的 system 注入

对应 03-生产Harness七大模式 模式 ①③④⑦。

工具分级模型

// lib/harness/tool-policy.ts
export type ToolRisk = "read" | "write";

export const TOOL_RISK: Record<string, ToolRisk> = {
getWeather: "read",
sendEmail: "write",
updateProfile: "write",
};

写操作 不直接在 execute 里执行,而是返回 pendingApproval 状态,由前端展示审批 UI 后再二次请求。

审批流设计

内存审批表(演示用)

// lib/harness/approvals.ts
const pending = new Map<
string,
{ toolName: string; args: unknown; traceId: string }
>();

export function createApproval(
toolName: string,
args: unknown,
traceId: string
) {
const id = crypto.randomUUID();
pending.set(id, { toolName, args, traceId });
return id;
}

export function consumeApproval(id: string) {
const item = pending.get(id);
pending.delete(id);
return item;
}

生产应落 Redis + TTL(如 5 分钟),并绑 userId

带 HITL 的 tool 包装

// lib/harness/tools.ts 片段
import { TOOL_RISK } from "./tool-policy";
import { createApproval } from "./approvals";

function wrapWithPolicy(
traceId: string,
name: string,
execute: (args: unknown) => Promise<unknown>
) {
return async (args: unknown) => {
if (TOOL_RISK[name] === "write") {
const approvalId = createApproval(name, args, traceId);
return {
status: "pendingApproval",
approvalId,
message: `操作 ${name} 需确认`,
};
}
return execute(args);
};
}

模型收到 pendingApproval 后应转述用户「请确认是否发送」;前端检测 tool result 里的 approvalId 渲染 确认 / 拒绝 按钮。

审批 API

// app/api/agent/approve/route.ts
import { NextResponse } from "next/server";
import { consumeApproval } from "@/lib/harness/approvals";
import { runToolByName } from "@/lib/harness/tools";

export async function POST(req: Request) {
const { approvalId } = await req.json();
const item = consumeApproval(approvalId);
if (!item) {
return NextResponse.json({ error: "expired" }, { status: 410 });
}
const result = await runToolByName(item.toolName, item.args, item.traceId);
return NextResponse.json({ result });
}

用户确认后,前端把 执行结果 作为 tool message 续聊,或走单独「继续 agent run」接口——最小项目可用 简化版:确认后直接展示结果,不强制再进 loop。

重复调用检测

// lib/harness/loop-guard.ts
import { createHash } from "crypto";

let lastKey = "";

export function checkDuplicateTool(toolName: string, args: unknown): boolean {
const key = createHash("sha256")
.update(toolName + JSON.stringify(args))
.digest("hex");
const dup = key === lastKey;
lastKey = key;
return dup;
}

onStepFinish 里:

onStepFinish: ({ toolCalls }) => {
for (const tc of toolCalls ?? []) {
if (checkDuplicateTool(tc.toolName, tc.args)) {
logSpan(traceId, "guard.duplicate_tool", { tool: tc.toolName });
// 生产:抛错终止或注入 system 警告
}
}
},

输入护栏

const MAX_USER_CHARS = 4000;

export function validateInput(messages: { content: string }[]) {
const last = messages.at(-1)?.content ?? "";
if (last.length > MAX_USER_CHARS) {
throw new Response(JSON.stringify({ error: "input_too_long" }), {
status: 400,
});
}
}

POST 入口最先调用。

步数触顶

maxSteps 用尽,SDK 会结束 run。Harness 可再加:

system: `${baseSystem}\n若即将达到步数上限,必须给出简短最终答复,禁止再调用工具。`,

自测清单

用例预期
查天气无审批,直接返回
「给 x@y.com 发推广邮件」返回 pendingApproval
拒绝审批不执行 sendEmail
同一查询触发两次相同 getWeatherduplicate 日志
超长粘贴400 input_too_long

面试怎么说 HITL

写操作工具在 Harness 网关标记为 L2+,execute 只创建 approval ticket,真正副作用在用户确认后的独立 API 执行,并写 audit log。模型不能跳过审批,因为 execute 路径里根本没有真实写逻辑。

下一步

03-加 Eval Harness 回归

延伸阅读