AgentLoop:一个循环如何让 LLM 持续行动

在使用 Agent 之前,我们和 AI 协作写代码的流程通常是这样的:把需求描述给网页端的 AI,它输出一版代码,我们复制粘贴到 IDE;遇到 bug 再把报错贴回去,改好后再粘回来。每一轮修改都需要手动搬运代码和错误信息,效率很低。

而 Agent 的出现改变了这个模式——它可以自主地持续处理代码,直到确认没有问题才停下来。它是怎么做到的?

答案其实不复杂:把 LLM 放进一个循环里。每次模型决定使用工具,就执行并将结果喂回模型;模型判断任务完成,循环结束。这就是 AgentLoop 的基础实现。

核心机制:循环 + 工具调用

在这里插入图片描述
LLM 本身是无状态的,你问一句它答一句,答完就结束。但如果我们把它的每次回答"接住",判断它是不是想调工具,是的话就执行、把结果塞回去再问一次——这就形成了一个循环。

关键在于模型返回时会带一个 stop_reason 字段。如果值是 tool_use,说明它还想继续干活;如果是其他值(比如 end_turn),说明它认为任务完成了。我们只需要根据这个字段决定是继续还是退出。

伪代码大概是这样:

while True:
    response = 调用 LLM(messages)
    messages.append(response)

    if response 没有调用工具:
        break

    执行工具,拿到结果
    messages.append(工具结果)

翻译成实际的 Python 代码:

def agent_loop(query):
    messages = [{"role": "user", "content": query}]

    while True:
        resp = client.messages.create(
            model="claude-sonnet-4-20250514",
            messages=messages,
            tools=[bash_tool],
        )
        messages.append({"role": "assistant", "content": resp.content})

        # 模型没调工具 → 任务结束
        if resp.stop_reason != "tool_use":
            break

        # 逐个执行模型请求的工具调用
        tool_results = []
        for block in resp.content:
            if block.type == "tool_use":
                output = execute_bash(block.input["command"])
                tool_results.append(make_tool_result(block.id, output))

        # 把执行结果喂回去,进入下一轮
        messages.append({"role": "user", "content": tool_results})

就这么几十行。模型负责想"下一步做什么",代码负责"动手做",循环负责把结果传回去让模型继续想。三者各司其职。

工具是怎么告诉模型的

你可能注意到了代码里的 tools=[bash_tool]。这个 bash_tool 不是随便写的字符串,它是一个结构化的定义,告诉模型三件事:这个工具叫什么、干什么用、需要哪些参数。

以 bash 工具为例,定义大概长这样:

bash_tool = {
    "name": "bash",
    "description": "执行 bash shell 命令",
    "input_schema": {
        "type": "object",
        "properties": {
            "command": {
                "type": "string",
                "description": "要执行的 shell 命令"
            }
        },
        "required": ["command"]
    }
}

模型拿到这个定义后,就知道自己可以输出一个叫 bash 的工具调用,带上 command 参数。它并不是真的在"执行命令",它只是输出了一个结构化的请求——“我想调用 bash,参数是 ls -la”。真正执行的是我们的代码。

这也是为什么后面我们可以给模型加更多工具:读文件、写文件、搜索……每个工具都是一份定义,模型看完就知道什么时候该用哪个。工具越多,模型的能力边界就越宽,但循环本身不需要改。

消息是怎么流动的

理解 agent loop 最容易遗漏的一点是:每一轮循环,messages 数组都在增长。它就像一个不断膨胀的对话历史,每一步的输入、输出、工具结果都堆积在里面。

以上面"创建 TODO.md"的场景为例,messages 的变化过程是这样的:

初始状态:
  [user: "帮我看下当前目录有没有 TODO.md,没有就创建一个"]

第一轮循环后:
  [user: "帮我看下当前目录有没有 TODO.md,没有就创建一个"]
  [assistant: 调用 bash("ls TODO.md")]
  [user: 工具结果 → "No such file or directory"]

