欢迎光临
我们一直在努力

第 10 章《AgentLoop 编排核心》· AgentLoop 编排怎么做?6 阶段流水线 + TurnContext 实战(nanobot)

本文回答什么问题:AgentLoop 的 6 阶段流水线怎么实现?TurnContext 是什么?session_key / workspace / hooks / outbound 在源码里如何编排?

目标读者:LLM Agent 开发者 / 想修改核心循环的工程师 预计阅读时间:18 分钟 源码版本:GitHub HKUDS/nanobot main 分支主线代码(仓库相对路径)

AgentLoop(nanobot/agent/loop.py,约 2058 行)是 nanobot 通道层回合编排者——把"用户消息"变成"Agent 回复"的全过程。本章聚焦主入口 _dispatch 和 6 阶段流水线源码。

1. 整体定位:AgentLoop vs AgentRunner

  • AgentLoop(本章):回合——管 session_key / workspace / hooks / outbound 投递
  • AgentRunner(第 11 章):多循环——管 LLM 调用 / 流式 / 工具执行 / 迭代上限

调试分流原则:通道问题看 loop.py,模型问题看 runner.py。

核心要点速查(建议收藏)

  • 核心文件:nanobot/agent/loop.py(约 2058 行)
  • 主入口:AgentLoop._dispatch(msg) 处理单回合;run_forever() 主循环
  • TurnContext:runner + tools + hooks + session 的载体,每个回合构造一次
  • 6 阶段:Inbound → session 解析 → workspace 解析 → Context 构建 → Runner 多轮 → Outbound 投递
  • 3 个关键子方法:_resolve_session / _resolve_workspace / _handle_one_turn

2. 6 阶段流水线源码拆解

Channel

MessageBus_outbound

SessionManager

AgentRunner

AgentTurnHook

TurnContext

AgentLoop

MessageBus

Channel

MessageBus_outbound

SessionManager

AgentRunner

AgentTurnHook

TurnContext

AgentLoop

MessageBus

#mermaid-svg-vJmcrNWSonwqKmRN{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vJmcrNWSonwqKmRN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vJmcrNWSonwqKmRN .error-icon{fill:#552222;}#mermaid-svg-vJmcrNWSonwqKmRN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vJmcrNWSonwqKmRN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vJmcrNWSonwqKmRN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vJmcrNWSonwqKmRN .marker.cross{stroke:#333333;}#mermaid-svg-vJmcrNWSonwqKmRN svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vJmcrNWSonwqKmRN p{margin:0;}#mermaid-svg-vJmcrNWSonwqKmRN .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vJmcrNWSonwqKmRN text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-vJmcrNWSonwqKmRN .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vJmcrNWSonwqKmRN .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-vJmcrNWSonwqKmRN .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-vJmcrNWSonwqKmRN .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-vJmcrNWSonwqKmRN #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-vJmcrNWSonwqKmRN .sequenceNumber{fill:white;}#mermaid-svg-vJmcrNWSonwqKmRN #sequencenumber{fill:#333;}#mermaid-svg-vJmcrNWSonwqKmRN #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-vJmcrNWSonwqKmRN .messageText{fill:#333;stroke:none;}#mermaid-svg-vJmcrNWSonwqKmRN .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vJmcrNWSonwqKmRN .labelText,#mermaid-svg-vJmcrNWSonwqKmRN .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-vJmcrNWSonwqKmRN .loopText,#mermaid-svg-vJmcrNWSonwqKmRN .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-vJmcrNWSonwqKmRN .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-vJmcrNWSonwqKmRN .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-vJmcrNWSonwqKmRN .noteText,#mermaid-svg-vJmcrNWSonwqKmRN .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-vJmcrNWSonwqKmRN .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vJmcrNWSonwqKmRN .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vJmcrNWSonwqKmRN .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-vJmcrNWSonwqKmRN .actorPopupMenu{position:absolute;}#mermaid-svg-vJmcrNWSonwqKmRN .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-vJmcrNWSonwqKmRN .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-vJmcrNWSonwqKmRN .actor-man circle,#mermaid-svg-vJmcrNWSonwqKmRN line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-vJmcrNWSonwqKmRN :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

consume_inbound InboundMessage

① resolve session_key + workspace scope

② build TurnContext runner/tools/hooks

pre_turn_hook message

runner.run spec

OutboundMessage + event stream

post_turn_hook message response

⑬ append() 持久化

publish_outbound with TurnRoute

consume_outbound then send

阶段 ① · Inbound 接入

async def _dispatch(self, msg: InboundMessage):
# … 见阶段 ②

AgentLoop.run_forever() 主循环从 bus.consume_inbound() 拿消息,转交 _dispatch。

阶段 ② · session 解析

async def _dispatch(self, msg: InboundMessage):
# 1. 解析 session_key(默认 f"{channel}:{chat_id}",可被 metadata["session_key"] 覆盖)
session_key = msg.session_key # InboundMessage 已提供 property

# 2. 解析 workspace(根据 chat_id 找 ~/.nanobot/workspaces/<key>/)
workspace = self._resolve_workspace(msg)
if workspace is None:
await self._send_error(msg, "no workspace available")
return

_resolve_workspace 决定 workspace scope——这是 nanobot/security/workspace_access.py 的核心逻辑(详见第 29 章)。

阶段 ③ · TurnContext 构造

