Nanobot-Eino 上手笔记:从克隆到跑通,以及架构拆解

📅 2026/7/7 2:10:48
Nanobot-Eino 上手笔记:从克隆到跑通,以及架构拆解
第一部分知识点总结一、ReAct Agent Loop 多步推理引擎整个项目的核心在pkg/agent/agent.go基于 Eino 框架的react.Agent构建但做了大幅度的业务包装。1.1 ChatStream 完整流程入口是Agent.ChatStream(ctx, sessionID, input)每次用户消息进入时走以下流程命令拦截识别/new新建会话并归档记忆、/stop停止当前 session 下的所有 agent 和 subagent 任务、/restart重启进程。这是纯字符串前缀匹配不经过 LLM。MCP 懒加载ensureMCPConnected首次收到消息时才建立 MCP 服务器连接。通过sync.Once保证每个 MCP 配置只初始化一次——这是一个关键设计不在启动时阻塞也不在每个请求都重复连接。Pre-consolidationMaybeConsolidateByTokens在构建提示词之前先检查历史消息的 token 数是否超过contextWindow/2超过则触发记忆整理详见第三节。构建消息buildMessages先拼 system prompt固定前缀 loadPrompts(promptDir)加载的外部模板文件SOUL.md、USER.md、TOOLS.md、AGENTS.md 等 always-on 技能的全文 可用技能 XML 摘要。再拼历史消息从session.Manager读取的[]*schema.Message数组保证 append-only 结构最大化 LLM 的 Prompt Cache 命中率。React Agent 执行调用react.NewAgent创建的 agent通过reactutil.NewReactAgent封装支持ChatModel和ToolCallingChatModel两种模型接口自动检测。StreamToolCallChecker是标准 Eino 模式逐 chunk 消费流检测是否包含ToolCalls做工具调用循环终止判断。MaxStep限制推理步数默认 20防止无限循环。Post-consolidation流式输出完成后用WithMessageFuture收集中间消息工具调用结果、assistant 思考过程追加到 session 历史后再次检查是否触发整理。1.2 reactutil 的设计意图pkg/reactutil/react.go是一个独立包被agent和subagent同时引用。它存在的唯一目的就是避免循环依赖。agent创建 React Agentsubagent也要创建 React Agent但subagent被agent引用——如果把 React Agent 创建逻辑放在agent包里subagent反向引用就环了。抽到独立的reactutil后两者都可以单向依赖它。// reactutil.NewReactAgent 自动检测模型类型 if tcm, ok : chatModel.(emodel.ToolCallingChatModel); ok { agentCfg.ToolCallingModel tcm } else if cm, ok : chatModel.(emodel.ChatModel); ok { agentCfg.Model cm }goCopy1.3 RunInboundLoop —— 消息处理的核心调度器pkg/app/runloop.go的RunInboundLoop函数实现了一个 per-session 的 worker 模式从MessageBus.ConsumeInbound逐条拉取消息。用sync.Map维护sessionKey - chan *InboundMessage的映射。新 session 首次出现时启动一个独立 goroutine 消费该 channel。后果同 session 的消息串行处理保证上下文一致不同 session 并行处理。processMessage里还有一个值得注意的细节遇到可重试的 transient errorapperr.Retryable会 sleep 2 秒后重试一次完整的ChatStream而不是只重试某个原子操作。二、持久化记忆系统2.1 双层存储设计pkg/memory/memory.go实现了一个基于 Markdown 文件的双层记忆层文件写入策略用途长期记忆MEMORY.md覆盖写入每次 consolidation 全量替换结构化事实用户偏好、关键决策、长期上下文历史日志HISTORY.md追加写入append-only按时间戳标记的对话摘要grep 可搜索type MemoryStore struct { dir string // 存储目录默认 ~/.nanobot-eino/memory/ }goCopy2.2 Consolidation 流程记忆整理的核心是Consolidator.Consolidate方法consolidator.go触发条件调用方在MaybeConsolidateByTokens里计算当前历史消息的 token 数超过contextWindow/2时触发。LLM 驱动整理给模型提供一个专用的 system promptconsolidation_prompt要求模型读取 MEMORY.md 和历史对话输出更新后的 MEMORY.md 和新一期 HISTORY.md 条目。模型必须调用save_memory工具来提交结果。降级策略优先用tool_choice: required强制模型调用save_memory。如果模型不支持tool_choice降级为tool_choice: auto然后在输出里匹配 fenced code block 中的 JSON 来提取结果。连续 3 次 LLM 调用失败触发raw archive不再调 LLM直接把原始消息写入 HISTORY.md 作为止损。Token 感知裁剪MaybeConsolidateByTokens以targetTokens contextWindow/2为目标在 user 消息边界处切割历史数组保证不破坏对话轮次完整性。多轮整理最多 5 轮每轮切掉最旧的一部分直到达标。三、飞书 WebSocket 长连接pkg/channels/feishu.go使用飞书开放平台的 WebSocket 长连接模式而非 HTTP 回调larksuite/oapi-sdk-go的larkws包提供底层支持。3.1 与 HTTP 回调的差异WebSocket 长连接HTTP 回调连接模式客户端主动建立持久保持飞书服务端向配置 URL 发 POST网络要求不需要公网可访问的 URL需要公网 IP/域名 HTTPS部署复杂度低本地可直接调试需要反向代理 / 内网穿透断线处理需要自己维护重连逻辑飞书保证至少一次投递3.2 连接管理wsClient : larkws.NewClient(appID, appSecret, larkws.WithEventHandler(handler)) wsClient.Start() // 阻塞式建立 WebSocket 连接goCopyStartFeishuChannel函数启动一个 goroutine 运行wsClient.Start()Stop()方法通过 context cancel 触发优雅关闭。3.3 消息路由收到P2MessageReceiveV1事件后的处理链路去重sync.Map存储已处理的message_id防止飞书重推。内容提取支持 text、post富文本、share_chat转发消息、interactive卡片回调等消息类型统一提取纯文本。权限过滤根据配置中的allowFrom白名单过滤发送者。群聊策略mention模式只响应 机器人的消息或open模式响应所有消息。3.4 发送端发送回复时将 Markdown 内容拆解为飞书卡片格式larkim.CreateMessageReq支持自动分片——当内容超过飞书单条消息限制时按段落边界切割成多条。四、子任务系统Background Subagentpkg/subagent/manager.go实现了一个轻量级的后台子任务执行器。4.1 架构用户消息 → Agent 调用 spawn 工具 ↓ SubagentManager.Spawn(sessionID, task, bus.Metadata) ↓ 独立 goroutine 启动 React Agent受限工具集 ↓ 完成后通过 MessageBus 发布完成通知Copy4.2 核心约束受限工具集子任务只能使用filesystemshellweb工具明确排除message/spawn/cron/MCP防止子任务递归创建子任务或直接发消息。Per-session 任务追踪map[string]context.CancelFunc维护每个 session 下的活跃子任务CancelBySession支持一键终止用户发送/stop时触发。完成通知子任务执行完毕后通过MessageBus.PublishInbound注入一条senderIDsubagent的系统消息到主 Agent 的输入队列主 Agent 在下一个回复中会被告知子任务已完成。五、MCP 协议懒加载 Skills 插件体系5.1 MCP 懒加载MCPModel Context Protocol工具通过pkg/tools/mcp.go集成但连接建立不在启动时ToolConfig.MCP存储配置数组name、type、command/url等NewTools明确不处理 MCP。首次消息到达时ensureMCPConnected才通过sync.Once建立连接支持三种传输类型stdio本地命令行工具sseServer-Sent EventsstreamableHttpHTTP 流式底层使用officialmcpcloudwego/eino-ext/components/tool/mcp/officialmcp这是 Eino-ext 提供的官方 MCP 工具适配器。5.2 Skills 插件体系pkg/skill/manager.go实现了一个与 OpenClaw/Python nanobot 兼容的技能管理器Skill 定义每个技能是一个目录内含SKILL.mdYAML frontmatter Markdown 正文。加载优先级workspace~/.nanobot-eino/workspace/skills/ builtinconfigs/skills/workspace 同名覆盖 builtin。可用性检测isAvailable检查nanobot.metadata中声明的 CLI 二进制exec.LookPath和环境变量依赖。Always-on 技能标记always: true的技能全文注入 system prompt其他技能仅注入 XML 摘要Agent 按需用read_file加载。Skills 摘要BuildSkillsSummary生成带available属性、依赖缺失信息和安装指南的 XML 块。六、各核心包职责与协作关系包职责被谁引用pkg/agent核心 Agent Loop消息处理、MCP 懒加载、记忆整理、React Agent 调用cmd/server、cmd/nanobotpkg/memoryMEMORY.md HISTORY.md 双层存储LLM 驱动 consolidationagentpkg/model12 种 LLM 提供商的统一工厂OpenAI / Azure / Claude / DeepSeek / OpenRouter / Qianfan / Ollama / Ark / Gemini / SiliconFlow 等cmd/server、cmd/nanobotpkg/tools工具实现文件系统、Shell黑白名单 超时、Web 搜索/抓取、消息发送、MCP 配置不在此建立连接agent、subagent、apppkg/subagent后台子任务管理器受限工具集的独立 React Agentagentpkg/reactutilReact Agent 创建工具函数解耦 agent/subagent 循环依赖agent、subagentpkg/busMessageBus带缓冲的 pub/sub 双通道Inbound / Outbound支持ReplyTo线程回复app、agent、channelspkg/channels飞书 WebSocket 长连接适配消息接收、去重、类型解析、卡片发送apppkg/session会话管理JSONL 持久化 内存缓存append-only 消息列表agentpkg/skillSkill 管理器YAML 解析、可用性检测、始终激活技能提取、XML 摘要生成agentpkg/cron定时任务调度robfig/cron 封装支持 at / every / cron 三种模式JSON 持久化agentpkg/configViper 配置加载支持 YAML/JSON/TOMLNANOBOT_环境变量覆盖所有入口pkg/app应用层胶水代码ToolConfig 构建、Channel 启动、优雅关闭、InboundLoopcmd/server、cmd/nanobotpkg/promptPrompt 模板加载从prompts/目录读取外部文件拼接到 system promptagent协作关系图cmd/nanobot (cobra CLI) ├─ gateway → runGateway (组装全部组件 → RunInboundLoop) ├─ agent → 交互式 REPL └─ onboard → 初始化配置 cmd/server → main (同上legacy 版本) 关键数据流 config → model.NewChatModel → agent.NewAgent ├── memory.NewMemoryStore ├── session.NewSessionManager ├── tools.NewTools (不含 MCP) ├── subagent.NewSubagentManager (引用 agent.ChatStream) └── skill.Manager.LoadSkills bus.MessageBus ← channels/feishu → PublishInbound → RunInboundLoop → processMessage → agent.ChatStream → channel.Send ← PublishOutboundCopy第二部分克隆与部署过程记录问题 1GitHub HTTPS 443 端口被阻断现象git clone https://github.com/wall/nanobot-eino.git直接报fatal: unable to access https://github.com/...: Failed to connect to github.com port 443: Connection was reset。原因国内网络环境下 GitHub 的 443 端口间歇性被阻断尤其是高峰期。解决方案 A推荐—— SSH 协议替代 HTTPSgit clone gitgithub.com:iamclancyliang/nanobot-eino.gitbashCopy方案 B —— 配置 HTTP 代理如已有一个本地代理git config --global http.proxy http://127.0.0.1:10809 git config --global https.proxy http://127.0.0.1:10809bashCopy问题 2仓库地址 404现象git clone https://github.com/wall/nanobot-eino.git返回 404。原因go.mod里的 module path 是github.com/wall/nanobot-eino作者 GitHub 用户名为wall但实际仓库在github.com/iamclancyliang/nanobot-eino。我在 README 互动教程链接里才注意到正确的组织名。wall可能是一个组织名或旧用户名但实际公开仓库并不存在wall/nanobot-eino。解决使用正确的仓库地址git clone gitgithub.com:iamclancyliang/nanobot-eino.gitbashCopy问题 3SSH Key 配置与飞书验证死循环现象切换到 SSH 克隆后GitHub 要求 SSH Key。配置好 SSH Key 后推送/拉取正常但后续配置飞书应用时飞书开放平台要求验证应用拥有者发送验证邮件到 GitHub 注册邮箱点击验证链接后跳回飞书又要求重新验证——形成死循环。原因飞书的验证流程对 GitHub 关联邮箱的认可度不稳定推测与飞书开放平台的 session 机制有关。解决清理浏览器缓存和飞书开放平台的登录 session。在飞书开发者后台直接通过「应用凭证」页面重新获取 App Secret不需要再次验证。如果仍无法解决换一个飞书账号重新创建应用——用手机号注册的新飞书账号通常不会陷入此循环。问题 4Go 版本不匹配 GOPROXY 加速现象本地 Go 版本是1.24.4但项目go.mod声明go 1.25.6。go mod download时大量依赖从proxy.golang.org拉取超时。解决Go 版本下载安装 Go 1.25.6go.dev/dlWindows 下直接运行 MSI 安装包覆盖旧版本即可。GOPROXY 加速$env:GOPROXY https://goproxy.cn,direct go mod downloadpowershellCopy或者永久设置go env -w GOPROXYhttps://goproxy.cn,directpowershellCopy注意goproxy.cn对cloudwego/eino-ext及其子模块的同步还是比较及时的比goproxy.io完整。如果特定 commit hash 拉不到可以临时切direct。问题 5飞书 App Secret OCR 识别错误现象从飞书开发者后台复制 App Secret 时OCR 工具或其他截图工具的文字识别把i识别成l、0识别成O、1识别成l等。配置到config.yaml后用go run ./cmd/nanobot gateway启动飞书 SDK 报invalid app secret。原因飞书的 App Secret 包含大小写字母 数字的混合字符串某些字符在默认字体下容易混淆。解决绝对不要截图然后 OCR。飞书后台的 App Secret 旁边有「复制」按钮直接点击复制。如果已经从截图识别了逐字符对照原图人工校对特别关注i/I/l/1、o/O/0这类易混淆字符。问题 6飞书事件订阅未配置导致机器人不回复现象go run ./cmd/nanobot gateway启动正常日志显示Gateway started, waiting for messages...但在飞书群里 机器人发消息没有任何响应日志也没有收到消息的记录。原因WebSocket 长连接模式虽然不需要配置 HTTP 回调地址但仍需要在飞书开发者后台的「事件订阅」页面订阅im.message.receive_v1事件。添加「机器人已获得用户发送的消息」权限im:message。发布应用版本并让管理员审批通过企业内部应用。如果这些步骤没完成飞书不会推送事件到 WebSocket 连接。解决飞书开放平台 → 应用 → 事件订阅 → 添加接收消息 v1.0事件。权限管理 → 添加im:message权限。版本管理与发布 → 创建新版本 → 提交审批 → 管理员通过。确认.env或config.yaml中的channels.feishu.appId和appSecret与后台一致。对于群聊机器人确认已添加到群并开启mention策略groupPolicy: mention机器人时才会响应。小结nanobot-eino 是一个结构清晰、工程实战性很强的 Go AI Agent 项目。最值得学习的设计决策MCP 懒加载用sync.Once将连接建立推迟到首次使用避免启动阻塞。双层记忆 降级策略LLM 驱动的整理作为主路径连续失败 3 次后降级到 raw archive保证系统不会因 LLM 不可用而丢失历史。reactutil 包解决循环依赖小包大智慧。per-session worker基于 channel goroutine 的并发模型简洁高效地实现了同 session 串行跨 session 并行。受限子任务通过 tool 白名单机制防止子任务失控而不是依赖 prompt 约束后者不可靠。整个项目约 6000 行 Go 代码但架构层次分明每个包的职责边界清晰是很适合学习 AI Agent 工程化的参考实现。