BemoDB 2.0

Back

Coding Agent 已经不只是“会聊天的补全器”。用户说一句“把这个测试修绿”,系统要自己读文件、改代码、跑测试、看失败、再改一轮,直到目标成立。这件事能不能稳定发生,主要取决于包裹模型的那一层执行框架。

BemoCode 就是围绕这件事搭起来的项目。它用大约 5500 行 TypeScript,把 Claude Code 一类终端 Coding Agent 的核心机制做成可读、可跑、可改的最小实现:Agent Loop、工具系统、权限、上下文压缩、记忆、技能、Sub-Agent、MCP,以及 /goal/loop、Auto Mode 这组自治能力。它不追求复刻某个商业产品的全部边界处理,而把重点放在讲清一件事:模型为什么能动手干活。

这篇文章沿源码和项目文档拆开 BemoCode:哪些设计真正构成 Agent,哪些是工程防护,哪些又和 Claude Code、Codex CLI、Cursor、Aider 形成了不同分工。

BemoCode 的价值,在于把 Agent 从“会说建议”推进到“能在本地闭环执行”的那一套 harness。

它到底在补哪一段缺口#

普通 Chatbot 的交互很短:

用户问题 -> 模型生成文本 -> 用户自己去执行
text

Coding Agent 的目标更长:

用户目标 -> 读代码 -> 改文件 -> 跑测试 -> 看错误 -> 再修复 -> 直到停止条件成立
text

中间至少有五个缺口,只靠模型参数补不上:

缺口模型单独做不到的地方BemoCode 的对应层
环境感知看不到本地文件、Git 状态、命令输出System Prompt 动态上下文 + 文件/搜索/Shell 工具
真实执行文本不会自动改仓库Tool Use 协议 + 工具执行器
多步反馈一次回答无法预知测试结果Agent Loop 把结果重新喂回模型
风险控制可能误删、误 push、误改关键文件权限模式、规则、危险命令检测、确认与 Auto Mode
长任务状态上下文会爆,会话会断压缩、记忆、会话恢复、Goal/Loop 预算

一句话概括:BemoCode 是一个终端里的 Coding Agent Harness。模型负责理解与决策;Harness 负责持续提供上下文、执行工具、校验权限、管理长度、控制预算,并把 Skills、Sub-Agent、MCP 接到同一条循环上。

它的边界也写得很清楚:

  • 它不是基础模型,不负责训练和推理服务。
  • 它不是 OS 级沙箱,权限检查发生在应用进程内。
  • 它不是完整 IDE,主界面仍是行式 CLI。
  • 它不是多 Agent 团队系统;Sub-Agent 采用 fork-return,不共享工作区协作。
  • 它按公开可观察行为与通用 Agent 写法重做,属于学习实现,不声称复刻 Claude Code 源码。

这些边界很重要。很多“仿 Claude Code”项目会在介绍里把能力吹成产品级;BemoCode 的文档更强调:先把闭环做对,再谈生态与成熟度。

项目结构:为什么读起来像一张地图#

源码集中在 src/,职责分得比较直接:

文件大约行数职责
agent.ts2190双后端循环、流式解析、工具调度、压缩、Plan、自治
tools.ts884工具定义、执行器、权限规则、危险命令检测
autonomy.ts462Auto Mode 分类、/goal/loop 的规则与提示词
cli.ts449参数解析、REPL、命令、会话与信号
ui.ts386启动页、工具输出、Diff、用量展示
memory.ts392四类记忆、索引、语义召回、异步预取
mcp.ts266stdio JSON-RPC MCP 客户端
prompt.ts253静态/动态系统提示词与项目指令
subagent.ts199内置/自定义 Sub-Agent 与 fork-return
skills.ts175Skill 发现与 inline/fork
session.ts63会话保存与恢复
frontmatter.ts41Markdown frontmatter 解析

依赖刻意压得很薄:TypeScript、Anthropic SDK、OpenAI SDK、chalk、glob。没有完整 CLI/TUI 框架,也没有 bundler 重链路。好处是一次请求的调用链可以顺着文件读完;代价是 agent.ts 已经接近“上帝对象”,模型适配、上下文策略、工具编排都堆在同一处。

项目还附带了一套分步教程:docs/ 讲原理,steps/ 给出每章可独立运行的最小实现,并且支持 node steps/run.mjs 7 这种不需要 API key 的 mock 演示。于是它同时具备三层身份:

  1. 可运行的 coding agent
  2. 可逐步复现的教程仓库
  3. 可对照 Claude Code 概念的架构标本

核心:Agent Loop 为什么是全部差异的起点#

BemoCode 最关键的一句话几乎贯穿所有章节:Agent 的主动性,来自宿主程序反复执行状态转换,而不是模型“天生会持续工作”。

最小循环可以写成:

messages.push(userMessage);

while (true) {
  const response = await model(messages, tools);
  messages.push(response.assistantMessage);

  if (response.toolCalls.length === 0) break;

  const results = await executeTools(response.toolCalls);
  messages.push(asToolResults(results));
}
ts

循环是否继续,取决于模型有没有继续发出 tool call;代码只负责把循环转起来。这一点把 Coding Agent 和普通聊天机器人彻底分开:

  • 聊天机器人:一次输入,一次输出
  • Agent:一次目标,多轮“决策 -> 行动 -> 观察 -> 再决策”

消息数组如何变长,比任何抽象定义都直观:

第 1 轮
  user: 修这个 bug
  assistant: 我要 read_file
  user: tool_result(文件内容)

第 2 轮
  assistant: 我要 edit_file
  user: tool_result(编辑成功)

第 3 轮
  assistant: 已修复
  (没有 tool_use,循环结束)
text

所谓“Agent 记得自己做过什么”,在这一层首先是完整消息历史不断追加。工具结果用 user 角色装回,是 Anthropic 协议要求;OpenAI 兼容路径则使用 role: tool。BemoCode 为两条后端分别维护历史,避免每次请求前做有损转换。

