The Closing Window
写给把 Agent 会话开到红线的人:一个笔记插件 image
Photo by Amanda Jones on Unsplash

写给把 Agent 会话开到红线的人:一个笔记插件

AI Insights

正文之前,先说说这篇是怎么写出来的

这篇文章大部分是 AI 写的。我想一开始就讲清楚,因为我越来越多地看到有平台一刀切地封禁 AI 生成内容,我认为这是个错误。

我是工程师,不是作家。写作对我来说是真的费劲。多数时候我还是会写,因为我享受这个过程,也因为它逼着我把一个话题真正想透,这本身就有回报。它给我带来清晰。但那是好几个小时,我并不总是有。

所以这周摆在我面前的选择不是"写得好的文章还是 AI 文章",而是"AI 文章还是根本没有文章"。我发现了一个真正有用的东西,几乎是碰巧发现的,而且我只在一份 newsletter 里看到过它被提及。别的地方一处都没有。如果分享它的代价是承认有个模型帮我把句子理顺了,那就这样吧。

一刀切的禁令把"AI 生成"当成了"毫无价值"的替身。有时候确实是。为了点击量被批量生产出来的垃圾很多,我一点也不打算替它们辩护。但这个替身判断本身是坏的。真正重要的是底下有没有真东西:一个发现、一个你真的撞上过的 bug、一个你真的动手做出来的东西。去判断那个。我做这件事不是为了互动数据。我做是因为我相信,读到这篇的人里会有三四个真的去把这东西装上,并且会庆幸自己装了。

好了,进入正题。


TL;DR

我一直想在 Agent 会话里给自己留笔记,但没有好办法。把各种方案研究了一遍,大多数都不行。后来一份 newsletter 里冒出来一个叫 bb 的 Agent IDE,它是唯一一个能让我自己动手把缺失功能补上的工具。前后二十分钟左右。插件在这里,MIT 协议。

真正的问题:笔记留下来了,上下文没留下

如果你整天跟编码 Agent 打交道,这个场景你已经很熟了。

你给 Claude 派一个任务。它一路做下来,做完了。任务是真的完成了。

然后,就在同一口气里,它告诉你另外三件事。一个明显写错但不在本次范围内的函数。一个跟文档自相矛盾的配置值。一个从三月起就一直被跳过的测试。全都是真问题,全都跟你刚做的事情相关,但没有一个是你要它做的。好的 Agent 会把这些问题摆到台面上。Claude 摆出来的特别多。

问题出在时机上。那个瞬间本来就是整个周期里最忙的时刻。工作还躺在一个 worktree 里,我还得看看到底改了什么、跑一遍、判断我信不信它,然后把分支合回 main。那才是当下压在身上的事,我的注意力都在那儿。

于是三个货真价实的发现,偏偏在我最腾不出空间的时候到达,还要跟一次合并抢我脑子里的同一块地方。接下来对它们做什么,必须在大约一分钟内决定,因为再往后我就一头扎进 diff 里了。

很长一段时间里,接下来发生的事就是一个任务应用,或者一个我用别的软件打开的纯文本待办文件。我在 Warp 里待过一阵,后来是 Superset,手上永远同时转着三个终端窗口,横跨至少同样多的项目。

而且那些清单我是真的在维护。一个项目一份,一丝不苟地推进,每一条最终都跟进了。没有东西掉在地上。

它依然是错的系统,原因有两个,我花了一阵子才把它们分开来看。

第一,写下这条笔记的代价是那次会话。 记录一个念头意味着离开我正在做的事、切到另一个应用、把它敲进去、再切回来。就算十五秒吧。十五秒足够了。那正是我脑子里还攥着 Agent 刚做完那件事的形状的窗口,而每一次来回都在上面戳一个小洞。一个下午这么来六次,这个下午就全花在找回自己的位置上了。维护清单从来不是难的部分。难的是它每一次都要求的那个中断。

第二个更糟,而且这才是真正推动我动手的原因。 另一个应用里的任务没有办法把自己的上下文一起带走。我会写下"检查 sync handler 里的重试逻辑",然后半天和 100 万 token 之后再读一遍,这句话在技术上清清楚楚,实际上毫无用处。我当时为什么标它?Claude 那会儿在干什么?输出里的哪一部分让我觉得它重要?那些都在一千行开外的 scrollback 里,而那个窗口我早就关了。

我试过写更长的笔记。那不过是在给一个我人还站在中间的会话做摘要,成本比这条笔记本身的价值还高。

于是笔记活了下来,它的含义没有。最后我要么从零开始重建当时的推理,要么更糟,坐在那儿对上下文半记半忘,然后通常还得靠 Claude 帮我把窟窿填上。

我想要的东西又小又具体:留一条挂在那个具体轮次上的笔记,就挂在这件事冒出来的地方,而且不用为此离开会话,这样等我回来的时候,围绕它的那段对话会跟着一起回来。不是一条关于上下文的笔记。是一条活在上下文里的笔记。

