游乐游手机版
首页/AI教程/文章详情

可控Agent:将预算工具与失败状态纳入协议

时间:2026-08-06 21:13
从状态机出发构建可控Agent,需将预算、工具契约和失败状态写入协议。任务应具备多种终态,如预算耗尽、待人工授权等。预算需同时限制轮次、墙钟时间、模型及工具调用次数和输出大小,避免失控。

“模型决定调用工具,工具结果再交给模型”——这个描述仅覆盖了Agent的最小循环,远远无法应对生产环境中那些棘手的现实问题:重试机制、幂等性保障、权限控制、成本管理,以及最关键的人工干预。本文从状态机出发,梳理如何构建一个真正可控的Agent,并提供工具契约、执行预算、上游适配和故障注入的具体方法,让每一次动作都能被解释、限制和恢复。

可控 Agent 不是一个 while 循环:把预算、工具和失败状态写进协议

最小循环为何会失控

演示代码的写法通常是这样:模型返回工具调用就执行,否则结束。但这里面藏着几个危险假设——模型总会收敛、工具可以无限制重复执行、参数天然合法、上游失败只需要重试、历史消息可以无限增长。生产环境必须把这些假设变成显式约束,不能有任何模糊地带。

一个任务至少应该具备以下这些终态:

type RunStatus =| "completed"| "budget_exhausted"| "waiting_for_approval"| "tool_failed"| "provider_failed"| "cancelled"

“失败”不能只有一个布尔值。工具失败可以换参数重新尝试,上游超时也有有限的重试次数,权限不足时必须等待人工授权,预算耗尽则应该生成阶段性结果并优雅停止。每一种状态都对应不同的处理策略。

用预算包住循环

预算不仅要设置轮次上限,还要同时考虑墙钟时间、模型调用次数、工具调用次数和输出大小。单一的 maxSteps 根本阻止不了一次工具调用运行半小时——那是赤裸裸的生产事故。

type Budget = {deadline: number;modelCallsLeft: number;toolCallsLeft: number;outputBytesLeft: number;};function assertBudget(b: Budget) {if (Date.now() >= b.deadline) throw new Error("DEADLINE_EXCEEDED");if (b.modelCallsLeft <= 0) throw new Error("MODEL_BUDGET_EXHAUSTED");if (b.toolCallsLeft <= 0) throw new Error("TOOL_BUDGET_EXHAUSTED");}

需要注意,每次执行前就要扣预算,而不是成功之后再扣——否则超时和异常会绕过计数,导致预算形同虚设。预算值应该由任务类型决定,只读问答和代码迁移显然不应该共享同一个上限。

工具契约要比函数签名更完整

工具的声明不能只写一个函数签名,至少需要包含输入模式、读写属性、并发安全性、超时和审批策略:

type ToolSpec = {name: string;validate(input: unknown): I;readOnly: boolean;concurrencySafe: boolean;timeoutMs: number;approval: "never" | "on-write" | "always";execute(input: I, signal: AbortSignal): Promise;};

默认值应该尽可能保守:未声明只读就按写操作处理,未声明并发安全就串行执行。参数校验必须在权限判断之前完成,否则攻击者可能用畸形路径绕过规则。

对于有副作用的工具,增加幂等键是必要的:

const idempotencyKey = `${runId}:${stepId}:${toolName}`;const previous = await resultStore.get(idempotencyKey);if (previous !== undefined) return previous;const result = await tool.execute(input, signal);await resultStore.put(idempotencyKey, result);return result;

当然,这不能替代上游自身的幂等能力。像付款、发信和删改数据这样的关键动作,仍然需要服务端用唯一约束或事务来保证“最多一次”。

把模型接入层做成窄接口

Agent 不应该直接依赖某个 SDK 的完整响应对象。先归一化为内部事件:

type ModelEvent =| { type: "text"; text: string }| { type: "tool_call"; id: string; name: string; input: unknown }| { type: "usage"; inputTokens: number; outputTokens: number }| { type: "stop"; reason: string };interface ModelGateway {stream(request: ModelRequest, signal: AbortSignal): AsyncIterable<ModelEvent>;}

具体上游通过环境配置注入,例如:

LLM_BASE_URL=https://your-gateway.exampleLLM_API_KEY=replace_meLLM_MODEL=your-tested-model

这样做的目的不是承诺“随时切换”,而是让兼容性测试有一个固定的入口。不同服务在鉴权头、流式事件、工具调用字段、错误码和上下文限制上差异很大,必须逐项验证。

一个可恢复的执行骨架

