Tool Calling 与 Agent Loop:让模型建议进入有限状态机
核验日期:2026-07-16。模型供应商对 tool/function calling 的字段和并行能力不同;本文把模型适配器放在边缘,核心循环使用项目自有状态、验证、授权和终止语义。
Tool Calling 让模型返回“调用哪个工具、参数是什么”的结构化建议。应用执行工具,把结果作为观察值回填,再让模型继续或给出最终答案。Agent Loop 是这段多步控制流。模型不是执行器:它不能绕过验证、权限、审批、幂等、超时和审计。
一次调用的完整边界
工具注册表包含名称、面向模型的描述、输入/输出 Schema、执行函数、风险级别、所需权限、是否有副作用、幂等策略、超时、并发组和成本。描述帮助模型选择,不是访问控制。每次运行只暴露当前任务允许的最小工具集,减少误选、token 和攻击面。
工具 Schema 设计
名称使用稳定动词/领域,如 search_technologies、get_technology。参数表达业务意图,不暴露任意 SQL、shell、URL 或文件路径。枚举、最大长度、数组数量和未知字段策略尽量收紧;复杂动作拆成 preview/commit,提交引用不可变预览 ID。
Schema 验证之后还要语义验证:ID 是否存在、开始时间早于结束时间、资源属于当前租户、数量在动态配额内。授权基于服务端 principal,不接受模型参数中的 userId 作为身份。工具结果也做 Schema/大小验证并标记可信来源。
有限状态机
状态必须持久化时,模型请求、call proposal、规范化参数、审批和执行结果分别记录。不能只保存最终 messages,因为审批状态与副作用身份可能丢失。终止原因至少区分 final、max_steps、deadline、budget、cancelled、policy_denied、tool_failed 和 model_failed。
最小 TypeScript 循环
type Turn =
| { type: "final"; text: string }
| { type: "tool"; callId: string; name: string; input: unknown };
type Tool = {
validate(input: unknown): unknown;
execute(input: unknown, signal: AbortSignal): Promise<unknown>;
};
async function runAgent(
model: { next(history: readonly unknown[], signal: AbortSignal): Promise<Turn> },
tools: ReadonlyMap<string, Tool>,
signal: AbortSignal,
maxSteps = 8,
) {
const history: unknown[] = [];
for (let step = 0; step < maxSteps; step += 1) {
signal.throwIfAborted();
const turn = await model.next(history, signal);
if (turn.type === "final") return { text: turn.text, reason: "final" as const };
const tool = tools.get(turn.name);
if (!tool) {
history.push({ callId: turn.callId, error: "unknown_tool" });
continue;
}
try {
const input = tool.validate(turn.input);
const output = await tool.execute(input, signal);
history.push(turn, { callId: turn.callId, output });
} catch (error) {
history.push(turn, { callId: turn.callId, error: String(error) });
}
}
return { text: "", reason: "max_steps" as const };
}这段代码只说明循环骨架,尚未包含授权、审批、总 deadline、token/费用、持久化和 trace,所以不能直接承载生产写操作。它有明确最大步数,未知工具作为观察值返回,不会动态执行任意名称。
Deadline、预算与取消
每次运行有总 deadline,模型与工具得到剩余子预算。重试、审批等待和并行调用都消耗同一预算。maxSteps 限制控制迭代,token/费用预算限制模型消耗,工具预算限制外部调用;三者不可互相替代。用户取消通过 AbortSignal 传播,但已经提交的副作用只能查询结果或补偿。
无限“再试一次”可能形成昂贵循环。工具错误返回机器可判定的 code/retryable/safeMessage,模型不决定重试策略;执行器根据幂等性、attempt 和 deadline 决定。永久错误直接结束或请用户修正。
并行调用与资源上限
模型可能在同一 turn 提出多个互不依赖调用。只有只读、无共享写冲突且结果顺序不影响语义时才并行;写工具、受配额工具或存在依赖的调用串行或通过工作流建模。并行度受 semaphore/bulkhead 控制,不能等于模型输出数量。
工具结果按 call id 回填,不能靠数组位置猜。部分成功时记录每个结果;若业务要求全有或全无,使用后端事务/补偿服务,不让 Agent 临时拼事务。
幂等与副作用
每个有副作用调用生成稳定 operation id/idempotency key,和规范化参数哈希绑定。网络超时后先查询同 key 结果,不盲目重发。相同 key 不同参数返回冲突。工具服务沿用全栈后端部分的事务、outbox 和审计机制实现一次业务效果,Agent Loop 不提供 exactly-once 魔法。
高风险动作采用“提案—预览—审批—提交”:预览返回不可变内容哈希、影响范围、费用和到期时间;审批绑定用户、精确参数和版本;提交拒绝过期或变化的预览。模型不能在审批后改参数。
工具结果仍是不可信输入
网页搜索、工单、邮件和 MCP 工具可能返回注入文本。工具结果只能作为观察数据,不能改变高优先级策略。限制返回大小和字段;敏感工具只获得必要参数;对外 URL、HTML、Markdown 做安全渲染;不要把 tool error 的 stack/secret 回填模型。
工具调用前 guardrail 检查参数与策略,调用后 guardrail 检查结果和外泄风险。最终输出再验证引用与敏感内容。三处职责不同,不能只在最终文本做一次过滤,因为副作用可能已经发生。
确定性假模型测试
假模型返回预编排 turns,可验证:一次查询后结束;未知工具被拒绝;无效参数不会执行;工具超时传播;错误反馈后模型修正;达到步数终止;用户取消;审批前不执行;相同 operation id 不重复副作用;引用只来自工具结果。
const model = new ScriptedModel([
{ type: "tool", callId: "c1", name: "search_technologies", input: { query: "edge" } },
{ type: "final", text: "已找到匹配项 [source:s1]" },
]);真实模型只补充“能否稳定选对工具与参数”的概率评测。CI 不能依赖它,否则版本/网络/配额让控制流测试不稳定。
可观测性
每个 run/turn/tool call 建 span,记录安全的模型/模板/工具版本、step、finish/termination、input/output token、首 token/总延迟、queue、审批等待、工具 code 和重试。参数与结果默认脱敏或只存哈希/大小。指标使用低基数工具名、错误类和模型族,不把用户 ID 放 label。
一次错误从 run trace 找第一个偏离:错误工具选择、Schema 拒绝、授权拒绝、审批过期、执行失败、结果解析、模型继续还是终止。只看最终回答会把多个根因都叫“Agent 不聪明”。
何时不需要 Agent Loop
步骤固定、分支明确、要求强事务或可由普通代码决定时,写函数/状态机/队列工作流。模型只在需要语义分类、信息抽取或开放式规划的位置参与。单次分类不需要循环;一个只读检索加回答也可能只需固定 RAG pipeline。减少模型可决定的控制面,通常提升可测性、成本和安全。
交付检查
工具注册表、Schema、运行时和语义验证、授权检查、审批与幂等、有限循环、deadline/取消、错误分类、持久状态、trace、假模型测试和真实模型评测缺一不可。生产写工具还要有 kill switch、速率/费用限制和事故 runbook。
上下文增长与结果压缩
每次工具结果原样回填会使上下文快速膨胀。工具输出先按 output Schema 验证,再裁成模型完成下一步所需字段;大列表分页,大文档返回引用/摘要与可按 ID 继续读取的资源。压缩不能丢掉 call id、错误码、版本、引用和影响决策的边界条件,原始结果保存在受权系统而非 messages。
循环预算每步重新计算:保留系统策略、当前目标、未完成调用和最新观察;对旧工具结果做结构化摘要;已确认的业务状态存字段而非自然语言。若剩余上下文不足以安全完成,终止并返回可恢复状态,不静默裁掉审批或用户约束。
一致快照与时间变化
多步 run 期间数据会变化。只读比较可以固定 dataVersion/asOf,让后续工具在同一快照查询;必须读取最新状态的写动作则在提交前重读并使用 version/ETag 条件更新。模型看到的旧结果不能证明当前资源仍可操作。
工具结果带 observedAt/sourceVersion,最终引用展示核验时间。审批预览绑定资源版本;若变化,原审批失效并生成新预览。这样不会把“模型几分钟前看见的事实”当成事务锁。
多模型或路由器
有些系统用小模型分类/路由,大模型规划/回答。路由器也是概率组件,需要独立数据集、拒绝/升级条件和成本;错误路由可能让不具能力或不满足数据政策的模型处理请求。高风险/敏感任务先按确定性策略限定候选,再由路由器在允许集合中选择。
父 run 汇总所有子调用的 step、token、deadline 和费用,取消向下传播。不要让每个子 Agent 各自拥有不相干的最大步数,导致总上限乘法增长。路由失败有安全默认值,不能动态调用客户端给出的模型名。