在实际操作中,这意味着笔记挂在 Claude 列出它那些发现的那条消息上。而那条消息里本来就已经装着推理过程、文件名,以及其他所有我原本要在最糟糕的时刻硬塞进一个任务标题里的东西。

我最先看了哪些方案

动手之前我在这上面花了几天,我把整个搜寻过程在这里总结一下,因为答案的形状比我的结论更有用。

最后所有事情都取决于一个问题:Agent 是否必须一直跑在终端里?

如果是,那选择很少。 iTerm2 是唯一真正能做到这件事的终端。它有一个真正的批注功能,能把一条笔记锚定到 scrollback 里的一段文本上,而且因为这是图形界面的操作而不是 shell 输入,所以在 Claude Code 占着终端的时候它照样能用。这是一个正当的现成答案,如果你想读到这里就打住、直接去用它,那是个合理的结局。我当时用的 Warp 有书签,但书签完全不带文字,所以它们只是导航标记,别的什么也不是。它自己的文档就写着,会话一关书签就没了。kitty 和 WezTerm 有位置标记或者脚本钩子,但没有批注。Ghostty 两样都没有。tmux 什么都没内置,但如果你想用弹窗加一个侧边窗格自己拼一个出来,它是最好的底座。

Claude Code 自带的功能能带你走一段,然后就到头了。 Hooks 可以往记录里打标记,但它们是在 Claude 的事件上触发的,不是在我注意到什么的时候。#/memory 写进 CLAUDE.md,那是持久的长期记忆,跟我想要的正好相反。斜杠命令需要输入框,而我在一个轮次进行中够不着它。会话记录是磁盘上的 JSONL,你可以在外部往里追加,但没有任何东西会实时渲染出来。至于各种 scratchpad 类的 MCP 服务器,它们是旁路存储:笔记进得去,但没有锚定在任何东西上,于是恰好复现了我想要逃开的那个问题。

有一件事要主动避开。 如果你在想"那我从外面把文字注入到正在运行的终端里",别这么干。那条路要经过 TIOCSTI,它被用于提权攻击的历史长到 Linux 从 6.2 开始默认禁用它,OpenBSD 更是在好几年前就把它整个删掉了。直接往 TTY 设备写更糟。那些字节会落在 Agent 当下正在绘制的画面中间,屏幕就变成一团垃圾了。

这一切的结果是一个岔路口。上面那些限制,每一条都是终端的属性,不是 Agent 的属性。输出是流向 TTY 的字节流,前台进程占着输入,scrollback 会滚掉,而且没有任何东西带着稳定的 ID。根本不存在一个可以把笔记挂上去的对象,所以每一种绕行方案,本质上都是在试图伪造一个出来。

于是诚实的选项只剩两个:用 iTerm2 并接受它的模型,或者找到一个会话由可寻址对象而非字节构成的东西。第二个我没有候选。然后我走运了。

那次偶然

就在那几天当中,bb 出现在了 Ben's Bites 上,那是我订阅的 AI newsletter 之一。

提到它的篇幅很短。大意是:这是一个 Agent IDE,而且极其可定制。这就够了,主要是因为它到来的时机。我当时已经深陷在一个问题里,而"可定制"正是我要找的全部答案。换成任何别的一周,我都会一眼扫过去。

我想把运气的成分说清楚。现在每周都有新的 Agent 编码工具、终端、编排器和各种封装冒出来。没人评估得完。我不是靠尽职调查找到 bb 的。它落到我桌上的那一周,我恰好有一个跟它形状吻合的问题,而我之所以能认出来,只是因为我花了几天把那个形状想清楚了。

它为什么合适

bb 被归类到"IDE"这一栏,这其实会误导人,因为你不在里面编辑文件。它做的是编排:它把编码 Agent(Claude Code、Codex、Cursor,以及任何讲 ACP 的东西)跑在一个个具名线程里,每个线程都有自己的 git worktree,它是 MIT 协议,并且完全跑在你自己的机器上,用的是你本来就在付费的订阅。

我这套环境里有两点,让它对我的冲击比对别人可能更大。

家庭实验室。 我在本地网络的好几台机器上跑任务。一台负责常驻的 Agent 工作,另一台有 GPU,Mac Mini 是日常主力机。以前协调这些意味着一堆 SSH 会话,以及大量地去记哪个窗口是哪个。bb 把这件事做对了:每台机器上跑一个真正的客户端守护进程,回连到我主力机上的服务端。这跟 SSH 有实质性的区别。守护进程知道项目、环境和 provider,所以我可以起一个线程说"这个拿到那边去跑",worktree 就会在那台主机上连同它自己的环境一起被创建出来。每台机器还带着一个权限上限,所以我的沙箱机器可以停在完全访问权限上,而笔记本保持锁紧,我在线程里做的任何事都无法把它抬高。

我工作的两种模式。 白天我是 AI 工程师,这份工作依然要写大量软件。下班以后我就是大家现在开始叫的那种 builder,基本上意味着我同时开着太多项目。这两种模式都是"很多并行的 Agent 会话、好几台机器、很容易跟丢线索"。bb 就是为这个造的。

