AI 代理的质量取决于外骨骼:工具契约、权限与错误分类法

·阅读约15分钟·Evergreen Tools Team
AI agent harness architecture

💡 工具推荐编写工具契约与错误分类时,试试 Evergreen Tools 的 JSON格式化工具, API测试工具, 正则可视化工具

The New Stack 在 2026 年 8 月 30 日发表了一篇非常务实的文章:《你的 AI 代理质量取决于包裹它的外骨骼》。开篇就是一句大实话:大多数代理项目比演示要难得多。演示里一切都顺利——用户问了和演示几乎一样的问题,工具返回成功,策略没变。然后真实用户来了:问题措辞稍有不同,账户记录不完整,工具返回错误,上周刚改了策略,或者代理撞上了能力边界——它能读发票,但不能改发票。真正的工程工作从这里才开始。文章的核心论断:「模型只是服务的一部分,外骨骼才是其余部分——应用围绕模型搭建的脚手架,负责喂给它正确的输入、检查它的输出,在故障扩散之前抓住它们。」

1. 为什么演示魔法活不到生产

开发者早就熟悉测试外骨骼(test harness)的概念:把代码包起来,在受控条件下运行。生产级代理需要同样的包装。模型输出得好,并不能证明代理准备好干真活了——证明这一点是外骨骼的职责:限制错误调用能造成的伤害的工具契约、即使在指令试图绕过时也在模型之外强制执行的权限、团队真正能检查的上下文路径和追踪记录,以及从用户会先踩到的失败中构建出来的测试。文章说得直白:「把这些做对了,演示魔法就能扛住生产环境的毒打。」

// A tool contract from The New Stack's billing example:
// input/output schemas, a timeout, and DEFINED error
// states. The idempotency key and the error split do a
// lot of work in that schema.
{
  "name": "update_billing_plan",
  "description": "Apply a previously quoted plan change to an account.",
  "input": {
    "account_id": "uuid (server-verified)",
    "quote_id": "uuid",
    "idempotency_key": "uuid"
  },
  "output": {
    "status": "applied | rejected",
    "effective_date": "date"
  },
  "timeout_ms": 5000,
  "errors": {
    "retryable": ["RATE_LIMITED", "UPSTREAM_TIMEOUT"],
    "terminal": ["QUOTE_EXPIRED", "APPROVAL_REQUIRED", "ACCOUNT_NOT_FOUND"]
  }
}
// "An error message is a prompt." -- The New Stack

2. 工具契约:给每个工具定好边界

工具是外骨骼和生产系统接触的地方,所以工具要带契约。代理工具就是「一个可能做出错误选择的调用者」的 API:一句简短的工具描述能帮模型选对工具,但保护不了 API 免受非法输入或不安全请求的侵害。给每个工具一个明确的职责,配上输入输出 schema、超时和定义好的错误状态。文章给的账单工具示例(代码块一)就是标准模板:input 里声明 account_id、quote_id 和幂等键;output 声明状态和生效日期;超时 5000ms;错误被明确分成可重试(RATE_LIMITED、UPSTREAM_TIMEOUT)和终态(QUOTE_EXPIRED、APPROVAL_REQUIRED、ACCOUNT_NOT_FOUND)。

Tool contracts and error states
// Contract validation middleware: the harness checks
// every tool call BEFORE it reaches production systems.
// The model can make incorrect choices; the contract
// cannot be bypassed by a clever instruction.
function enforceContract(contract, args) {
  for (const [field, type] of Object.entries(contract.input)) {
    if (args[field] === undefined) {
      throw { code: "MISSING_FIELD", field, hint: "Provide " + field };
    }
    if (type.includes("uuid") && !isUuid(args[field])) {
      throw { code: "INVALID_UUID", field };
    }
  }
  if (!args.idempotency_key) args.idempotency_key = crypto.randomUUID();
  return args;
}
// Permissions live OUTSIDE the model. Even if an injected
// instruction says "skip the check", this code runs before
// the tool call and cannot be prompted away.

3. 错误消息就是提示词

