nanobot 面试备考系列第二篇,覆盖「核心机制」共 30 题(上下文工程 8 + 记忆 7 + 工具安全 8 + MCP 7),均附详细解答。题号沿用全局编号(本篇 Q21–Q50),对应 nanobot 源码解读系列(03–06)。 上一篇:基础与架构(Q1–Q20);下一篇:进阶与生产(Q51–Q70)。


三、上下文工程(8 题)

Q21. 什么是 ContextBuilder,它和 ContextGovernor 分工是什么?

  • 【考点】装配 vs 治理。
  • 【详细解答】ContextBuilder 负责一次性装配:把系统提示、记忆文件(SOUL/USER/MEMORY)、工具 schema、历史对话拼成「这一轮开始前的初始上下文」。它是「装」的动作,发生在循环开始前。ContextGovernor 负责每轮治理:模型产出后、下一轮前,对上下文做修畸形、补结果、结果落盘、在飞压缩、历史裁剪等。它是「修」的动作,发生在循环内每轮。一个管「开局怎么拼」,一个管「过程中怎么保持合法可控」。两者配合,才既灵活又健壮——Builder 让你自由定义上下文来源,Governor 保证运行时不跑偏。

Q22. Governor 每轮有 9 步治理,挑最关键的三步讲讲。

  • 【考点】抓重点。
  • 【详细解答】最关键三步:① 剥占位符——移除模板残留标记(如 {user_name}),防止模板语法泄漏进模型视野或被原样回显;② 剔畸形 tool_call / 补缺失结果——识别坏 JSON、缺字段、工具名不存在的调用,修复或丢弃,并把缺失的结果回填,避免整轮崩溃;③ 历史裁剪 / 在飞压缩——窗口快满时砍久远历史、对超大中间结果实时摘要,保证不爆窗且成本可控。其余步骤(结果落盘、记忆写入等)是持久化与一致性保障。能讲清「这三步分别解决『脏输入 / 坏调用 / 长上下文』三类问题」就很好。

Q23. 「剥占位符」是在剥什么?为什么模型输出里会有占位符要剥?

  • 【考点】模板与生成物混淆的风险。
  • 【详细解答】占位符是系统在提示/模板里用于动态插入内容的标记(如 {user_name}{today})。它们本应在拼装上下文时被替换成真实值,但有时会泄漏:要么拼装时漏替换,要么模型在生成时把看到的占位符又原样写回输出。如果不剥,模型后续可能基于「占位符」而不是「真实值」推理,用户也会看到奇怪的 {xxx}。Governor 剥除保证模型看到和产出的是干净语义文本,不被模板语法污染。这其实反映了「模板拼接 + 生成式模型」混合系统里一个真实且易踩的坑。

Q24. 「在飞(in-flight)压缩」是什么?和事后历史裁剪有什么区别?

  • 【考点】两种压缩时机。
  • 【详细解答】在飞压缩指当前这一轮进行中,对过长的中间内容(比如一个工具返回了 8000 字网页)实时做摘要,避免单次就把窗口撑爆——是「救急」。历史裁剪指轮次之间,对久远消息做丢弃或归档——是「控长期成本」。区别在时机和对象:在飞压缩针对「本轮内刚产生的超大中间物」,历史裁剪针对「之前轮次的旧对话」。两者都为了不爆窗,但一个防单次突发,一个防缓慢累积。面试里点出「压缩有两种时机、两种目的」就显出你对上下文工程的体系化理解。

Q25. 上下文窗口快满了,nanobot 的优先级策略是什么?先砍哪部分?

  • 【考点】裁剪优先级判断。
  • 【详细解答】优先级原则是「保功能、保最近、砍陈旧」:① 优先保护系统提示与工具 schema(没有它们模型不会调用工具、不知自己是谁,属于功能必需);② 保护最近的用户输入与最新几轮对话(当前任务直接相关);③ 优先裁剪久远历史、已失效的中间工具结果、可归档的长文本。具体顺序由 Governor 的裁剪规则与优先级决定。能说出「先砍不影响当前任务完成的旧料,绝不砍让模型能工作的系统件」这个权衡,就到位了。

