1. 前言
我们设计一个“寻宝游戏”(也就是链式依赖任务):三个文件形成线索链,大模型必须依次读取它们,才能拼出最终密码。
三个相互关联的文件内容设计如下:
step1.txt 内容如下:
线索1:请去读取 step2.txt 文件获取下一步指示。
step2.txt 内容如下:
线索2:做得好!密码的前半部分是 'Agent',请继续读取 step3.txt 获取后半部分。
step3.txt 内容如下:
线索3:密码的后半部分是 'Loop'。请将密码拼接后告诉用户最终的完整密码是什么。
我们在上一篇文章中已经实现了大模型对文件内容的读取,我们现在使用上一篇实现的功能执行以下内容:
请帮我读取 step1.txt 文件,并严格按照文件中的线索一步步寻找,直到拼凑出最终的完整密码告诉我。
结果如下:
我们看到只执行了第一步就不再执行了。
所以我们发现上一篇文章实现的功能,大模型是无法在不需人工干预的情况下自主执行多步工具的调用。
所以我们需要一个能反复调用工具、观察结果并继续决策的循环体,这就是 Agent Loop。
2. 为什么需要 Agent Loop ?
大型语言模型(LLM)在代码生成、逻辑推理等方面表现惊人。然而,它们本质上是一个“静态”的推理引擎:只能基于已有的上下文生成文本,无法主动与外部世界交互——不能执行命令、读取文件、运行测试、查看报错信息等。为了让大模型操作外部工具,OpenAI 提出了 Function Calling(工具调用)机制,我们也在上一篇文章分析和实现了这个功能。
很明显我们不但需要实现大模型调用工具,还需要实现一个能反复调用工具、观察结果并继续决策的循环体。
因为从前言中的例子我们可以知道在没有循环调用工具的情况下,我们只能手动将工具的输出粘贴回对话中,充当人肉中转站。这不仅低效,更违背了“自主智能体”的初衷。
而 Agent Loop 正是为了解决这一问题而应运而生:将模型与工具连接起来,让模型根据环境反馈自主决定下一步动作,直到任务完成。
那么怎么实现呢?
3. 核心概念:一个循环 + 工具
Agent Loop 的架构极其简洁:
while 模型请求调用工具:
执行工具调用
将结果反馈给模型
它的运行流程则可以用下图表示:
+----------+ +-------+ +---------+
| 用户 | ---> | LLM | ---> | 工具 |
| prompt | | | | 执行 |
+----------+ +---+---+ +----+----+
^ |
| 工具结果 |
+----------------+
(循环到模型不再调用工具)
大模型每次生成响应后,我们需要检查它是否要求调用工具。如果有,就执行对应工具,将结果以“工具消息”的形式追加到对话历史中,然后再次调用大模型。大模型在新的上下文中继续推理,可能再次调用工具,也可能直接给出最终答案。
4. 代码实现
下面我们基于 Python 详细解析 Agent Loop 的实现。
4.1 环境准备
import os
import json
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
# 加载环境变量(如 DEEPSEEK_API_KEY)
load_dotenv()
# ---------- 初始化客户端 ----------
# 创建 OpenAI 客户端实例,使用 DeepSeek API 密钥和基础 URL
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com"
)
加载环境变量,初始化 OpenAI 客户端。
4.2 工具定义
# ---------- 工具定义 ----------
tools = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "读取文本文件内容。",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "要读取的文件路径"},
"encoding": {"type": "string", "enum": ["utf-8", "gbk"], "description": "文件编码格式"}
},
"required": ["path"]
}
}
}
]
这里使用 OpenAI 的 function calling 格式定义了一个 read_file 工具。工具描述了名称、用途和参数,大模型会根据这些信息决定何时调用、传什么参数。
4.3 工具实现
# ---------- 工具实现 ----------
class ReadFileTool:
def execute(self, path: str, encoding: str = "utf-8") -> str:
try:
file_path = Path(path).expanduser()
if not file_path.exists():
return f"❌ 文件不存在: {path}"
return file_path.read_text(encoding=encoding)
except Exception as e:
return f"❌ 读取失败: {str(e)}"
file_tool = ReadFileTool()
工具的执行逻辑被封装在 ReadFileTool 类中。它接收路径和编码,返回文件内容或错误信息。错误信息也作为正常输出返回,让模型能处理异常情况。
上面几个步骤,我们在前面的文章已经实现过了,相信大家不会陌生了。接下来才是本篇文章的重点。
4.4 核心循环:Agent Loop
# -- 核心模式:一个不断调用工具的 while 循环,直到模型停止 --
def agent_loop(messages: list):
"""
核心代理循环:
不断地调用大模型,如果模型返回了工具调用,则执行工具并将结果发回给模型,
直到模型不再需要调用工具并返回最终文本回复为止。
"""
while True:
# 调用大模型,传入历史消息、工具列表,并让模型自动决定是否调用工具
response = client.chat.completions.create(
model="deepseek-chat", # 使用的模型名称
messages=messages,
tools=tools,
tool_choice="auto"
)
msg = response.choices[0].message
# 将助手的回复(可能包含工具调用,也可能是普通文本)添加到历史记录中
messages.append(msg)
# 如果模型没有调用工具,说明已完成任务,返回模型回复的文本内容
if not msg.tool_calls:
return msg.content
# 否则,模型决定调用工具,遍历所有的工具调用
for tool_call in msg.tool_calls:
if tool_call.function.name == "read_file":
# 解析模型生成的工具调用参数
args = json.loads(tool_call.function.arguments)
print(f"\033[33m🔧 调用工具: {tool_call.function.name}, 参数: {args}\033[0m")
# 执行工具函数
result = file_tool.execute(**args)
# 打印工具执行结果的前200个字符
print(f"✅ 工具执行结果:\n{result[:200]}\n")
# 将工具执行结果作为 tool 类型的消息追加到历史记录中
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"name": tool_call.function.name,
"content": result
})
上述代码便是 Agent Loop 的核心实现,主要功能如下:
- 循环调用大模型,每次传入最新的消息列表和工具定义。
- 将模型的响应(
msg)追加到历史。 - 检查
tool_calls:如果没有,返回msg.content结束循环。 - 如果有工具调用,遍历每个调用,解析参数,执行对应工具,并将结果以
tool角色消息加入历史(包含tool_call_id以便关联)。 - 循环继续,大模型看到工具结果,会继续分析并决定下一步。
4.5 交互终端
if __name__ == "__main__":
# 初始化历史消息列表,包含系统提示词,系统提示词,用于指导助手的行为
history = [
{"role": "system", "content": "你是一个文件读取助手,必要时可以调用工具帮助用户读取文件内容。"}
]
# 启动交互式终端会话
while True:
try:
# 获取用户输入
query = input("\033[36m用户 >> \033[0m")
except (EOFError, KeyboardInterrupt):
# 处理 Ctrl+D 或 Ctrl+C 退出的情况
break
# 处理正常退出的输入
if query.strip().lower() in ("q", "exit", "退出"):
break
# 将用户输入追加到历史记录中
history.append({"role": "user", "content": query})
# 启动代理循环进行对话
final_answer = agent_loop(history)
# 打印助手给出的最终答案
if final_answer:
print(f"\033[32m助手: {final_answer}\033[0m\n")
交互终端主循环并支持多轮对话,每次用户输入后都调用 agent_loop,并打印大模型的最终答案。整个程序就是一个可在终端交互的自主文件读取助手。
5. 链式依赖任务测试
我们在在终端启动程序后,输入以下内容:
请帮我读取 step1.txt 文件,并严格按照文件中的线索一步步寻找,直到拼凑出最终的完整密码告诉我。
执行结果如下:
我们可以看到整个过程是完全自主的,大模型根据每次工具返回的线索,动态规划下一步,直到任务完成,这就是 AI Agent 的核心功能 — Agent Loop。
6. Agent Loop 工作原理总结
一个典型的 Agent Loop 包含以下步骤:
-
用户输入:用户的消息作为第一条消息加入历史中。
-
调用模型:将完整对话历史(包括系统提示)和工具定义发送给 LLM。
-
解析响应:
- 如果模型没有调用工具(
tool_calls为空),则返回最终内容,循环结束。 - 如果模型调用了工具,则执行对应工具。
- 如果模型没有调用工具(
-
执行工具:遍历所有工具调用,运行对应的工具函数,收集结果。
-
反馈结果:将每个工具的结果包装成
tool角色的消息,追加到历史中。 -
回到步骤2:模型在新的上下文中再次推理。
这个过程一直持续,直到大模型认为任务完成,不再请求工具。这就是自主智能体的最小闭环。
7. 总结
我们用不到30行代码,实现了将大模型的推理能力与外部工具的执行能力无缝连接,使大模型能够根据环境反馈自主决策,完成多步复杂的任务。这就是 AI 智能体的最小核心模式 —— Agent Loop。
无论是简单的文件读取,还是复杂的代码编写、代码运行,其背后都是同一个逻辑原理:模型 → 工具 → 结果 → 模型。理解并掌握这个逻辑原理,你就掌握了智能体设计的钥匙。在此基础上,你可以添加策略、记忆、多智能体协作等高级特性,但核心永远不会变——Agent Loop。
一个工具 + 一个循环 = 一个智能体,就是智能体的核心秘密。
我是 Cobyte,欢迎添加 v: icobyte,学习交流 AI 全栈。