真实 agent.ts 在这个骨架上叠加了很多生产问题:

  • Anthropic 与 OpenAI 不同的流事件和消息结构
  • 429 / 503 / 529 与网络瞬断的指数退避重试
  • 多个只读工具并行执行
  • tool call id 与 result 严格配对
  • 权限拒绝也要作为结果返回模型
  • 上下文超限前的分层压缩
  • Goal、Loop、Plan、Auto、Sub-Agent 的额外终止规则
  • Ctrl+C 中止当前请求,同时保留会话

这也解释了为什么“做一个 Agent demo”容易,而“做一个能跑几十轮的 Agent”难。真正难的是 while 周围那些让协议不破、状态不乱、费用不炸、危险动作不漏的东西。

工具系统:模型如何真正改变世界#

模型本身只能生成文本和结构化调用意图。真正改文件、跑命令的是工具执行器。BemoCode 把工具看成三层契约:

  1. 给模型的 Schema:名字、描述、参数
  2. 执行前策略:权限、模式、并发、参数校验
  3. 返回给模型的结果:稳定格式、错误语义、截断与落盘

内置工具大致分几类:

类别工具作用
读取read_filelist_filesgrep_search看内容、结构、匹配位置
修改write_fileedit_file覆盖写入或精确替换
执行run_shell测试、git、安装依赖等
扩展skillagenttool_search工作流、子任务、延迟工具激活
网络web_fetch拉网页并转文本
规划enter_plan_modeexit_plan_mode延迟加载的规划切换
自治schedule_wakeup仅 dynamic loop 期间临时挂载

最能体现“工具设计决定能力上限”的,是 edit_file

为什么不用整文件重写#

edit_file 要求 old_string 在文件中唯一匹配,再替换为 new_string。失败时明确报错,不猜位置。这样做有几个直接收益:

  • 修改范围小,无关格式扰动少
  • Diff 更容易审查
  • 唯一性失败会把错误暴露给模型,让它重新定位
  • 对并发修改更敏感

配合 read-before-edit 和 mtime 检查,系统还会拦截一类典型 TOCTOU 问题:

Agent 读取 A.ts
用户同时改了 A.ts
Agent 按旧版本替换
用户改动被覆盖
text

mtime 不是完美版本号,但比完全不检查可靠得多。更严格的实现可以记录内容哈希,并让编辑携带期望版本;BemoCode 选择了“够用且可解释”的中间点。

并行执行看副作用,不看工具名字#

多个互不影响的读取可以并行,写文件和 Shell 保持顺序。判断标准看的是副作用与依赖:

  • 有没有共享可变状态
  • 是否依赖前一个调用结果
  • 重排会不会改变语义
  • 是否产生外部副作用

web_fetch 虽然不改本地文件,但可能访问外部服务、泄露 URL 信息,所以在 Auto Mode 中不会被当成无条件安全的本地读取。

结果要同时服务两个消费者#

工具结果有两个读者:模型和人。

  • 模型需要足够完整的内容,才能继续判断
  • 人需要快速扫描,不想被健康检查刷出几百行灰字

于是 UI 默认只展示最多 5 行摘要,消息历史仍保留完整结果;超过约 30KB 的结果会落到 ~/.mini-claude/tool-results/,上下文里只留预览和路径。这是“呈现压缩”和“语义压缩”的分离。很多 Agent 产品在这里会混成一层,结果要么终端被淹没,要么模型拿不到继续工作所需的信息。

权限:应用级刹车,而不是沙箱幻觉#

Agent 一旦能跑 Shell,就同时具备了修 bug 和毁掉仓库的能力。BemoCode 的权限层是应用级策略防线,目标是降低误操作概率,而不是替代容器或 OS sandbox。

权限模式包括:

模式行为倾向典型用途
default读取直接执行,写入与危险动作按策略确认日常交互
acceptEdits自动接受文件编辑,其他风险仍检查高频编码
plan只读分析,限制修改先调研再实施
dontAsk不能询问时拒绝需确认动作非交互任务
bypassPermissions绕过常规确认仅可信隔离环境
auto静态规则后,再由风险分类器判断长时间自治任务

决策顺序非常关键:确定性硬规则优先于概率模型

工具调用
  -> Plan Mode 是否禁止
  -> deny 规则是否命中
  -> allow 规则是否命中
  -> 是否静态安全读取
  -> 按模式走 bypass / dontAsk / 用户确认 / Auto 分类器
text

明确命中 deny 的命令,不应再问分类器“你觉得安全吗”。这是 fail-closed 思路的一部分:安全关键路径必须可解释、可复现。

危险命令检测覆盖递归删除、破坏性 git、sudo、mkfs、dd、杀进程、关机等模式。优点是快、可解释;缺点也很清楚:基于字符串和正则,不理解完整 Shell AST。别名、间接脚本、编码命令、解释器嵌套都可能绕过。Claude Code 一类生产系统会用 tree-sitter 做更深的静态分析;BemoCode 在教程规模下选择用正则覆盖常见高风险模式。

威胁模型文档写得也比较老实:

  • 模型误删:有确认与危险检测,但仍可能漏判
  • Prompt Injection:工具权限独立于模型文本,但低风险工具组合仍可能有风险
  • MCP Server:仍走统一循环,外部进程本身可执行代码
  • Sub-Agent:Auto/Plan 会传播,其他模式仍需审计
  • Shell 资源耗尽:有超时和输出上限,缺少 CPU/内存/网络隔离

结论很明确:跑不可信任务时,应额外使用 worktree、容器、受限 key 和人工审查。权限层是刹车,不是牢笼。

上下文工程:有限窗口里的“无限错觉”#

Coding Agent 的上下文压力,远高于普通聊天。工具结果会不断塞入日志、源码和搜索命中;如果只追加不压缩,最终会出现 API 拒绝、延迟飙升、早期约束被淹没、缓存命中率下降。

