Agent 的运行时负责把模型提出的动作变成受约束的执行:先确认动作是否合法,再调用工具,记录实际结果,并决定是否继续。提示词帮助模型理解任务,工具契约让输出可解析,运行时负责兑现系统边界。
延续订单异常调查场景,本篇从一次查询循环展开,说明消息协议、预算、幂等、错误处理和上下文管理怎样配合。代码明确区分可运行的局部函数与依赖应用实现的教学伪代码,不把一个循环当作完整生产框架。
1. 三种契约各自解决什么
| 契约 | 负责什么 | 不能单独保证什么 |
|---|---|---|
| 执行状态 | 继续、等待、停止、成功与失败的转换 | 工具结果必然正确 |
| 工具 Schema 与业务校验 | 参数结构、动作范围、对象权限 | 模型理解与业务事实必然正确 |
| 提示词与上下文 | 任务、证据、行为要求与输出约定 | 强制授权、事务一致性或隔离 |
“输出符合 JSON”与“动作允许执行”是两个判断。订单号格式正确,也可能属于另一个客户;返回了自然语言答案,也可能只是需要补充信息,不能据此标记成功。
运行时应保留明确的终态与暂停态,例如 succeeded、failed、cancelled、budget_exhausted、waiting_for_input、waiting_for_approval。用户看到的结果、后台任务状态和监控指标应使用一致语义。
2. 循环首先要保留完整的调用关系
模型请求工具后,下一轮上下文需要包含模型发出的调用及对应结果。只追加结果而遗漏原调用,会破坏消息关联;具体消息角色、结果块与调用 ID 格式由供应商协议决定。
下面是教学伪代码,接口由应用实现:
def run_agent(goal, runtime):
state = runtime.start(goal)
while True:
stop = runtime.stop_reason(state)
if stop:
return runtime.finish(state, stop)
# 预约本次输入及输出上限,调用后按实际 usage 结算
reservation = runtime.reserve_model_budget(state)
response = runtime.model_call(state, reservation)
runtime.settle_and_record_response(state, reservation, response)
if not response.tool_calls:
return runtime.assess_answer(state, response)
for call in response.tool_calls:
stop = runtime.stop_reason(state)
if stop:
runtime.mark_unexecuted_calls(state, response)
return runtime.finish(state, stop)
outcome = runtime.execute_checked(state, call)
runtime.record_tool_result(state, call.id, outcome)
if outcome.requires_pause:
runtime.mark_unexecuted_calls(state, response)
return runtime.pause(state, outcome.reason)
这里有几项不能省略的约束:
- 预算和取消检查发生在动作前,不能等所有工具执行完才发现超限
- 工具调用数与模型轮数分别计量,一轮响应可以包含多个调用
- 未执行的调用要被标记,恢复时不能伪装为已成功
- 回答需要按任务标准判断,不能只检查“没有 tool call”
- 外部写操作需要操作记录与恢复协议,进程内 messages 不足以支持崩溃恢复
日志应记录模型版本、提示词版本、调用标识、状态转换与可公开的结果摘要。记录完整敏感输入并不是可观测性的前提;按数据权限控制采样、脱敏、保留期限与访问范围。
3. 调用次数由依赖关系决定
“每个工具必然对应两次模型调用”会重复计数。常见的单工具问答确实有两次推理:先选择工具,再基于结果回答;但前一次工具的结果解释,也可以同时产生下一个工具调用。
| 调用结构 | 模型调用次数 |
|---|---|
| 一个工具,再由模型形成回答 | 常见为 2 次 |
| 三个独立工具由同一响应提出,统一汇总 | 可以是 2 次 |
| 三个工具逐次依赖前一个结果,再形成回答 | 常见为 4 次 |
| 工具结果可以直接作为最终交付 | 可能无需额外综合调用 |
这不是固定性能承诺。模型可能追问、重试或改变计划,实际费用还取决于各轮输入输出和工具开销。
在订单案例里,订单详情与事件记录如果相互独立,可以一起查询;支付状态查询若需要先获得支付请求 ID,就应等待前一步。并行化应来自真实依赖分析,不能仅因为模型一次返回多个调用就全部并发执行。
4. Schema 定义结构,业务服务定义权限
下面是供应商无关的工具描述示例。接入 SDK 时应转换为对应字段,并核对其支持的 JSON Schema 子集。
{
"name": "query_order_events",
"description": "查询当前授权主体可访问订单的事件,按发生时间返回事件类型、版本与来源。仅查询,不修改订单。",
"parameters": {
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^O[0-9]{12}$"
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 10
}
},
"required": ["order_id"],
"additionalProperties": false
}
}
default 是描述性标注,标准 JSON Schema 校验不会自动把缺失值填入对象。应用需要自行补默认值,并再次检查有效范围。参见 JSON Schema 的注解说明。
Schema 无法单独防止 SQL 注入或越权。查询应使用参数化 SQL 和受控字段,租户与主体来自认证上下文;不能把“SQL 以 SELECT 开头”当作安全边界。命令执行也应使用受控程序及参数,避免把模型字符串直接拼入 shell。
工具描述则需要解释什么时候使用、参数含义、返回内容与失败类型。Anthropic 的工具设计实践强调结合任务评估迭代工具,而不是只关注函数封装。可用性需要通过真实任务验证,不能由描述字数推断。
工具较多时,可以按任务范围筛选或检索候选工具。是否需要检索由总上下文、语义重叠与调用准确性决定,不存在通用的“超过 30 个注意力就下降”阈值。检索遗漏必要工具时,还需要扩展候选或明确报告能力不足。
5. 幂等键必须代表同一业务操作
网络重试可能复用一次 tool call ID;模型重规划却可能生成新的 ID。因此,使用 session ID 与 tool call ID 拼接,只能覆盖一部分重试,不能保证同一业务请求不被重复执行。
以退款为例,应用应创建并保存 refund_request_id,把后续调用绑定到这个业务操作。工具服务按租户与请求 ID 原子去重,并拒绝同一 ID 携带不同金额或订单。操作记录至少区分待提交、已受理、成功、失败与结果未知。
| 发生的情况 | 处理原则 |
|---|---|
| 同一请求再次提交 | 查询或返回已有操作状态 |
| 同一 ID,参数改变 | 拒绝并报告冲突 |
| 超时,不知道支付方是否已受理 | 先查状态,再按接口契约恢复 |
| 用户明确发起另一笔合法退款 | 创建新的业务请求,重新验证可退余额 |
| 运行时中断后恢复 | 从持久操作记录恢复,不重新猜测完成情况 |
工具提供方不支持幂等或状态查询时,不能声称自动重试安全。可以限制自动执行范围,或将结果未知的操作转交人工核实。审计日志用于追踪,数据库约束与业务协议用于防重,两者不能替代。
6. 错误处理取决于动作语义
参数错误、限流、超时和权限拒绝需要不同处理;异常名称本身还不够。读取超时与写入超时最大的区别,是后者可能已经产生副作用。
| 情况 | 默认处理方向 |
|---|---|
| 可纠正参数错误 | 返回结构化错误,允许在预算内修正 |
| 只读请求的瞬时故障 | 按工具契约做有限退避重试 |
| 写操作结果未知 | 保存未知状态,查询已有操作或升级处理 |
| 权限拒绝 | 拒绝动作;不允许模型改身份或绕过限制 |
| 凭据失效、系统持续故障 | 暂停或失败,按应用流程恢复 |
| 未识别异常 | 记录并升级,不默认当作模型可纠正错误 |
| 用户取消、总时限到达 | 停止新动作,处理已开始操作的状态 |
给模型的错误应包含安全的错误类别、当前状态与允许的下一步,不泄露密钥、堆栈中的敏感值或内部访问路径。模型可以建议如何修正,但运行时仍执行重试次数和权限限制。
不能规定“所有错误必须回传模型”或“循环本身不能 catch”。上层需要捕获终止与系统异常并清理资源,下层则负责把允许恢复的失败转换为观察结果。
7. 无进展检测要结合结果
相同动作重复出现,可能是失控循环,也可能是分页、轮询或重试。检测应结合参数、结果是否变化、允许的间隔及工具语义。
下面的可运行 Python 函数仅识别最近 action 事件中的重复信号;它不负责证明系统陷入死循环:
import json
from collections import Counter
def repeated_action_without_progress(events, window=6, threshold=3):
if window < 1 or not 1 <= threshold <= window:
raise ValueError("invalid window or threshold")
recent = [e for e in events if e["kind"] == "action"][-window:]
counts = Counter()
for event in recent:
if event["progress"]:
counts.clear()
else:
args = json.dumps(
event["arguments"], sort_keys=True,
separators=(",", ":"), ensure_ascii=False,
allow_nan=False,
)
counts[(event["tool"], args)] += 1
return any(count >= threshold for count in counts.values())
先筛选 action 再截取窗口,才能表达“最近六次动作”,而不是“最近六条消息”。参数键排序只消除 JSON 对象顺序差异,不会识别所有语义等价参数。progress 应来自可核验状态变化或应用判据,不能仅依赖模型说“有进展”。
这个信号可触发重新评估、暂停或退出;合理轮询还需专门的时间与频率约束。检测函数不能替代总时限和预算。
8. 提示词传达要求,执行层保证边界
运行时组装上下文时,可以按任务规则、工具定义、当前输入、任务状态和外部证据组织内容。它们的存放位置不等于授权等级:检索文档中的指令仍是不可信内容,不能因为被拼入上下文就获得执行权。
对订单调查,可使用这样的任务约定:
目标:查明指定订单的当前处理状态,并解释支持结论的证据
范围:只读调查,不创建支付、不退款、不修改订单
事实:订单与支付状态以工具结果为准,保留来源和查询时间
未知:超时不表示失败,缺少必要信息时说明缺口
交付:列出已确认状态、仍未确认事项和建议的下一步
这些要求也应体现在可用工具、权限和结果校验中。不能只在提示词中写“先确认”,却把未经校验的写接口直接交给模型。
提示词、工具定义和上下文组装逻辑应版本化,记录它们与模型配置的组合。灰度和回滚要基于任务评估,不能认为“提示词只是文案”而跳过验证。
Few-shot、显式计划或简短决策说明是否有帮助,应在目标模型与任务上比较。没有通用的“示例最多五个”或“关键任务必须展示完整思维链”规则。审计应依赖动作、证据和结果,模型生成的推理文本不等于真实内部机制,也不能保证解释忠实。
9. 上下文预算要在调用前成立
每次推理需要容纳固定指令、工具定义、用户输入、历史与输出预留。如果固定部分已经超限,就应缩小工具范围、分段处理输入或报告限制;不能把负数预算继续交给摘要函数。
一种可用的组装顺序是:
- 保留必要任务约束和结构化当前状态
- 选择本轮真正需要的工具定义
- 为输出和协议开销预留空间
- 按预算加入最近的完整消息组与相关历史
- 对更早材料先提取或摘要,再从上下文中移出
- 调用前重新计量,确认总量满足模型限制
工具调用及其结果应作为完整协议组处理。先删除旧历史再“摘要旧历史”,会丢掉摘要所需材料;直接截断 JSON 字符串也可能破坏结构。较大的工具结果应由工具返回分页、字段选择或摘要视图,并保留受控的原始结果引用。
摘要可能遗漏或歪曲事实。订单号、请求 ID、确认状态和待办应尽量保存在结构化状态中,重要证据保留可回查来源,不能只存在于一段不断重写的自然语言摘要里。
在每轮新增量为 T、固定前缀为 S、全量重发的假设下,N 轮输入量为 N×S + T×N×(N+1)/2。这不等于账单金额;缓存命中、输入输出单价和其他费用需按具体服务计量。不要用某一供应商的折扣比例概括所有模型服务。
10. 用异常路径验证运行时
正常查询只能证明基本链路可走通。上线前更有区分度的用例包括:
| 用例 | 预期观察 |
|---|---|
| 模型提出不存在的工具 | 拒绝调用,并记录明确错误 |
| 查询无权访问的订单 | 服务端拒绝,模型无法改身份绕过 |
| 一次返回多个调用,途中预算耗尽 | 后续调用未执行且有状态记录 |
| 写接口超时但已受理 | 恢复时查询原请求,不生成重复操作 |
| 用户取消长任务 | 不再发起新动作,已开始操作可追踪 |
| 相同参数合理轮询 | 遵守间隔和时限,不机械当成攻击 |
| 摘要后继续调查 | 订单标识、未知事项与证据引用仍保留 |
| 模型提前声称完成 | 结果校验能识别未完成事项 |
运行时的价值,是让每次动作有依据、每次结果可追踪、每次停止有明确语义。只有这些基础成立,后续的记忆、规划与多 Agent 协作才有可靠的执行载体。