本文回答什么问题:AgentRunner.run() 内部怎么循环?
_request_model怎么调 LLM?_execute_tools怎么跑工具?迭代上限 / 流式 / 推理块怎么协调?目标读者:LLM Agent 开发者 / Provider 适配者
预计阅读时间:16 分钟
源码版本:GitHub HKUDS/nanobotmain 分支主线代码(仓库相对路径)
AgentRunner(nanobot/agent/runner.py,约 1505 行)是 nanobot 模型层多循环——把"单次 LLM 调用"扩展为"多轮 LLM + 工具执行"的迭代过程。
1. 整体定位:为什么 AgentRunner 单独成模块
如果让 AgentLoop 直接调 LLM,AgentLoop 就要处理 200 轮迭代 + 工具调用 + 流式——膨胀到 5000 行。AgentRunner 抽出"模型层"关注,AgentLoop 只负责调度。
核心要点速查(建议收藏)
- 核心文件:
nanobot/agent/runner.py(约 1505 行) - 主入口:
AgentRunner.run(spec, hook)跑单回合(可能 200 轮) - 3 个核心子方法:
_request_model(messages)/_execute_tools(tool_calls)/_finalize(result) - 迭代上限:默认 200 次(
Config.max_iterations) - 4 类 stop_reason:
tool_calls/completed/max_iterations/error
2. AgentRunner.run() 主循环
asyncdefrun(self,spec:AgentRunSpec,hook:AgentTurnHook|None=None)->AgentRunResult:messages=list(spec.messages)outbound:list[OutboundMessage]=[]iteration=0whileiteration<spec.max_iterations:iteration+=1# 1. 调 LLMresponse=awaitself._request_model(messages)outbound.extend(response.stream_events)# 流式片段# 2. 决定是否继续ifresponse.stop_reason=="completed":# 已生成最终回复,退出循环outbound.append(self._finalize(response))returnAgentRunResult(messages=messages,outbound_messages=outbound,iteration=iteration)ifresponse.stop_reason=="tool_calls":# 3. 跑工具tool_results=awaitself._execute_tools(response.tool_calls)messages.extend(tool_results)continue# 下一轮# 4. 达到 max_iterationsreturnAgentRunResult(messages=messages,outbound_messages=outbound,iteration=iteration,stop_reason="max_iterations")3. 3 个核心子方法
3.1_request_model(messages)→ LLMResponse
asyncdef_request_model(self,messages:list[Message])->LLMResponse:response=awaitself._provider.chat(messages=messages,tools=self._spec.tools.to_openai_schema(),# 工具清单转 OpenAI 格式stream=True,# 流式)# 流式累积content=""tool_calls:list[ToolCallRequest]=[]stream_events:list[OutboundEvent]=[]asyncforchunkinresponse:ifchunk.type=="content_delta":content+=chunk.text stream_events.append(StreamDeltaEvent(content=chunk.text))elifchunk.type=="tool_call_delta":tool_calls.append(chunk.tool_call)elifchunk.type=="reasoning_delta":stream_events.append(StreamDeltaEvent(content=chunk.text,kind="reasoning"))returnLLMResponse(content=content,tool_calls=tool_calls,stream_events=stream_events,stop_reason="tool_calls"iftool_callselse"completed",)关键:
- 所有 8 个 Provider 都通过
LLMProvider.chat()统一接口(详见第 17 章) - 流式片段直接转
OutboundEvent让 AgentLoop 投递给通道 - 工具调用转
ToolCallRequest(详见第 17 章)
3.2_execute_tools(tool_calls)→ list[ToolResult]
asyncdef_execute_tools(self,tool_calls:list[ToolCallRequest])->list[Message]:results:list[Message]=[]forcallintool_calls:tool=self._spec.tools.get(call.name)iftoolisNone:results.append(Message(role="tool",name=call.name,content=f"Error: tool '{call.name}' not found"))continue# scope 检查(workspace / network / pairing,详见第 29 章)ifnotself._tool_scope.allows(tool,call):results.append(Message(role="tool",name=call.name,content="Error: permission denied"))continue# 调工具result=awaittool.execute(**call.arguments)results.append(Message(role="tool",name=call.name,content=result.content))returnresults关键:
- 工具调用按
call.name查 ToolRegistry - scope 校验失败的工具返回错误消息(不抛异常)
- 工具结果作为
role="tool"消息反馈给 LLM
3.3_finalize(response)→ OutboundMessage
def_finalize(self,response:LLMResponse)->OutboundMessage:returnOutboundMessage(channel=self._spec.channel,chat_id=self._spec.chat_id,content=response.content,event=None,# 纯文本)4. 4 类 stop_reason
| stop_reason | 含义 | 后续动作 |
|---|---|---|
tool_calls | LLM 想调工具 | _execute_tools→ 下一轮 |
completed | LLM 已生成最终回复 | 退出循环 |
max_iterations | 达到 200 轮 | 强制结束(返回当前结果) |
error | LLM 调用失败 | 返回错误消息(详见 §5) |
5. 错误处理
asyncdef_request_model(self,messages):try:response=awaitself._provider.chat(...)exceptProviderErrorase:# fallback provider(详见第 20 章)ifself._fallback:response=awaitself._fallback.chat(...)else:returnLLMResponse(content=f"⚠{e}",stop_reason="error")returnresponse3 层容错:
- 单 Provider 失败 → FallbackProvider 兜底
- Fallback 失败 → 返回错误消息给通道
- AgentLoop 收到错误消息 → 通知用户
6. 实战:增加轮次上限
# config.yamlmaxIterations:50# 默认 200,降为 50# Pythonfromnanobot.configimportload_config config=load_config()print(config.max_iterations)# 507. 常见问题 / 避坑
Q:LLM 流式输出什么时候结束?
A:Provider 的stream返回的 chunk 序列结束 →_request_model完成。AgentLoop 把累积的stream_events投递给通道渲染。
Q:max_iterations太小会怎样?
A:LLM 多轮任务未完成就强制结束,可能产生"半成品"回复。建议设 50-200。
Q:工具调用超时怎么办?
A:每个 Tool.execute 有自己的timeout(默认 60s);超时会抛ToolTimeoutError,被_execute_tools捕获后转role="tool"错误消息。
8. 小结
- 主循环:
while iteration < max_iterations: _request_model → 决定 stop_reason → _execute_tools / 退出 - 关键模块:3 个核心子方法:
_request_model/_execute_tools/_finalize - 设计要点:4 类 stop_reason:
tool_calls/completed/max_iterations/error - 常见坑:3 层容错:Provider 错误 → FallbackProvider → 返回错误消息
本文要点速查
run()主循环默认 200 轮,见 §2- 3 个核心子方法见 §3
- 4 类 stop_reason见 §4
- 下一步:第 12 章《ContextBuilder 系统 Prompt》—— 阶段 ③ 的
build_messages()详细展开
按角色推荐
- LLM Agent 开发者:必读(模型层核心)
- LLM Provider 适配者:必读(Provider 接口契约)
- 系统架构师:选读(知道 AgentRunner.run 即可)
- 聊天通道开发者:选读
- Tool / MCP 工具开发者:选读(知道 Tool 怎么被调用即可)
下一步
- 第 12 章《ContextBuilder 系统 Prompt》——
_request_model之前的 messages 怎么构造(主题群"Agent 核心",第 3 周) - 第 17 章《LLMProvider 抽象》——
chat()接口完整契约(主题群"LLM Provider",第 4 周) - 第 20 章《FallbackProvider 兜底》——
_request_model错误的兜底路由(主题群"LLM Provider",第 4 周)
tags:#nanobot#AI Agent#LLM#Python#源码解析#AgentRunner#工具调用