加护栏与审批门(HITL)
目录
目标
在 01-最小骨架 上增加生产 Harness 的三项能力:
- 工具分级:只读自动 / 写操作需审批(HITL)
- 重复调用检测:同 tool+args 连续 2 次则阻断
- 输入与步数护栏:消息长度、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 |
| 同一查询触发两次相同 getWeather | duplicate 日志 |
| 超长粘贴 | 400 input_too_long |
面试怎么说 HITL
写操作工具在 Harness 网关标记为 L2+,execute 只创建 approval ticket,真正副作用在用户确认后的独立 API 执行,并写 audit log。模型不能跳过审批,因为 execute 路径里根本没有真实写逻辑。