BemoCode 采用分层处理:

  1. 执行期截断:单次结果过大时保留头尾,避免单条输出占满内存
  2. 大结果落盘:超过约 30KB 的结果持久化,消息只留预览和路径
  3. Budget 收紧:使用率升高后逐步缩小工具结果预算
  4. Snip:清理较旧、可重新读取的结果;缓存仍热时可延后
  5. Microcompact:空闲超过约 5 分钟后做轻量清理
  6. Auto-compact:接近有效窗口约 85% 时,让模型生成摘要替换早期历史

这里有两个容易被忽略的工程点。

第一,压缩不能随意删消息。工具协议要求每个 tool_use 都有对应 result,ID 必须配对,角色顺序必须合法。清理单位应是完整 turn 或调用-结果对。随便删数组元素,线上最常见的结果是 400 协议错误。

第二,摘要会丢细节。精确错误文本、用户早期隐含约束、未验证假设、细粒度修改上下文,都可能在摘要中消失。因此摘要提示词应要求区分“已验证事实”“已完成工作”“未解决问题”“下一步”,而不是生成一段空泛概述。

System Prompt 也按缓存友好方式拆成两层:

  • 静态核心:身份、边界、工具原则、编码规范
  • 动态环境:cwd、Git、Skills、Agents、Memory、当前模式

稳定前缀尽量不变,频繁变化的内容后置或注入首条用户消息,以便 Anthropic Prompt Caching 复用。消息尾部可打缓存断点,但实现时必须克隆消息,避免 cache_control 污染长期会话状态。

上下文工程的目标,是让有限窗口里的 token 对当前决策最有帮助。BemoCode 因此选择按需读取、Skill 延迟加载、结果裁剪、记忆选择性召回,避免启动时扫描全库全文。

记忆:跨会话保留什么,不保留什么#

会话历史回答“这次发生了什么”;长期记忆回答“未来还值得重复知道什么”。如果把所有历史都注入每次请求,成本和噪声都会失控。

BemoCode 的记忆按项目维度存放:

~/.mini-claude/projects/<cwd 的 SHA-256 前 16 位>/memory/
├── MEMORY.md
├── user/
├── feedback/
├── project/
└── reference/
text

四类记忆对应不同用途:

  • user:用户偏好
  • feedback:用户纠正
  • project:项目事实与约定
  • reference:可长期复用的参考信息

项目路径做 hash,既避免完整本地路径直接暴露在目录名中,也让不同工作目录自然隔离。

召回采用异步预取:新一轮任务开始时启动记忆检索,主循环不必同步等待。优点是首请求延迟更低;缺点是记忆可能赶不上当前轮。设计要求很严格:不能为了记忆阻塞主循环,也不能在工具调用中途随意改已经发出的消息。来不及插入就留给下一轮。

适合写入长期记忆的,是用户明确说“以后都这样做”的偏好、稳定的项目约定、明确纠正,以及可被未来任务验证的事实。不适合写的是临时状态、未验证猜测、密钥和偶然路径。

当前不足也很清楚:可视化管理弱、索引有 200 行 / 25KB 限制、hash 目录对用户不友好、语义相关不等于事实正确。这些都说明记忆系统仍然是 harness 能力,而不是“模型突然会记事了”。

Skills:把工作方法外置成可发现资产#

Skill 不是新模型,也不是硬编码工具。它是一份可发现的 Markdown 工作流说明,告诉 Agent:

  • 什么时候该用
  • 应该按什么步骤做
  • 能调用哪些工具
  • 如何解释参数和输出

发现路径主要是:

~/.claude/skills/<name>/SKILL.md
./.claude/skills/<name>/SKILL.md
text

项目级可以覆盖用户级,方便仓库对通用能力做项目特化。加载时解析 frontmatter,提取 name、description、是否允许用户调用、是否 fork 等信息。

两种执行模式:

模式上下文关系适合场景
InlineSkill 内容直接加入当前 Agent短流程、需要共享当前上下文
Fork创建隔离子任务,返回摘要长流程、探索任务、避免污染主上下文

模板支持 $ARGUMENTS${ARGUMENTS}${CLAUDE_SKILL_DIR},让同一份 Skill 可以接受不同参数和路径。

Tool 与 Skill 可以这样区分:

Tool 扩展动作空间,Skill 扩展工作方法。前者解决“能做什么”,后者解决“应该怎样做”。

这层设计的工程意义在于:流程知识从主 Prompt 和 TypeScript 逻辑中外置出来,项目规则可以独立迭代,不必每次改 agent 源码。

Sub-Agent:隔离上下文,而不是凭空增加智力#

大任务全塞进一个 Agent,上下文很快会满。BemoCode 的 Sub-Agent 采用 fork-return:

父 Agent
  -> 调用 agent 工具
  -> 创建独立消息历史的子 Agent
  -> 子 Agent 完成探索/规划/执行
  -> 只把摘要结果返回父 Agent
text

内置角色包括:

  • explore:只读探索
  • plan:只读分析与方案设计
  • general:通用任务

子 Agent 继承父级模型和后端,并根据角色限制工具。exploreplan 只有读取类工具;general 工具更完整,但禁止继续调用 agent,避免无限递归。

为什么要隔离:

  • 探索大型目录、读多份文档、分析失败日志时,中间过程噪声很大
  • 这些噪声进入主上下文会快速挤掉关键约束
  • 父 Agent 只需要结论,不需要全部检索轨迹

它也明确不是 Agent Team:

  • 没有共享消息总线
  • 子任务之间不能直接通信
  • 没有共享锁和冲突解决
  • 没有任务依赖图调度
  • 没有 worktree 隔离与自动合并

更准确的名字是 isolated delegation。它解决的是“主上下文装不下”,而不是“多角色协作编程”。

Plan Mode:先调查,再获得修改权#

Plan Mode 的关键,在于改变可用工具和权限,而不仅是让模型多写一份计划:

进入 Plan Mode
  -> 允许读取、搜索、分析
  -> 禁止普通文件修改与危险执行
  -> 生成计划文件
  -> 用户选择执行、手动执行、清空后执行或继续规划
text

