智能体主循环:generate → 执行工具 → 回灌
30 秒导读: gptme 的「自主」不是魔法,而是一个三层嵌套的循环。用户说一句话,agent 可能自己连跑好几步;每一步先让模型生成回复,把回复里的工具跑掉,再把工具输出回灌进 对话历史——只要模型还想调工具,就自动再生成一步,直到它不再调工具才停下、把控制权还给你。 本章只讲这台发动机,全部代码集中在
gptme/chat.py。
本章是 gptme 文档 的核心一章。它只回答一个问题:「让 AI 自己动手干活」这句话, 在代码里到底是怎么转起来的? 工具怎么被解析和执行(→02)、消息与上下文 怎么组装(→04)、钩子本身怎么定义(→05), 都不在这里展开。
1. 这是什么(零基础也能懂)
先建立一个直觉:什么叫「agent 自己跑多步」
想象你在终端里对一个 AI 助手说:「帮我看看这个项目为什么测试挂了。」
一个只会聊天的模型只能回你一段话:「你可以先跑 pytest 看看。」然后就停了,等你手动去跑、
再手动把结果贴回来。
而一个 agent 会这样:它先说「我先跑一下测试」——然后真的自己去跑了 pytest,拿到报错,
不用你插手,接着说「看到了,是 foo.py 第 12 行的问题,我去看一眼」——又自己打开了文件,
读完再说「我改一下」——自己改了,再自己重跑测试确认通过。整个过程你只说了开头那一句。
这就是 agent 主循环干的事:把「模型生成一段话」和「真的去执行那段话里的动作」这两件事, 串成一个能自动往前滚的循环。
一句话定义
gptme/chat.py 是 gptme 的中央控制循环:它反复地「让模型生成回复 → 执行回复里的工具 →
把工具结果回灌进历史 → 再让模型生成」,直到本轮没有工具要跑,才回到用户。
用起来什么样
下面是一次真实交互的形态(agent 在一句用户输入后,自己跑了两步):
User: 这个仓库有多少个 Python 文件?
Assistant: 我数一下。
```shell
find . -name '*.py' | wc -l
```
System: 42 ← 工具输出被"回灌"进历史,循环没有停,自动再生成
Assistant: 一共 42 个 Python 文件。 ← 这一步没有工具了,循环停下,把控制权还给你
User: ← 轮到你说下一句
注意:用户只打了一次字,agent 却生成了两次(中间夹着一次工具执行)。「什么时候该继续、 什么时候该停」不是模型用特殊 token 声明的,而是这台循环根据「上一条回复里还有没有可运行的工具」 自己判断的——这是本章最关键的机制。
一句话直觉/类比
把主循环想成一个记账员盯着一台传送带:模型往传送带上放一段回复,记账员检查里面有没有 「待办工具」;有就执行、把结果贴回账本(历史),再让模型看着更新后的账本继续写;直到某段回复 里一个待办工具都没有,记账员才合上账本,喊「下一位」。
2. 顶层全景(它大概怎么转)
三层嵌套的循环
主循环不是一个 while,而是三层,由外到内一层比一层「小」。看懂这张图,本章就懂了一半:
chat() ← 会话级:一次进程,做初始化、锁模型、装历史、收尾
└─ _run_chat_loop() ← 轮次级:一个 while,反复取"下一条输入"
每次拿一条输入(队列里的 / 用户敲的)
└─ _process_message_conversation() ← 步骤级:一个 while,自主跑多步
└─ step() → step() → step() … ← 每步 = 生成一次 + 执行工具一次
↑______________________|
只要上一步回复里"还有可运行的工具"就再来一步
一句话读这张图:「轮次」是「你说一句话」,「步骤」是「agent 为了回应这句话,自己动手的每一下」。 一轮里可以有很多步,一步里可以有很多个工具。
一步(step)内部的数据流
最内层的 step() 是发动机的活塞。它做的事恰好就是标题那三个动词:
prepare_messages reply() execute_msg()
历史 ──▶ 裁剪/组装上下文 ──▶ 模型生成一段回复 ──▶ 解析回复里的工具、逐个执行
▲ │ │
│ ▼ ▼
└────────────── 回灌:回复 + 工具输出都 append 回历史 ◀───────┘
生成(generate)→ 执行(execute tools)→ 回灌(feed back)——回灌完历史变长了,
外 层循环若判定「还有工具要跑」,就拿着这份更长的历史再进一次 step()。 自主推进就来自这个回环。
部件一句话职责
| 部件 | 干什么 | 位置(gptme/chat.py) |
|---|---|---|
chat() | 会话入口:初始化、锁定模型/流式、装载历史、切工作目录、收尾钩子 | chat.py:47 |
_run_chat_loop() | 轮次循环:统一「队列输入」和「用户输入」,拦截 /命令,处理中断 | chat.py:176 |
_process_message_conversation() | 步骤循环:自主跑多步,判断何时停,后台自动命名 | chat.py:333 |
step() | 单步:组装上下文 → 生成回复 → 执行工具,yield 出每条消息 | chat.py:520 |
_get_user_input() / _should_prompt_for_input() | 决定「该问用户」还是「直接替用户续跑」(崩溃恢复) | chat.py:483 / chat.py:449 |
_drain_external_prompt_queue() | 把磁盘上的持久排队 prompt 并进内存队列 | chat.py:317 |
3. 核心原理(逐个机制,由浅入深)
3.1 会话启动:chat() 把桌子摆好
它要解决的小问题: 循环真正转起来之前,得先确定用哪个模型、能不能流式、历史从哪来、 工作目录在哪、以及——如果我是被别的 agent 内联调用的,别把人家的输出格式搞乱。
做了哪些事(按代码顺序):
-
锁定输出格式,并保存调用者的格式。
_prev_output_format = get_output_format(), 再set_output_format(...);整个函数体包在try/finally里,finally里还原成调用者的格式 (chat.py:87-89、chat.py:170-173)。这一步专为「subagent 内联 chat」设计,见 §3.6。 -
初始化 + 触发会话开始钩子。
init(...)装配模型/工具/确认策略(chat.py:93);随后触发HookType.SESSION_START,钩子可以往initial_msgs里追加消息(如重放 todo 状态) (chat.py:96-104)。 -
确定模型、按能力关掉流式。 若模型不支持流式而调用方要了流式,就静默降级
stream = False(chat.py:116-122)——这样后面step()不必再操心。 -
装载历史、切到工作目录。
LogManager.load(logdir, initial_msgs=..., create=True)把历史 读进内存(chat.py:126);os.chdir(workspace)(chat.py:134)让工具的相对路径落在正确的项目里。 -