Q26. 为什么记忆文件(MEMORY.md 等)通常不进每轮 token,而是按需拼装?

  • 【考点】成本与相关性。
  • 【详细解答】三个理由:① 成本——记忆可能很大(长期积累),每轮全量注入会白白吃掉大量 token;② 相关性——并非每轮都用到所有记忆,无关记忆反而会干扰模型判断;③ 灵活性——记忆更新后不必每次全量重传,按需检索/注入即可。所以常见做法是用检索(如相关片段)或条件拼装,把「此刻真正用得上的记忆」注入上下文。这本质是把记忆当「外部知识库」而非「每轮必带上下文」,和 RAG 的思路一致。

Q27. 如果 Governor 修了一次畸形 tool_call,会对模型的「自我认知」产生什么副作用?怎么缓解?

  • 【考点】隐性纠偏的代价。
  • 【详细解答】副作用:模型看不到「我被修了」,它可能以为自己发出的坏调用被执行了,于是重复犯同样错误;而且回填的「格式错误已忽略」若表述模糊,模型可能误判工具能力。缓解:① 回填信息要明确且中性(「工具 X 的参数不是合法 JSON,已忽略,请重试」);② 在 system 提示里写清工具调用的格式约定,从源头降低畸形率;③ 对重复畸形设步数上限,避免无限重试烧钱;④ 必要时把「模型常犯的格式错误」写进记忆,跨轮纠正。核心:治理层的修复要「可观测地告诉模型」,而不是悄悄改完。

Q28. 用一句话总结这套上下文治理的设计哲学。

  • 【考点】抽象概括能力。
  • 【详细解答】「把模型当作不可信但可引导的生产者,用一层确定性的治理(Governor)保证进出的上下文始终合法、可控、不爆窗。」展开就是:模型擅长生成但不保证格式正确、不保证不超窗、不保证不污染,所以框架不该信任它的原始输出,而要用确定性的后处理来兜底。这和「永不信任用户输入、永远校验」的 Web 安全哲学一脉相承,只是对象换成了 LLM。

四、记忆与状态(7 题)

Q29. MemoryStore 里 SOUL / USER / MEMORY.md 各自的语义和责任是什么?

  • 【考点】记忆分层。
  • 【详细解答】三者是记忆的分层:① SOUL = Agent 的稳定人格/系统设定,相当于「我是谁、我的职责边界」,很少变;② USER = 当前用户画像与偏好(如「张三偏好简洁回答」),随用户不同而不同;③ MEMORY.md = 跨会话的长期事实记忆(「我们之前约定过 X」),会随交互积累。注入时分层拼装,保证「我是谁 / 对方是谁 / 我们记得什么」各司其职。好处是更新互不干扰——改用户偏好不必动人格,沉淀长期事实不必污染系统设定。

Q30. history.jsonl 的「游标」是干嘛的?为什么不用整文件读取?

  • 【考点】增量读取效率。
  • 【详细解答】游标记录「已消费到第几行」。每轮只需从游标位置追加读取新行,而不是重读整个 history.jsonl。为什么重要:长会话下历史文件会很大,每轮全量读取既慢又费 token,还可能超时。游标让「长会话低开销」成为可能——已处理的历史不必反复加载,新消息追加即可。这和多 loader/cursor 分页的思路一样,是处理「持续增长的状态」的标准做法。

Q31. Dream 模块是做什么的?它和 Consolidator / AutoCompact 什么关系?

  • 【考点】记忆提炼链路。
  • 【详细解答】Dream 基于 GitStore 后端,对「尚未被消费的消息尾」做提炼,把有价值的片段沉淀成长期记忆(写入 MEMORY.md 之类),是「记忆沉淀」动作,偏长期。Consolidator / AutoCompact 负责把过长的上下文压缩成 [Archived Context Summary],是「上下文瘦身」动作,偏即时。关系:Dream 解决「聊过的东西怎么记住以后用」,AutoCompact 解决「现在上下文太长怎么压」。一个管长期记忆、一个管当下上下文,两者互补,构成「短期上下文 ↔ 长期记忆」的闭环。