计划保存在 ~/.mini-claude/plans/,避免和仓库业务文件混在一起。审批选项也有明确语义:

  • 清空并执行:清掉计划探索噪声,再按计划动手
  • 直接执行:保留当前上下文继续
  • 手动执行:只保存计划,用户自己操作
  • 继续规划:继续只读

工程上最重要的一点是:Plan Mode 必须写进工具执行器,不能只停在 UI 状态。如果界面显示“只读”,模型仍能直接 write_file,那只读就只是装饰。

MCP:把外部能力接进统一工具协议#

BemoCode 实现了 stdio 传输的 JSON-RPC MCP 客户端:

启动 MCP Server 子进程
  -> initialize
  -> tools/list
  -> 把工具转成模型 Schema
  -> 模型调用 mcp__server__tool
  -> tools/call
  -> 统一结果返回模型
text

连接是 lazy 的:启动页不阻塞,首次实际对话时再初始化,超时约 15 秒。配置来自全局与项目级 .claude/settings.json.mcp.json,项目级覆盖同名定义。动态工具用 mcp__server__tool 命名,避免与内置工具冲突。

MCP 解决的是生态接入问题:Agent 不必为每个数据库、浏览器、设计工具或企业服务写专用适配器。但协议标准化不等于安全。MCP Server 可以执行自己的代码、访问自己的数据,返回内容也可能包含 Prompt Injection。所有 MCP 调用仍应经过权限、超时和审计策略。

当前实现重点覆盖 stdio tools,尚未完整覆盖 HTTP/SSE 认证、resources/prompts、复杂重连和远程租户治理。对学习项目来说,stdio 足够说明“外部工具如何进入统一循环”;对生产系统来说,远程 MCP 还要补一整套治理层。

自治三件套:要不要继续、什么时候继续、能不能动手#

普通 Agent 通常由用户驱动每一轮。BemoCode 的 Phase 3 把“持续自主工作”拆成三个入口:

  • /goal 决定要不要继续
  • /loop 决定什么时候开始下一次
  • Auto Mode 决定能不能放行某个动作

/goal:条件驱动的停止裁判#

用户给出完成条件后,Agent 多轮执行;每个 turn 结束,由独立评估器判断:

{"ok": true, "reason": "..."}
{"ok": false, "reason": "..."}
{"ok": false, "impossible": true, "reason": "..."}
json

三态里最见功力的是 impossible。它是死循环刹车:条件如果根本不可能达成,评估器主动喊停,不让主模型空转到预算耗尽。评估器系统提示还会防一种滥用:主模型声称“干不成”只算证据,不算证明;评估器必须独立从记录确认,拿不准就返回 ok: false 而不带 impossible

评估器通常不给工具,只看 transcript;输出最好用 schema 约束,避免自由发挥。BemoCode 还叠加轮数、费用等预算上限。把“是否完成”交给单独评估器,能减少主 Agent 自我宣布完工,但评估器本身也可能误判,所以仍要结合测试、Diff 和可观察事实。

/loop:固定间隔或自定节奏#

/loop 把任务放进会话内定时器:

  • 固定间隔:适合周期性检查
  • dynamic:临时挂载 schedule_wakeup,让模型决定下次唤醒
  • 延迟限制在 60 到 3600 秒
  • 总次数和费用仍有上限
  • 当前不持久化到系统后台,退出进程后任务消失

它和 /goal 的分工不同:/goal 追一个条件直到达成;/loop 负责把任务再次投进时间线。

Auto Mode:不是永远自动确认#

Auto Mode 在应用权限前增加风险分类层:

用户意图 + 工具调用
  -> 静态规则
  -> 风险分类器
  -> 允许 / 阻止 / 交互确认
text

BemoCode 采用两阶段思路:先快速识别可能阻断的动作,再结合用户原始目标判断该动作是否被明确授权。给分类器的记录是 reasoning-blind transcript:保留用户目标和工具调用,去掉普通 assistant 解释,避免模型用自己先前生成的说辞为危险动作背书。解析失败默认拒绝,即 fail closed。连续阻止次数和总阻止次数也有上限,防止分类器失控。

任何自动循环都必须把停止条件当作一等公民:

  1. 成功条件:目标达成且有证据
  2. 失败条件:重复错误、权限拒绝、外部不可用
  3. 预算条件:回合、费用、时间或 token 到顶
  4. 人工条件:Ctrl+C、拒绝授权、主动退出
  5. 不可能条件:评估器明确判断继续无意义

没有停止条件的“自主 Agent”,只是一个不可控的重试循环。

双后端与流式:兼容带来的真实代价#

BemoCode 支持 Anthropic Messages API 与 OpenAI Chat Completions 兼容接口。CLI 根据环境变量选择路径:配置了 OPENAI_API_KEYOPENAI_BASE_URL 时优先走兼容后端,否则使用 Anthropic 配置。模型名优先读 BEMOCODE_MODEL,也兼容旧变量 MINI_CLAUDE_MODEL

为什么维护两套消息历史:

  • Anthropic 使用 assistant content 中的 tool_use 与 user 中的 tool_result
  • OpenAI 使用 assistant tool_calls 与独立 role: tool

直接维护两套历史,实现简单,也避免有损转换。代价是策略重复,修改时容易只修一边;会话和压缩逻辑必须保证行为一致。更成熟的方向是内部统一事件模型,再由 adapter 负责协议编解码。当前项目还没有完成这层解耦。

流式输出也远不止“把字符串一点点打印”。程序必须同时组装:

  • 可见回答文本
  • Thinking 内容
  • 一个或多个工具调用的名称、ID、增量 JSON 参数
  • token 使用量、缓存使用量、结束原因

只有工具参数 JSON 完整后,才能交给执行器。中途 Ctrl+C 通过 AbortController 中断当前请求,保留 REPL 进程。

Anthropic 路径还有一个细节:安全只读工具可以在流式 content_block_stop 时尽早启动。模型还在继续生成后续文本或下一个工具参数时,已经完整的 read_file / grep_search 可以先跑起来。OpenAI 兼容路径通常等响应更完整后再批量并行。这不是炫技,而是在长链路里把等待时间往前叠。