文章里最值得抄进笔记的一句话:「错误消息就是提示词。」模型会读取工具返回的任何内容并据此行动。ERR_422 教不会代理任何东西;而「APPROVAL_REQUIRED: annual plan changes need human sign-off」则精确地告诉它下一步该做什么。所以错误分类不是细节:可重试 vs 终态决定了代理是会无限循环,还是把任务干净利落地交还给人类。代码块三演示了错误分类处理器:可重试错误按退避策略重试(幂等键保证不重复执行),需要审批的错误进入人工审批队列,终态错误带着 guidance 返回。

// Error taxonomy handler: an error message is a prompt.
// "ERR_422 teaches the agent nothing. APPROVAL_REQUIRED:
// annual plan changes need human sign-off tells it
// exactly what to do next."
async function handleToolError(err, ctx) {
  const isRetryable = err.retryable?.includes(err.code);
  if (isRetryable && ctx.attempts < 2) {
    await sleep(backoff(ctx.attempts)); // 500ms, 2s
    return retry(ctx);                   // idempotent: same key
  }
  if (err.code === "APPROVAL_REQUIRED") {
    await enqueueHumanApproval({ task: ctx.task, reason: err.message });
    return { status: "awaiting_approval", ticket: ctx.ticket };
  }
  return { status: "failed", code: err.code, guidance: err.hint };
}
// Retryable vs terminal is not a detail: it decides
// whether the agent loops forever or hands off.

4. 权限必须在模型之外强制执行

模型的指令可能来自任何地方——包括被注入的恶意内容。所以权限不能放在提示词里,而要放在模型之外的代码里:即使某条指令试图绕过检查,代码仍然先于工具调用运行,无法被提示词「说服」。代码块四演示了基于动作-资源对的动作策略:读、写、审批、删除各有权责范围,payment:apply 需要财务审批角色。授权通过后写审计日志——代理的身份、动作、资源、时间,全部留痕。文章的原话是「权限在模型之外强制执行,即使某条指令试图绕过它们」。

Trace records and failure tests

5. 可检查的追踪记录

外骨骼的第三个职责是让团队真正能检查代理干了什么:上下文路径(这个代理看到了哪些数据、按什么顺序)和追踪记录(它调用了哪些工具、结果如何)。没有这些,出问题的时候你只能对着一个「它说它做了」的黑盒干瞪眼。文章提醒:如果通过 MCP 定义工具,部分 schema 管道可能由 MCP 处理——但契约本身仍然是你自己的,包括超时、错误分类和幂等行为。工具契约、权限、追踪,这三件套共同回答了「这个代理能不能上生产」。

// Permissions enforced outside the model: even when an
// instruction attempts to bypass them, the harness checks
// the ACTION, not the model's intent.
const ACTION_POLICY = {
  "read": ["account", "docs", "repo"],
  "write": ["draft", "ticket"],
  "approve": [],        // never, without human
  "delete": [],         // never, without human
  "payment:apply": ["finance-approver"],
};

function authorize(agentId, action, resource) {
  const allowed = ACTION_POLICY[action] || [];
  if (!allowed.includes(resource)) {
    throw { code: "FORBIDDEN", hint: resource + " requires a different role" };
  }
  auditLog({ agentId, action, resource, at: Date.now() });
}
// "Permissions enforced outside the model even when an
// instruction attempts to bypass them." -- The New Stack

6. 用真实失败构建测试

最后,外骨骼需要测试——而且是从「用户会先踩到的失败」里构建的测试,不是合成出来的快乐路径。代码块五演示了账单代理的回归测试:过期报价不能被应用、重试必须使用同一个幂等键、审批缺失时不得调用支付接口。基准分数衡量响应质量,但这些测试证明代理不能搞坏你的系统。文章结尾的总结值得记住:「模型提供推理,外骨骼提供模型自身没有的边界。」2026 年,把外骨骼做对,就是代理工程的核心竞争力。

