数据截至 (上游 commit 473388d36390)
ChatAgent — 单体智能体的核心循环
这章讲什么:
ChatAgent是 CAMEL 的心脏。所有上层编排(对话、工厂)里的每一个「角色」最终都是一个ChatAgent。看懂它的step()循环,就看懂了 CAMEL 一大半。
1. 它要解决的小问题
你问 LLM「柏林的人口除以 1000 是多少?」。模型不会算数,但它可以要求调用一个计 算器工具。于是流程不是「问一次答一次」,而是:
问 → 模型说「请调 calculator(8500000/1000)」→ 我们真去执行 → 把结果 8500 喂回去 → 模型才给出最终答案。
ChatAgent.step() 要解决的,就是自动把这个「想 → 调工具 → 看结果 → 再想」的来回循环跑完,对调用者只暴露一次 step()。
2. 思路 / 直觉
核心是一个 while True 循环。每一圈做同一件事:把当前记忆变成上下文 → 发给模型 → 看模型这次是「要调工具」还是「给最终答案」:
- 要调工具 → 执行工具、把工具结果写回记忆、
continue(再转一圈)。 - 给最终答案(没有工具调用)→
break,返回。
这样调用者一次 step(),内部可能跑了 1 圈,也可能跑了 5 圈。
3. 图示:一次 step 的控制流
step("问题")
│
▼
把问题写进记忆
│
┌──▶ ① 记忆 → 上下文(必要时摘要压缩)
│ ▼
│ ② 发给模型 model_backend.run()
│ ▼
│ ③ 模型要调工具吗?
│ ├── 要 ──▶ 执行工具 _execute_tool()
│ │ 把工具结果写回记忆
│ │ (到达 max_iteration?→ 跳出)
│ └────────────────┘ 否则回到 ①
│ │
│ └── 不要(给了最终答案)──▶ 跳出循环
▼
把最终回答写进记忆 → 返回 ChatAgentResponse
怎么读:左边的回环箭头就是「再想一圈」。只有 ③ 判定「模型没再要工具」(或撞到迭代上限)才离开循环。
4. 原理演示(简化版循环)
这段演示 step() 的骨架——真实代码有摘要、流式、token 计量等,这里只留主干:
# 示意,非源码:抓住主干
def step(self, user_input):
self.update_memory(user_input, role="user") # 1. 写入记忆
iteration = 0
while True:
context, _ = self.memory.get_context() # 2. 记忆 → 上下文
response = self.model_backend.run( # 3. 发给模型
context, tool_schemas=self.tool_schemas
)
iteration += 1
if response.tool_call_requests: # 4. 模型想调工具
self.record_assistant_tool_calls(response)
for req in response.tool_call_requests:
result = self.execute_tool(req) # 执行并
# execute_tool 内部把结果写回记忆
if iteration >= self.max_iteration: # 5. 撞上限就停
break
continue # 否则再转一圈
break # 6. 没工具调用 = 收敛
self.record_final_output(response)
return self.to_response(response)
重点看第 4-6 步:有工具调用就
continue,没有就break——这一个分支就是「自主循环」与「单轮问答」的全部区别。
5. 真实实现
真正的循环在 _step_impl(step 只是给它套上流式/超时):
chat_agent.py:3005开始while True:——主循环入口。chat_agent.py:3016-3018self._get_context_with_summarization()——从记忆取上下文,必要时触发摘要压缩(见 04-foundations.md)。chat_agent.py:3025self._get_model_response(...)——发请求,内部带速率限制重试(_get_model_response,chat_agent.py:3645)。chat_agent.py:3067if tool_call_requests := response.tool_call_requests:——这一行就是「模型这次要不要调工具」的分叉点。chat_agent.py:3104result = self._execute_tool(tool_call_request)——逐个执行内部工具。chat_agent.py:3111-3116撞到max_iteration就break;否则continue(:3054)。chat_agent.py:3158没有工具调用时的break——收敛出口。
工具执行本身在 _execute_tool(chat_agent.py:4091):取出工具、tool(**args)、出错则把异常捕获成字符串结果(chat_agent.py:4127-4131)而非崩溃——这点很关键,见下。
6. 关键细节 / 坑
① 工具报错不崩溃,而是把错误当结果喂回模型。 _execute_tool 用 try/except 把异常变成 "Tool execution failed: ..." 字符串(chat_agent.py:4127-4131)。模型下一圈会看到这个错误,可能自己改参数重试。容错被内建进循环。
② 外部工具(external tools)会中断循环、把控制权交还调用者。 如果模型请求的是注册在 _external_tool_schemas 里的工具,ChatAgent 不执行,而是 break 把请求原样返回(chat_agent.py:3108-3109),让外部代码决定怎么做。这是 human-in-the-loop / 自定义副作用的钩子。
③ max_iteration 是防失控的闸。 不设(None)则可能无限调工具。设成 1 就退化成「最多一次模型调用」。位置:chat_agent.py:3111-3116。
④ 记忆里要不要留工具调用痕迹是可配的。 prune_tool_calls_from_memory=True 时,step 结束会清掉记忆里的工具调用消息以省 token(chat_agent.py:3174-3175,调 memory.clean_tool_calls())。
7. 代码地图
| 主题 | 文件 | 符号 |
|---|---|---|
| 入口(套流式/超时) | camel/agents/chat_agent.py | ChatAgent.step |
| 真正的循环 | camel/agents/chat_agent.py | ChatAgent._step_impl |
| 工具调用分叉 | camel/agents/chat_agent.py | _step_impl(tool_call_requests) |
| 执行单个工具 | camel/agents/chat_agent.py | ChatAgent._execute_tool |
| 发请求 + 重试 | camel/agents/chat_agent.py | ChatAgent._get_model_response |
| 异步版循环 | camel/agents/chat_agent.py | ChatAgent.astep / _astep_non_streaming_task |