Skip to content

理解 Agent 的工作循环:ReAct、Function Calling 与 Multi-Agent

约 3238 字大约 11 分钟

AIAgentReActFunction Calling

2025-10-03

Agent 不只是“大模型加几个工具”。这篇学习笔记从一个完整的任务循环出发,梳理 ReAct 如何驱动行动、Function Calling 如何连接工具,以及 Multi-Agent 如何拆分和协作复杂任务。

最近在补 Agent 相关的知识。刚开始看一些演示时,我对 Agent 的理解比较简单:给大模型挂上几个工具,再写一段 Prompt,它就能自己完成任务。

继续往下看才发现,工具只是 Agent 能够接触外部世界的入口。一个任务怎样开始、怎样选择工具、拿到结果后怎样继续、什么时候结束,以及复杂任务怎样拆分,这些才是 Agent 系统真正需要解决的问题。

梳理到最后,我发现 ReAct、Function Calling 和 Multi-Agent 刚好对应了三个关键环节:Agent 如何行动、如何使用工具、如何与其他 Agent 协作。这篇文章记录一下目前对这三部分的理解。

从一次回答变成一个过程

普通的大模型调用比较像一次问答:输入问题,模型生成答案,本轮结束。

Agent 不太一样。它面对的往往不是一个只靠已有知识就能回答的问题,而是一个需要持续获取信息、执行动作的目标。例如:

command

检查服务最近一小时为什么频繁告警,并整理一份排查建议。

要完成这个目标,模型可能需要查询监控指标、读取错误日志、查看最近发布记录,然后把几部分信息放在一起判断。任何一个步骤返回的结果,都可能改变下一步动作。

我现在更愿意把 Agent 理解为一个运行在循环中的决策组件:

在这个过程里,大模型负责理解当前状态并提出下一步动作,Agent 引擎负责保存状态、调用工具、处理异常和控制循环。两者放在一起,才是一个完整的执行过程。

ReAct:边行动,边修正判断

ReAct 是 Reasoning and Acting 的组合。它强调的不是先制定一份完整计划再一口气执行,而是让推理和行动交替进行:

  1. 根据当前目标和已有信息判断下一步;
  2. 选择并执行一个动作;
  3. 读取外部环境返回的结果;
  4. 用新的结果更新判断;
  5. 重复以上过程,直到任务完成。

ReAct 原论文里把这个过程描述为 Thought、Action、Observation 的交替。不过落到实际系统中,我觉得更重要的是理解这套反馈机制,不一定要把模型完整的思考过程展示或保存下来。

还是以前面的服务告警为例:

这里有一个很明显的特点:开始时并不知道最终要调用多少次工具。查询监控以后,如果发现 CPU 没有问题,就没必要继续沿着 CPU 过载的方向排查,而应该转去看日志。

这正是 ReAct 比固定工作流灵活的地方——下一步不是预先写死的,而是由当前观察结果决定。

一个最小循环

抛开具体框架,一个简化后的 Agent 循环大概是这样:

MAX_STEPS = 8

def run_agent(goal, session):
    state = load_state(session)

    for _ in range(MAX_STEPS):
        decision = model.decide(
            goal=goal,
            context=state.context,
            tools=tool_registry.available_tools(session.user),
        )

        if decision.type == "final":
            return decision.content

        tool_call = validate(decision.tool_call)
        tool_result = execute(tool_call)
        state.append(tool_call, tool_result)

    return "执行步骤超过上限,已保留当前进度。"

代码不复杂,但里面有几个不能省略的部分:

  • state 保存任务走到哪里,以及工具返回了什么;
  • validate 校验模型给出的工具名和参数;
  • MAX_STEPS 防止模型陷入无休止的循环;
  • available_tools 根据用户身份和当前场景限制可用工具;
  • 工具失败时要区分参数错误、业务错误和临时故障,而不是一律重试。

所以 ReAct 不只是 Prompt 技巧,它最终还是要依赖一个可靠的循环控制器。

Function Calling:连接模型与程序

理解 ReAct 以后,接下来的问题是:模型如何执行 Action?

早期的一种做法,是让模型按照约定输出类似下面的文本:

Action: query_metrics
Action Input: {"service": "order-service", "minutes": 60}

程序再通过字符串解析提取工具名和参数。这种方式能用,但格式稍有变化就可能解析失败。

Function Calling 把这一步变成了结构化协议。应用先向模型描述有哪些工具、每个工具接收什么参数;模型需要工具时,返回一个符合 Schema 的调用请求。

整个流程并不是“模型直接执行函数”,而是模型提出调用,应用完成执行:

例如给模型一个查询服务指标的工具:

{
  "name": "query_service_metrics",
  "description": "查询指定服务在一段时间内的监控指标",
  "parameters": {
    "type": "object",
    "properties": {
      "service": {
        "type": "string",
        "description": "服务名称"
      },
      "minutes": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1440,
        "description": "向前查询的分钟数"
      }
    },
    "required": ["service", "minutes"],
    "additionalProperties": false
  }
}

模型可能返回:

{
  "name": "query_service_metrics",
  "arguments": {
    "service": "order-service",
    "minutes": 60
  }
}

收到这段数据后,应用至少还要做三件事:

  1. 检查工具是否真实存在,参数能否通过 Schema 校验;
  2. 检查当前用户是否有权查询这个服务;
  3. 调用监控系统,并把结果整理成模型容易理解的格式。

模型只负责选择工具和生成参数,工具能不能执行、应该以什么身份执行,仍然由应用控制。这条边界很重要。

工具要小而明确

学习 Function Calling 时,我一开始觉得把现有接口全部注册给模型就可以了。后来发现,给模型的工具并不是越多越好。