// Tests built from the failures users will find first:
// the harness gets a regression suite that replays real
// failures, not synthetic happy paths.
describe("billing agent harness", () => {
  it("does not apply a quote that expired", async () => {
    const res = await runAgent("apply quote Q-99", {
      toolState: { quote: { id: "Q-99", status: "EXPIRED" } },
    });
    expect(res.status).toBe("failed");
    expect(res.code).toBe("QUOTE_EXPIRED");
    expect(finance.apply).not.toHaveBeenCalled();
  });

  it("retries with the SAME idempotency key", async () => {
    const key = "op-1";
    await runAgent("update billing", { idempotencyKey: key });
    expect(finance.apply).toHaveBeenCalledWith(expect.objectContaining({ idempotencyKey: key }));
  });
});
// "Tests built from the failures users will find first."
// Benchmark scores measure quality; these tests prove the
// agent cannot break your systems.

📌 常见问题 FAQ

什么是 agent harness(代理外骨骼)?

外骨骼是应用围绕模型搭建的脚手架:负责给模型喂正确的输入、检查它的输出,在故障扩散前抓住它们——就像测试外骨骼把代码包起来在受控条件下运行。模型只是服务的一部分,外骨骼是其余部分。

什么是 agent harness(代理外骨骼)?

外骨骼是应用围绕模型搭建的脚手架:负责给模型喂正确的输入、检查它的输出,在故障扩散前抓住它们——就像测试外骨骼把代码包起来在受控条件下运行。模型只是服务的一部分,外骨骼是其余部分。

什么是 agent harness(代理外骨骼)?

外骨骼是应用围绕模型搭建的脚手架:负责给模型喂正确的输入、检查它的输出,在故障扩散前抓住它们——就像测试外骨骼把代码包起来在受控条件下运行。模型只是服务的一部分,外骨骼是其余部分。

什么是 agent harness(代理外骨骼)?

外骨骼是应用围绕模型搭建的脚手架:负责给模型喂正确的输入、检查它的输出,在故障扩散前抓住它们——就像测试外骨骼把代码包起来在受控条件下运行。模型只是服务的一部分,外骨骼是其余部分。

什么是 agent harness(代理外骨骼)?

外骨骼是应用围绕模型搭建的脚手架:负责给模型喂正确的输入、检查它的输出,在故障扩散前抓住它们——就像测试外骨骼把代码包起来在受控条件下运行。模型只是服务的一部分,外骨骼是其余部分。

工具契约里最重要的是什么?

输入输出 schema、超时和定义好的错误状态,以及幂等键。The New Stack 的账单示例把错误明确分成可重试(RATE_LIMITED)和终态(QUOTE_EXPIRED、APPROVAL_REQUIRED),因为「错误消息就是提示词」——模型会读取错误内容并据此行动。

工具契约里最重要的是什么?

输入输出 schema、超时和定义好的错误状态,以及幂等键。The New Stack 的账单示例把错误明确分成可重试(RATE_LIMITED)和终态(QUOTE_EXPIRED、APPROVAL_REQUIRED),因为「错误消息就是提示词」——模型会读取错误内容并据此行动。

工具契约里最重要的是什么?

输入输出 schema、超时和定义好的错误状态,以及幂等键。The New Stack 的账单示例把错误明确分成可重试(RATE_LIMITED)和终态(QUOTE_EXPIRED、APPROVAL_REQUIRED),因为「错误消息就是提示词」——模型会读取错误内容并据此行动。

工具契约里最重要的是什么?

输入输出 schema、超时和定义好的错误状态,以及幂等键。The New Stack 的账单示例把错误明确分成可重试(RATE_LIMITED)和终态(QUOTE_EXPIRED、APPROVAL_REQUIRED),因为「错误消息就是提示词」——模型会读取错误内容并据此行动。

工具契约里最重要的是什么?

输入输出 schema、超时和定义好的错误状态,以及幂等键。The New Stack 的账单示例把错误明确分成可重试(RATE_LIMITED)和终态(QUOTE_EXPIRED、APPROVAL_REQUIRED),因为「错误消息就是提示词」——模型会读取错误内容并据此行动。

权限为什么必须在模型之外强制执行?

