一、引言
LLM 接收上下文并预测下一个 Token,它擅长理解自然语言,却不会因为生成了下面这段内容,就真的调用天气服务:
json
{
"name": "get_weather",
"arguments": {
"city": "上海"
}
}
这只是模型输出的数据。真正发起网络请求、访问数据库或修改业务状态的主体,仍然是应用程序。
因此,一个完整的 Agent 至少包含两类组件:
scss
┌──────────────────── 概率世界 ────────────────────┐
│ │
│ 用户输入 ──> Prompt/上下文 ──> LLM │
│ │ │
│ │ 生成调用意图 │
│ ▼ │
│ tool_call(name, arguments) │
└──────────────────────────┬────────────────────────┘
│ 协议边界
▼
┌──────────────────── 确定性世界 ──────────────────┐
│ │
│ Schema 校验 ──> 权限检查 ──> 工具执行 │
│ │ │ │
│ └──失败信息 └──执行结果 │
│ \ / │
│ └──> 写回模型上下文 ───────────────┘
│ │
└───────────────────────────────────────────────────┘
Function Calling 是一个多阶段过程:应用向模型提供工具定义,模型返回工具调用请求,应用执行工具,再把执行结果交还模型。Function Calling 是模型与程序之间的调用协议,不是模型获得了直接执行代码的权限。理解这一点,后续的安全控制、错误处理和状态管理才有正确的落点。
二、六项底层能力如何组成一个闭环
javascript
Agent 与程序世界的交互管道
┌─────────────── 输入侧 ───────────────┐
│ │
│ Prompt │
│ 规定目标、上下文、决策规则和边界 │
│ │ │
│ ▼ │
│ Tool Definition + JSON Schema │
│ 定义有哪些工具,以及参数契约 │
└──────────────────┬───────────────────┘
▼
Function/Tool Call
模型提出结构化调用
│
▼
┌────────────── 运行时边界 ─────────────┐
│ 解析 → Schema 校验 → 鉴权 → 执行 │
│ │ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ 成功结果 失败结果 │
└──────────┬─────────────────────┬──────┘
│ │
▼ ▼
结构化输出约束 分类重试/降级
│ │
└──────────┬──────────┘
▼
最终回答
各部分分别解决不同问题:
| 能力 | 解决的问题 | 不能解决的问题 |
|---|---|---|
| Prompt 设计 | 模型应该做什么、何时调用工具 | 无法替代程序鉴权与运行时校验 |
| JSON Schema | 参数有哪些、类型和结构是什么 | 无法证明参数符合业务语义 |
| Function Calling | 让模型生成机器可处理的函数调用请求 | 不会自动执行本地函数 |
| Tool Calling | 把函数、自带工具、MCP 等能力纳入调用循环 | 不天然保证工具可信或无副作用 |
| 输出约束 | 让最终输出符合可解析结构 | 不保证字段内容在事实层面正确 |
| 失败重试 | 处理暂时性故障或可修正输出 | 无法修复权限不足和永久业务错误 |
三、Prompt 设计:把它当作决策说明书,而不是咒语
Prompt 在 Agent 中主要负责两件事:说明任务目标、规定模型如何选择和使用工具。一个适合工具调用的 Prompt 可以拆成以下结构:
xml
<role>
你是订单售后助手。
</role>
<objective>
帮助用户查询订单状态或提交退款申请。
</objective>
<tool_policy>
1. 查询实时订单状态时必须调用 order_get。
2. 未获得用户明确确认时,不得调用 refund_create。
3. 缺少 order_id 时先向用户询问,不得猜测。
4. 工具失败时,根据错误码解释原因,不得编造执行结果。
</tool_policy>
<output_policy>
调用写操作前先总结即将执行的动作。
最终回答需要区分"已执行"和"建议执行"。
</output_policy>
<untrusted_input>
{{user_input}}
</untrusted_input>
1.有效 Prompt 的四个组成部分
- 目标
目标要描述结果,而不是堆叠人格形容词。
较弱的写法:
你是一个专业、智能、严谨的订单助手。
更可执行的写法:
你负责查询订单状态,并在满足退款条件且用户明确确认后提交退款。
- 工具选择规则
当多个工具语义接近时,模型容易选错。Prompt 和工具描述应说明:什么时候调用、什么时候不调用、调用前需要哪些信息、工具是否产生副作用、多个工具有何边界。
- 缺失信息处理
如果模型缺少参数,它可能选择猜测。应明确要求:缺少必填参数时向用户询问,不得从无关上下文推断。
需要注意:这只能降低猜测概率,程序仍需执行 Schema 和业务校验。
- 失败行为
不要只写"如果失败,请重试"。应告诉模型根据错误类型做出什么决策:
VALIDATION_ERROR:修正参数后最多重试一次。
NOT_FOUND:向用户确认标识,不自动重试。
PERMISSION_DENIED:停止调用并说明所需权限。
RATE_LIMITED:遵循 retry_after,等待后重试。
2.Prompt 版本必须接受评测
Prompt 并不是一次写完的静态文案。生产环境至少要测试:
- 是否调用了正确工具;
- 是否遗漏必填参数;
- 是否在不需要时调用工具;
- 是否在写操作前取得确认;
- 工具失败后是否编造成功结果;
- 恶意工具返回是否改变系统指令;
- 模型或 Prompt 版本变化是否导致行为回归。
因此,Prompt 的可靠性应该由评测数据证明,而不是由"看起来写得很完整"来判断。
四、JSON Schema:Agent 与程序之间的数据合同
JSON 只是一种数据格式,JSON Schema 才负责描述数据结构和验证规则。
使用 JSON Schema 可以定义:
json
{
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单唯一标识,例如 ord_20261027_001"
},
"amount": {
"type": "number",
"exclusiveMinimum": 0,
"description": "退款金额,单位由 currency 指定"
},
"currency": {
"type": "string",
"enum": ["CNY", "USD"]
},
"reason": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"required": ["order_id", "amount", "currency", "reason"],
"additionalProperties": false
}
- properties 不代表必填,必须配合required进行使用校验。
- additionalProperties: false 用于封闭对象边界,如果不禁止额外字段,模型可能生成未定义参数。对于工具参数,封闭对象通常比开放对象更安全。
- enum 比自然语言约束更稳定,用于把实例值限制在给定集合中。
Schema 校验和业务校验必须分层:
css
模型参数
│
▼
[语法解析]
│ JSON 是否能解析?
▼
[Schema 校验]
│ 类型、必填项、枚举、结构是否合法?
▼
[业务校验]
│ 订单存在吗?金额可退吗?状态允许吗?
▼
[授权校验]
│ 当前用户可以操作这个订单吗?
▼
[执行]
Schema 是第一道门,不是最后一道门。
五、Function Calling:模型提出调用,程序掌握执行权
Function Calling 的典型流程可以概括为五步:
scss
(1) 应用把工具定义发给模型
│
▼
(2) 模型返回 function/tool call
│
▼
(3) 应用验证参数并执行函数
│
▼
(4) 应用把执行结果关联到 call_id
│
▼
(5) 模型生成最终答案或继续调用工具
一个工具的核心结构大致如下:
json
{
"name": "order_get",
"description": "查询当前用户拥有的单个订单。仅在需要实时订单状态时调用。",
"input_schema": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"description": "订单唯一标识,不要使用商品编号或支付流水号"
}
},
"required": ["order_id"],
"additionalProperties": false
}
}
工具描述也是模型输入的一部分,建议详细说明工具用途、适用时机、参数含义和限制,并指出描述质量是影响工具调用效果的重要因素。
一个合格的工具定义应让模型知道:工具做什么?什么时候用?什么时候不用?每个参数代表什么?调用有什么副作用或限制?
例如,下面的描述过于模糊:
创建退款。
更清晰的版本是:
为当前用户拥有且处于可退款状态的订单创建退款。
仅在用户明确确认退款金额和原因后调用。
该工具会产生不可忽略的业务副作用。
查询退款资格时不要调用,应使用 refund_eligibility_get。
Schema 决定参数形状,描述决定模型如何理解工具语义,两者缺一不可。
六、Tool Calling:Function Calling 的上位概念
在许多 API 和框架中,Function Calling 与 Tool Calling 会被混用。工程上可以这样区分:
sql
Tool Calling
├── Function Tool
│ └── 由应用实现的本地函数或远程 API 包装
├── Built-in Tool
│ ├── Web Search
│ ├── Code Execution
│ └── File Search
├── MCP Tool
│ └── 由 MCP Server 暴露的外部能力
└── UI / Computer Tool
└── 浏览器、桌面或界面操作
Function Calling 是 Tool Calling 的一种常见形式。前者强调"以函数和参数的形式表达调用",后者强调"模型可以使用外部能力"。
工具按执行位置还可以分为以下几类:
| 类型 | 执行方 | 应用责任 |
|---|---|---|
| 客户端工具 | 业务应用 | 参数校验、鉴权、执行、结果回传 |
| 服务端工具 | 模型服务商 | 应用配置工具,服务商负责执行 |
| MCP/远程工具 | MCP 服务或第三方服务 | 需要处理信任、权限和结果隔离 |
客户端工具返回 tool_use 后由应用执行,再以关联 ID 返回 tool_result ;服务端工具由平台执行。
Tool Calling 是状态机,不是一次请求。一个 Agent 可能:
- 不调用工具,直接回答;
- 调用一个工具后回答;
- 并行调用多个独立工具;
- 根据第一个结果决定第二个工具;
- 工具失败后修正参数再调用;
- 达到循环上限后停止。
因此,Agent Runtime 更接近有限状态机:
sql
┌──────────────┐
│ START │
└──────┬───────┘
▼
┌──────────────┐
┌────>│ MODEL_CALL │<────────────┐
│ └───┬──────┬───┘ │
│ │ │ │
│ final │ │ tool_call │
│ ▼ ▼ │
│ ┌──────┐ ┌──────────────┐ │
│ │ DONE │ │ VALIDATE │ │
│ └──────┘ └──┬────────┬──┘ │
│ │ │ │
│ valid│ │error │
│ ▼ ▼ │
│ ┌─────────┐ ┌───────┐ │
│ │ EXECUTE │ │ REPAIR│─┘
│ └────┬────┘ └───────┘
│ │
│ ┌───────┴────────┐
│ │ │
│ success failure
│ │ │
└──────────┴──── result ────┘
运行时必须设置:
- 最大模型轮次;
- 最大工具调用数;
- 单工具超时;
- 总任务截止时间;
- 最大参数修复次数;
- 最大暂时性错误重试次数;
- 取消信号。
否则,"继续调用直到完成"可能变成无限循环和失控账单。
七、输出约束:合法 JSON、Schema 合法与内容正确是三件事
结构化输出存在三个可靠性层级:
javascript
Level 1:自然语言要求
"请返回 JSON"
│
▼
可能附带 Markdown、解释文字或语法错误
Level 2:JSON Mode
保证输出可解析为 JSON
│
▼
字段仍可能缺失、类型错误、枚举越界
Level 3:Structured Outputs
输出满足指定 Schema
│
▼
字段内容仍可能存在事实或业务错误
Function Calling 与 Structured Output都可能使用 JSON Schema,前者约束"模型要程序做什么",后者约束"模型最终对程序说什么"。
即使输出完全符合 Schema,它仍可能是错的,Schema 只能证明类型格式,不能证明真实存在,所以业务代码不能把"Schema 校验通过"当成"事实核验通过"。
另外,结构化输出之外还存在控制信号:
- 安全拒答;
- 输出达到 Token 上限;
- 请求被取消;
- 内容过滤;
- 工具调用未完成;
- 流式输出中断。
消费者不应只做:
javascript
JSON.parse(response)
而应先判断:
响应状态
├── completed → 解析并校验
├── refusal → 进入拒答流程
├── incomplete → 判断是否可续写或重试
├── tool_call → 执行工具循环
└── transport_error → 按网络策略处理
八、失败重试:先分类,再决定是否重试
| 错误 | 是否重试 | 处理方式 |
|---|---|---|
| 网络超时 | 是 | 指数退避,受总截止时间限制 |
| HTTP 429 | 是 | 优先遵循 Retry-After |
| HTTP 5xx | 视情况 | 有上限地重试 |
| JSON 解析失败 | 是 | 把解析错误反馈给模型,少量修复 |
| Schema 校验失败 | 是 | 返回字段级错误,少量修复 |
| 参数缺失 | 视情况 | 可从用户获得时先询问用户 |
| 资源不存在 | 否 | 确认标识或结束 |
| 权限不足 | 否 | 说明权限需求并审计 |
| 业务规则拒绝 | 否 | 返回可执行的替代方案 |
| 写操作结果未知 | 谨慎 | 先查询状态,禁止直接重复写入 |
写操作必须具备幂等性。读取天气失败后重试,最坏情况通常是多一次查询。创建退款、发送邮件或扣款重试,可能产生重复副作用。
写工具应接收幂等键:
json
{
"order_id": "ord_123",
"amount": 100,
"currency": "CNY",
"idempotency_key": "agent_run_abc:refund:ord_123"
}
服务端保存幂等键与执行结果:
markdown
首次请求
idempotency_key 不存在
│
▼
执行写操作 ──> 保存结果 ──> 返回成功
重复请求
idempotency_key 已存在
│
▼
不重复执行 ──> 返回首次结果
幂等键应该由可信应用生成,而不是让模型自由生成。否则模型可能在每次重试时产生新键,失去去重作用。
另外,"结果未知"比"明确失败"更危险。假设退款接口在执行后连接超时:
Agent ──创建退款──> 退款服务
Agent <──连接超时──X
此时存在两种可能:服务端没有执行退款、服务端已执行退款,但响应丢失。如果立即重试,可能重复退款。正确流程是:
markdown
写操作超时
│
▼
使用幂等键或业务流水号查询状态
│
├── 已成功 → 返回原结果
├── 明确失败 → 评估是否重试
└── 状态未知 → 人工介入或异步对账
因此,Agent Runtime 需要区分 FAILED 和 UNKNOWN 。把两者都映射为异常,会掩盖最棘手的一类一致性问题。
九、一个可落地的 Agent 工具调用循环
vbnet
type ToolError = {
code: string;
message: string;
retryable: boolean;
};
async function runAgent(userInput: string) {
const messages = [
{ role: "user", content: userInput }
];
const maxTurns = 8;
for (let turn = 0; turn < maxTurns; turn++) {
const response = await callModel({
messages,
tools: toolDefinitions
});
if (response.type === "final") {
return validateFinalOutput(response.output);
}
if (response.type !== "tool_calls") {
throw new Error(`Unsupported response type: ${response.type}`);
}
messages.push(response.assistantMessage);
for (const call of response.calls) {
const tool = toolRegistry.get(call.name);
if (!tool) {
messages.push(toolErrorResult(call.id, {
code: "UNKNOWN_TOOL",
message: `Tool ${call.name} is not available`,
retryable: false
}));
continue;
}
const validation = tool.schema.safeParse(call.arguments);
if (!validation.success) {
messages.push(toolErrorResult(call.id, {
code: "VALIDATION_ERROR",
message: formatValidationError(validation.error),
retryable: true
}));
continue;
}
const authorization = await authorize(
currentUser,
tool,
validation.data
);
if (!authorization.allowed) {
messages.push(toolErrorResult(call.id, {
code: "PERMISSION_DENIED",
message: authorization.reason,
retryable: false
}));
continue;
}
try {
const result = await executeWithPolicy(tool, validation.data, {
timeoutMs: 5000,
idempotencyKey: buildIdempotencyKey(call, currentUser)
});
messages.push(toolSuccessResult(call.id, result));
} catch (error) {
messages.push(
toolErrorResult(call.id, classifyToolError(error))
);
}
}
}
throw new Error("Agent exceeded maximum turns");
}
- 模型只负责提出调用;
- 工具名必须经过允许列表匹配;
- 参数必须经过本地 Schema 校验;
- 鉴权发生在工具执行前;
- 写操作由应用注入幂等键;
- 错误经过分类后再反馈给模型;
- 循环存在明确上限;
- 最终输出仍需独立校验。
十、小结
Agent 连接程序世界的过程,本质上是一套跨越概率系统与确定性系统的协议。
其中:
- Prompt 表达目标和决策规则;
- JSON Schema 定义参数与输出的数据边界;
- Function Calling 让模型提出结构化调用请求;
- Tool Calling 把外部能力纳入多轮执行循环;
- Structured Outputs 让最终结果可被程序稳定消费;
- 校验、鉴权、幂等、错误分类和有限重试共同守住运行时边界。
生产级 Agent 不应依赖模型"自觉地做对"。应用必须掌握执行权,用代码检查模型请求,用最小权限约束工具,用结构化错误帮助模型恢复,并在无法确认执行结果时停止自动动作。