async function runAgent(ctx: RunContext): Promise<RunStatus> {while (true) {if (ctx.signal.aborted) return "cancelled";try {assertBudget(ctx.budget);} catch {return "budget_exhausted";}ctx.budget.modelCallsLeft--;let decision: ModelDecision;try {decision = await ctx.model.decide(ctx.messages, ctx.signal);} catch {return "provider_failed";}if (decision.type === "final") {await ctx.events.append({ type: "final", text: decision.text });return "completed";}const tool = ctx.tools.get(decision.name);if (!tool) return "tool_failed";const input = tool.validate(decision.input);if (tool.approval === "always" || (tool.approval === "on-write" && !tool.readOnly)) {await ctx.checkpoints.sa ve({ decision, input });return "waiting_for_approval";}ctx.budget.toolCallsLeft--;let result: ToolResult;try {result = await executeWithTimeout(tool, input, ctx.signal);} catch {return "tool_failed";}ctx.messages.push(toToolResult(decision.id, result));}}

人工批准后从 checkpoint 恢复,而不是让模型重新规划。重新规划可能生成不同参数,也会让审批对象失效,这一点尤其重要。

重试只对“可证明瞬时”的错误开放

适合自动重试的通常是连接重置、限流和明确的 5xx;参数错误、鉴权失败和内容策略拒绝则不应该盲目重试。指数退避要加入随机抖动,并受总 deadline 约束:

const delay = Math.min(4000, 250 * 2 ** attempt) * (0.5 + Math.random());if (Date.now() + delay >= budget.deadline) throw error;await sleep(delay, signal);

流式响应的情况更棘手:连接断开前已经输出的内容可能无法安全续传。除非协议提供可恢复游标,否则应该把该轮标为不完整,重新发起新轮次并在 UI 中替换旧草稿,而不是直接拼接。

上线前做故障注入

以下是一些值得提前模拟的场景:

  • 模型连续返回不存在的工具名。
  • 同一个写工具调用被投递两次。
  • 工具在产生副作用后超时。
  • 流式响应在半个 JSON 参数处断开。
  • 审批等待期间任务被取消。
  • 上下文压缩后丢失关键权限信息。

断言的不仅仅是“没有崩溃”,还包括预算未穿透、审计事件连续、重复动作未执行,以及用户能看到明确的终态。

总结

Agent 的核心不是循环,而是循环外的约束系统。预算限制它能走多远,工具契约限制它能做什么,幂等和 checkpoint 决定失败后能否恢复,窄接入层则让上游差异变得可测试。把这些状态写进协议,Agent 才从演示程序变成真正可以运营和审计的系统。

来源:https://juejin.cn/post/7668316523341758470
上一篇生成式AI赋能规模化个性化网络钓鱼攻击与闭环防御机制 下一篇WorkBuddy使用心得第二篇全面实用技巧与经验总结
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
CAD零基础入门教程:坐标输入、图层管理与基础绘图命令
AI教程 · 2026-09-01

CAD零基础入门教程:坐标输入、图层管理与基础绘图命令

本文面向CAD零基础学习者,系统讲解坐标输入、图层管理与基础绘图命令的核心用法。通过分步实操与常见问题排查,帮助新手建立精确绘图习惯,掌握规范出图的基础能力。

CAD从入门到项目交付:绘图、标注、图块与实战工作流
AI教程 · 2026-09-01

CAD从入门到项目交付:绘图、标注、图块与实战工作流

掌握CAD的核心在于建立“画得准、标得清、复用快、交付稳”的工作流。本文提供从环境设置、高频命令组合、标注规范、图块标准化到项目分阶段交付的完整路径,帮助初学者避免常见返工陷阱,独立完成可检查、可复用、可打印的工程图纸。

Claude Code 登录指南:个人、Teams 与企业账号区分与授权步骤
AI教程 · 2026-09-01

Claude Code 登录指南:个人、Teams 与企业账号区分与授权步骤

本文详细解析 Claude Code 登录前的账号类型区分方法,涵盖个人订阅、Teams 席位与企业 Enterprise 席位的授权路径差异。提供终端登录命令、环境变量排查及常见异常处理步骤,帮助用户快速完成正确授权并避免登录路径混淆。

Claude Code 文件修改前的权限模式配置与命令审批指南
AI教程 · 2026-09-01

Claude Code 文件修改前的权限模式配置与命令审批指南

本文详细介绍Claude Code在修改文件前的权限模式配置方法,包括defaultMode可选值、permissions allow与deny规则设置、多层级配置文件管理以及 status验证技巧,帮助开发者安全高效地使用AI编程助手。

Claude Code接入VS Code后先测扩展和终端命令
AI教程 · 2026-09-01

Claude Code接入VS Code后先测扩展和终端命令

在VS Code中接入Claude Code后,建议优先验证扩展面板与集成终端两条入口。本文提供标准检查顺序、关键命令与常见故障排查路径,帮助你快速确认环境就绪,避免后续开发受阻。