同样是大模型,为什么聊天机器人只能给建议,Agent 却能查资料、调用接口、修改文件,还能在失败后继续处理?本文用一个订单查询案例,拆开工具调用、任务规划、状态管理、多轮执行与失败恢复,并给出一份可在本地运行的 Python 示例。

Agent 开发:从会回答到能完成任务

一、先看一个很容易被误解的场景

假设用户给系统发了一句话:

帮我看看 A1001 为什么还没到。如果物流延误,整理一份催办建议,先不要替我发送。

一个纯文本模型,即使回答得很像客服,也未必掌握这笔订单的实际情况。它可能知道“物流延误通常与天气、仓储、中转有关”,但这些常识不能证明 A1001 的包裹为什么没有送达。

一个能实际处理这件事的系统,需要完成更多动作:识别订单号,查询订单,拿到运单号,补查物流,判断证据是否足够,再生成符合用户要求的答复。如果查询超时,还要判断能否重试;如果订单不存在,就应停下来说明原因。

这里有一个容易忽略的要求:用户只同意生成催办建议。即使系统已经知道收件人和消息模板,也不能把“有能力发送”理解成“已经获得发送授权”。

这一小段业务,就包含了 Agent 开发里最核心的几个问题:

  • 谁决定下一步做什么?
  • 谁真正执行接口调用?
  • 上一步的结果保存在哪里?
  • 失败以后怎样继续?
  • 什么情况下算完成,什么情况下必须停下?

本文的订单、运单和物流记录均为虚构教学数据。后面的本地示例用固定决策规则模拟模型这一层,方便先观察执行闭环;它不会连接真实订单系统,也不会调用付费模型接口。

二、Agent 到底是什么?先把几个角色分清

对于本文讨论的工具型 Agent,可以把它理解为:模型根据目标和已有结果提出下一步动作,应用程序执行动作并返回结果,这个过程持续到完成目标或触发停止条件。

这是一种工程上的理解方式,不代表所有产品都遵循完全相同的实现,也不意味着一定需要多智能体。

把一个最小系统拆开,通常能看到下面几个部分。

部分负责什么放到订单案例里
目标与约束说明要完成什么、边界在哪里查询未送达原因,只生成催办建议
模型决策根据当前信息选择动作或输出答复先查订单,再决定是否补查物流
工具契约定义能调用什么、参数与返回值是什么get_order(order_id)
执行器校验请求、调用实际代码、处理异常调用订单服务,拦截非法参数
状态保存进度、结果、错误与待处理事项已查到运单 S9001,物流尚未返回
停止与验收判断何时结束,以及结果是否满足要求证据充分后答复,超时耗尽则明确失败

这里最值得建立的认识是:模型提出调用,应用负责执行;应用还要决定这次执行是否被允许。

例如,模型返回一个工具名,并不等于这段函数已经运行。模型说“已经帮你催办”,也不能替代消息服务的发送回执。开发者要把模型输出、工具执行和业务结果分别记录,才知道系统到底做到了哪一步。

有些系统把循环、状态和工具处理封装在 SDK 或平台里;自己写循环时,这些责任就需要由自己的代码承担。

三、一次工具调用,完整经过了什么?

先把“规划十几个步骤”的复杂情况放在一边,只看第一次查询订单。

Agent 工具调用闭环:模型、执行器与外部系统如何配合

1. 应用把工具能力告诉模型

模型需要知道:工具叫什么,适合解决什么问题,需要哪些输入。以 OpenAI Responses API 的函数工具格式为例,定义可以长这样:

order_tool = {
    "type": "function",
    "name": "get_order",
    "description": "按订单号查询订单状态及运单号,不会修改订单。",
    "strict": True,
    "parameters": {
        "type": "object",
        "properties": {
            "order_id": {"type": "string", "description": "例如 A1001"}
        },
        "required": ["order_id"],
        "additionalProperties": False
    }
}

这个定义只是告诉模型如何申请调用,实际查询代码仍需由应用实现。strict 约束调用参数的结构;用户有没有权限看这笔订单、订单号是否属于当前租户,仍要由服务端校验。

2. 模型返回调用请求

模型可能返回一个名为 get_order 的函数调用,参数是订单号。调用还带有标识,用于把稍后的结果对应回这次请求。