Q32. [Archived Context Summary] 这串标记哪来的?模型怎么知道这是「压缩过的」?

  • 【考点】压缩产物的可识别性。
  • 【详细解答】由 Consolidator/AutoCompact 在裁剪历史时生成并插回上下文。这串标记本身就是给模型(和人)的显式信号:表明此处是一段历史的摘要,不是原始对话。模型据此理解「前面发生过什么」而不必看全文,相当于给上下文打了个「此处已压缩」的书签。设计上很关键——压缩不能悄悄发生,否则模型会困惑「为什么对话断片了」;用显式标记让模型把摘要当成可信的上下文延续,而不是当成用户刚说的话。

Q33. 记忆用 GitStore 做后端,有什么好处和坑?

  • 【考点】版本化记忆的权衡。
  • 【详细解答】好处:① 版本化——每次记忆变更有 commit,可回溯「上周它记得什么」;② 可追溯/可协作——多人或多 Agent 改同一份记忆能看 diff;③ 天然支持 Dream——Dream 的差异提炼依赖 Git 的「未提交尾」概念;④ 可推远程做共享。坑:① I/O 与锁开销——每次写记忆是 git 操作,比写文件慢;② 并发写冲突——多实例同时提交可能冲突,需要锁或队列;③ 仓库膨胀——长期不 gc 会变大;④ 云配置——远程同步要配凭证和网络。所以 GitStore 适合「记忆是重要资产、要审计」的场景,高频小写入要谨慎。

Q34. 如果会话特别长,记忆压缩失败或丢了一段,会有什么表现?怎么排查?

  • 【考点】故障排查思路。
  • 【详细解答】表现:模型「忘了」早先约定、回答前后矛盾、或上下文突兀截断、或反复问已问过的问题。排查路径:① 看 history 游标是否还在推进(卡住说明消费链路断了);② 看 GitStore 是否有未提交/冲突(压缩写不进);③ 看 AutoCompact 日志是否报错(摘要生成失败);④ 检查 MEMORY.md 是否被意外覆盖(并发写)。定位思路是「先确认状态有没有被持久化,再确认压缩链有没有报错,最后看是不是并发踩了」。

Q35. 多轮对话里,怎么保证「上一轮工具结果」不会污染下一轮的新问题?

  • 【考点】会话隔离。
  • 【详细解答】工具结果作为 tool 角色消息存在于该会话的历史内,新一轮的用户输入是独立的 user 消息;Governor 的历史裁剪与角色隔离保证模型按角色区分「这是工具返回」和「这是用户新问」。隔离的边界是 session:同一 session 内历史连续(工具结果自然延续上下文,这恰恰是想要的),跨 session 则完全独立。如果业务上希望「上一轮的工具结果绝不进下一轮」,可以开新 session 或清空历史游标。关键区分:会话内连续是特性(任务延续),会话间隔离是默认(互不影响)

五、工具系统与安全边界(8 题)

Q36. Tool / Schema / ToolResult 这三个抽象各自解决什么问题?

  • 【考点】工具抽象分层。
  • 【详细解答】① Tool = 可执行能力,核心是 run() 方法,封装「怎么干活」,与「怎么声明、怎么返回」无关;② Schema = 给模型看的参数声明(name / description / input_schema),决定模型能否正确生成调用,是「模型与工具之间的契约」;③ ToolResult = 统一返回结构(成功/失败/内容),让 Runner 无论调什么工具都能用同一种方式处理结果。三者分离的价值:能力、声明、返回各可独立演化;Schema 直接喂 LLM 函数调用;ToolResult 统一让治理层(补结果/判错误)对一切工具一视同仁。

Q37. 工具是怎么被「发现」和注册的?支持插件吗?

  • 【考点】可扩展性。
  • 【详细解答】通过 ToolRegistry 集中注册(名字 → Tool 实例)。发现靠 ToolLoader:一方面用 pkgutil 扫描框架内置的工具包,另一方面用 Python 的 entry_points(包入口点)做插件式发现——第三方发布一个带 entry_points 的 pip 包,安装后 nanobot 启动时就能自动加载其工具,无需改核心代码。这体现「框架定义接口、生态贡献实现」的开放设计,也是 Production 框架该有的可扩展性。