重试只针对瞬时错误:429、503、529、连接重置、超时。等待时间指数退避并加抖动,最多 3 次。认证失败和参数错误不应重试。Extended Thinking 也要做模型能力判断:Claude 4.6 用 adaptive thinking,其他兼容 Claude 4 模型按支持情况开启,不兼容模型关闭。把供应商字段直接发给所有 OpenAI 兼容服务,很容易导致请求失败。

成本统计目前使用固定单价估算,因此 UI 中的美元值只能理解为近似遥测,不能用于账单核对。这对连接 DeepSeek 等兼容模型时尤其明显。

System Prompt 如何被拆成“可缓存”和“会变化”#

如果把 Agent 只理解成工具循环,会漏掉另一半工程:模型每一轮看到的系统上下文,是如何被组装、缓存、隔离的。

BemoCode 的 prompt.ts 把这件事拆得很清楚。

静态核心写死身份、行为边界、工具使用原则和编码规范。动态环境再拼 cwd、Git 分支与最近提交、可用 Skills、可用 Agents、Memory 索引、当前模式。CLAUDE.md 与日期则尽量注入首条用户消息的 system-reminder,尽量不改写静态 system 前缀。

这样做的直接动机是 Prompt Caching:

  • 稳定前缀可被服务端复用
  • 项目特有内容如果放在最前面,后面的缓存会整体失效
  • 工具 schema 与静态 system 一起进入缓存边界,多轮对话的第二轮起会明显省输入成本

Anthropic 路径里,静态块带 cache_control: { type: "ephemeral" },动态块放在后面;消息列表还会在最后一条的尾部打一个滚动断点。实现时必须克隆消息,不能把传输层元数据写进长期历史,否则会话保存、压缩、恢复都会被污染。

