这是 nanobot 面试备考系列的第一篇,覆盖「基础与架构」共 20 题(项目定位 12 题 + 核心引擎 8 题),均附详细解答。建议先遮住解答自答,再对照补盲区。 完整 70 题分三篇发布:基础与架构 / 核心机制 / 进阶与生产,对应 nanobot 源码解读系列(00–09)。题号沿用全局编号(本篇 Q1–Q20)。
一、项目概览与定位(12 题)
Q1. 先简单介绍一下 nanobot 是什么,以及它和 LangChain、AutoGen 这类框架最大的区别在哪?
- 【考点】能否一句话定位项目并讲清差异化。
- 【详细解答】nanobot 是香港大学 HKUDS 团队开源的、面向生产部署的多 Agent 框架。它的核心主张不是「帮你编排一条链」,而是「把 Agent 当成一项可持续运行的服务跑起来给别人用」。最大区别有三点:第一,它用 AgentLoop(产品层)+ AgentRunner(引擎层) 的分层,把「怎么和通道交互」和「怎么跑模型循环」解耦,而 LangChain 早期更像编排库、AutoGen 更像多 Agent 对话原型;第二,nanobot 原生支持 Channel / API / SDK / Cron 四种接入面,统一汇流到 MessageBus,意味着同一套 Agent 既能给同事用网页聊、也能被系统定时调;第三,它在权限与安全边界上投入明显(工具层的 SSRF/workspace 双防线、服务端注入用户身份),而很多竞品默认把安全交给使用者自己。一句话:LangChain 是「积木」,AutoGen 是「多角色聊天台」,nanobot 是「Agent 运行时 + 接入网关」。
Q2. 你刚才说它「面向生产」,具体体现在哪几个设计上?
- 【考点】能否举出 3 个以上生产导向的设计证据。
- 【详细解答】至少五个层面:① 分层解耦——AgentLoop/AgentRunner 拆开,引擎可被 Web/API/Cron 复用,通道升级不影响模型逻辑;② 工具安全边界——web 工具有 SSRF 三道防线(IP 黑名单 / PinnedDNS / 逐跳重定向重验),filesystem 有 workspace 路径边界,不靠模型自觉;③ Provider 容错——FallbackProvider 做透明降级 + 熔断,主模型挂了能切备胎且不雪崩;④ 上下文治理——ContextGovernor 每轮修畸形、补结果、压缩、裁剪,长会话可控不爆窗;⑤ 多接入面 + Cron——覆盖真实运维场景(定时简报、系统对接),而不是只有 demo 式的交互界面。这些都不是原型框架会优先做的事。
Q3. 为什么 nanobot 要把 AgentLoop 和 AgentRunner 拆成两层,而不是一个类搞定?
- 【考点】理解「产品层 vs 引擎层」的职责边界。
- 【详细解答】本质是关注点分离 + 复用。AgentLoop 面向「通道一轮交互」:它管消息总线收发、SessionManager、从登录态注入用户身份、构建一次运行所需的 AgentRunSpec、把增量 token 以 SSE 推给用户。AgentRunner 面向「模型循环」:它管上下文装配、调 LLM、调度工具执行、判断停止、跑上下文治理。拆开的好处:同一套 Runner 引擎可以被 Web Channel、HTTP API、Python SDK、Cron 四种 Loop 复用,换通道不用改引擎;引擎可以脱离任何具体通道独立单测;以后换模型后端只动 Runner 依赖的 Provider,不动 Loop。如果揉成一个类,通道逻辑和模型逻辑纠缠,测试时要 mock 整个 HTTP 层,且无法支撑多接入面。这是典型的「运行时(runtime)vs 编排(orchestration)」分层思想。
Q4. 如果新人问「我该从哪个文件开始读 nanobot 源码」,你会指哪几个,为什么?
- 【考点】对代码骨架的熟悉度。
- 【详细解答】推荐顺序:①
agent/loop.py——产品层入口,看懂「外部消息怎么变成一次运行」;②agent/runner.py——引擎主循环,看懂「模型↔工具↔上下文」怎么转;③context.py+context_governance.py——上下文怎么装配、每轮怎么被治理,这是 nanobot 的精华;④agent/tools/(base/registry/loader/shell/filesystem/web)——工具抽象与安全边界;⑤providers/(registry/factory/fallback_provider/openai_compat_provider)——模型路由与容错。先 loop+runner 建立主链路心智,再下钻各子系统,避免一上来陷进 MCP 或记忆的细节里迷路。
Q5. AgentRunSpec 这个对象存在的意义是什么?它解决了什么耦合问题?
- 【考点】解耦抽象的意图。
- 【详细解答】AgentRunSpec 是 AgentLoop 交给 AgentRunner 的「一次性运行规格」,里面打包了本次运行需要的模型选择、工具集、上下文来源、用户身份、停止条件等。它的价值在于把「通道相关的环境信息」和「引擎执行所需的最小输入」解耦:Runner 不再 import 任何 Channel/HTTP/Session 的概念,它只认 RunSpec。这样 Runner 可被单元测试(构造一个假 Spec 即可)、可被不同 Loop 复用、可被 SDK 直接驱动(SDK 也是构造 Spec 投进去)。如果没有它,Runner 就得直接依赖 Loop 的上下文对象,通道一变引擎就得改,无法做到「四接入面共用一个引擎」。
Q6. nanobot 支持哪些「接入面」(surface)?它们之间是怎么统一的?
- 【考点】多入口的统一机制。
- 【详细解答】四类:Channel(聊天通道,如网页/IM)、API(OpenAI 兼容的 HTTP 接口)、SDK(Python 客户端库)、Cron(定时触发)。它们的统一点是 MessageBus + AgentLoop:每个接入面只负责把外部事件翻译成统一的内部消息格式,投进 MessageBus;AgentLoop 从总线消费消息、构建 RunSpec、驱动 Runner。差异仅仅在「事件从哪来、怎么触发」——Channel 是人说的话,API 是 HTTP body,SDK 是代码调用,Cron 是时钟。汇流之后,下游完全一样。这就是「多前端、单引擎」的架构。
Q7. 消息总线(MessageBus)在架构里扮演什么角色?为什么不直接函数调用?
- 【考点】事件驱动 vs 直接调用的取舍。
- 【详细解答】MessageBus 是各接入面与 AgentLoop 之间的解耦层,提供异步、可广播、可持久化的消息流。如果直接函数调用(接入面 → loop.run()),通道和引擎就被写死了:要加一个 Cron 触发就得改 Loop 接口,多个通道并发时还要自己管锁,且无法做「消息落盘重放/审计」。用总线后,「谁触发、怎么触发」和「怎么跑 Agent」彻底分离——新增接入面只需往总线投消息,不用碰引擎;总线还能天然支撑并发、广播(一个事件触发多个 Agent)、以及未来的可观测性埋点。代价是引入一点异步复杂度,但对生产系统值得。
Q8. 它既然是多 Agent 框架,那单 Agent 场景下开销大吗?会不会「杀鸡用牛刀」?
- 【考点】框架泛用性与过度设计判断。
- 【详细解答】单 Agent 时 SubagentManager 根本不启用,额外开销主要来自 AgentLoop+Runner 的基本闭环,这是合理的固定成本。而且即便单 Agent,多接入面、权限注入、上下文治理、Provider 容错这些能力在「生产场景」依然有用——比如你做个内部问答助手,照样要 API 接入、照样要防工具越权、照样要主模型挂了能降级。所以不算纯浪费。真正的「杀鸡」场景是:你只想要一个本地一次性脚本,跑完就扔,那确实用不着这套运行时,直接调 SDK 更轻。判断是否过度设计,看「要不要长期给别人用、要不要多入口、要不要容错」,三者有一个就值。
Q9. 给 nanobot 画一张最简架构图,你会画哪几个核心方块和箭头?
- 【考点】能否抓住主干。
- 【详细解答】核心方块:四个接入面(Channel/API/SDK/Cron)→ MessageBus → AgentLoop → AgentRunner →(LLM ↔ Tools ↔ ContextGovernor 的三角闭环);两侧依赖注入:MemoryStore(记忆)与 Provider(模型)。箭头主干是「接入面汇聚到总线 → 总线驱动 Loop → Loop 驱动 Runner → Runner 内模型/工具/治理互相喂」。记忆和 Provider 是 Runner 的旁挂依赖,不是主链上的必经节点。讲的时候强调:Loop 不碰模型,Runner 不碰通道,这就是分层的直观体现。
Q10. nanobot 默认记忆/上下文放本地文件系统,多实例部署会出什么问题?怎么解?
- 【考点】有状态服务的水平扩展意识。
- 【详细解答】默认 MemoryStore / Dream 后端是本地文件系统(Dream 还用 GitStore)。多副本部署时,副本 A 写的记忆副本 B 看不到,同一用户的两次请求被负载均衡到不同实例就会「失忆」,甚至并发写同一 history.jsonl 会互相覆盖或冲突。解法有三档:① 轻量——用粘性会话(sticky session),保证同一用户落到同实例;② 标准——把 MemoryStore/Dream 后端换成共享存储(S3/对象存储、数据库、或共享挂载卷),GitStore 也可配远程同步;③ 折中——会话状态放 Redis 这类共享 KV,长期记忆放共享卷。面试里能说出「默认本地存储是有状态单机的假设,水平扩展必须外置状态」就到位了。
Q11. 它的开源协议是什么?商用需要注意什么?
- 【考点】合规意识。
- 【详细解答】以仓库根目录 LICENSE 文件为准(nanobot 采用宽松协议,但具体条款以官方为准)。商用注意三点:① 保留原始版权声明与许可文本;② 扫一遍依赖项的传递性协议——有些工具依赖(如某些网络/解析库)可能带不同条款,要确认兼容;③ 若做了修改并分发,部分协议要求公开改动。面试中体现「先看 LICENSE、再扫依赖、再评估分发场景」的习惯即可,不必背具体条款数字。
Q12. 你个人认为 nanobot 现在最大的短板或风险点在哪里?
- 【考点】批判性思维,不盲目吹捧。
- 【详细解答】可谈几个真实短板:① 生态与文档成熟度不及 LangChain,踩坑时社区答案少;② 默认本地存储对云原生不友好,生产部署要多做一层状态外置;③ 安全边界依赖配置正确——workspace 根、SSRF 开关如果误配,就等于敞开,框架不会替你兜底所有情况;④ 多 Agent 编排相比 AutoGen/CrewAI 仍偏早期,复杂编排(如动态图、人工介入节点)能力有限;⑤ 可观测性/评测(Eval)不是开箱重点,生产排障要靠自己接。能客观指出 2–3 点并带上「如果是我怎么做」的改进思路,比一味夸更得分。
二、核心引擎与主循环(8 题)
Q13. 描述一次完整的「用户发消息 → 拿到回复」在 AgentRunner 里的循环步骤。
- 【考点】主链路全流程掌握。
- 【详细解答】以 OpenAI 风格对话协议为例:① AgentLoop 把用户消息、工具集、规格打包成 RunSpec 交给 Runner;② Runner 通过 ContextBuilder 装配本轮上下文(系统提示 + 记忆 + 工具 schema + 历史);③ 调 LLM,拿到输出(可能是纯文本,也可能是 tool_calls);④ 若没有 tool_calls 且触发停止条件(如模型判停)→ 把最终文本返回 Loop 层推送用户,结束;⑤ 若有 tool_calls → 逐个执行对应 Tool,把结果封装成
tool角色消息追加回上下文;⑥ 回到 ③ 继续下一轮,直到出现停止信号。关键点:模型每轮只「看到」累积后的上下文,工具结果是作为新消息喂回去的,而不是藏在某个隐藏变量里。
Q14. runner 是怎么知道「该停下来」的?有哪些停止信号?
- 【考点】循环终止条件。
- 【详细解答】停止信号有五类:① 模型自停——本轮输出没有 tool_calls 且产出终态文本(模型认为任务完成);② 显式 stop 标记——模型或规格返回了停止指令;③ 步数上限——达到 max_steps,防止无限循环;④ token 预算——累计 token 超过上限,治理层强制收尾;⑤ 用户取消 / 不可恢复错误——如 LLM 持续 5xx 且 Fallback 也失败,或治理层修复畸形输出多次仍失败。面试中强调「停止是多层防护,不是只靠模型自觉」,体现你对失控风险的意识。
Q15. 流式输出(SSE)是在哪一层实现的?为什么放这一层?
- 【考点】流式职责的归属。
- 【详细解答】流式在 AgentLoop(产品层)实现,对接通道的 SSE 响应;AgentRunner 以异步生成器或回调把增量 token 向上抛,自己不关心「这些字怎么到用户屏幕」。为什么放 Loop 层?因为「如何把字推给用户」是传输/通道关注点——Web 通道用 SSE,SDK 可能用迭代器,Cron 根本不需要流式。把流式放引擎层会把传输细节污染进引擎,导致 Runner 无法被非流式场景复用。分层后 Runner 只负责「产出 token 流」,Loop 负责「把流接到正确的通道」,引擎保持传输无关(transport-agnostic)。
Q16. 如果 LLM 返回了一个格式损坏的 tool_call(参数 JSON 坏掉),nanobot 怎么处理?
- 【考点】健壮性设计(呼应 ContextGovernor)。
- 【详细解答】靠 ContextGovernor 的治理步骤兜底,典型是「剔畸形 tool_call + 补缺失结果」:Governor 在每轮把上下文过一遍,识别到参数不是合法 JSON / 缺必填字段 / 工具名不存在,会尝试修复(如补全缺失括号)或直接丢弃该调用,并以明确的错误结果(如「工具调用格式错误,已忽略」)回填上下文。这样模型下一轮有机会纠正,整轮不会因为一个坏调用崩溃。这比「解析失败就抛异常中断」健壮得多,也体现「模型是不可信生产者,治理层必须兜底」的设计哲学。
Q17. AgentRunner 是同步还是异步?为什么?换成同步实现会有什么后果?
- 【考点】异步模型的理解。
- 【详细解答】异步(async)。原因:LLM 调用(网络往返几秒)、工具 I/O(查库/抓网页)、流式推送都是高延迟等待,异步能让事件循环在等 A 用户时去服务 B 用户,提升并发吞吐。若改同步:单个请求会阻塞事件循环,多用户并发时严重排队;SSE 流式会卡顿(要等整段生成才能发);且无法高效支持 Cron/Channel 并发触发。代价是代码要用 async/await、工具也要异步实现,但这是生产运行时的基本门槛。
Q18. 一轮循环里,工具执行结果是怎么「喂回」给模型的?追加在同一次请求还是下一次?
- 【考点】工具结果的回填机制。
- 【详细解答】工具结果作为
tool角色的消息追加进上下文,在下一轮请求时连同整个历史一起发给模型(OpenAI 风格函数调用协议即如此:assistant 带 tool_calls → 你回 tool 消息 → 再发一次完整对话让模型继续)。模型不是在「同一次 HTTP 调用里」看到结果,而是下一轮「看到」上轮的工具结果再决定下一步或总结。这点常被误解,讲清「函数调用是多轮对话协议的一部分,不是单次 RPC 的返回值」能体现你对协议本质的理解。
Q19. 如果工具执行超时或抛异常,框架会怎么兜底?会中断整轮吗?
- 【考点】错误隔离。
- 【详细解答】工具异常被 Runner 捕获,转化为结构化的 ToolResult(含错误信息/状态码),作为 tool 消息回填上下文,模型可据此调整策略(比如换种方式或放弃该工具)。除非达到 max_steps 或发生不可恢复错误(如 LLM 完全不可用),否则不会直接中断整轮。超时由工具自身或治理层约束(如工具设超时、超时就返回超时结果)。核心思想是「单点工具失败不该拖垮整轮」,把失败变成模型可观测、可应对的信息,而不是进程级崩溃。
Q20. 画一下 AgentLoop 与 AgentRunner 的职责边界,哪些状态归谁管?
- 【考点】边界厘清。
- 【详细解答】AgentLoop 管:通道消息收发、SessionManager(会话生命周期)、运行规格构建(RunSpec)、流式推送(SSE)、用户身份注入(从登录态取)。AgentRunner 管:上下文装配、模型调用、工具调度、停止判定、上下文治理(ContextGovernor)。共享的是 AgentRunSpec(Loop 造、Runner 用)与 ContextGovernor 实例(常由 Loop 建好传给 Runner)。一句话边界:Loop 拥有「外部世界与这次运行的元信息」,Runner 拥有「模型循环的每一帧状态」。身份这种跨轮、跨工具都必须一致的,放在 Loop 注入并通过 ContextVar 传递,Runner 不直接碰登录态。
下一篇:nanobot 面试通关(二)核心机制 30 题——覆盖上下文工程、记忆系统、工具安全边界与 MCP 集成。