AgentDepthAgent 工程学习指南
输入概念或问题,检索正文和待编写目录。
第一次学习?从 L0 总览开始 →

第一个可运行实验:带停止条件的工具调用循环

我的学习 →

正在读取学习记录…

一次工具调用拿到结果后,助手还要决定:已经能回答了吗,还是需要再查一次?Agent Loop(智能体循环)就是重复“读取当前信息 → 选择下一步 → 执行动作 → 收到结果”的过程,直到任务完成或满足停止条件。

想象你找一份文件:先看当前目录,决定去哪个文件夹,打开它,看到新的内容,再决定继续找还是结束。程序中的“观察”也是读取当前可用信息;“动作”则是查询、读取或其他工具调用。每次动作带来新信息,下一轮据此继续。

两轮完成一次天气查询

沿用 L2 的天气例子,任务是“查北京天气并回答”。用固定天气表和脚本化选择观察循环,流程是:

轮次当前可用信息选择的动作得到什么
第 1 轮问题已知,天气结果还没有调用 weather,city 为北京固定表返回 26°C
第 2 轮已有天气结果组织最终答复完成,结束循环

状态是程序保存的当前情况:有没有结果、走了几轮、任务是否结束。last_result 保存上一轮的结果,events 保存过程记录,status 保存任务结果。循环靠这些数据推进。

events 可以理解成按时间记下的办事记录。第一轮写入“请求北京天气”,随后写入“工具返回26°C”,第二轮写入最终答复。每条记录的 type 表示发生了什么;请求与工具结果使用相同的 call_id,例如 call-1,方便找到哪条结果对应哪次请求。这个列表供程序和读者回看过程;下一轮直接使用的是 last_result 中的工具结果。可观察轨迹会继续讲怎样连接任务、调用和资料来源。

当前信息进入选择步骤,工具请求经过检查和执行,结果回到当前信息;最终答复或轮数耗尽时退出
工具结果沿下方箭头回到下一轮。选择最终答复时结束;达到轮数上限时返回预算耗尽。

循环就像“再看一眼、再做一步”的助手。程序还需要一个停止开关,免得它查了一百次同一份天气,仿佛靠刷新能刷新出晴天。

保存完整程序

先在 PowerShell 创建并进入学习目录:

Bash
mkdir "F:\学习\l3-first-project" -Force
cd "F:\学习\l3-first-project"

保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。

创建学习文件夹,保存下面的代码为 agent.py。它使用 Python 自带的 argparse 读取命令参数,json 输出结构化结果;天气来自固定数据,选择过程由代码脚本决定。

先在 PowerShell 创建并进入学习目录:

Bash
mkdir "F:\学习\l3-first-project" -Force
cd "F:\学习\l3-first-project"

保存下面的文件后,在这个终端运行 dir 核对文件名,再执行本节给出的运行命令。

Python
"""用脚本化请求和固定天气表观察工具调用循环。"""
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 ,把当前例子的文件夹拖入终端并回车,运行下面的命令。

Bash
ls
python -X utf8 agent.py --case normal

--case 表示要观察的场景,normal 是正常路径。argparse 将这个参数读到 args.case,再传给 run。有关命令参数的说明可查 Python argparse 文档。

对照输出看这两轮发生了什么

正常路径输出:

JSON
{
  "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”对应起来;下一轮可以使用这个结果。

现在回到源码:

  1. last_result = None 表示起初没有工具结果。
  2. 第一轮跳过最终答复分支,创建天气请求。
  3. execute(request) 检查工具名和城市,返回固定天气。
  4. 返回值存入 last_result,事件中记下请求与结果。
  5. 第二轮发现 last_result 已存在,组织答复并返回 completed。

return 会结束整个 run 函数,因此循环也结束。真正接入模型后,应用把工具结果回传模型,由模型返回下一个请求或最终答复;请求检查、轮数限制与状态记录仍由应用组织。

最大轮数怎样影响结果

for step in range(max_steps) 限制最多执行多少轮。默认上限是 3,正常路径只用了 2 轮。

先运行一轮上限:

Bash
python -X utf8 agent.py --case normal --max-steps 1

输出的 status 是 budget_exhausted,意思是轮数预算已经用完。事件里有查询请求与工具结果,但还没有机会进入第二轮生成答复。

再运行两轮上限:

Bash
python -X utf8 agent.py --case normal --max-steps 2

输出为 completed,并出现 final。两条命令的工具结果相同,差别是程序有没有得到下一轮处理结果的机会。

设置实际经过的步骤状态
normal,上限 1查询天气、收到结果budget_exhausted
normal,上限 2查询天气、收到结果、组织答复completed
budget,上限 2两轮都继续查询budget_exhausted

budget 场景故意持续请求工具:

Bash
python -X utf8 agent.py --case budget --max-steps 2

你会看到 call-1 和 call-2,各有对应结果,最后停止。这里计算的是循环轮数;Token、时间与费用预算在后面的预算篇继续加入。

检查失败怎样结束这一轮

正常过程看清后,分别运行:

Bash
python -X utf8 agent.py --case invalid
python -X utf8 agent.py --case unauthorized

invalid 把城市改成空字符串。execute 抛出 ValueError,run 接住异常,写入 rejection 事件并返回 rejected。

unauthorized 将工具名改成 delete_file,应用的任务清单里只有 weather,因此在工具检查处停止。这里观察的是工具允许列表;读取具体用户资源时,还会用到 L2 的身份和归属检查。

两条路径都有请求和拒绝事件。错误提示解释具体检查项,便于你修正输入后再运行。

场景最后一个事件状态接下来怎么处理
normalfinalcompleted展示答复
invalidrejection:城市要求rejected修改城市参数
unauthorizedrejection:工具要求rejected核对任务工具清单
budgettool_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,把这个固定的选择过程扩展为根据新观察改变下一步动作。

课后练习展开习题收起习题

4 道练习 · 客观题规则评分 · 其余题目待批改

正在读取本地记录…

不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。

练习 1 / 4 · v1.0.0

选择题 · 为什么循环停止了?

运行 budget 场景,max_steps=2,输出 budget_exhausted。这说明什么?

对应知识点:L3-loop、L3-termination

选择一个答案
练习 2 / 4 · v1.0.0

简答题 · 退出码与任务状态

程序退出码为 0,但 JSON 中 status=rejected。可以对用户说任务完成了吗?为什么?

对应知识点:L3-state

练习 3 / 4 · v1.0.0

代码题 · 写出预算验收断言

对 run('budget', max_steps=2) 写测试:确认状态、请求次数和没有 final 事件。可提交 Python 或伪代码。

对应知识点:L3-loop、L3-budget

练习 4 / 4 · v1.0.0

场景题 · 接入真实订单工具前

准备把 weather 换成真实创建订单工具。只改函数名是否足够?列出至少三项需要补充或验证的边界。

对应知识点:L3-errors、L3-human

图片查看器
100%

文章配图 · 按 Esc 关闭