Q38. 讲讲 shell 工具的安全边界,怎么防止执行危险命令?

  • 【考点】命令执行风险。
  • 【详细解答】shell 工具绝不「把模型生成的字符串直接丢给 bash」。边界策略:① workspace 限制——只能在允许的目录内执行;② 命令策略——白名单或黑名单,禁用危险子命令(如 rm -rfcurl | sh);③ 禁用 shell 逃逸——过滤 ; && | 管道到危险程序、反引号等,防止模型拼接出破坏性命令;④ 低权限运行——进程以受限用户跑,即使逃逸也伤害有限。核心原则:模型是「提需求的」,不是「有 root 的」,执行层要像对待不可信输入一样对待它的命令。

Q39. filesystem 工具怎么限制文件访问范围?越界了怎么办?

  • 【考点】路径穿越防护。
  • 【详细解答】所有路径在 workspace_policy 层做规范化(realpath 解析 ..、符号链接),再校验是否仍在允许的 workspace 根目录内。越界(如模型给 /etc/passwd../../secrets)直接拒绝并返回错误结果,绝不以模型给的原始路径直接 open()。这防的是「路径遍历攻击」——模型(或被注入的提示)试图读计划外的文件。关键是校验必须在「规范化之后」做,因为攻击者常用 ../ 或符号链接绕过前缀字符串匹配。

Q40. web 工具里的 SSRF 三道防线具体是哪三道?为什么需要 PinnedDNS?

  • 【考点】SSRF 防护细节。
  • 【详细解答】三道:① IP 黑名单——解析目标 IP 后,拦截内网/保留地址(10.x、127.x、169.254.x 元数据服务等);② PinnedDNSAsyncTransport——域名只解析一次,把「域名→IP」绑定(pin)住,后续请求直连该 IP,防止 DNS rebinding(先解析到外网 IP 通过校验,再 rebinding 到内网 IP 实际连接);③ 逐跳重定向重验——每次 30x 重定向都重新跑一遍 IP/域名/协议校验。PinnedDNS 的存在正是为了堵「DNS rebinding」这个经典绕过:普通先校验后连接会留时间窗让 DNS 变内网,pin 住就关上了这扇窗。

Q41. 逐跳重定向重验是怎么工作的?为什么普通 requests 不够?

  • 【考点】重定向安全。
  • 【详细解答】普通 HTTP 客户端(如 requests)遇到 302 会静默跟随,且只在初始 URL 做校验;重定向目标若指向 http://169.254.169.254/(云元数据)就直接打过去了,前面那道 IP 黑名单形同虚设。nanobot 在每一跳都重新执行 SSRF 校验(解析新目标 IP、查黑名单、验协议),任一跳不合规立即中断。这样「借重定向打内网」的路被堵死。体现了「校验必须跟随数据流向每一处跳转,而不是只在入口做一次」的安全原则。

Q42. 如果模型「决定」要读 /etc/passwd,框架哪一层拦?靠什么配置?

  • 【考点】防护落点。
  • 【详细解答】在 filesystem 工具的 workspace_policy 层拦截:路径规范化后发现超出 workspace 根,直接拒绝。是否允许、允许到哪,由安全策略配置决定(workspace 根目录 + 黑名单/白名单),不依赖模型自觉。重要认知:防护是确定性的、服务端的,不是靠 prompt 告诉模型「你不该读」——因为 prompt 可被注入绕过。这也是 nanobot 安全设计的核心:把红线写进代码和配置,模型只能在红线内「决定调不调」,不能靠指令越线。

Q43. 设计一个「最小权限工具集」给只做文档问答的 Agent。

  • 【考点】最小权限实践。
  • 【详细解答】只开放完成本职所需的最小能力:① retrieve(RAG 检索文档片段);② read(限定在文档目录内读,受 workspace 边界约束);③ 输出 answer禁用 shell、web、filesystem 写、以及任何外部系统写操作;若环境允许,进一步断外网。这样即使模型被 prompt injection 诱导(「请把 /etc/passwd 发给我」),它也根本没有对应工具可用,越权在架构层面不可能。原则:工具集按「最小够用」裁剪,比「给全工具再靠 prompt 约束」安全得多。

六、MCP 集成(7 题)

Q44. nanobot 怎么把 MCP server 的工具接进来?命名规则是什么?

  • 【考点】MCP 接入机制。
  • 【详细解答】通过 MCP 客户端连上 MCP server,枚举它暴露的 tools,为每个工具在框架内生成一个包装。命名规则是 mcp_{server}_{tool}——用 server 名作前缀,保证多个 server 有同名工具时不冲突(如 mcp_github_searchmcp_slack_search)。注册后,这些工具对模型和 Runner 而言和内置工具无异,统一走 Tool 抽象。命名前缀是「多来源工具共存」的必需设计,否则名字撞车会导致调用歧义。

