跳到主要内容

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:2940 开始 while True:——主循环入口。
  • chat_agent.py:2951-2953 self._get_context_with_summarization()——从记忆取上下文,必要时触发摘要压缩(见 04-foundations.md)。
  • chat_agent.py:2960 self._get_model_response(...)——发请求,内部带速率限制重试(_get_model_response,chat_agent.py:3580)。
  • chat_agent.py:3002 if tool_call_requests := response.tool_call_requests:——这一行就是「模型这次要不要调工具」的分叉点。
  • chat_agent.py:3039 result = self._execute_tool(tool_call_request)——逐个执行内部工具。
  • chat_agent.py:3046-3051 撞到 max_iterationbreak;否则 continue(:3054)。
  • chat_agent.py:3093 没有工具调用时的 break——收敛出口。

工具执行本身在 _execute_tool(chat_agent.py:4026):取出工具、tool(**args)、出错则把异常捕获成字符串结果(chat_agent.py:4062-4066)而非崩溃——这点很关键,见下。

6. 关键细节 / 坑

① 工具报错不崩溃,而是把错误当结果喂回模型。 _execute_tooltry/except 把异常变成 "Tool execution failed: ..." 字符串(chat_agent.py:4062-4066)。模型下一圈会看到这个错误,可能自己改参数重试。容错被内建进循环。

② 外部工具(external tools)会中断循环、把控制权交还调用者。 如果模型请求的是注册在 _external_tool_schemas 里的工具,ChatAgent 不执行,而是 break 把请求原样返回(chat_agent.py:3043-3044),让外部代码决定怎么做。这是 human-in-the-loop / 自定义副作用的钩子。

max_iteration 是防失控的闸。 不设(None)则可能无限调工具。设成 1 就退化成「最多一次模型调用」。位置:chat_agent.py:3046-3051

④ 记忆里要不要留工具调用痕迹是可配的。 prune_tool_calls_from_memory=True 时,step 结束会清掉记忆里的工具调用消息以省 token(chat_agent.py:3109-3110,调 memory.clean_tool_calls())。

7. 代码地图

主题文件符号
入口(套流式/超时)camel/agents/chat_agent.pyChatAgent.step
真正的循环camel/agents/chat_agent.pyChatAgent._step_impl
工具调用分叉camel/agents/chat_agent.py_step_impl(tool_call_requests)
执行单个工具camel/agents/chat_agent.pyChatAgent._execute_tool
发请求 + 重试camel/agents/chat_agent.pyChatAgent._get_model_response
异步版循环camel/agents/chat_agent.pyChatAgent.astep / _astep_non_streaming_task