Agent运行时:循环、工具与提示词

目录

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. 上下文预算要在调用前成立

每次推理需要容纳固定指令、工具定义、用户输入、历史与输出预留。如果固定部分已经超限,就应缩小工具范围、分段处理输入或报告限制;不能把负数预算继续交给摘要函数。

一种可用的组装顺序是:

  1. 保留必要任务约束和结构化当前状态
  2. 选择本轮真正需要的工具定义
  3. 为输出和协议开销预留空间
  4. 按预算加入最近的完整消息组与相关历史
  5. 对更早材料先提取或摘要,再从上下文中移出
  6. 调用前重新计量,确认总量满足模型限制

工具调用及其结果应作为完整协议组处理。先删除旧历史再“摘要旧历史”,会丢掉摘要所需材料;直接截断 JSON 字符串也可能破坏结构。较大的工具结果应由工具返回分页、字段选择或摘要视图,并保留受控的原始结果引用。

摘要可能遗漏或歪曲事实。订单号、请求 ID、确认状态和待办应尽量保存在结构化状态中,重要证据保留可回查来源,不能只存在于一段不断重写的自然语言摘要里。

在每轮新增量为 T、固定前缀为 S、全量重发的假设下,N 轮输入量为 N×S + T×N×(N+1)/2。这不等于账单金额;缓存命中、输入输出单价和其他费用需按具体服务计量。不要用某一供应商的折扣比例概括所有模型服务。

10. 用异常路径验证运行时

正常查询只能证明基本链路可走通。上线前更有区分度的用例包括:

用例 预期观察
模型提出不存在的工具 拒绝调用,并记录明确错误
查询无权访问的订单 服务端拒绝,模型无法改身份绕过
一次返回多个调用,途中预算耗尽 后续调用未执行且有状态记录
写接口超时但已受理 恢复时查询原请求,不生成重复操作
用户取消长任务 不再发起新动作,已开始操作可追踪
相同参数合理轮询 遵守间隔和时限,不机械当成攻击
摘要后继续调查 订单标识、未知事项与证据引用仍保留
模型提前声称完成 结果校验能识别未完成事项

运行时的价值,是让每次动作有依据、每次结果可追踪、每次停止有明确语义。只有这些基础成立,后续的记忆、规划与多 Agent 协作才有可靠的执行载体。

评论