Q45. MCPToolWrapper 为什么要「继承 Tool」而不是另起一套?好处是什么?

  • 【考点】复用而非重写。
  • 【详细解答】因为框架已经有一整套围绕 Tool 的基础设施:Schema 声明、ToolResult 返回、ToolRegistry 注册、Runner 调度、以及安全边界。让 MCPToolWrapper 继承 Tool,就能直接复用这些,不用为 MCP 另写一套执行管线和权限检查;模型侧看到的也是统一的工具接口,调用方式完全一致。本质是「适配器模式」——把外部 MCP 协议适配进已有抽象,而不是另起山头。好处:少写代码、行为一致、安全策略统一生效。

Q46. MCP 工具的 schema 是怎么从 MCP 格式翻译成框架内部格式的?

  • 【考点】协议适配。
  • 【详细解答】MCP 工具用 JSON Schema 描述参数(inputSchema)。框架把它映射到内部的 Schema 结构(name / description / parameters),必要时做字段类型对齐(如 MCP 的类型名与框架期望的差异)和描述补全,使 LLM 函数调用能正确生成参数。翻译是单向或双向的:调用时把模型的参数转成 MCP 要求的格式发出去,返回时把 MCP 的 content 包成框架的 ToolResult。核心是「协议差异在适配层消化,模型和 Runner 看不到 MCP 细节」。

Q47. 如果 MCP server 中途断线,nanobot 会怎么处理?能自动重连吗?

  • 【考点】连接韧性。
  • 【详细解答】框架实现重连逻辑:检测到连接断开后,尝试重建会话并重新注册工具集;重连期间,对相关工具的调用返回明确错误(如「MCP server 不可用」),而不是假装成功或卡死。具体行为由 mcp 客户端的重连/健康检查配置控制(重试次数、间隔)。设计要点:失败要「显式且可观测」,让模型和上层能据错误降级(换工具或告诉用户),而不是静默丢结果。

Q48. 热重载(hot reload)指什么场景?实现上有坑吗?

  • 【考点】动态更新。
  • 【详细解答】指 MCP server 在运行时新增/移除工具后,框架无需重启就能刷新可用工具集。坑有三:① 正在执行的调用可能因工具集变更而失效,需要妥善处理进行中的任务;② 注册表切换要原子(要么旧要么新,不能半旧半新导致调用到已删工具);③ 上下文里的 schema 要同步更新,否则模型还在调已下线的工具。所以热重载不是「重新读个列表」那么简单,要配合锁/版本号和进行中任务的保护。

Q49. 如果一个 MCP server 提供了 200 个工具,全塞进上下文会不会爆?怎么控?

  • 【考点】工具爆炸(tool explosion)。
  • 【详细解答】会爆。200 个 tool schema 不仅吃光上下文窗口,还会严重干扰模型「选哪个工具」的判断(选择越多越容易选错)。控制手段:① 按需注册——只注册当前任务相关的工具子集;② 命名空间分组——按 server/领域组织,便于路由;③ 工具检索/路由——用检索把「与当前问题最相关的 N 个工具 schema」动态注入,其余不进上下文。这跟 RAG 思路一致:工具多了就要「按需给模型看」,而不是全量铺开。

Q50. 自研 MCP server 和直接用框架内置工具,什么情况选哪个?

  • 【考点】架构选型。
  • 【详细解答】经验法则:① 现成能力(文件/Shell/Web/检索)直接用内置工具,零额外进程;② 当能力需要跨进程、跨语言、或被多个客户端共享时(比如公司已经有一个用 Go 写的 MCP 服务,Java/Python 系统都要调),用 MCP server——它把「能力即服务」解耦出来,一次实现多处复用;③ 当要接入第三方生态(已有现成 MCP server)时,直接接最省事。MCP 的价值在「解耦与复用」,如果只是单框架内部用,内置工具更轻。

下一篇:nanobot 面试通关(三)进阶与生产 20 题——覆盖 Provider 模型路由、多 Agent 委派、产品化与部署。