AgentLoop:一个循环如何让 LLM 持续行动
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。这是一个非常优秀的学习资源,推荐读者结合项目内容一同学习。
更多推荐



所有评论(0)