因为模型的指令可能来自任何地方,包括被注入的恶意内容。如果权限只写在提示词里,一条「跳过检查」的注入指令就能绕过。放在模型之外的代码里,即使指令试图绕过,检查仍然先于工具调用执行,无法被提示词说服。

权限为什么必须在模型之外强制执行?

因为模型的指令可能来自任何地方,包括被注入的恶意内容。如果权限只写在提示词里,一条「跳过检查」的注入指令就能绕过。放在模型之外的代码里,即使指令试图绕过,检查仍然先于工具调用执行,无法被提示词说服。

权限为什么必须在模型之外强制执行?

因为模型的指令可能来自任何地方,包括被注入的恶意内容。如果权限只写在提示词里,一条「跳过检查」的注入指令就能绕过。放在模型之外的代码里,即使指令试图绕过,检查仍然先于工具调用执行,无法被提示词说服。

权限为什么必须在模型之外强制执行?

因为模型的指令可能来自任何地方,包括被注入的恶意内容。如果权限只写在提示词里,一条「跳过检查」的注入指令就能绕过。放在模型之外的代码里,即使指令试图绕过,检查仍然先于工具调用执行,无法被提示词说服。

权限为什么必须在模型之外强制执行?

因为模型的指令可能来自任何地方,包括被注入的恶意内容。如果权限只写在提示词里,一条「跳过检查」的注入指令就能绕过。放在模型之外的代码里,即使指令试图绕过,检查仍然先于工具调用执行,无法被提示词说服。

错误分类为什么重要?

可重试 vs 终态决定了代理的行为:可重试错误按退避策略重试(配合幂等键保证不重复执行),需要审批的错误进入人工审批队列,终态错误带着明确的下一步指引返回。分类错了,代理要么无限循环,要么把任务搞砸。

错误分类为什么重要?

可重试 vs 终态决定了代理的行为:可重试错误按退避策略重试(配合幂等键保证不重复执行),需要审批的错误进入人工审批队列,终态错误带着明确的下一步指引返回。分类错了,代理要么无限循环,要么把任务搞砸。

错误分类为什么重要?

可重试 vs 终态决定了代理的行为:可重试错误按退避策略重试(配合幂等键保证不重复执行),需要审批的错误进入人工审批队列,终态错误带着明确的下一步指引返回。分类错了,代理要么无限循环,要么把任务搞砸。

错误分类为什么重要?

可重试 vs 终态决定了代理的行为:可重试错误按退避策略重试(配合幂等键保证不重复执行),需要审批的错误进入人工审批队列,终态错误带着明确的下一步指引返回。分类错了,代理要么无限循环,要么把任务搞砸。

错误分类为什么重要?

可重试 vs 终态决定了代理的行为:可重试错误按退避策略重试(配合幂等键保证不重复执行),需要审批的错误进入人工审批队列,终态错误带着明确的下一步指引返回。分类错了,代理要么无限循环,要么把任务搞砸。

用 MCP 定义工具还需要契约吗?

需要。MCP 可能帮你处理部分 schema 管道,但契约本身仍然是你自己的:包括超时、错误分类和幂等行为。文章原话:「The contract itself is still yours to define.」

用 MCP 定义工具还需要契约吗?

需要。MCP 可能帮你处理部分 schema 管道,但契约本身仍然是你自己的:包括超时、错误分类和幂等行为。文章原话:「The contract itself is still yours to define.」

用 MCP 定义工具还需要契约吗?

需要。MCP 可能帮你处理部分 schema 管道,但契约本身仍然是你自己的:包括超时、错误分类和幂等行为。文章原话:「The contract itself is still yours to define.」

用 MCP 定义工具还需要契约吗?

需要。MCP 可能帮你处理部分 schema 管道,但契约本身仍然是你自己的:包括超时、错误分类和幂等行为。文章原话:「The contract itself is still yours to define.」

用 MCP 定义工具还需要契约吗?

需要。MCP 可能帮你处理部分 schema 管道,但契约本身仍然是你自己的:包括超时、错误分类和幂等行为。文章原话:「The contract itself is still yours to define.」