一个名字模糊、参数很多的通用接口,会让模型很难判断什么时候该用,也更容易填错参数。相比下面这样的万能接口:

operate_system(action, target, payload, options)

职责单一的工具更容易理解和控制:

query_service_metrics(service, minutes)
query_error_logs(service, start_time, end_time)
get_recent_deployments(service, limit)

工具的描述应该包含适用场景,参数最好能通过枚举、范围和必填约束排除非法状态。用户身份、租户编号等应用本来就知道的信息,也没有必要再让模型填写。

Multi-Agent:把不同职责拆开

单个 Agent 可以拥有很多工具,也可以处理很长的上下文。但随着任务变复杂,它会逐渐遇到几个问题:

  • 工具太多,选择工具的准确率下降;
  • Prompt 同时承担多个角色,规则容易互相干扰;
  • 不同任务需要的上下文不同,全部混在一起会浪费 token;
  • 某些步骤需要独立复核,不能让执行者自己给自己确认。

Multi-Agent 的思路,是把不同职责拆成多个相对独立的 Agent,再通过明确的协议传递任务。

例如做一次技术调研,可以拆成下面几个角色:

这里的协调 Agent 不需要掌握所有工具。它主要负责拆分任务、分发任务和汇总结果;检索 Agent 只关注资料来源,代码验证 Agent 只负责运行实验,校验 Agent 则检查事实和结论是否一致。

常见的协作方式

目前看到的多 Agent 协作大致有三类:

方式运行过程比较适合的场景
Supervisor由一个协调者拆分任务并汇总结果调研、报告、复杂问题分析
Handoff当前 Agent 把任务交接给更合适的 Agent客服分流、不同业务领域切换
Pipeline多个 Agent 按固定顺序处理提取、生成、审核等稳定流程

多 Agent 并不意味着几个模型随意聊天。要让协作过程稳定,Agent 之间最好传递结构化任务,而不是把整段聊天记录原样丢给下一个 Agent。

例如一次任务交接可以包含:

{
  "task_id": "task_1024",
  "goal": "验证某个开源组件是否支持流式输出",
  "known_facts": [
    "目标版本为 2.1.0",
    "需要在 Java 环境中验证"
  ],
  "constraints": [
    "只使用官方文档和实际运行结果"
  ],
  "expected_output": "结论、示例代码、验证日志"
}

这样做的好处是,接收方知道目标、已知事实和输出要求,系统也能够记录任务经过了哪些 Agent、执行了多久、在哪一步失败。

什么时候不需要 Multi-Agent

Multi-Agent 会增加模型调用次数、上下文传递和调试难度。如果一个 Agent 加几个工具就能稳定完成任务,继续拆分通常没有太大意义。

我觉得是否拆分,可以先看三个问题:

  1. 子任务之间是否真的需要不同的工具或权限?
  2. 子任务是否能够并行,或者需要独立复核?
  3. 拆分以后,任务边界能否用清楚的数据结构描述?

如果这三个问题都没有明确答案,先做好单 Agent 往往更实际。

三部分是怎样连起来的

分别理解之后,再回头看这三个概念,它们其实处在同一条执行链上:

  • ReAct 决定整个任务怎样一步步推进;
  • Function Calling 让每一步可以安全地连接外部程序;
  • Multi-Agent 让复杂步骤能够按职责继续拆分;
  • 状态、权限、超时和审计则为整个过程提供边界。

模型能力决定了 Agent 能理解多复杂的问题,而引擎设计决定了它能否稳定地把事情做完。

实际落地还要补什么

把循环跑起来只是第一步。要让 Agent 长时间稳定运行,还需要补上不少传统软件工程能力。

状态管理

对话记录不等于任务状态。除了消息,还要记录当前目标、已完成步骤、工具结果、失败次数和待确认动作。上下文过长时,需要对历史信息做摘要,只保留后续决策真正需要的事实。

终止条件

不能完全依赖模型自己说“任务完成”。系统还需要最大步骤数、最长运行时间、token 预算和工具调用次数等硬限制。

权限与确认

查询类工具和写入类工具应当区别处理。删除数据、发送消息、产生费用等操作,在执行前应该进行权限校验,必要时等待用户确认。

可观测性

一次任务最好有统一的 trace_id,能够串起模型请求、工具调用、Agent 交接、耗时和错误。否则一旦结果异常,很难知道问题出在模型判断、工具参数还是外部服务。

评测

Agent 的输出不是每次都完全相同,所以不能只靠“看起来不错”判断效果。可以准备一组固定任务,持续观察任务完成率、工具调用成功率、平均步骤数、成本以及人工介入比例。

这些部分听起来没有 ReAct 或 Multi-Agent 那么新,但真正影响系统稳定性的,往往就是这些边界。

写在最后

这次梳理让我对 Agent 有了一个更具体的认识。

它不是在聊天模型外面简单套一层工具,而是把模型放进一个可以反复获取反馈的执行循环中。ReAct 提供了循环的基本思路,Function Calling 建立了模型和程序之间的结构化接口,Multi-Agent 则提供了进一步拆分复杂任务的方法。

三者本身都不难理解,难的是把状态、权限、异常和终止条件一起考虑进去。换句话说,大模型带来了新的决策方式,但把这种不确定的决策接入真实系统,最终还是离不开扎实的软件工程。

后续如果继续实践,我准备先从单 Agent 和少量只读工具开始,把 ReAct 循环、日志和评测跑顺,再考虑加入写操作和多 Agent 协作。这样每增加一层能力,都能清楚地知道它解决了什么问题。

延伸阅读