理解 Agent 的工作循环:ReAct、Function Calling 与 Multi-Agent
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 的组合。它强调的不是先制定一份完整计划再一口气执行,而是让推理和行动交替进行:
- 根据当前目标和已有信息判断下一步;
- 选择并执行一个动作;
- 读取外部环境返回的结果;
- 用新的结果更新判断;
- 重复以上过程,直到任务完成。
ReAct 原论文里把这个过程描述为 Thought、Action、Observation 的交替。不过落到实际系统中,我觉得更重要的是理解这套反馈机制,不一定要把模型完整的思考过程展示或保存下来。
还是以前面的服务告警为例:
.png)
这里有一个很明显的特点:开始时并不知道最终要调用多少次工具。查询监控以后,如果发现 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 的调用请求。
整个流程并不是“模型直接执行函数”,而是模型提出调用,应用完成执行:
.png)
例如给模型一个查询服务指标的工具:
{
"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
}
}收到这段数据后,应用至少还要做三件事:
- 检查工具是否真实存在,参数能否通过 Schema 校验;
- 检查当前用户是否有权查询这个服务;
- 调用监控系统,并把结果整理成模型容易理解的格式。
模型只负责选择工具和生成参数,工具能不能执行、应该以什么身份执行,仍然由应用控制。这条边界很重要。
工具要小而明确
学习 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,再通过明确的协议传递任务。
例如做一次技术调研,可以拆成下面几个角色:
.png)
这里的协调 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 加几个工具就能稳定完成任务,继续拆分通常没有太大意义。
我觉得是否拆分,可以先看三个问题:
- 子任务之间是否真的需要不同的工具或权限?
- 子任务是否能够并行,或者需要独立复核?
- 拆分以后,任务边界能否用清楚的数据结构描述?
如果这三个问题都没有明确答案,先做好单 Agent 往往更实际。
三部分是怎样连起来的
分别理解之后,再回头看这三个概念,它们其实处在同一条执行链上:
.png)
- ReAct 决定整个任务怎样一步步推进;
- Function Calling 让每一步可以安全地连接外部程序;
- Multi-Agent 让复杂步骤能够按职责继续拆分;
- 状态、权限、超时和审计则为整个过程提供边界。
模型能力决定了 Agent 能理解多复杂的问题,而引擎设计决定了它能否稳定地把事情做完。
实际落地还要补什么
把循环跑起来只是第一步。要让 Agent 长时间稳定运行,还需要补上不少传统软件工程能力。
状态管理
对话记录不等于任务状态。除了消息,还要记录当前目标、已完成步骤、工具结果、失败次数和待确认动作。上下文过长时,需要对历史信息做摘要,只保留后续决策真正需要的事实。
终止条件
不能完全依赖模型自己说“任务完成”。系统还需要最大步骤数、最长运行时间、token 预算和工具调用次数等硬限制。
权限与确认
查询类工具和写入类工具应当区别处理。删除数据、发送消息、产生费用等操作,在执行前应该进行权限校验,必要时等待用户确认。
可观测性
一次任务最好有统一的 trace_id,能够串起模型请求、工具调用、Agent 交接、耗时和错误。否则一旦结果异常,很难知道问题出在模型判断、工具参数还是外部服务。
评测
Agent 的输出不是每次都完全相同,所以不能只靠“看起来不错”判断效果。可以准备一组固定任务,持续观察任务完成率、工具调用成功率、平均步骤数、成本以及人工介入比例。
这些部分听起来没有 ReAct 或 Multi-Agent 那么新,但真正影响系统稳定性的,往往就是这些边界。
写在最后
这次梳理让我对 Agent 有了一个更具体的认识。
它不是在聊天模型外面简单套一层工具,而是把模型放进一个可以反复获取反馈的执行循环中。ReAct 提供了循环的基本思路,Function Calling 建立了模型和程序之间的结构化接口,Multi-Agent 则提供了进一步拆分复杂任务的方法。
三者本身都不难理解,难的是把状态、权限、异常和终止条件一起考虑进去。换句话说,大模型带来了新的决策方式,但把这种不确定的决策接入真实系统,最终还是离不开扎实的软件工程。
后续如果继续实践,我准备先从单 Agent 和少量只读工具开始,把 ReAct 循环、日志和评测跑顺,再考虑加入写操作和多 Agent 协作。这样每增加一层能力,都能清楚地知道它解决了什么问题。