# 3. 构造 TurnContext(把 runner / tools / hooks 打包)
ctx = TurnContext(
msg=msg,
session_key=session_key,
workspace=workspace,
runner=self._runner, # AgentRunner 单例
tools=self._tools, # ToolRegistry 单例
hooks=self._hooks, # AgentTurnHook chain
config=self._config,
)

TurnContext 是回合的"数据快照"——AgentRunner 通过 ctx.runner.run(spec) 拿到。

阶段 ④ · Hook 前置

# 4. pre_turn_hook(可取消整个回合)
if not await self._hooks.before_turn(ctx):
return # hook 决定取消

典型用法:WebUI 的"批准"按钮 → 等用户批准后再放行 Runner。

阶段 ⑤ · Runner 多轮循环

# 5. 构造 AgentRunSpec(从 ctx 派生出 spec)
spec = AgentRunSpec(
messages=self._ctx_builder.build_messages(ctx), # 历史 + 当前
tools=ctx.tools,
max_iterations=self._config.max_iterations,
)

# 6. 跑 AgentRunner(可能 200 轮,详见第 11 章)
result = await ctx.runner.run(spec, hook=self._hooks)

阶段 ⑥ · Outbound 投递 + Hook 后置

# 7. post_turn_hook(可改 result)
await self._hooks.after_turn(ctx, result)

# 8. 持久化 session
await self._session_manager.append(ctx.session_key, result.messages)

# 9. 投递 outbound(每个 OutboundMessage 进 MessageBus)
for out_msg in result.outbound_messages:
await self._bus.publish_outbound(out_msg)

# 10. TurnEnd 事件(可选,带 latency)
await self._bus.publish_outbound(OutboundMessage(
channel=msg.channel,
chat_id=msg.chat_id,
content="",
event=TurnEndEvent(latency_ms=elapsed_ms),
))

3. 4 个核心子方法

_resolve_session(msg) → session_key

def _resolve_session(self, msg: InboundMessage) > str:
return msg.session_key_override or f"{msg.channel}:{msg.chat_id}"

_resolve_workspace(msg) → Path

def _resolve_workspace(self, msg: InboundMessage) > Path | None:
workspace_id = self._workspace_access.resolve(
channel=msg.channel,
chat_id=msg.chat_id,
sender_id=msg.sender_id,
)
if workspace_id is None:
return None
return Path.home() / ".nanobot" / "workspaces" / workspace_id

_handle_one_turn(ctx) → OutboundMessage list

主流程,见 §2 阶段 ③-⑥。

_send_error(msg, reason) → OutboundMessage

async def _send_error(self, msg: InboundMessage, reason: str):
await self._bus.publish_outbound(OutboundMessage(
channel=msg.channel,
chat_id=msg.chat_id,
content=f"⚠ {reason}",
))

4. 3 个核心决策

决策 1 · 为什么 AgentLoop 管 session_key / workspace,不管 LLM 调用?

单一职责:loop.py 改 session 路由不影响 LLM 行为;runner.py 改流式输出不影响 session 路由。

决策 2 · 为什么 TurnContext 是 dataclass 而非 dict?

类型安全 + IDE 自动补全;改字段会破坏编译。

决策 3 · 为什么 hooks 可以取消回合?

WebUI “abort” 按钮、审批流、配对确认都可以在 hook 里 return False 中止整个回合。

5. 常见问题 / 避坑

Q:AgentLoop 是单例吗?

A:。gateway 启动时构造一次,多通道共享。

Q:_dispatch 报错会怎样?

A:try/except 包裹,捕获后 _send_error(msg, str(exc)) 通知通道,继续 run_forever() 不退出。

Q:为什么用 before_turn 而不是 pre_turn?

A:before_turn 返回 False 取消回合;after_turn 可改 result——3 种语义区分。

6. 小结

  • 6 阶段流水线:Inbound → session → workspace → TurnContext → Hook → Runner → Hook → 持久化 → Outbound
  • TurnContext 是回合数据快照
  • 调试分流:
  • Hook 系统 允许取消 / 改写 / 监控回合

本文要点速查

  • 6 阶段源码定位:见 §2 各阶段代码片段
  • TurnContext 字段:msg + session_key + workspace + runner + tools + hooks + config
  • 3 个核心子方法:_resolve_session / _resolve_workspace / _handle_one_turn
  • 下一步:第 11 章《AgentRunner LLM 循环》—— 阶段 ⑤ 详细展开

  • 按角色推荐

    • LLM Agent 开发者:必读(后续 11-16 章都基于本章 6 阶段)
    • 系统架构师:必读(核心循环必读)
    • LLM Provider 适配者:选读(知道 AgentRunner.run 怎么调即可)
    • 聊天通道开发者:选读(知道通道消息怎么进 loop 即可)
    • Tool / MCP 工具开发者:选读(知道 Tool 在 TurnContext 里)

    下一步

    • 第 11 章《AgentRunner LLM 循环》 —— 阶段 ⑤ 的 _request_model + _execute_tools(主题群"Agent 核心",第 3 周)
    • 第 13 章《Hook 系统》 —— before_turn / after_turn 完整 hook 类型(主题群"Agent 核心",第 3 周)
    • 第 14 章《会话管理 SessionManager》 —— 阶段 ⑧ 的 append + 原子写(主题群"Agent 核心",第 3 周)

    tags:#nanobot #AI Agent #LLM #Python #源码解析 #AgentLoop #编排

    赞(0)
    未经允许不得转载:171主机测评 » 第 10 章《AgentLoop 编排核心》· AgentLoop 编排怎么做?6 阶段流水线 + TurnContext 实战(nanobot)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址