Agent 循环与执行器

本页目录

让模型调用工具,需要把建议变成可核验的执行记录。本文从一次库存查询建立最小循环,再加入结果关联、停止状态、依赖与未知写结果。读完应能指出某个动作只是被提出、已经执行,还是已经通过任务验收。

从固定流程到反馈决策

固定工作流由程序预先安排主要步骤,例如读取订单、抽取字段、校验、入库;模型可以参与某一步,但主要路径由代码控制。Agent 则让模型根据观察动态决定下一步,例如测试失败后自行定位相关文件,再选择新的修复动作。两者可以组合:一个确定性外层流程也能包含受限的 Agent 子任务。Building effective agents

ReAct 研究将推理与行动交替组织,让模型依据环境反馈更新后续处理。ReAct 这不是按“有没有聊天界面”划分产品:聊天系统也可以调用工具,Agent 也可能一次回答就完成简单请求。

模型推理产生表示和输出,外部动作由某个执行环境落实。客户端工具由应用执行;服务端工具可能由供应商执行。因此权限和审计边界分布在实际执行组件中,不能假定所有操作都在本地宿主,也不能把模型生成的命令文本视为已经执行。

模型响应文本或结构化调用执行前核验名称、参数、权限、预算工具执行本地或远端服务结果与任务状态调用 ID、返回值、证据模型响应结束、一次工具返回、业务目标完成,是三个不同的完成边界。模型提出动作,执行器核验并执行,结果进入下一次决策

完成声明与完成证据

完成是一项待验证的声明。这个模型允许在没有执行、没有观察时直接宣称完成,验收层仍独立检查证据。观察也有时间边界:修改前的读取不能证明修改后的状态。

正在呈现知识画面
完成声明与完成证据

执行成功、读到结果、声明完成是三件事;验收必须检查实际证据。

一次工具调用的完整路径

工具定义同时是模型接口和执行契约

名称帮助模型识别动作,描述解释用途与限制,schema 定义参数结构。以只读库存查询为例,描述应说明查的是可售数量、商品使用哪个标识,以及结果的时间或版本含义;“获取库存”四个字无法表达这些边界。

{
  "name": "lookup_stock",
  "description": "按商品 SKU 查询当前可售数量;只读,不预留库存。",
  "input_schema": {
    "type": "object",
    "properties": {"sku": {"type": "string"}},
    "required": ["sku"],
    "additionalProperties": false
  }
}

这是 Claude 风格的工具定义;其他协议的字段名可能不同。schema 约束形状,不自动验证身份、商品是否属于当前租户或操作者能否查看。结构化输出也不能代替执行端验证:执行端必须只分派已注册工具,并重新核验输入与权限。Tool use with Claude

一次查询可以按下面的记录追踪:

阶段示例记录必须保留的含义
模型提出动作调用 ID c17,lookup_stock,sku=A1这是一项请求,尚未执行
执行前核验工具存在、参数有效、主体有读权限允许执行的范围
工具实际返回available=3,库存版本 v83这是哪次读取的结果
回传观察调用 ID c17 对应上述结果防止结果错配
模型继续决策回答可售数量或提出下一步查询不等于已预留库存

执行结果最好包含明确状态、必要数据与来源定位。错误也应区分无此商品、参数无效、权限不足、限流或结果未知;笼统返回“失败,重试”会诱导无意义循环。

消息协议必须完整

Claude 客户端工具通过 tool_use 与匹配的 tool_result 关联;同一响应可包含多个调用。应用应保留完整 assistant 响应,并按协议组织所有相应结果。thinking、不透明状态和非文本内容不能被“只保存 text”的简化逻辑丢掉。

错误格式反馈可能影响之后的模型行为,但不是在请求中“悄悄训练了模型权重”。协议要求与推测性的行为解释应分开,具体工具消息排列规则以 工具接口文档 为准。

响应停止与任务状态

模型没有再请求工具,只说明这次模型响应已停止。它可能正确完成了任务,也可能遗漏验证、需要用户补充信息、被截断,或错误地宣称成功。应用需要独立记录任务状态,例如 running、waiting、succeeded、failed、budget_exhausted,并用任务验收条件决定何时 succeeded。

下表是 Claude 接口中的常见分支,完整枚举和后续请求格式见 Stop reasons and fallback。

停止原因意义处理方向
tool_use请求客户端工具完成核验、执行与结果回传
end_turn模型结束本轮检查任务验收条件后收尾
max_tokens输出达到限制检查完整性;不要执行截断参数
pause_turn服务端长工具流程暂停按该接口要求续接,应用仍负责循环限额
refusal模型拒绝当前生成记录原因,按业务策略终止或提供替代

流式传输不会解除 max_tokens。如果工具参数 JSON 只收到一半就断流,不能猜测剩余字段后执行;如果服务端工具仍在运行,也不能把 pause 当作客户端应再次执行同一副作用的信号。

以“修复导入重复提交”为例,验收条件可能是补丁存在、普通测试通过、重启恢复测试通过。模型输出“已修复”但只跑了普通测试时,任务仍是未完成。验收标准应在开始时明确,并关联到实际产物版本。

依赖、重放与副作用

并行资格来自依赖关系

同时读取两份独立文档通常可以并行;“查询库存后按结果预留”有数据依赖,不能先猜参数并行发出。即便两个动作输入独立,若同时修改同一个文件或同一资源,也可能发生冲突。

执行器需要同时考虑数据依赖、共享资源、服务并发上限与权限。并行节约的是可重叠的等待时间,代价包括峰值负载和结果合并;不能因为模型同一轮提出多个调用就无条件全部并发。

调用 ID 不等于业务幂等键