真正让我保持心流的那几个功能

我跳过功能巡礼,直接说改变了一天怎么过的那四件事。

任务变成线程。 我可以在应用里记下一个任务,然后一键把它变成一个正在运行的 Agent 线程。听上去是件小事。但"我注意到了什么"和"有个 Agent 正在处理它"之间的那道缝,正是我的想法过去卡死的地方,而把这道缝压缩成一次点击,意味着待办事项就活在我本来就在工作的地方,而不是躺在 Vikunja 或者 Jira 里等我回去找它。

给线程开分支。 你可以在会话历史的任意一点开分支。这个分支克隆的是真正的 provider 会话,而不只是一份对话记录,所以 Agent 是真的记得到那个点为止的一切。这就是那个"等等,如果我们换个做法呢"的按钮。我用得非常勤。Claude Code 原生也能做同样的事,但没有 bb 这么顺手。

侧边对话。 你可以在主线程之外开一段简短的旁支对话,而不打扰它。Claude 在跑的过程中注意到一个没关上的"gate",或者规格里某个被漏掉的子项,又或者我需要顺手查一件相邻的事情,而主线程的上下文保持干净。

挂在轮次上的笔记。 这就是那个不存在的功能。所以我把它做了出来。

这个插件

bb 的插件 API 不是通常那种"我们开放了六个钩子"意义上的插件 API。一个插件是直接扩展服务端的 TypeScript 代码:它有自己的 SQLite 数据库、HTTP 和 RPC 端点、后台任务、会渲染进界面的设置项、应用里的 React 组件,以及它自己的 bb 子命令,用起来跟任何内置命令一样。bb 自己发布的几乎所有东西都是这么搭的,这才是这套 API 诚实与否的真凭实据。

整件事花了十五分钟,也许二十分钟。

它做的事:把鼠标悬停在线程里的任意一条消息上,点"Add note",侧边面板就会打开一个 Notes 标签页,里面已经有一个锚定到那条消息的输入框。写下来,Cmd+Enter 保存。全程不离开会话,这是这件事一半的意义所在。

另一半是之后回来时拿到的东西。笔记按时间线顺序排列,所以从上往下读这个面板,就是把这次会话按顺序走了一遍。每条笔记都带着它所依附的那条消息的一个片段,而且这条笔记依然坐在一个我可以滚动的线程里属于它自己的位置上。所以我不必在记录的当下去总结上下文。我只要指一下就继续往前走,而等我回来的时候推理还在那儿,因为我从来没有把笔记和产生它的那段对话分开过。

另外还有一个 bb note add 命令,所以脚本或者钩子也可以丢一个标记进去。

它拒绝做的事

这些笔记永远不会到达模型。 它们从不被注入上下文,从不发给 provider,从不影响任何一个轮次。它们是我的。

这些笔记的价值在于,它们是对 Agent 正在做的事情的实时旁白。它们变成模型输入的那一秒,就不再是观察,而变成了指令,而我也就失去了工作流里唯一那个只属于我自己思考的地方。

一个诚实的例外:注册了这个 CLI 命令,就意味着 Agent 能发现"记笔记"这件事存在,并且写一条。内容依然永远不会回到它那里,但它确实能写。与其把这件事藏起来,我让这样产生的笔记标上"via bb note"而不是"You",这样我永远分得清哪些是我自己写的。

粗糙的地方

笔记渲染在侧边面板里,而不是内嵌在消息下方,而后者才是我最初期望的样子。

你只能给用户消息和助手消息做批注,不能给工具调用或者它们的输出做批注。API 的类型就是这么定义的。需要的时候,CLI 是那个逃生口。

而且 bb 还在 0.38.0,跑得很快。我关于它插件接口说的任何话都有保质期。如果你要在上面搭东西,把这个成本算进去。

可以推而广之的那部分

通常,想要一个小功能意味着提一个需求然后等着,前提是你还能提得上去。也许它会被做出来,多半不会,而无论哪种情况,决定权都在一个优先级跟你不一样的人手里。这不怪任何人。当一个产品要服务十万个人、十万种不同的工作方式时,本来就会是这样。

这笔交易过去是无法避免的,因为自己把功能做出来的代价,比没有它活着更高。而这正是正在改变的部分。"干脆自己做"的成本已经降得足够低,以至于对于一个真正很小、收益却不成比例的功能来说,等待现在成了那个昂贵的选项。

也就是说,它正在变成一个选择。而那些继续等待需求被实现的人,将越来越多地是在选择等待。

如果你想继续享受把 Agent 开到红线的那份快感,那就去找跟你工作方式合拍的工具。


Session Notes 这个插件是我写的。bb 不是我写的,我跟这个项目也没有关系。bb 是 MIT 协议且免费的,在 getbb.app。插件同样是 MIT 协议,在 github.com/pablooliva/bb-ide-session-notes-plugin。写作时的版本:bb 0.38.0,plugin SDK 0.4.6。

Powered by Buttondown.