开发者常说的 Function Calling,重点就在这个边界:模型生成的是一个可交给程序处理的调用请求。真正的数据库查询或 HTTP 请求发生在执行器中。

3. 执行器校验并运行工具

执行器先检查工具是否在允许列表里,再检查参数、权限和当前状态,最后调用订单服务。否则,模型一旦给出错误工具名或额外参数,程序就可能执行不符合预期的动作。

这里的“工具”可以是一段本地函数,也可以是包装好的数据库查询、HTTP 接口或其他外部能力。工具怎么连接,是工程实现的问题;是否允许执行,是应用的控制逻辑。

4. 工具结果返回给模型

例如,订单工具返回:

{
  "order_id": "A1001",
  "status": "shipped",
  "shipment_id": "S9001",
  "source": "demo_order_db"
}

接下来,模型才有机会依据这个结果判断:订单已经发货,但“未送达的具体原因”还没有证据,需要继续查询物流。

OpenAI 的官方文档把函数调用描述为应用与模型之间的多步交互,包括提供工具、接收调用、执行代码、回传结果,再获得答复或更多调用。具体字段应以所用 API 的文档为准。官方调用流程

一个 JSON 合法,不代表业务就做对了

格式校验能回答“字段齐不齐、类型对不对”,却无法单独回答“订单属于谁、信息是否过期、这个动作是否合规”。即使结构化输出符合 Schema,内容依然可能有错误。结构化输出的错误处理说明

举一个实际开发中很常见的反例:order_id 是合法字符串,但来自另一个用户。Schema 校验完全可能通过,业务权限校验却必须拒绝。这两层都要有。

四、任务规划:先列清依赖,再决定哪些可以并行

“帮我处理一下”是目标,不是可直接执行的程序。Agent 需要把目标转成动作,而开发者要为动作提供边界。

订单案例可以拆成下面这张任务表:

步骤需要的输入产生的结果能否直接并行
确认订单号用户请求A1001不必调用工具
查询订单订单号与用户身份状态、运单号后续依赖它
查询物流上一步的运单号最新节点、异常信息等运单号可用后执行
查询催办规则业务规则入口催办条件与建议模板条件独立时可并行读取
生成建议订单、物流、相关规则有依据的建议等所需证据齐备
发送催办收件人、内容、授权发送回执本例没有授权,应保留为待办

并行的前提是输入已经齐备,而且动作之间不存在必须遵守的先后关系。还没拿到运单号,就同时启动物流查询,通常只会制造无效请求。

规划也不需要一次写得特别大。对于边界清晰的短任务,先确定一个粗略顺序,拿到真实结果以后再调整下一步,往往比提前生成一长串未经验证的步骤更容易控制。

假设订单工具告诉我们“订单已取消”,原来设想的物流查询就可能不再需要。此时继续机械执行最初计划,反而会越走越偏。计划应该帮助执行,执行结果也应该能够修正计划。

单智能体、多工具、多智能体,是三个不同维度

一个 Agent 连续调用订单、物流、规则查询三个工具,仍可以由同一个决策主体完成。工具数量多,并不自动变成多智能体。

如果引入独立的检索 Agent、分析 Agent 和复核 Agent,还需要说明每一方的任务、输入输出、权限和交接条件。否则,表面上多了几个角色,底层只是反复转发同一份信息。

我更建议从任务依赖出发做选择:能由一个受控循环完成的流程,先把这个循环验证清楚;当任务存在相对独立的专业分工,再考虑拆出多个决策主体。并行带来的收益,要扣除重复上下文、协调与结果合并的成本。

五、状态管理:别把所有东西都塞进聊天记录

长任务最怕的情况之一,是界面还在显示“处理中”,程序却已经不知道自己停在哪一步。

聊天记录适合保留人机沟通和工具交互,但业务执行还需要更明确的状态。例如:任务标识、当前阶段、已获取的证据、待返回的调用、尝试次数,以及最终是否允许执行后续动作。

下面是一份应用自己定义的教学状态,它不是某个模型 API 的标准返回格式:

{
  "task_id": "order-demo-001",
  "goal": "查询订单并生成催办建议",
  "status": "waiting_tool",
  "current_step": "get_logistics",
  "pending_call_id": "call-002",
  "facts": {"order_id": "A1001", "shipment_id": "S9001"},
  "completed_steps": ["get_order"],
  "constraints": {"send_allowed": false},
  "attempt": 1
}

把这些字段提出来以后,很多问题就更容易解释:系统正在等待哪一个调用,刷新页面后应该恢复哪一个任务,同一个请求是否已经处理过,最后一次失败发生在哪里。

需要区分三种不同的“保存”

对话上下文用来让模型了解之前发生了什么。任务状态用来让程序判断当前进度和下一步。事件记录用来回看执行经过、排查问题。

它们可以互相引用,但不能互相替代。事件日志里有一句“开始发送”,不能证明发送成功;聊天里有一段总结,也不等于数据库中的任务状态已经更新。

对话上下文可以手动管理,也可以使用 API 提供的会话延续机制。例如 Responses API 支持通过前一个响应标识延续交互。但这并不会自动替业务系统补上任务持久化、权限控制和外部操作的去重逻辑。会话状态说明

恢复任务时,先恢复事实,再让模型继续

假设程序在查完订单后重启。正确的恢复起点,应该是已保存的订单结果、待处理的物流查询及其状态;如果只是把用户原话重新发给模型,就可能再次查询、重复生成,甚至重复执行外部动作。

对于写操作,还应保存能关联业务结果的标识,例如消息编号或工单编号。恢复时先查实际结果,再决定是否重试。只记住“上次好像发送过”,不够支撑可靠恢复。

六、多轮执行的核心:有反馈的循环,也要有明确出口

可以把运行过程写成这样一段逻辑:

读取目标与当前状态
→ 选择下一步动作
→ 校验动作与参数
→ 执行工具
→ 记录结果并更新状态
→ 判断继续、完成、暂停或失败

循环让系统有机会根据新证据调整动作,出口则避免它一直转下去。

我通常会先把下面几类终止条件写进设计,而不是等到“跑不动了”再补:

  1. 满足目标:已有证据足以回答用户,且需要交付的内容已经生成。
  2. 需要补充信息:关键参数缺失,不能靠猜测继续。
  3. 等待授权:下一步会产生外部影响,但当前尚未获得许可。
  4. 资源到限:步骤数、运行时间或预算达到应用设置的上限。
  5. 不可恢复失败:必要工具不可用,或多次尝试仍未获得有效结果。

还要区分“本轮模型输出结束”和“业务任务完成”。模型生成了一个完整句子,只能说明这一段输出结束;订单问题有没有查明,需要业务验收条件来判断。

同样,循环走到最大步数时,不应该为了给用户一个好看的结果,把状态标成成功。更合理的做法是保留当前证据,说明已完成哪些步骤、卡在哪里,并给出接下来的处理入口。

七、写一个能本地运行的最小闭环

下面用 Python 标准库做一个教学版本。把它保存为 agent_demo.py 后运行:

python agent_demo.py

这份代码刻意保留两个容易观察的行为:订单查询第一次抛出模拟超时,执行器只重试一次;拿到运单号之后,决策层再要求查询物流。

请注意:demo_policy 是固定规则,不是大模型。 这样可以先把“决策—执行—结果—再决策”看清楚。接入真实模型时,需要替换决策层,并按照对应 API 正确维护消息及调用结果。

"""Offline teaching demo: the policy is scripted, not a live LLM."""
import json
import re
from copy import deepcopy


def make_tools():
    attempts = {"order": 0}

    def get_order(order_id):
        attempts["order"] += 1
        if attempts["order"] == 1:
            raise TimeoutError("模拟一次查询超时")
        if order_id != "A1001":
            raise LookupError("订单不存在")
        return {"order_id": order_id, "shipment_id": "S9001",
                "status": "shipped", "source": "demo_order_db"}

    def get_logistics(shipment_id):
        if shipment_id != "S9001":
            raise LookupError("运单不存在")
        return {"shipment_id": shipment_id, "status": "delayed",
                "reason": "演示数据:中转延误", "source": "demo_carrier"}

    return {"get_order": get_order, "get_logistics": get_logistics}


