Appearance
Agent 是怎么运转的
前面我们已经知道,Agent 的核心不是像模型一样一次生成结果,而是围绕目标持续:
text
判断
↓
行动
↓
观察
↓
再判断但真正把一个 Agent 跑起来时,这个循环内部到底发生了什么呢?
我们从一次真实任务的执行过程来看:
一个 Agent 从接到任务开始,到最后完成、失败或者交给人工,中间到底经历了哪些步骤。
从一次 Agent Run 开始
先认识两个概念:
- Agent Run
- Agent Loop
Agent Run 可以理解成:
一次完整的 Agent 任务执行。
例如给 Coding Agent 一个任务:
修复项目中的登录接口 500 错误。
从 Agent 接收到这个目标开始,到最后:
- 修复成功
- 执行失败
- 达到停止条件
- 请求人工介入
这一整段过程,都可以看作一次 Agent Run。
一个 Run 内部通常不会只调用一次大模型。
因为 Agent 需要不断和外部环境交互。
例如:
text
读取日志
↓
分析错误
↓
搜索代码
↓
读取文件
↓
修改代码
↓
执行测试测试以后又可能失败。
于是任务不会结束,而是继续:
text
测试失败
↓
读取新的错误
↓
重新判断
↓
继续修改所以一次 Agent Run 内部,往往会包含很多轮重复执行。
这些不断重复的过程,就是:
Agent Loop。
可以先把两者理解成:
text
Agent Run
│
├─ Loop 1
├─ Loop 2
├─ Loop 3
├─ Loop 4
│
└─ 任务结束也就是说:
Run 是一次完整任务,Loop 是任务内部不断重复的执行循环。
一轮 Agent Loop 里发生了什么
把一个 Agent Loop 展开以后,可以大致看到下面几个步骤:
text
构建当前上下文
↓
模型决定下一步
↓
调用工具 / 执行动作
↓
获取环境反馈
↓
更新当前状态
↓
进入下一轮这几步不断重复,Agent 才能够把一个复杂任务逐步往前推进。
构建当前上下文
Agent 每一次让模型做判断之前,都需要先告诉模型:
我们现在到底处于什么状态?
这就是当前一轮的上下文。
上下文中可能包含:
- 系统指令
- 对话历史
- 用户目标
- 当前任务状态
- 已经执行过的步骤
- 上一轮工具调用结果
- 当前可用的工具
- 必要的外部资料
例如用户要求:
修复登录接口 500 错误。
Agent 第一轮可能拥有的上下文非常少:
text
系统指令:
你现在是 Coding Agent,请帮我完成这个任务。
目标:
修复登录接口 500 错误
当前信息:
暂无错误日志
可用工具:
读取文件
搜索代码
执行命令
修改文件
运行测试模型看到这些信息以后,可能判断:
现在还不知道错误原因,应该先查看日志。
于是进入下一步。
工具执行以后,Agent 获得:
text
NullPointerException
at LoginService.java:82第二轮的上下文就发生变化了:
text
目标:
修复登录接口 500 错误
已知信息:
LoginService.java 第 82 行发生 NullPointerException
已经执行:
读取错误日志
下一步需要:
继续定位异常原因所以 Agent 并不是每一轮都从头开始。
它真正做的是:
基于当前已经掌握的信息继续判断。
上下文越准确,模型越容易做出正确的下一步决策。
注:在实际系统中,这些上下文信息通常以结构化的消息历史(Message History / Tool Call & Tool Result)或工作记忆的形式追加并传入模型。
模型决定下一步动作
上下文准备完成以后,模型开始判断:
现在最应该做什么?
它可能决定:
- 直接生成最终回答
- 搜索资料
- 读取文件
- 查询数据库
- 执行代码
- 调用浏览器
- 请求用户补充信息
- 调用某个业务 API
- 结束当前任务
例如看到:
text
NullPointerException
at LoginService.java:82模型可能选择:
text
读取 LoginService.java再比如 Research Agent 已经搜索了一轮资料,但发现信息不足。
模型可能判断:
text
继续搜索官方文档所以模型在 Agent 中承担的一个重要职责就是:
根据当前状态选择下一步动作。
需要注意的是,模型通常只是产生:
“我要调用哪个工具,以及传什么参数。”
真正执行工具的并不是模型本身。
调用工具并执行动作
如果模型决定调用工具,Agent 系统就会真正执行这个动作。
例如模型生成:
text
read_file("LoginService.java")真正负责读取文件的,是 Agent 外部的工具执行环境。
可以简单理解成:
text
模型
↓
决定调用 read_file
↓
Agent Runtime / Harness
↓
真正读取文件
↓
返回结果所以更准确地说:
模型负责决定做什么,工具系统负责真正去做。
不同 Agent 拥有的工具完全不同。
Coding Agent 可能有:
text
read_file
search_code
write_file
run_command
run_testResearch Agent 可能有:
text
search_web
open_page
read_documentData Agent 可能有:
text
query_database
execute_sql
run_pythonBrowser Agent 则可能有:
text
click
type
scroll
open_url
download工具决定了 Agent 能和哪些外部环境发生交互。
获取环境反馈
工具执行以后,会返回结果。
这个结果可以叫:
- Observation
- Tool Result
- Feedback
名称虽然不同,但表达的是同一件事:
Agent 刚才做了一个动作,外部环境给出了什么结果?
例如:
text
read_file("LoginService.java")可能返回:
text
public User login(String username) {
User user = userRepository.findByUsername(username);
return user.getProfile();
}或者:
text
run_test()返回:
text
FAILED
NullPointerException:
userRepository.findByUsername(username) returned nullBrowser Agent 点击按钮以后,也可能获得:
text
页面跳转成功或者:
text
表单验证失败:Email is required这些反馈非常重要。
因为 Agent 不是只依赖自己刚才的推理继续往下走。
它需要不断从真实环境中拿到新的信息。
所以整个过程实际上是:
text
模型认为应该做 A
↓
真正执行 A
↓
环境返回结果
↓
再根据真实结果继续判断这就是 Agent 能够逐渐逼近真实任务结果的基础。
更新状态并进入下一轮
工具结果返回以后,Agent 不能只是“看一下”。
还要把新的信息加入当前任务状态。
例如:
text
目标:
修复登录接口 500 错误
已读取:
LoginService.java
当前发现:
findByUsername 可能返回 null
测试结果:
失败
任务状态:
尚未完成这些新状态会参与下一轮上下文构建。
于是:
text
新的状态
↓
新的上下文
↓
再次调用模型
↓
模型重新判断模型可能继续判断:
需要给用户不存在的情况增加空值处理。
然后:
text
修改代码
↓
运行测试
↓
测试结果
↓
更新状态
↓
再次进入模型这样一轮一轮执行下去。
所以一个完整的 Agent Loop 可以进一步展开成:
text
当前状态
↓
构建上下文
↓
LLM
↓
选择动作
↓
工具执行
↓
Observation
↓
更新状态
↓
进入下一轮前一步的结果,就是下一步判断的重要输入。
为什么 Agent 需要循环执行
既然模型已经可以推理,为什么不能一开始就让它生成完整计划,然后从头执行到尾?
原因在于:
真实任务的很多信息,在任务开始时根本不存在。
例如 Coding Agent 不可能在运行测试之前,就准确知道测试会返回什么。
Research Agent 也不可能在搜索之前,就知道:
- 会找到哪些页面
- 信息是否完整
- 不同来源有没有冲突
Browser Agent 也无法提前知道:
- 页面有没有弹窗
- 是否需要登录
- 表单有没有验证失败
- 点击以后会跳到哪里
真实任务更像:
text
做一步
↓
看看发生了什么
↓
再决定下一步而不是:
text
第一次就预测所有情况
↓
一次性执行完整计划例如:
text
修改代码
↓
运行测试
↓
失败测试失败这一信息,在真正执行测试之前并不知道。
有了新的错误以后,Agent 才能继续:
text
读取错误
↓
重新判断
↓
再次修改所以 Agent 天然就适合循环执行。
可以把它理解成:
text
Think
↓
Act
↓
Observe
↓
Think Again或者我们前面一直使用的:
text
判断
↓
行动
↓
观察
↓
再判断Agent 的关键,并不是第一次就把所有任务都想对。
而是:
能够在新的反馈出现以后继续修正。
Agent 怎么知道什么时候结束
既然 Agent 会循环运行,就一定还需要解决一个问题:
什么时候应该停止?
如果没有停止条件,Agent 很容易变成:
text
判断
↓
行动
↓
失败
↓
继续判断
↓
继续行动
↓
……最后无限循环。
所以一次 Agent Run 通常需要明确的退出条件。
任务已经完成
最正常的结束方式,就是目标已经完成。
例如 Coding Agent:
text
修改代码
↓
运行测试
↓
测试全部通过
↓
任务完成Research Agent:
text
已经找到足够资料
↓
关键事实完成验证
↓
生成报告
↓
任务完成Browser Agent:
text
填写表单
↓
提交
↓
页面显示成功
↓
任务完成所以 Agent 在每一轮结束以后,都需要判断:
当前结果是否已经满足用户目标?
如果已经满足,就可以退出 Agent Loop,并返回最终结果。
达到停止条件
任务不一定每次都能正常完成。
所以系统通常还会设计一些硬性停止条件。
例如:
- 最大执行步数
- 最大重试次数
- 最大运行时间
- Token 预算
- API 成本限制
- 连续工具失败次数
例如:
text
最大 Agent Loop:30 次执行到第 30 轮仍然没有完成任务:
text
继续执行?
↓
否
↓
停止 Agent Run或者工具连续失败:
text
调用 API
↓
失败
重试
↓
失败
再次重试
↓
失败
达到最大重试次数
↓
停止这些限制并不是为了让 Agent “变笨”。
而是为了防止:
- 无限循环
- 无限消耗 Token
- 不断重复错误动作
- 长时间占用计算资源
所以一个可运行的 Agent 系统必须知道:
什么时候不能再继续。
需要人工介入
还有一种很常见的情况:
Agent 不是结束任务,而是把控制权暂时交给人。
例如 Agent 准备执行:
text
删除文件
发送正式邮件
提交退款
部署生产环境
修改数据库这些动作可能风险比较高。
系统可以设计成:
text
Agent 准备执行操作
↓
风险检查
↓
需要人工确认
↓
暂停 Run
↓
用户允许 / 拒绝如果允许:
text
继续执行如果拒绝:
text
调整路径 / 停止任务还有一种情况是 Agent 自己无法继续。
例如:
text
连续搜索多次
↓
仍然无法确认关键信息
↓
请求用户补充资料或者:
text
没有权限访问数据库
↓
请求人工处理所以 Agent 并不一定追求:
完全无人参与。
更常见的是:
可以自动推进的大部分步骤自动推进,需要确认的地方再交给人工。
执行失败以后会发生什么
Agent 执行真实任务时,失败非常正常。
例如:
- API 超时
- 搜索没有结果
- 文件不存在
- SQL 执行错误
- 浏览器页面变化
- 模型判断方向错误
关键不在于:
Agent 会不会失败。
而在于:
失败以后系统怎么继续。
重试当前动作
如果失败更像暂时性的异常,可以直接重试。
例如:
text
调用 API
↓
Timeout
↓
等待
↓
再次调用或者:
text
网页加载失败
↓
刷新页面
↓
重新读取这种情况下,原来的决策本身可能没有问题。
只是工具执行暂时失败。
所以最简单的处理方式就是:
重试当前动作。
当然,重试通常也要有次数限制。
否则:
text
失败
↓
重试
↓
失败
↓
重试
↓
……仍然可能陷入循环。
调整路径继续执行
还有一些失败说明:
原来的方案不对。
这时继续重复同一个动作没有意义。
Agent 需要根据失败结果修改路径。
例如 Research Agent:
text
搜索关键词 A
↓
没有有效结果
↓
重新组织关键词
↓
搜索关键词 BCoding Agent:
text
修改方案 A
↓
测试失败
↓
分析新的错误
↓
采用方案 BData Agent:
text
按渠道查询
↓
没有发现异常
↓
换成按地区查询Browser Agent:
text
点击按钮
↓
页面没有变化
↓
重新定位按钮
↓
换一种操作方式这种能力就是 Agent 和普通固定脚本之间非常明显的区别之一。
系统可以利用失败反馈重新做决策,而不是一直走原来的路径。
停止并返回控制权
如果多次重试和调整以后仍然无法完成任务,就不应该无限尝试。
例如:
text
信息始终不足或者:
text
没有执行权限或者:
text
工具连续失败或者:
text
已经达到最大步数这时更合理的做法是:
text
停止当前 Agent Run
↓
返回当前进度
↓
说明失败原因
↓
告诉用户需要什么例如:
已经检查登录模块和数据库连接,目前无法继续确认问题,因为缺少生产环境日志。
这比 Agent 自己不断尝试更加可靠。
所以失败处理的最终原则可以概括成:
text
能重试 → 重试
需要换方案 → 调整路径
无法继续 → 停止并交还控制权一个完整的 Agent 执行过程
把前面的内容放到一起,我们再完整看一次 Agent 是怎么运行的。
假设用户提出:
修复项目里的登录接口 500 错误。
第一步,创建一次 Agent Run。
text
用户目标:
修复登录接口 500 错误系统准备当前上下文:
text
目标
+
项目基本信息
+
可用工具
+
当前任务状态模型开始第一次判断:
先读取错误日志。
于是模型请求:
text
read_log()工具真正执行以后返回:
text
NullPointerException
at LoginService.java:82系统把这个 Observation 写回任务状态:
text
已知:
LoginService.java 第 82 行出现空指针异常进入下一轮。
模型看到新的上下文以后判断:
应该读取 LoginService.java。
于是调用:
text
read_file("LoginService.java")工具返回代码。
模型分析以后发现:
text
userRepository.findByUsername(username)可能返回 null。
于是下一轮决定修改代码。
text
write_file(...)修改完成以后,模型决定运行测试。
text
run_test()结果:
text
FAILED
UserRepository mock is null这时候任务还没有完成。
系统把新的失败信息写回状态:
text
修改已经完成
但测试仍然失败
新的问题:UserRepository mock is null再次进入下一轮。
模型重新判断:
测试环境中的 Mock 配置可能有问题。
于是继续搜索测试代码。
text
search_code("UserRepository")找到相关测试以后继续修改。
再次执行:
text
run_test()这次返回:
text
PASSEDAgent 判断:
text
登录相关测试通过
↓
目标已经满足于是结束 Agent Run,并生成最终结果:
已修复登录接口空指针问题,同时补充了测试中的 UserRepository Mock 配置,相关测试已经通过。
整个执行过程可以压缩成:
text
用户目标
↓
创建 Agent Run
↓
构建上下文
↓
模型判断
↓
调用工具
↓
Observation
↓
更新状态
↓
重新构建上下文
↓
模型再次判断
↓
继续行动
↓
……
↓
满足停止条件
↓
结束 Run再抽象一层,就是:
text
Run
↓
Context
↓
Decision
↓
Action
↓
Observation
↓
State Update
↓
Loop
↓
Exit这基本就是一个 Agent 从接到任务到最终结束的完整运行过程。
总结
理解 Agent 怎么运转,可以先区分两个概念:
text
Agent Run
→ 一次完整任务执行
Agent Loop
→ Run 内部不断重复的执行循环一轮最基本的 Agent Loop 可以表示成:
text
构建上下文
↓
模型判断
↓
执行动作
↓
获取反馈
↓
更新状态
↓
再次循环其中最重要的一点是:
Agent 会把真实环境中的执行结果重新带回下一轮决策。
所以它不是:
text
想一次
↓
执行到底而更像:
text
判断
↓
行动
↓
观察
↓
修正
↓
继续行动一次 Agent Run 最终通常会以几种方式结束:
- 任务正常完成
- 达到停止条件
- 执行失败后无法继续
- 需要人工确认或接管
而当中间步骤失败时,Agent 可以:
text
重试
调整路径
停止并返回控制权所以真正理解 Agent 的运行原理以后,会发现 Agent 的关键并不是“模型一次能够想多远”。
而是:
系统能不能在行动、反馈和下一次决策之间形成一个稳定、可控、能够收敛的执行闭环。