CLAUDE.md 本身也比“读一个文件”复杂:

  • 从当前目录向上递归收集
  • 支持 @./path@~/path@/path 的 include
  • 有最大深度和环检测
  • 同时加载 .claude/rules/*.md

这套机制让项目指令、团队规则和本地规则可以分层存在,而不必把所有约束硬编码进 TypeScript。对 Agent 来说,提示词层就是最便宜的可配置代码。

一次真实请求怎么在系统里走完#

假设用户输入:

帮我检查项目健康状况,如果发现文档引用错误就修复并运行测试。
text

入口在 cli.ts。它读取 cwd、环境变量和参数,创建 Agent 与 UI,进入 readline REPL。用户输入由 readline 控制,而不是手写复制一行 You >,这样 Backspace 才不会把提示符弄坏。

进入 chat 后,系统先做几件事:

  1. 组装动态上下文,必要时启动记忆预取
  2. 检查上下文预算,必要时 snip / microcompact / auto-compact
  3. 带上当前 active tools 发流式请求

模型如果看到相关 Skill,可能先调用 skill 加载健康检查流程;也可能直接 list_filesgrep_searchread_file。每次工具调用都要经过:

Schema 解析
  -> Plan / deny / allow
  -> Auto 分类或人工确认
  -> 执行
  -> 结构化结果回流
text

只读通常直接放行;写文件和危险 Shell 取决于模式。完整结果进入消息历史,UI 只显示最多 5 行摘要。模型发现文档引用错误后,会先读目标文件确认上下文,再走 edit_file。编辑前检查 mtime,编辑后展示 Diff。

然后模型调用 run_shell 跑测试。如果失败,错误文本成为下一轮输入;如果通过,模型生成总结,Agent 记录 token、缓存命中和近似成本,并自动保存会话。

用户最终看到的,不应是底层 JSON 事件流,而应是:

BemoCode › 已完成健康检查。

修改:docs/xxx.md
验证:npm test 通过
风险:成本为近似估算
text

这就是执行细节与人类交互之间的边界。Agent 产品如果把所有底层细节都甩到终端上,会看起来“很强”,但实际不可读;如果把细节藏太狠,又会失去可审计性。BemoCode 选择了中间态:人看摘要,模型看完整结果,超大结果落盘可回读。

steps 教程体系:为什么它不只是 README#

BemoCode 和许多“从零实现 XXX”仓库的一个重要差别,是它把教学轨和生产轨拆开了。

docs/ 讲原理,src/ 是完整实现,steps/ 则给每一章一份可单独运行的最小代码。node steps/run.mjs 7 不需要 API key,靠本地 mock 模型驱动,就能看到上下文压缩真正触发;--diff 只显示这一章相对上一章新增的代码;--live 才接真实模型。

这套体系解决的是学习项目里最常见的三类失真:

  1. 文档写了伪代码,仓库里没有对应可跑实现
  2. 最终代码太重,初学者看不出“这一章到底加了什么”
  3. 演示依赖真实 API,成本高、结果不稳定

文档同步脚本、标记 lint、mock selftest,都是在维护“文档、代码、输出同源”。对博客作者和二次开发者来说,这意味着 BemoCode 不只是“一个 agent 成品”,而是一份可逐步复现的工程教材。

如果把它和 how-claude-code-works 这类源码解读项目对照,分工也很清楚:后者适合“从大系统往下拆”,BemoCode 适合“从小循环往上搭”。两条路径汇合后,读者对 Claude Code 一类产品的理解会完整得多。

和主流 Agent 产品比,差别到底在哪#

把 BemoCode 放进 2026 年的终端/IDE Agent 光谱里,更能看清它的定位。

产品主要交互面核心取向与 BemoCode 的关键差异
BemoCode终端 CLI小而完整的可读 harness规模小、机制透明,UI/沙箱/生态较弱
Claude Code终端、IDE、桌面、Web成熟 Claude 生态工作流产品化更深:Hooks、更强权限、更全工具与异常覆盖
Codex CLI终端,并联动 IDE/App/Web本地 coding agent + 项目指令 + MCP官方沙箱与审批体系更完整
Gemini CLI开源终端 CLI开源、搜索、脚本模式、MCP产品面与生态更广
Cursor AgentIDE 原生编辑器上下文与开发流融合IDE 体验强,终端/远程场景不如 CLI 自然
Aider终端Git-centric pair programmingGit 协作心智更直接,工具/自治面相对更窄

比较时应避免一句“谁更强”。合理表达是:

Claude Code、Codex、Cursor 等产品把成熟能力产品化;BemoCode 的定位不同。它是一个可读、可修改、可测试的 Agent Harness 教学与实验项目,刻意保留核心机制的透明度,用较小代码量展示工具循环、权限、上下文、扩展和自治如何组合。生产级产品在沙箱、模型路由、UI、遥测、插件生态和异常覆盖上明显更复杂。

几个更细的对照也很说明问题。

和 Claude Code#

概念映射很清楚:

  • agent.tsquery.ts / QueryEngine
  • tools.ts ↔ Tool 系统
  • prompt.ts ↔ prompts / CLAUDE.md
  • memory.ts ↔ memory
  • skills.ts ↔ SkillTool
  • subagent.ts ↔ AgentTool
  • mcp.ts ↔ mcpClient
  • autonomy.ts/goal /loop Auto Mode

差异主要在成熟度:Claude Code 有更多 continue reason、更多工具特化、更深的 Bash AST 安全分析、Hooks 平台能力、Coordinator/Swarm、更完整的异常恢复与企业审计。BemoCode 保留了骨架,删掉了大量“真实世界才能逼出来”的边界处理。

和 Codex CLI#

Codex 更强调系统级隔离与审批模型,项目指令侧有 AGENTS.md 等约定。BemoCode 更适合作为源码可读的实验床;如果关心“不可信任务能不能关进沙箱”,Codex 一类产品的隔离层更接近生产诉求。

和 Cursor#

Cursor 的优势在 IDE 原生上下文:当前文件、选区、诊断、Rules、内联 diff 体验。BemoCode 不依赖 IDE,远程 SSH、纯终端、CI 场景更自然,但缺少编辑器级交互密度。

和 Aider#

Aider 的心智非常 Git-centric:围绕 repo map、提交、审查变更组织工作。BemoCode 的工具/权限/自治面更丰富,但在“把每次改动稳定收束进 Git 工作流”这件事上,Aider 的产品焦点更集中。

和普通 Chat + 手工工具#

很多内部系统仍停留在“模型给建议,人去执行”。BemoCode 说明了跨过这道坎所需的最小集合:工具协议、执行循环、权限闸门、上下文压缩、停止条件。没有这些,模型再强也只是更会写建议的文本生成器。

设计决策背后的判断#

BemoCode 文档里用 ADR 风格记录了若干选择,这些判断比功能列表更值得保留。

先做行式 CLI,不做全屏 TUI#

Coding Agent 的核心工作发生在代码目录、Shell 和 Git 中。终端接近真实开发环境,管道、复制、远程 SSH 和 CI 都更自然。代价是长输出与历史管理体验有限。未来若引入 Web/TUI,应作为事件流订阅者,而不是让 Agent 逻辑依赖终端绘制。

精确字符串编辑优先于整文件重写#

对模型友好,Diff 小,失败可解释,再配合 mtime 检查降低覆盖风险。重复片段和大重构会别扭,但作为最小可靠编辑原语是合理起点。

模型结果与 UI 结果分离#

人和模型信息需求不同。把完整结果留给模型、摘要留给人,能同时保住可用性与可继续推理。

异步记忆预取#

记忆并非每轮都必要,不该成为固定延迟。插入时机必须严格,宁可晚一轮,也不破坏协议状态。

硬规则先于风险模型#

deny/plan 等确定性规则先判断,Auto 分类器只处理剩余风险。安全关键路径必须可解释。

Sub-Agent 用 fork-return#

控制上下文、实现简单、角色工具边界清楚。不适合需要同时编辑同一文件的多角色协作。

MCP 先做 stdio#

本地开发调试简单,权限边界清楚。远程连接必须另补认证、超时、审计和数据流评估。

自治任务必须有多重预算#

单一停止条件容易失效。轮数、费用、时间/次数共同构成最后防线。

测试策略:为什么不只测工具函数#

BemoCode 的测试分层说明它把 harness 当系统,而不是一堆工具函数:

层级目标
TypeScript 编译类型与产物完整
单元测试UI、Auto 规则、纯函数边界
集成测试Mock API 到 Agent Loop、工具链、权限、MCP、Goal/Loop
子进程 REPL真实 stdin/stdout、提示符、Ctrl+C
Live Test真实供应商 wire format,需 API key 单独跑
steps 文档检查章节同步、标记、示例流程

Mock API 很关键。真实 API 测试不稳定、昂贵、难复现;Mock Server 可以精确控制流式事件顺序、错误码和多轮工具链。这让“Agent 是否真的在协议层工作”变得可验证,而不是依赖人工试用感觉。

最有价值的测试场景,几乎都落在状态机而不是纯函数上:

  1. 模型连续调用多个工具,结果 ID 不错位
  2. 工具参数分多个流事件到达
  3. 工具失败后,错误作为结果回到模型
  4. edit_file 的 old string 不唯一时拒绝
  5. 文件 mtime 在读取后变化时不覆盖
  6. Plan Mode 下写入和危险 Shell 被拒绝
  7. Auto 分类器输出非法结构时 fail closed
  8. MCP 工具发现后可被模型调用
  9. Ctrl+C 中断请求后 REPL 仍可输入
  10. UI 只显示 5 行,完整结果仍在模型消息中
  11. Anthropic 与 OpenAI 两条路径都能完成同一工具链

仍需补强的部分同样说明了真实边界:Windows Shell 与路径、Unicode 宽度与中文输入法、并发改同一文件、MCP 崩溃重启、超大仓库性能、Prompt Injection 回归、全局安装后的工作目录与权限。Agent 测试如果只覆盖“happy path 能改文件”,几乎等于没测。

分步教程的 steps/ 体系也值得单独提:每章最小实现、文档代码块、运行输出尽量同源生成,减少“文档说一套、代码跑一套”。对学习型仓库来说,这比再堆几个 demo 更有价值。

架构债务与后续演进#

如果只谈优点,项目会被写成宣传稿。BemoCode 自己的 deep-dive 文档反而把债务写得很直白。

当前最大的集中点是 agent.ts。它同时承担:

  • 双后端请求与流式拼装
  • 工具调度与并行策略
  • 权限协作
  • 四层压缩
  • Plan Mode 状态
  • Goal / Loop / Auto 分支
  • Sub-Agent 与 MCP 接入
  • 预算与会话副作用

路径直接,读一次请求很方便;长期维护时,任何改动都容易牵一发而动全身。更合理的拆法是:

  • ModelAdapter:统一消息和流事件
  • AgentLoop:从 CLI、UI、自治模式中抽出状态机
  • ToolRuntime:注册、权限、执行、超时、Receipt
  • ContextManager:预算、压缩、摘要、协议不变量
  • MemoryStore:索引、召回、来源、过期、用户管理

还有几处产品债务:

  • 成本价格硬编码,不能当账单
  • 会话恢复更偏全局最近,而不是当前目录最近
  • 权限不是沙箱
  • UI 缺少完整结果查看和统一 /status
  • 没有工具调用 Receipt:参数摘要、权限决策、起止时间、结果位置

后续优先级也不该是“先再加 20 个工具”。更稳的顺序是:

  1. 正确性、安全和可观察性
  2. 日常使用体验
  3. 架构演进

不应急于做的事同样重要:在没有 sandbox 前开放更强系统权限;在没有可观察性前继续扩张自治;把所有第三方 MCP 视为可信;只为“看起来像成熟产品”而重写成全屏 UI。

用面试式问题压一遍理解#

把 BemoCode 讲清楚,可以用一组更硬的问题自检。

和普通 Chatbot 最大区别是什么?
Chatbot 主要生成文本;BemoCode 有宿主控制的 Agent Loop。模型提出工具调用,宿主检查权限并执行,再把结果作为下一轮上下文返回。核心是可控状态转换,而不是单次回答的文采。

为什么一次用户请求可能调用多次模型?
第一次调用可能只决定“先读哪些文件”或“跑什么测试”。工具结果回来后,模型才能继续判断。一次请求因此可能包含多轮 model -> tool -> result -> model,直到没有工具调用、目标达成或预算/权限触发停止。

为什么不能让模型直接执行 Shell?
模型只能提出动作,不能拥有宿主进程权限。执行前必须做参数校验、权限匹配、危险检测、超时和输出限制。否则 Prompt Injection 或模型误判会直接变成文件系统副作用。

权限系统能保证安全吗?
不能。它是应用级策略系统,不是沙箱。能拦截常见危险操作并要求确认,但正则不能完整理解 Shell,允许的工具也可能组合出意外结果。不可信任务仍需要容器、worktree、网络隔离和最小权限 Key。

Skill、Tool、Sub-Agent 分别解决什么?
Tool 是动作,解决“能执行什么”;Skill 是流程知识,解决“如何完成一类任务”;Sub-Agent 是上下文与权限隔离,解决“如何把探索或复杂子任务拆出去”。三者可组合,但不是同一层抽象。

Goal 和 Loop 有什么区别?
Goal 是条件驱动,持续执行直到评估器判断完成或不可能;Loop 是时间驱动,按固定或动态间隔再次唤醒。两者都必须受轮数、费用和时间边界限制。

Agent 如何知道何时停止?
普通对话在模型不再返回工具调用时停止;Goal 还需要独立评估器;Loop 由时间和次数调度;所有模式都有 Ctrl+C、最大回合和费用上限。真正可靠的停止还应有测试通过、Diff 检查等外部证据。

这些问题的共同答案是:BemoCode 真正值得展示的,是把不确定的模型输出包在确定性的工程边界里。

它暴露出的 Agent 工程共性#

读完 BemoCode,会看到一组跨产品都成立的判断。

1. Agent 的本质是一个受约束的 while 循环#

所有复杂性——权限、压缩、记忆、多 Agent、自治——都是围绕这个循环的增强和防护。离开循环谈“Agent 架构”,很容易漂成概念游戏。

2. 提示词是最便宜的代码#

系统提示词里的一句话,效果常常等同于一个 if 语句,实现成本却接近 0 行代码。很多行为问题的最优解,往往是先把约束写清楚,再考虑是否要加一层框架。

3. 工具设计决定能力上限#

让模型做它擅长的:理解意图、生成变更内容、判断下一步。让工具做模型不擅长的:精确字符串匹配、文件系统操作、进程管理、协议配对。edit_file 是典型分工。

4. 上下文管理是 Agent 的内存管理#

有限窗口要支撑长任务,靠的是预算、裁剪、落盘、摘要和缓存边界。上下文工程的目标是提高决策信噪比,不是堆更多文本。

5. 安全必须嵌进循环#

权限检查是循环中的一步,不是外挂 middleware。新工具如果忘记声明权限级别,默认应偏保守。应用级策略可以降低误操作,但无法替代沙箱。

6. 从几千行到几十万行的差距,主要在边缘情况#

生产级 Agent 多出来的代码,大多是运行环境兼容、网络不可靠、用户输入多样性、企业审计、恢复路径和 UI 细节。这些“无聊”代码不会出现在架构图中,却决定工具能否在真实世界里长期可用。

7. 协作边界比“更聪明的模型”更关键#

构建 coding agent 最核心的能力,是设计 LLM 和代码之间的边界:哪些由模型决定,哪些由代码保证。BemoCode 的多数决策都指向同一原则:模型决定“做什么”,代码确保“安全地、可恢复地、在预算内做”。

仍未完成的部分,反而更说明问题#

BemoCode 主动没做或只做了弱化版的能力,同样构成理解:

  • Hooks:把 agent 变成平台的关键机制,但发现、加载、错误隔离和 JSON 协议细节对理解主循环帮助有限
  • Coordinator / Swarm:任务分解和 Agent 间通信更多是 prompt 与协议调优问题
  • LSP 集成:能显著缩短修复环,但客户端协议和进程管理成本高
  • Bash AST 安全分析:正则覆盖常见模式,生产级需要更深静态分析
  • 统一内部消息 IR:双后端当前靠两套历史硬撑,后续应拆 adapter
  • 强隔离沙箱:应用级权限与 OS/容器隔离是不同层级

这些缺口说明一件事:做一个“能讲清楚的 Agent”和做一个“能在企业环境长期运行的 Agent”,共享同一骨架,却不共享同一成熟度账单。

适合怎么用这个项目#

如果目标是理解 Coding Agent,BemoCode 有三种用法:

  1. 顺着 docs/00docs/15 搭一遍
    每章补一块能力,并用 steps/run.mjs 看最小实现真正转起来。

  2. 拿一次真实请求追 agent.ts
    cli.ts 入口进 chat,看 system prompt 组装、流式解析、权限检查、工具执行、结果回灌、压缩触发和会话保存。

  3. 对照 Claude Code / Codex / Cursor 的产品文档
    不纠结私有实现细节,只比较产品抽象:循环、工具、权限、上下文、记忆、扩展、自治。多数差异是成熟度与场景,而不是“有没有 while”。

推荐的源码阅读顺序,也不是从 agent.ts 第一行硬啃到最后一行:

  1. 先读 Agent Loop,建立最小闭环
  2. 再读工具系统,理解副作用从哪里来
  3. 再看流式与双后端,理解协议差异
  4. 再看权限,理解刹车嵌在哪里
  5. 再看上下文压缩,理解长任务如何活下去
  6. 再看记忆、技能、Sub-Agent、MCP,理解扩展面
  7. 最后看自治与 agent.ts 总编排
  8. test/integration/ 验证实际行为,而不是只相信文档

如果目标是二次开发,优先改的方向也很清楚:

  • 把模型适配从 agent.ts 拆出
  • 把上下文策略和工具调度变成稳定接口
  • 给 MCP 补远程治理
  • 给权限补结构化 Shell 分析或外部沙箱
  • 给会话恢复补工作目录级策略
  • 给 UI 加完整结果查看入口,而不是只显示 5 行
  • 为每次工具调用生成 Receipt,让调试不再只靠“感觉”

术语表:把概念钉住#

术语含义
Agent Loop模型调用、工具执行、结果回流的循环
Harness包围模型的执行框架,负责工具、上下文、权限、恢复和停止
Tool Use模型结构化提出工具调用,由宿主执行
Tool Call / Result一次工具请求及其结果,必须按 ID 配对
Model Adapter屏蔽供应商协议差异的适配层
Prompt Caching缓存稳定提示词前缀,降低延迟和输入成本
Context Compaction将过长历史裁剪或摘要为更短上下文
Micro-compact不做完整摘要,只清理旧的低价值结果
TOCTOU检查时与使用时之间状态变化;mtime 防护针对该风险
Plan Mode只读调查和计划阶段,执行前获得用户选择
Skill外置的流程知识和工作流说明
Sub-Agent具有隔离上下文和受限工具的子任务 Agent
Fork-return子 Agent 独立运行,完成后返回摘要
MCP外部工具/数据源的标准化接入协议
Auto Mode在静态权限规则后增加风险分类的自治模式
Reasoning-blind风险分类时不把模型普通解释当作授权证据
Fail closed无法确定安全时默认拒绝
Receipt记录工具调用参数、权限、执行、结果和时间的可审计凭证
WorktreeGit 独立工作目录,用于隔离 Agent 修改

这张表的用处,不只是背名词。它提醒我们:Agent 工程里真正反复出现的,是协议、边界、预算、隔离和可恢复性,而不是某个模型品牌。

收束#

BemoCode 说明了一个很具体的事实:Coding Agent 的关键,在于是否建立起一条稳定的执行闭环。

模型负责理解目标和选择动作;Harness 负责把动作变成真实世界中的文件与进程变化,再把观察结果送回模型;权限、压缩、记忆、技能、Sub-Agent、MCP、Goal/Loop 都是在这条闭环上叠加的控制面。约 5500 行代码当然装不下生产级 Claude Code 的全部边界处理,但已经足够把骨架摊开:哪里是决策,哪里是执行,哪里是刹车,哪里是预算,哪里是扩展点。

如果把全文再压一次,可以记住五件事:

  1. 核心是 Agent Harness,聊天 UI 只是外壳。 UI 呈现状态,系统价值在循环、工具、权限和上下文。
  2. 工具回路把“建议”变成“行动”。 没有执行与回流,模型再强也只是文本生成器。
  3. 安全与预算必须嵌进循环。 事后补丁挡不住长任务里的误操作和费用失控。
  4. 上下文与记忆是工程问题。 有限窗口、异步召回、摘要丢失,都比“模型会不会记”更先决定可用性。
  5. 与成熟产品的差距主要在成熟度,不在概念。 Claude Code、Codex、Cursor 更完整;BemoCode 更透明。

回到最初的问题——BemoCode 的一些点是怎么实现的,它和现在的 Agent 有什么区别——可以压成一句:

它用可读的最小 harness,把“模型会说话”推进到“系统能在本地持续干活”;与成熟产品相比,差在生态、隔离、异常覆盖和产品化深度,相同的是那条由工具回路驱动的 Agent Loop。

如果还要继续往下挖,最值得追问的是:当循环跑到第 30 轮、上下文开始压缩、权限开始自动裁决、子任务开始 fork 时,系统如何仍然知道自己该停在哪里。那才是 Agent 工程真正开始变难的地方。

BemoCode 深度解析:从 5500 行代码看懂 Coding Agent 的工程骨架
https://bolaxious.cn/blog/2026/project/bemocode
Author Bolaxious
Published at July 15, 2026