def execute(call, tools, retries=1):
    name, args = call.get("name"), call.get("arguments")
    fields = {"get_order": "order_id", "get_logistics": "shipment_id"}
    if name not in fields or name not in tools:
        return {"ok": False, "error": "tool_not_allowed"}
    key = fields[name]
    if (not isinstance(args, dict) or set(args) != {key}
            or not isinstance(args[key], str)
            or not re.fullmatch(r"[AS][0-9]{4}", args[key])):
        return {"ok": False, "error": "invalid_arguments"}
    for attempt in range(retries + 1):
        try:
            data = tools[name](**args)
            return {"ok": True, "data": data, "attempts": attempt + 1}
        except TimeoutError:
            if attempt == retries:
                return {"ok": False, "error": "timeout"}
        except LookupError as exc:
            return {"ok": False, "error": "not_found", "detail": str(exc)}


def demo_policy(state):
    # 真正接模型时,只替换此决策层;当前用固定规则演示。
    observations = state["observations"]
    if not observations:
        return {"kind": "tool", "name": "get_order",
                "arguments": {"order_id": "A1001"}}
    latest = observations[-1]["result"]
    if not latest["ok"]:
        return {"kind": "final", "text": "查询失败,暂不能判断物流原因。"}
    if len(observations) == 1:
        return {"kind": "tool", "name": "get_logistics",
                "arguments": {"shipment_id": latest["data"]["shipment_id"]}}
    data = latest["data"]
    return {"kind": "final", "text": f"订单已发货;{data['reason']}。"
            "建议联系承运商核实。此处只生成建议,没有发送催办。"}


def run(policy=demo_policy, tools=None, max_steps=5):
    state = {"goal": "查询订单与物流,生成催办建议", "status": "running",
             "observations": [], "events": [], "answer": None}
    tools = make_tools() if tools is None else tools
    for step in range(1, max_steps + 1):
        action = policy(deepcopy(state))
        if action.get("kind") == "final":
            state.update(status="completed", answer=action["text"])
            return state
        if action.get("kind") != "tool":
            state["status"] = "invalid_action"
            return state
        call_id = f"demo-call-{step}"
        result = execute(action, tools)
        state["observations"].append({"call_id": call_id,
                                     "name": action["name"], "result": result})
        state["events"].append({"step": step, "call_id": call_id,
                               "name": action["name"], "result": result})
        if not result["ok"]:
            state["status"] = "failed"
            state["answer"] = "工具失败,证据不足;请检查记录或人工处理。"
            return state
    state["status"] = "step_limit"
    return state


if __name__ == "__main__":
    print(json.dumps(run(), ensure_ascii=False, indent=2))

运行结果里,正常示例应出现这些关键信息:

status: completed
第一个工具: get_order,attempts: 2
第二个工具: get_logistics,返回 delayed
answer: 订单已发货;演示数据:中转延误……没有发送催办。

这里的 completed 表示查询与建议生成这项任务结束,不能理解为包裹已经送达,也不能理解为系统已经成功催办。

这份示例做到了什么,还缺什么?

它包含工具允许列表、简单参数校验、有限重试、观察结果记录,以及步骤上限。正常执行中,决策层能够先使用订单结果,再发起物流查询。

它没有实现登录鉴权、真实服务连接、任务落库、跨进程恢复、模型请求超时和全局预算控制。state 还在内存里,程序退出后不会自动恢复。读者应把它当作运行机制的教学骨架,再根据业务补齐这些能力。

示例中的重试只针对我们明确声明的只读查询;真实 HTTP 请求还应设置连接和读取超时。真实模型返回的动作也要经过完整验证,不能直接把任意工具名映射到任意 Python 函数,更不能使用 eval 执行模型返回的字符串。

为了检查这份示例的控制逻辑,我在本地跑了六类离线检查:正常查询及一次超时恢复、非法工具、非法参数、订单不存在、超时耗尽、循环达到上限。检查均通过。这个结论仅覆盖教学代码的确定性分支,没有验证任何真实模型的工具选择准确率或线上业务效果。

八、换成真实模型时,最容易接错的是哪一层?

很多初学实现里,工具本身已经写对,循环却接得不完整:拿到模型输出就直接当答案展示,或者执行了工具,却没有把工具结果传回模型。

