前言 @
上个月在《AI Agent 开发杂谈》里聊了 NekoCode 的架构思路,但那篇偏概述。这篇文章换个角度,从工程实现层面拆开几个核心子系统:它们解决什么问题,代码里怎么落地,又如何互相配合。
UI 设计 @
在聊子系统之前,先说说 NekoCode 长什么样。毕竟一个终端工具,交互体验决定了你用不用得下去。
GUI 界面包含 session 切换、明暗两种色调、上下文状态,以及 Skill、MCP、Plugin 管理页面。
TUI 界面的整体风格我称之为「深夜书房」——暗色背景上,teal 色(#4ec9b0)偶尔闪现,像暗处猫眼的反光。色彩体系刻意克制,只用了六种颜色:
- 主文字
#a0a0a0:柔和灰,长时间看不累 - Teal 主色
#4ec9b0:用于 Assistant 标签、spinner、处理卡片边框 - User 暗金
#c9a96e:用户消息的左侧竖条,暖而不刺眼 - 蓝
#7a8ba0:推理过程的分隔线 - 红
#e06c75:错误和危险操作 - Diff 绿
#98c379:代码增行
弱化文字用 #666666 和 #808080,边框线用 #333333 隐入背景。整套配色没有高饱和色,在终端里看久了也不会视觉疲劳。
下面是整个 UI 最花心思的部分。Agent 执行任务时,界面会实时展示当前状态:
- Summary 行:猫颜文字 + 工具调用计数(“3 tools · 1 change”)+ 活跃的子 Agent 彩色圆点
- Activity 区:最近 5 条非持久工具调用,每条显示工具名和参数摘要
- Changes 区:持久工具块的结果展示,当前包括 edit、bash、write,其中 edit 会内联展示 diff
- Output 区:模型流式输出的文本,固定 6 行高度,内容自动滚动
- Thinking 区:模型的推理过程(CoT),固定 6 行高度
- Header 行:spinner + 阶段名(“Thinking”)+ 当前 skill + token 计数(↑↓)+ 微压缩计数(🧹)
整个处理卡片用 teal 色 NormalBorder 包裹,和已完成的消息在视觉上明确区分。
这里简单解释下:猫爪图标代表主 Agent 的工具调用,不同颜色的子 Agent 图标代表不同子 Agent 的工具调用。Header 里出现几个子 Agent 图标,就表示当前同时启动了几个子 Agent。
当 Agent 要执行 modify/danger 级别的操作时,底部弹出确认栏。确认栏用 box-drawing 字符画边框,显示具体命令和路径,而不是只显示工具名。等级标签 [modify] / [danger] 使用黄色,[blocked] 使用红色;[safe] 命令自动放行,不弹确认框。yes/no 按钮带背景色:绿色底 [enter] yes,红色底 [esc] no。
输入交互 @
输入框支持多行(最多 8 行),Alt+Enter 换行,Enter 发送。输入 / 时底部弹出命令补全面板,Tab/Shift+Tab 选择,Enter 填入。↑/↓ 翻阅历史消息。Ctrl+E 切换工具块的折叠/展开。处理中按 Enter 触发 BTW 中断——注入新消息打断当前 LLM 调用。
设计思路 @
UI 设计上主要遵循几个原则:
- 信息密度适中:终端空间有限,不能什么都展示。非持久工具收进 Activity,edit/bash/write 这类持久结果放到 Changes,Activity 限制 5 条,让用户聚焦在模型的文本输出上,细节按需展开。
- 状态可见:Agent 在干什么、调了什么工具、token 花了多少、有没有触发压缩——这些信息实时可见,不会让人焦虑「它是不是卡住了」。
- 操作低摩擦:safe 命令自动放行、斜杠命令有补全、BTW 随时打断——减少不必要的确认和等待。
- 视觉克制:不用高饱和色、不用闪烁动画(除了猫眼)、不用 emoji 堆砌。终端本身就适合安静、专注的氛围。
子系统全景 @
NekoCode 目前由 10 个子系统 组成,按职责分为三层:
核心层——Agent 运行的基础设施:
| # | 子系统 | 代码位置 | 一句话职责 |
|---|---|---|---|
| 1 | Agent 循环 | bot/agent/runtime/ |
ReAct 循环引擎,驱动 Reason → Execute → Feedback 全流程 |
| 2 | LLM 抽象层 | bot/llm/ |
多协议适配(Anthropic / OpenAI),流式调用,指数退避重试 |
| 3 | 上下文管理 | bot/contextmgr/ |
分层上下文构建 + 五级自动压缩 + 全局 Memory + 会话快照与恢复 |
| 4 | 工具系统 | bot/tools/ |
13 个固定注册工具 + 条件/动态工具,Bash 四级安全分级,oldString/newString 内容锚定编辑 |
治理层——约束模型行为、保障安全:
| # | 子系统 | 代码位置 | 一句话职责 |
|---|---|---|---|
| 5 | Hook 系统 | bot/hooks/ |
事件驱动治理,8 个注入点,12 个内置 Hook,5 种动作类型 |
| 6 | Policy 系统 | bot/policy/ |
Ledger 操作账本 + 探索效率追踪,为 Hook 提供决策数据 |
| 7 | Prompt 构建 | bot/prompt/ |
System Prompt 分层组装,动态注入项目规范、Skills、Hints |
扩展层——能力外延与工程支撑:
| # | 子系统 | 代码位置 | 一句话职责 |
|---|---|---|---|
| 8 | 子 Agent | bot/agent/subagent/ |
独立上下文的任务委派,3 种类型,8 槽位并发管理 |
| 9 | 扩展生态 | bot/extension/ |
Plugin / MCP / Skill 三大扩展机制的统一接入 |
| 10 | 代码索引 | bot/index/ |
Tree-sitter 多语言解析 + FTS5 全文搜索,加速代码理解 |
下面逐一展开。
1. Agent 循环 @
Agent 循环是整个系统的骨架,入口是 Agent.Run()。核心是一个 for 循环,每轮调用 runTurn():
Prepare → Reason → [有工具调用?] → Execute → Feedback → PostTurn
↓ 无
PostTurn → 结束
Prepare 阶段 @
每轮开始前做三件事:触发自动压缩(AutoCompactIfNeeded,按需执行 MicroCompact 或 Compact)、计算本轮工具配额(budget.ComputeQuota,基于剩余 token 动态分配)、执行 PreTurn Hook 注入 hints。
Reason 阶段 @
调用 LLM,传入当前上下文和工具定义。返回结果分三种情况:文本回复(ActionChat)→ 进入 PostTurn;工具调用列表(ActionExecuteTool)→ 进入 Execute;工具调用解析失败(GarbledToolCall)→ 标记后进入 PostTurn,由 Hook 处理。
Execute 阶段 @
executeAndFeedback 是执行的核心。先过 filterToolCalls——检查配额是否够用、执行 PreToolUse Hook(可能 block 某些调用)、注入 pre-tool hints。然后 Executor.ExecuteBatch 并行执行允许的工具(worker pool 上限 10),收集结果后执行 PostToolUse Hook 和 PostTool Hook。最后工具结果写回上下文,下一轮 Reason 会看到这些结果。
PostTurn 阶段 @
LLM 给出文本回复后,执行 PostTurn Hook。这里会检查回答质量、验证声明是否属实、是否有未完成任务等。如果 Hook 注入了 hint 或 block 了 final answer,循环继续;否则结束。
保护机制 @
- 步数上限:硬上限 150 步(
maxAgentSteps),防止死循环 - 连续 hint 检测:连续 3 次 PostTurn hint 注入(模型一直说话但不调工具)→ 强制停止
- 连续失败检测:连续 5 次 LLM 调用失败 → 强制停止
- 兜底合成:循环结束但没有文本输出时,额外发一次 LLM 请求要求总结;主路径失败还有 emergency fallback(30s 超时 + 自动压缩 + 不重试)
BTW 中断 @
用户可以在 Agent 执行中按 Enter 插入新消息。实现上通过 steering channel(buffer 4)接收中断,replaceCtx() 取消当前 LLM 调用的 context。被中断的调用返回 context.Canceled,Reason 阶段检测到后标记 Interrupted=true,runTurn 把上下文截断到调用前状态(TruncateTo),清空 steering channel 中的消息注入上下文,重新开始 Reason。
2. LLM 抽象层 @
LLM 层负责屏蔽不同供应商的协议差异。核心接口是 types.LLM,两个实现:
- Anthropic 协议(
bot/llm/anthropic/):原生 Messages API,支持 thinking/reasoning 内容分离 - OpenAI 兼容协议(
bot/llm/openai/):Chat Completions API,DeepSeek、MiniMax 等国内模型通过此协议接入
流式处理 @
tools.CallLLM 封装了统一的流式调用逻辑。支持三个回调:TextCallback(文本增量)、ReasoningCallback(推理过程增量)、CheckDone(检查是否应提前终止)。流式解析时自动分离 reasoning content 和普通 content。
重试机制 @
withRetry 实现指数退避重试,处理网络抖动和临时性 API 错误。重试次数和间隔可配置。
Thinking 控制 @
子 Agent 的 thinking 模式强制关闭(节省 token),主 Agent 根据模型配置决定。跨协议统一控制——Anthropic 通过 thinking.budget_tokens,OpenAI 兼容协议通过 reasoning_effort 或 extra_body 参数。
3. 上下文管理 @
上下文管理是 Agent 系统最复杂的子系统之一。核心挑战:窗口有限、前缀缓存宝贵、压缩不能丢关键信息。
分层构建 @
Build() 按以下顺序组装上下文:
Layer 0: System Prompt(最稳定,最大化前缀缓存命中)
Layer 0M: 项目记忆(~/.nekocode/memory.md)
Layer 0.5: Archive(压缩摘要)+ Skills + Hints
Layer 1: 对话消息(经过 filterValidMessages 过滤)
Layer 2: Todo 列表
filterValidMessages 做两件事:移除没有对应 tool result 的 assistant tool_call 消息(孤儿调用);移除没有对应 tool_call 的 tool 消息(孤儿结果);空 content 的非 system 消息补 .(某些 API 不允许空 content)。
五级压缩 @
由 compact.Compactor 驱动,基于上下文窗口剩余量自动触发,默认阈值如下:
| 级别 | 剩余 token | 动作 | 是否调 LLM |
|---|---|---|---|
| Normal | > 44,800 | 无操作 | - |
| Warning | ≤ 44,800 | 仅告警 | - |
| MicroCompact | ≤ 35,200 | 清除旧的只读工具结果,保留最近 5 个 | 否 |
| Compact | ≤ 25,600 | LLM 生成结构化摘要,压缩最旧消息 | 是 |
| Blocking | ≤ 6,400 | 拒绝新输入,强制压缩 | 是 |
MicroCompact 是轻量级操作——只清除标记为 compactable 的工具结果(read/grep/glob 等只读工具的旧输出),不触发 LLM 调用,不影响前缀缓存。Compact 调用独立 LLM 客户端生成摘要。
摘要合并 @
多次 Compact 后会产生多个 Archive。Summarize() 调用 MergeClient(独立 LLM 客户端)将新旧 Archive 合并为一个,避免 Archive 膨胀。
Project Memory @
项目记忆当前是全局文件 ~/.nekocode/memory.md,启动时加载并注入 Layer 0。文件按 5 个固定段落组织:Tech Stack、Active Goals、Completed Tasks、Key Architecture Map、User Preferences。代码里已经有 MergeFromCompaction,用于把压缩摘要里的关键事实合并回 memory,但它不是按 session 单独写入 ~/.nekocode/sessions/<id>/memory.md。
快照与恢复 @
ManagerSnapshot 捕获完整状态——SystemPrompt、Skills、Archive、Memory、Hints、Messages、CompactBoundary、Budget、Tracker。支持会话存档和恢复,/sessions 命令依赖此机制。
4. 工具系统 @
注册机制 @
工具通过 Registry 统一管理,基于泛型 common.Registry[Tool]。每个工具实现 Tool 接口(Name()、Description()、Parameters()),注册后自动生成 LLM function calling 的 tool definitions。支持条件注册和动态注册(Skill 加载时动态添加工具)。
固定工具与条件工具 @
| 工具 | 功能 | 安全等级 | 执行模式 |
|---|---|---|---|
| bash | Shell 命令执行 | Safe~Blocked(智能分级) | Sequential |
| read | 文件读取 + 二进制检测 | Safe | Parallel |
| write | 文件创建/覆盖 | Modify | Sequential |
| edit | oldString/newString 内容锚定替换 | Modify | Sequential |
| list | 目录列表 | Safe | Parallel |
| tree | 目录树可视化 | Safe | Parallel |
| glob | 文件模式匹配 | Safe | Parallel |
| grep | ripgrep 内容搜索 | Safe | Parallel |
| web_search | Web 搜索 | Safe | Parallel |
| web_fetch | 网页抓取 | Safe | Parallel |
| question | 向用户提问 | Safe | Sequential |
| todo_write | 任务列表更新 | Safe | Sequential |
| task | 子 Agent 委派 | Safe | Parallel |
| image_gen | AI 文生图,配置了图片模型后注册 | Safe | Sequential |
| project_info | 代码索引查询,索引管理器可用时注册 | Safe | Parallel |
| skill | Skill 加载工具,扩展层动态注册 | Safe | Sequential |
固定注册在 bot/tools/catalog.RegisterAll 里的工具是 bash、read、write、list、tree、glob、edit、grep、web_search、web_fetch、question、todo_write、task。image_gen 只有在配置了图片生成模型时注册,project_info 在代码索引管理器可用时注册,skill 由扩展层动态注册;MCP 工具则来自外部服务器。
Bash 四级智能分级 @
不是简单的「允许/禁止」二分,而是按关键词智能分级:
- Safe(自动放行):
git log、git diff、git status、go version、go vet、ls、cat、ps、du、file等纯输出命令 - Modify(弹框确认):默认级别
- Danger(警告确认):
curl、wget、rm、chmod、kill、reboot、git push、git reset --hard - Blocked(直接拒绝):
sudo、eval、ssh、curl | bash、dd、mkfs
Edit 内容锚定 @
使用 oldString/newString 模式而非行号替换。hasSufficientEditAnchor 检查锚定充分性:oldString ≥ 200 字符,或包含 ≥ 5 个非空行。锚定充分时,即使文件未被 read 过也能放行(ReadBeforeWriteHook 的例外)。编辑后自动跑 gofmt 检查 Go 文件语法。
工具结果预算 @
写入上下文前会经过 BudgetResult。当前实现只对 grep 结果做首尾保留:保留前 50 行和后 50 行,中间用截断标记替代,避免大范围搜索结果撑爆上下文窗口;其他工具结果暂不在这里做通用 50KB 截断。
5. Hook 系统 @
Hook 是 NekoCode 最核心的治理机制。设计思想:不修改 Agent 循环代码,通过事件注入约束模型行为。
8 个注入点 @
UserSubmit → PreTurn → PreModelRequest → [Reason] → PreToolUse → [Execute]
↓
PostToolUse → PostTool
↓
[Reason 返回文本] → PostTurn → Stop
5 种动作类型 @
| 动作 | 效果 |
|---|---|
Hint |
注入提示到下一轮上下文,引导模型行为 |
Stop |
立即终止 Agent 循环 |
BlockTool |
阻止某个工具调用执行 |
RequireTool |
要求模型必须调用某类工具(否则 block final answer) |
BlockFinal |
阻止模型输出最终答案(要求修正后重试) |
12 个内置 Hook @
| Hook | 注入点 | 职责 |
|---|---|---|
| QuotaHook | PreTurn | 工具调用配额管理 |
| ToolResultGuardrailHook | PreModelRequest | 工具结果过多时提醒检查未完成任务 |
| ReadBeforeWriteHook | PreToolUse | 未读文件不准改 |
| ReadOnlySpiralHook | PostTool | 连续只读不写 → 要求总结 |
| VerificationHook | PostTurn | 有未完成任务但没调工具 → block |
| ExplorationExhaustedHook | PreTurn | 探索配额耗尽 → 要求行动 |
| ExplorationGuardHook | PreToolUse | 探索耗尽后 block 探索类工具 |
| ExploreCascadeHook | PostTool | 过多 researcher 子 Agent → 提醒收敛 |
| ProgressStallHook | PostTool | 连续 8 轮无进展 → 强制要求 edit/write/bash |
| CompletionQualityHook | PostTurn | 回答质量检查 |
| GarbledCircuitBreaker | PostTurn | 累计 5 次乱码 → Stop |
| FinalCheckHook | PostTurn | 最终答案诚实性校验(3 条规则) |
FinalCheckHook 的三条规则 @
- missing_verification:修改了非文档文件但没有通过验证,且回答未声明「未验证」→ block
- unsupported_test_claim:回答声称测试通过,但 ledger 无验证记录 → block
- unreported_tool_error:存在工具错误,但回答声称完成且未提及失败 → block
Response Gate @
BlockFinal 和 RequireTool 不是无限重试。responseGate 限制最多 2 次重试,超过后放行答案,防止「被 block → 重试 → 再被 block」的死循环。
状态共享 @
Hook 之间通过 store(map[string]int64)和 strVals(map[string]string)共享状态。Policy Manager 每轮开始时同步 Ledger 数据到 store,Hook 可通过 StatePatch 修改状态。
6. Policy 系统 @
Policy Manager 是 Hook 系统和 Agent 循环之间的桥梁,持有三个核心组件:
- HookReg:Hook 注册表引用
- Ledger(
bot/policy/ledger/):操作账本,记录文件读取、修改、验证、工具错误、被 block 的工具调用 - Exploration(
bot/policy/budget/):探索效率追踪器——评估探索类工具调用的收益递减
每轮同步 @
ResetTurnBetween 在每轮开始时将 Ledger 状态同步到 Hook store:
StoreLedgerModified:已修改文件数StoreLedgerVerified:是否有通过验证StoreLedgerErrors:工具错误数StoreLedgerNonDocModified:是否有非文档类修改StoreLedgerProgress:本轮是否有新进展(新读取/修改/验证)
Hook 基于这些状态做出决策。例如 ProgressStallHook 检查 StoreLedgerProgress,连续 8 轮为 0 则触发。
探索追踪 @
ExplorationTracker 评估探索类工具调用的效率。每次探索类调用后更新评分,评分归零且调用超过 10 次时,ExplorationExhaustedHook 触发。
7. Prompt 构建 @
System Prompt 采用分层组装策略,由 bot/prompt/ 负责构建:
- 基础层:角色设定(黑猫形象、软萌风格)、核心准则(先理清思路、按需探索、信任权威数据等)
- 工程规范层:不添加超出需求的功能、不设计假设性需求、修改成本大时直接重写
- 输出规范层:工具调用时禁止解释性文字、ASCII 树必须套代码块、inline code 仅用于短路径
- 工具准则层:优先专用工具、并行只读、串行修改、edit/write 前必须 Read
- 动态注入层:Skills 列表、Hints(Hook 注入)、项目规范(NEKOCODE.md)、环境信息(OS/Arch/日期/工作目录)
动态注入的内容放在 System Prompt 末尾,保证前缀缓存最大化——基础层几乎不变,动态层变化频繁但放在最后。
8. 子 Agent @
子 Agent 通过 task 工具触发,三种类型:
| 类型 | 用途 | 可用工具 |
|---|---|---|
| executor | 执行编码任务 | read/write/edit/bash/grep/glob/list |
| researcher | 代码探索/调研 | read/grep/glob/list/web_search/web_fetch |
| verify | 验证修改 | read/grep/glob/list/bash |
Slot 管理 @
subSlotManager 控制并发——最多 8 个 slot,每个有独立颜色索引(0-7,TUI 中区分)。Acquire 在无可用 slot 时阻塞等待(sync.Cond),Release 后唤醒等待者。
上下文隔离 @
子 Agent 使用 NewSub 创建轻量级 ContextManager——独立 SystemPrompt 和 Messages,不继承主 Agent 对话历史。这种「纯净委派」避免上下文污染,也更省 token(前缀缓存更友好)。
回调桥接 @
子 Agent 的工具调用通过 TaskCallbackFn 回调到主 Agent 前端——sub_tool_start、sub_execute_tool、sub_agent_start、sub_agent_end。回调携带 subID:colorIdx 标签,前端据此渲染不同颜色的子 Agent 卡片。
设计原则 @
遵循「Self-Serve First」——主 Agent 优先自己完成任务,只有满足三个条件才委派:跨 5 个以上文件、需要独立上下文、单回合确实太复杂。
9. 扩展生态 @
三大扩展机制统一接入:
- Plugin(
bot/extension/plugin/):兼容 Claude Code 插件格式,支持 GitHub 和本地路径安装。当前主要接入插件声明的 Hooks、MCP 配置、commands 与 agents 等资源 - MCP(
bot/extension/mcp/):JSON-RPC 2.0 协议,接入外部工具服务器。当前实现以 stdio 进程客户端为主,并通过动态工具适配器暴露 MCP tools - Skill(
bot/extension/skill/):YAML 定义技能包,/<skill>一键加载。Skill 可注入 System Prompt 片段,并通过动态 skill 工具按需加载
10. 代码索引 @
基于 Tree-sitter 的多语言代码解析 + SQLite FTS5 全文搜索。
- 符号索引:解析函数、类型、变量定义,支持
symbol:查询 - 依赖分析:解析 import/include,支持
deps:查询 - 全文搜索:FTS5 索引所有源码,支持
search:查询 - 文件定位:文件名模糊匹配,支持
file:查询
通过 project_info 工具暴露给 Agent,减少不必要的 read/grep 调用,加速代码理解。
结尾 @
NekoCode 的 10 个子系统围绕几个核心原则设计:分层解耦,比如上下文分层、Hook 独立于循环;纵深防御,通过多层 Hook 交叉验证;预算意识,把前缀缓存、配额管理和探索收益递减检测放进主循环;失败可恢复,依靠快照/恢复、兜底合成和 response gate 限流。
目前项目还在实验阶段,Hook 规则和压缩策略还有很多调优空间。如果你对某个子系统的实现细节感兴趣,docs/ARCHITECTURE.md 有更完整的代码级文档。欢迎到
GitHub 提 issue 或 PR。