调用 ID 用来把响应关联回提议。模型重试时可能产生新的调用 ID,但表达同一个业务动作;这时仅按调用 ID 去重,仍可能重复创建订单或发送通知。业务操作键应由应用按明确事务语义产生、保存,并由支持幂等的服务核验同一键与同一请求是否一致。

执行器订单服务数据库请求:操作键 K42提交已完成响应丢失×不能把超时直接当作未执行;用业务操作键查询或按服务的幂等协议重试。超时后的关键问题:动作失败,还是结果未能返回?
失败状态是否可直接重试合理处理
只读查询超时通常可以,但数据可能变化有限重试并保留读取时点
参数校验失败原样重试无意义返回可修正的具体字段错误
无权限不通过改措辞绕过在授权范围内调整或报告阻塞
写操作超时、结果未知不能假定没执行按操作键查询结果或使用服务幂等协议
已完成动作,回传丢失重做可能重复副作用重放已保存结果并核对业务状态

取消同样不是回滚:停止模型生成或取消本地等待,不保证远端已经开始的动作被撤销。执行器应把“确认未执行”“已完成”“结果未知”区分开,恢复时先消除未知状态。

超时以后,重试会写几次?

把网络结果与业务结果分开:请求没有返回,不等于没有写入。这里服务端事实始终可见,调用方却只有未知状态;查询或同键重试才取得确认。去重必须由服务端保证,模型记住“我已调用”不能提供同等保证。

正在呈现知识画面
超时以后,重试会写几次?

调用 ID 标识尝试,操作键标识业务操作;服务端去重才能使相同操作键不重复产生副作用。

一个可运行的本地执行器示例

下面用预设模型响应模拟一次只读库存查询。它验证工具白名单、参数、调用结果关联与请求上限,不调用模型 API,也不执行真实业务写入。生产系统还需要持久状态、身份与权限、超时、完整协议适配和审计。

STOCK = {"A1": 3}

def execute(call):
    if call.get("name") != "lookup_stock":
        return {"ok": False, "error": "unknown_tool"}
    args = call.get("input")
    if not isinstance(args, dict) or set(args) != {"sku"}:
        return {"ok": False, "error": "invalid_arguments"}
    if not isinstance(args["sku"], str):
        return {"ok": False, "error": "invalid_sku"}
    if args["sku"] not in STOCK:
        return {"ok": False, "error": "not_found"}
    return {"ok": True, "available": STOCK[args["sku"]]}

def run(model, max_requests=3):
    observations, seen = [], set()
    for _ in range(max_requests):
        response = model(list(observations))
        if response["kind"] == "final":
            # 模型结束不等于业务验收成功。
            return {"state": "model_ended", "text": response["text"],
                    "observations": observations}
        if response["kind"] != "tool":
            return {"state": "invalid_response"}
        call = response["call"]
        call_id = call.get("id")
        if not isinstance(call_id, str) or not call_id or call_id in seen:
            return {"state": "invalid_call_id"}
        seen.add(call_id)
        observations.append({"call_id": call_id, "result": execute(call)})
    return {"state": "budget_exhausted", "observations": observations}

def scripted(observations):
    if not observations:
        return {"kind": "tool", "call": {
            "id": "c17", "name": "lookup_stock", "input": {"sku": "A1"}}}
    assert observations[0] == {
        "call_id": "c17", "result": {"ok": True, "available": 3}}
    return {"kind": "final", "text": "当前查询到可售数量为 3,尚未预留。"}

result = run(scripted)
assert result["state"] == "model_ended"
assert execute({"name": "delete_stock"})["error"] == "unknown_tool"
assert execute({"name": "lookup_stock", "input": {"sku": 1}})["error"] == "invalid_sku"
assert run(scripted, max_requests=1)["state"] == "budget_exhausted"
print(result["text"])

例子在一次工具调用后、还没取得最终响应时耗尽请求预算,会明确返回 budget_exhausted。这比将任何循环退出都映射为“成功”更准确。真实应用也应持久保存已经完成的调用,避免进程重启后丢失副作用记录。

工具边界与循环收敛

把“最多运行十轮”作为最后保险还不够。应观察每轮是否获得新证据、修改了产物、消除了错误;连续得到相同失败且条件没有变化时,重复动作通常无法推进。可以返回更具体的错误、改变检索范围,或报告缺失条件,而不是无限重发。

工具输出需要限制大小并保留可继续读取的位置。截断日志时明确标注截断区间、总量及文件定位,不能让模型误以为拿到了完整证据。上下文管理见 上下文工程。

专用工具将参数与业务语义暴露给执行器,便于校验、授权和记录。Shell 工具提供更通用的能力,但命令允许列表本身不足以限制程序所能做的全部操作;仍需通过执行身份、文件和网络访问范围、隔离环境与资源限制控制实际权限。具体策略应匹配已经授权的任务,不要让每一步可逆操作都退化为重复确认,详见 安全与防护。

补充:环境模拟器与真实执行

Agent 训练和评估可以使用环境模拟器:策略提出动作,模拟器产生观察,再让策略继续。语言世界模型尝试学习这种环境响应,例如 Qwen-AgentWorld 模型卡 描述了面向多类交互环境的模拟能力。这与真正修改文件或调用业务服务属于不同执行路径。

模拟器可能生成貌似合理但状态不一致的结果。评估时不仅看单次观察是否像真,还要看连续动作的因果一致性、资源约束与长期状态是否保持;模拟环境中的高分不能直接证明真实工具执行成功率。是否适合作为模拟器或 Agent,应分别测量,不能用激活参数量断言某类角色天然可行或不可行。

继续阅读:工具接口与 MCP。