如果采用 Responses API,模型可能返回函数调用项。应用执行后,需要按调用标识回传结果;模型收到新结果后,才有下一轮判断的依据。

可以用下面的教学片段理解结果对应关系,这不是完整可运行的 API 接入代码:

# call 来自模型返回的 function_call 项。
# observation 来自应用执行工具后的结果。
tool_result = {
    "type": "function_call_output",
    "call_id": call.call_id,
    "output": json.dumps(observation, ensure_ascii=False)
}
# 再按所用 API 的会话方式,把它交回模型。

实际接入时,要处理多条调用、模型拒绝、响应不完整以及工具异常。手动维护历史时,应保留 API 要求延续的输出项,而不能只保存展示给用户的文本;使用服务端会话延续时,也要遵守该接口的状态和保留策略。

还有一个很实际的接口设计问题:工具结果不要只返回“成功”两个字。订单工具最好能返回业务标识、状态、数据来源和必要的时间信息;失败时返回可供程序识别的错误类型。模型和执行器需要的是能够据此继续处理的信息。

如果要连接真实数据库,建议让模型调用有明确用途的业务工具,例如“按当前用户查询订单”,而不是一开始就暴露可以执行任意语句的入口。身份、租户范围和查询权限尽量由应用注入,避免让模型自行决定这些边界。

九、失败恢复:先判断失败性质,再谈重试

Agent 的状态流转与失败恢复:重试、暂停、确认和终止

Agent 出错以后,最危险的处理方式之一,是把所有异常都包装成“再试一次”。不同失败,需要不同的下一步。

失败情况更合适的处理不宜直接做什么
参数缺失或格式错误补全信息,或让决策层纠正参数原样重复同一个坏请求
只读查询短暂超时在限次和超时策略内重试无限等待、无限重试
无权限访问停止该动作,说明权限问题换个身份继续探测
查询结果为空说明未查到,确认标识与数据范围编造一个看似合理的结果
写操作返回前断线查询业务结果,按幂等机制核对直接再发送、再创建一次
缺少执行授权保存待办并请求确认把用户的咨询当作授权
达到步骤或预算上限返回已完成部分与明确阻塞点把半成品标记为全部完成

超时,不等于什么都没发生

对查询来说,超时通常意味着我们暂时没拿到结果。对创建工单、发送消息这类写操作来说,服务端可能已经完成动作,只是响应没有传回来。

因此,恢复的第一步应是确认外部事实。如果消息系统能返回业务请求编号,就用它查询是否已经发送;如果接口支持幂等键,重试时沿用同一业务操作的键,让服务端识别重复请求。

幂等键不是在日志里随便存一个字符串就生效。它需要接收方或自己的业务存储真正检查并去重。这个机制没实现之前,“失败后自动再发”很容易制造重复消息。

重试也应该被观察到

只记录最终成功,会让运维人员看不到中间反复失败的现象。建议把尝试次数、错误类别、各次耗时与最终状态一起留下,这样才能区分偶发抖动和长期退化。

在本文示例中,attempts: 2 说明订单工具经过一次失败才拿到结果;如果只看最终答复,这个过程就会完全消失。

十、可观测性:让“处理中”变成能解释的状态

给 Agent 加一块大屏并不难,难的是画面里的每一个状态都有可靠的数据来源。

如果工具仍在执行,界面应该能说明正在等待哪个调用;如果工具已经返回,而模型正在生成下一步,界面应切换到模型处理状态。如果任务已经结束,也应结束相应的活跃提示。

对于一个最小项目,我建议先记录以下字段:

  • 任务 ID、调用 ID、步骤序号,以及调用之间的关联。
  • 工具名、开始与结束时间、状态、重试次数。
  • 经脱敏或裁剪后的参数与结果摘要。
  • 模型请求用量和耗时(以接口实际返回或可核对记录为准)。
  • 最终输出、外部结果标识,以及业务验收结论。

这些记录比一句“Agent 正在努力处理”更有解释力,也有利于把问题定位到具体层:模型选择了错误工具,执行器拦截了请求,外部服务超时,还是工具结果已经回来但没有进入下一轮。