第二轮循环后:
  [user: "帮我看下当前目录有没有 TODO.md,没有就创建一个"]
  [assistant: 调用 bash("ls TODO.md")]
  [user: 工具结果 → "No such file or directory"]
  [assistant: 调用 bash("touch TODO.md")]
  [user: 工具结果 → ""(成功,无输出)]

第三轮循环后:
  [user: "帮我看下当前目录有没有 TODO.md,没有就创建一个"]
  [assistant: 调用 bash("ls TODO.md")]
  [user: 工具结果 → "No such file or directory"]
  [assistant: 调用 bash("touch TODO.md")]
  [user: 工具结果 → ""]
  [assistant: "已创建 TODO.md"]

注意最后一轮,模型的回复里没有 tool_use 块,只有普通文本,所以循环退出。但此时 messages 已经有 6 条记录了——模型每一步决策都基于完整的上下文,它能看到自己之前做了什么、结果是什么。

这也引出一个问题:如果循环跑了太多轮,messages 会越来越长,最终超出模型的上下文窗口,当然这个我们先不深入讨论。

循环会不会停不下来

一个很自然的担忧:如果模型一直决定调工具,循环岂不是永远不结束?

确实有可能。模型偶尔会陷入"做了又检查、检查了又做"的死循环。所以实际的 agent 几乎都会加一个 max_turns 上限:

def agent_loop(query, max_turns=20):
    messages = [{"role": "user", "content": query}]

    for turn in range(max_turns):  # 用 for 代替 while True
        resp = client.messages.create(messages=messages, tools=[bash_tool])
        messages.append({"role": "assistant", "content": resp.content})

        if resp.stop_reason != "tool_use":
            break

        tool_results = []
        for block in resp.content:
            if block.type == "tool_use":
                output = execute_bash(block.input["command"])
                tool_results.append(make_tool_result(block.id, output))
        messages.append({"role": "user", "content": tool_results})
    else:
        # 跑满轮次还没结束,强制退出
        print("达到最大轮次,停止执行")

for 循环加一个上限,比裸的 while True 安全得多。Claude Code 默认是 20 轮,你也可以根据场景调整。有些框架还会加"连续相同工具调用检测"——如果模型连续三次调了同一个工具且参数一样,就认为它卡住了,直接中断。

这些都是生产环境的考量。对于理解原理来说,while True 就够了;但真正写 agent 时,别忘了加个保险。

一个具体场景

用户输入:“帮我看下当前目录有没有 TODO.md,没有就创建一个”。

第一轮:模型决定调 bash,执行 ls TODO.md,发现文件不存在。
第二轮:模型看到上一步的报错,决定调 bash,执行 touch TODO.md
第三轮:模型确认任务完成,输出"已创建 TODO.md",不再调工具,循环结束。

我们看到了,模型并不是一步到位的。它每一步都能看到之前的执行结果,根据实际情况调整下一步动作。命令写错了也没关系,错误信息会回到模型,它会自己修正。这就是 agent 和"一次性生成代码"最大的区别——它可以观察、调整、重试。

那 Claude Code ?

Claude Code 是一个产品级的 agent,它的主循环文件 query.ts 有一千七百多行。但剥开所有保护机制,骨架就是上面那个 while True

在骨架之上,它额外做了很多事:

  • 流式执行:模型还没生成完就开始执行工具,不用等全部输出
  • 异常恢复:token 超限、模型报错、用户中断,每种都有对应的处理策略
  • 上下文管理:对话太长自动压缩历史,防止超出窗口
  • 权限分级:有些工具直接跑,有些要用户确认

但这些全是"在循环上做加法"。核心循环从第一天起就没变过。

小结

回到开头的问题:agent 是怎么做到"一直处理"的?

答案就是一个循环。给模型消息和可用工具,模型决定下一步——调工具就执行并反馈结果,不调就结束。Claude Code 如此,市面上能看到的 agent 框架,骨架都是这个。把LLM放在循环当中,就可以鞭策它一直为我们干活了。

注:本文参考了 GitHub 开源学习项目:learn-claude-code。这是一个非常优秀的学习资源,推荐读者结合项目内容一同学习。

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