第 11 章《AgentRunner LLM 循环》· nanobot AgentRunner 源码深度解析:_request_model + _execute_tools 多轮工具调用
2026/9/4 2:56:51 网站建设 项目流程

本文回答什么问题: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_callsLLM 想调工具_execute_tools→ 下一轮
completedLLM 已生成最终回复退出循环
max_iterations达到 200 轮强制结束(返回当前结果)
errorLLM 调用失败返回错误消息(详见 §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")returnresponse

3 层容错:

  1. 单 Provider 失败 → FallbackProvider 兜底
  2. Fallback 失败 → 返回错误消息给通道
  3. AgentLoop 收到错误消息 → 通知用户

6. 实战:增加轮次上限

# config.yamlmaxIterations:50# 默认 200,降为 50# Pythonfromnanobot.configimportload_config config=load_config()print(config.max_iterations)# 50

7. 常见问题 / 避坑

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 → 返回错误消息

本文要点速查

  1. run()主循环默认 200 轮,见 §2
  2. 3 个核心子方法见 §3
  3. 4 类 stop_reason见 §4
  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#工具调用

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询