需要注意,公开执行事件可以展示操作顺序和结果;它不等同于模型内部的神经活动,也不能把没有记录的内容补成真实思考过程。动画可以帮助阅读状态,但不能替代状态本身。

我们之前做的只读可视化项目,也是在这个边界上工作:读取可获取的执行事件,将真实状态映射到界面。对于单个 Agent,可以把不同工具通道分别展示,没有必要把每个工具都包装成独立智能体。

相关开源实现:agent-display-codex。其中真实数据接入是在原开源项目基础上的适配;视觉效果与执行事实应分别理解。

十一、怎样判断一个 Agent 真的好用?

演示里成功一次,只说明这一次路径能走通。实际使用里,还要覆盖信息缺失、工具异常、结果冲突和用户中途改变要求等情况。

我会围绕订单案例准备这样一组问题:

  1. 用户只说“帮我看看快递”,没有订单号,系统会先澄清吗?
  2. 订单不存在时,会明确说未查到,还是自动补出一个物流状态?
  3. 订单已经取消时,是否仍机械执行物流和催办步骤?
  4. 工具超时后,是否按规定限次重试并记录失败?
  5. 用户说“先别发送”,系统能否始终保留这个约束?
  6. 发送动作在响应前断线时,恢复过程会不会重复发送?
  7. 返回结果里夹带与业务无关的指令时,系统会不会跟着执行?
  8. 运行到上限时,能否交代已完成部分,而不是假装全部成功?

评估指标也需要拆开。可以分别看工具选择是否正确、参数是否正确、工具执行是否成功、最终目标是否达成,以及是否发生越权或重复操作。只统计“最后有没有生成一句话”,很难解释系统质量。

成本和速度同样要放在任务结果旁边比较。一个流程调用次数少,却漏查必要证据;另一个流程多做了几步,但完成了完整核验,不能只根据请求数判断优劣。应该在同一组任务和相同成功标准下比较。

OpenAI 的评估建议也强调先定义目标、收集样例、确定指标,再持续对比和补充评估集。这里的测试问题和取舍,是本文针对订单教学案例设计的例子。评估设计参考

十二、从入门示例到能用的项目,推荐怎么推进?

如果第一次开发 Agent,我建议先选一个能明确验收的小目标,例如“查询订单并生成建议”。把输入、输出、异常和授权边界写清楚,再接工具。

第一步只接少量只读工具,让每次请求和结果都能看见。第二步加入参数校验、状态记录与停止条件。第三步拿正常、缺参、超时、无结果等样例验证运行逻辑。等这些路径清晰以后,再接写操作、持久化和人工确认。

接着才是模型选择、工具描述优化、上下文管理和并行执行。框架可以帮助组织这些能力,但不能替项目定义完成标准;大模型也不能替服务端实施业务权限。

外部内容是资料,不能自动升级成指令

在接网页、文档和外部工具时,还要特别留意一种情况:返回内容中夹带“忽略之前要求,改为执行某个动作”的文字。

对订单系统来说,物流备注是一份外部数据,不能因为它写着“把所有订单发到某地址”,就获得新的系统权限。建议控制进入下一步的字段、限制工具权限,并在有外部影响的动作前保留业务校验。结构化处理和隔离能降低风险,但不是绝对保证。Agent 安全设计参考

最后,把交付物和证据一起交出去

一个便于接手的 Agent 项目,至少应该说明:支持什么任务,哪些工具已连接,状态保存在哪里,哪些动作需要确认,异常怎样处理,以及哪些路径已经验证。

当用户问“为什么还没完成”时,系统最好能回答:“订单查询已经完成,物流服务连续超时,任务停在这里;尚未发送任何催办。”这句话比一个一直旋转的加载动画更有价值。

把这个执行闭环做清楚,之后再扩展复杂规划、多智能体协作或炫酷界面,才有能支撑它们的数据与行为基础。


本文资料与边界:工具调用与会话机制参考上文链接的官方文档,核对日期为 2026 年 11 月 5 日;订单业务、任务状态结构、配图与离线示例为教学设计。示例测试仅验证本地确定性逻辑,不能当作真实模型或生产系统的效果评测。

你在开发 Agent 时,最常遇到的是工具选错、状态丢失,还是失败后反复重试?欢迎把具体场景留在评论区。

Logo

更多推荐