第一个可运行实验:带停止条件的工具调用循环
正在读取学习记录…
一次工具调用拿到结果后,助手还要决定:已经能回答了吗,还是需要再查一次?Agent Loop(智能体循环)就是重复“读取当前信息 → 选择下一步 → 执行动作 → 收到结果”的过程,直到任务完成或满足停止条件。
想象你找一份文件:先看当前目录,决定去哪个文件夹,打开它,看到新的内容,再决定继续找还是结束。程序中的“观察”也是读取当前可用信息;“动作”则是查询、读取或其他工具调用。每次动作带来新信息,下一轮据此继续。
两轮完成一次天气查询
沿用 L2 的天气例子,任务是“查北京天气并回答”。用固定天气表和脚本化选择观察循环,流程是:
| 轮次 | 当前可用信息 | 选择的动作 | 得到什么 |
|---|---|---|---|
| 第 1 轮 | 问题已知,天气结果还没有 | 调用 weather,city 为北京 | 固定表返回 26°C |
| 第 2 轮 | 已有天气结果 | 组织最终答复 | 完成,结束循环 |
状态是程序保存的当前情况:有没有结果、走了几轮、任务是否结束。last_result 保存上一轮的结果,events 保存过程记录,status 保存任务结果。循环靠这些数据推进。
events 可以理解成按时间记下的办事记录。第一轮写入“请求北京天气”,随后写入“工具返回26°C”,第二轮写入最终答复。每条记录的 type 表示发生了什么;请求与工具结果使用相同的 call_id,例如 call-1,方便找到哪条结果对应哪次请求。这个列表供程序和读者回看过程;下一轮直接使用的是 last_result 中的工具结果。可观察轨迹会继续讲怎样连接任务、调用和资料来源。
循环就像“再看一眼、再做一步”的助手。程序还需要一个停止开关,免得它查了一百次同一份天气,仿佛靠刷新能刷新出晴天。
保存完整程序
先在 PowerShell 创建并进入学习目录:
mkdir "F:\学习\l3-first-project" -Force
cd "F:\学习\l3-first-project"保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。
创建学习文件夹,保存下面的代码为 agent.py。它使用 Python 自带的 argparse 读取命令参数,json 输出结构化结果;天气来自固定数据,选择过程由代码脚本决定。
先在 PowerShell 创建并进入学习目录:
mkdir "F:\学习\l3-first-project" -Force
cd "F:\学习\l3-first-project"保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。
"""用脚本化请求和固定天气表观察工具调用循环。"""
import argparse
import json
def execute(request):
# 检查工具名和城市,随后提供固定天气结果。
if not isinstance(request, dict) or set(request) != {'name', 'arguments'}:
raise ValueError('请求必须包含 name 和 arguments')
if request['name'] != 'weather':
raise ValueError('工具不在本任务允许列表中')
args = request['arguments']
if not isinstance(args, dict) or set(args) != {'city'}:
raise ValueError('参数必须只包含 city')
if not isinstance(args['city'], str) or not args['city'].strip() or len(args['city']) > 80:
raise ValueError('城市必须是 1–80 个字符的非空字符串')
return {'city': args['city'].strip(), 'temperature': 26, 'mock': True}
def run(case='normal', max_steps=3):
if case not in {'normal', 'invalid', 'unauthorized', 'budget'}:
raise ValueError('未知实验场景')
if type(max_steps) is not int or max_steps < 1 or max_steps > 10:
raise ValueError('max_steps 必须是 1–10 的整数')
# events 按时间保存请求、拒绝、工具结果和最终答复;用于事后回看。
# last_result 保存工具返回值,下一轮直接读取它来组织回答。
events = []
last_result = None
for step in range(max_steps):
# 根据上一轮结果选择下一步,固定脚本代替模型的选择。
if last_result is not None and case != 'budget':
answer = f"{last_result['city']}的模拟温度为 {last_result['temperature']}°C;不是实时天气。"
events.append({'type': 'final', 'answer': answer})
return {'simulation': True, 'status': 'completed', 'events': events}
request = {'name': 'weather', 'arguments': {'city': '北京'}}
if case == 'invalid':
request['arguments']['city'] = ''
if case == 'unauthorized':
request['name'] = 'delete_file'
# call_id 是本次调用编号;请求和对应结果使用同一个编号,便于配对。
call_id = f'call-{step + 1}'
events.append({'type': 'model_request', 'scripted': True, 'call_id': call_id, 'request': request})
try:
last_result = execute(request)
except ValueError as error:
events.append({'type': 'rejection', 'call_id': call_id, 'error': str(error)})
return {'simulation': True, 'status': 'rejected', 'events': events}
events.append({'type': 'tool_result', 'call_id': call_id, 'result': last_result})
# 达到最大轮数时,保留轨迹并明确返回停止状态。
return {'simulation': True, 'status': 'budget_exhausted', 'events': events}
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--case', choices=['normal', 'invalid', 'unauthorized', 'budget'], default='normal')
parser.add_argument('--max-steps', type=int, default=3)
args = parser.parse_args()
print(json.dumps(run(args.case, args.max_steps), ensure_ascii=False, indent=2))在该文件夹的资源管理器地址栏输入 powershell,运行:
macOS:先在 Python 篇的学习文件夹中打开终端,执行 source .venv/bin/activate。再输入 cd ,把当前例子的文件夹拖入终端并回车,运行下面的命令。
ls
python -X utf8 agent.py --case normal--case 表示要观察的场景,normal 是正常路径。argparse 将这个参数读到 args.case,再传给 run。有关命令参数的说明可查 Python argparse 文档。
对照输出看这两轮发生了什么
正常路径输出:
{
"simulation": true,
"status": "completed",
"events": [
{
"type": "model_request",
"scripted": true,
"call_id": "call-1",
"request": {"name": "weather", "arguments": {"city": "北京"}}
},
{
"type": "tool_result",
"call_id": "call-1",
"result": {"city": "北京", "temperature": 26, "mock": true}
},
{
"type": "final",
"answer": "北京的模拟温度为 26°C;不是实时天气。"
}
]
}simulation、scripted 和 mock 标明脚本与固定数据来源。正常路径包含三种事件:model_request 是动作请求,tool_result 是执行结果,final 是最终答复。
请求和结果使用同一个 call_id。应用据此将“查询北京”与“北京 26°C”对应起来;下一轮可以使用这个结果。
现在回到源码:
last_result = None表示起初没有工具结果。- 第一轮跳过最终答复分支,创建天气请求。
execute(request)检查工具名和城市,返回固定天气。- 返回值存入
last_result,事件中记下请求与结果。 - 第二轮发现
last_result已存在,组织答复并返回completed。
return 会结束整个 run 函数,因此循环也结束。真正接入模型后,应用把工具结果回传模型,由模型返回下一个请求或最终答复;请求检查、轮数限制与状态记录仍由应用组织。
最大轮数怎样影响结果
for step in range(max_steps) 限制最多执行多少轮。默认上限是 3,正常路径只用了 2 轮。
先运行一轮上限:
python -X utf8 agent.py --case normal --max-steps 1输出的 status 是 budget_exhausted,意思是轮数预算已经用完。事件里有查询请求与工具结果,但还没有机会进入第二轮生成答复。
再运行两轮上限:
python -X utf8 agent.py --case normal --max-steps 2输出为 completed,并出现 final。两条命令的工具结果相同,差别是程序有没有得到下一轮处理结果的机会。
| 设置 | 实际经过的步骤 | 状态 |
|---|---|---|
| normal,上限 1 | 查询天气、收到结果 | budget_exhausted |
| normal,上限 2 | 查询天气、收到结果、组织答复 | completed |
| budget,上限 2 | 两轮都继续查询 | budget_exhausted |
budget 场景故意持续请求工具:
python -X utf8 agent.py --case budget --max-steps 2你会看到 call-1 和 call-2,各有对应结果,最后停止。这里计算的是循环轮数;Token、时间与费用预算在后面的预算篇继续加入。
检查失败怎样结束这一轮
正常过程看清后,分别运行:
python -X utf8 agent.py --case invalid
python -X utf8 agent.py --case unauthorizedinvalid 把城市改成空字符串。execute 抛出 ValueError,run 接住异常,写入 rejection 事件并返回 rejected。
unauthorized 将工具名改成 delete_file,应用的任务清单里只有 weather,因此在工具检查处停止。这里观察的是工具允许列表;读取具体用户资源时,还会用到 L2 的身份和归属检查。
两条路径都有请求和拒绝事件。错误提示解释具体检查项,便于你修正输入后再运行。
| 场景 | 最后一个事件 | 状态 | 接下来怎么处理 |
|---|---|---|---|
| normal | final | completed | 展示答复 |
| invalid | rejection:城市要求 | rejected | 修改城市参数 |
| unauthorized | rejection:工具要求 | rejected | 核对任务工具清单 |
| budget | tool_result,随后达到上限 | budget_exhausted | 根据已有结果调整后续安排 |
程序正常输出 JSON 时,终端进程可以正常退出。任务结果要读 JSON 的 status:它精确表达完成、拒绝或预算耗尽。
改城市,让结果跟着请求变化
在 run 中找到创建请求的一行,把 "北京" 改为 "上海"。运行正常场景,核对请求的城市、工具结果的城市和最终答复都变为上海。温度仍是 26,因为天气函数提供固定值。
接着把天气函数里的 'temperature': 26 改成 18,再运行。最终答复应变成 18°C。这说明答复使用了 last_result 中的结果。完成后恢复城市和温度,便于继续对照原示例。
如果 normal 没有 final,先检查轮数是否至少为 2;如果返回 rejected,查看最后一条事件里的具体错误;如果提示找不到文件,确认终端位于 agent.py 所在文件夹。
从循环走向灵活决策
这个循环里,脚本的规则是“有结果就回答,无结果就查询”。模型适配器是负责发送当前信息、把模型响应转换为动作的函数。接入它后,选择可以更灵活:发现资料不完整时补查,得到足够内容时回答。应用仍负责检查和执行动作,保存结果,并处理停止条件。
接入顺序可以保持清晰:先让一次工具请求可执行,再让结果进入下一轮,最后处理多次动作、预算和人工接管。后面学习 ReAct 时,你会看到“根据结果选择下一步”怎样组织成一种具体范式。
循环的关键变化只有一个:上一轮的结果成为下一轮的输入。能沿着 last_result 解释两轮程序,再预测一次轮数或城市改动,你就掌握了这条最小主线。
把两轮过程连起来
第一轮提出天气请求,程序检查并执行,把结果存入 last_result;第二轮读取这个结果,生成回答并结束。events 留下请求、结果和答复,call_id 把一次请求与它的结果连起来。接下来读 ReAct,把这个固定的选择过程扩展为根据新观察改变下一步动作。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 为什么循环停止了?
运行 budget 场景,max_steps=2,输出 budget_exhausted。这说明什么?
对应知识点:L3-loop、L3-termination
简答题 · 退出码与任务状态
程序退出码为 0,但 JSON 中 status=rejected。可以对用户说任务完成了吗?为什么?
对应知识点:L3-state
代码题 · 写出预算验收断言
对 run('budget', max_steps=2) 写测试:确认状态、请求次数和没有 final 事件。可提交 Python 或伪代码。
对应知识点:L3-loop、L3-budget
场景题 · 接入真实订单工具前
准备把 weather 换成真实创建订单工具。只改函数名是否足够?列出至少三项需要补充或验证的边界。
对应知识点:L3-errors、L3-human
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。