最让我不满意的软件是我自己写的。2021 年我创建了 Badger,并主导它到 2025 年,目的是让优化器能在控制室里运行。这一点做到了,但我给它套上的那个窗口需要花时间学:重跑一个存好的例程(routine)只要点一下,而一个新任务即使从模板开始,仍要挑环境(environment),设变量,写目标函数与约束,若再换一种没有用过的算法,它还会带来一堆超参数。因此 Otter 一开始的范围很窄:像对 Claude Code 说话那样说一句「请把 FEL 强度调上去」,由一个智能体(agent)把这句话变成一个例程,你批准,Badger 执行。
后来我给它起名 Otter,取自 OperaTor in TERminal(终端里的操作员),范围也是随这个名字放宽的:控制室里总有一个终端,用 ssh 登进去也有,即使没有桌面环境也是如此;数据和读数据的软件本来就在那里,于是一个新任务多半只是再加一个工具。调机只是我最先想解决的那件事。
2025 年 10 月我画过一个自己的智能体循环,前面是一个分类器,然后是规划器,再往后是各种工具。这个循环从来没有做出来。实际运行的是 Claude Code 本身,加上我为每个任务写的工具与技能(skill),它运行在 Osprey 的安全层之下,Osprey 是 LBNL 开源的框架,专为控制系统里的智能体而写。智能体从不直接向机器写入任何东西。
一个例程需要的信息,大多在运行存档里或者在某个人的脑子里;我当时不知道的是,查找这些信息能不能快到让值班的人愿意等。第一次在真机上试是 2026 年 5 月 14 日在 LCLS 的一次通宵班。LBNL 关于 Osprey 的 IPAC'26 进展文章(我是十七位作者之一)用一段话描述了这次 LCLS 调机。本页的数字来自之后更详细的两次班,6 月在 LCLS,7 月在 FACET-II,两台都是 SLAC 的加速器。
- 133 s从运行人员的一句话到一个通过校验的例程,中位数
- 0智能体在 LCLS 提出的带约束运行中,束损越限的次数
控制室主机上的 Otter
Otter 以一个普通账号运行在控制室的主机上,不用容器。这台主机不联外网,也不能在上面安装任何东西,因此我在集群上把整棵目录树构建好再推过去,代价是失去了可复现性。严格地说,Otter 是 Osprey 在 SLAC 的一套部署(我是 Osprey 的第二大代码贡献者),我在它之上写的是 MCP 服务器和工具,技能,知识包和部署本身。
技能是可以阅读和修改的 Markdown 文档。最小的一个是 /propose-routine,它编写一个例程;/auto-tune 把几个例程串成一轮分阶段运行,在同一个 Badger 窗口里完成;FACET-II 还有 /auto-tune-facet-yaml,它把一整次启动写成 YAML,交给该实验室验证过的运行器(runner)。这些技能调用我写的三个 MCP 服务器,一共几十个工具,它们交回的每一个值都注明出处。一个小的离线知识包补上存档里查不到的东西;当几个来源说法不一时,以实时读数为准,其次是存档,笔记排在末尾。
智能体自己的控制系统接口关掉了写入,因此它不能自己去设任何一个过程变量(Process Variable,PV),但什么都可以读。Shell 和网络访问一律拒绝,任何会改动机器的操作都要先获批准:在 LCLS 是每一次保存和每一次启动,在 FACET-II 则是整条工作流一次批准。通向机器的唯一路径是一次受限的 Badger 运行,起始状态先存下来,运行人员可以保留结果也可以退回;或者是运行器的一步,若失败就恢复它的快照。为了 Otter,我在 Badger 的一个分支上加了一小块远程控制的接口。
6 月在 LCLS 的班
2026 年 6 月 26 日我们在硬 X 射线线上。十点刚过,运行人员提出要把 FEL 信号调到最大,束损监测器作为软约束,算法由智能体自己选。智能体在大约五百次存档的运行里给旋钮(knob)排序,四块匹配四极铁遥遥领先;它实时读了几个候选的束损监测器,留下真正有读数的那一个(地图与运行人员的笔记对哪个是哪个的说法并不一致)。运行人员批准了三阶段的计划,熟悉的 Badger 窗口就出现在了控制台上。
第一阶段趋于平缓时,智能体请求 Badger 停止,Claude Code 的权限分类器却拒绝了它三次,大约耗掉 28 次评估的束流时间。它没有绕过关口,只是等着并转而问运行人员;运行人员输入「please go ahead and I'll approve」,停止才生效,第二阶段随即在同一个窗口里换上。这一阶段只多了半个百分点,于是智能体自己取消了第三阶段,在 22:27 结束。脉冲能量从 0.468 mJ 升到 1.819 mJ。应当说明,其中大部分是恢复,因为这一轮运行开始时脉冲能量正处于一次下跌之中;结果比运行人员当晚早些时候自己做的分段扫描高出约 4%,而这个差距在逐次运行的涨落范围之内,因此公允的说法是智能体达到了他们的水平,而不是超过了他们。

当晚早些时候有一次用极值搜索(extremum seeking)做的精调,存档里几乎没有可以照抄的东西,智能体就按我从文献里提炼的一条笔记来做(文献里给的参数值对我们装的这个优化器是不对的)。那次运行用了一百多次评估达到 1.786 mJ,是那一轮运行之前最好的一次单次运行。整个晚上它编写了十八个例程,每一个都来自一句话的请求。

FACET-II 的五次启动
FACET-II 的注入器启动(start-up)是按顺序的六个调机问题,每一个都已经是一个 Xopt 例程,放在一个验证过的运行器后面。例程和运行器都属于 SLAC 的 autonomous-control 程序包,没有人自动化过它们周围的那一部分:每一步之前要有东西检查机器是否就绪,并按机器里实际的束流选参数;每一步之后要读日志并决定下一步怎么办(有时这意味着把人叫醒)。这一层是我做的。
2026 年 7 月 1 日晚运行人员定的规矩是「先把 YAML 给我看」。智能体把每一次启动写成一段短的 YAML 工作流,先给运行人员看,然后一次按键跑完全部六个阶段,中间不需要任何人。五次启动就这样接连跑下来,多数在 1600 pC,智能体在这个电荷量下编的范围原封不动地用到了两次更高电荷量的启动上。运行器本身每次用 16 到 24 分钟,3 小时 18 分钟里其余的时间都花在运行器之外,在步与步之间和启动与启动之间。有一次速调管相位没能稳定下来,智能体读了报错回溯(traceback),判断为瞬态故障,重新提交同一条工作流,一分钟左右就过去了。

每个阶段都完成了,但这与调好了并不是一回事。能散阶段把色散区的束斑尺寸减小了 17 到 86 µm,但这是在它自己的扫描范围内算的,扫描从不测量它接手时的设定值。优化回路里量到的发射度在五次启动里有四次下降,但没有一次的降幅超过自身不确定度的两倍,因此我在这里不声称任何结果。我唯一信得过的一个读数来自该装置自带的发射度测量程序,在启动 E 之前和之后各测了一次,不在优化回路之内:几何平均从 5.34 ± 0.13 µm 变为 5.10 ± 0.05 µm,水平方向下降而垂直方向上升,一边的改善是拿另一边换来的。只有启动 E 做过这样的检查。
出现的问题
部署的优化器里有一个字段在 schema 里换了位置,收敛检查却不出声地解析成空,报告说没有旋钮碰到边界;实际上有一块四极铁在第一阶段的大部分时间里都钉在下限上,包括最好的那个点。FACET-II 的验证只检查一步是否完成,因此只要干净地退出就算通过,不管有没有达到目标。当晚运行人员四次纠正同一个选择(把束损作为惩罚项,而不要中止),这些纠正一条也没有留到下一个会话。
在四个 FACET-II 会话里,智能体看到一些没有连上的读回值,就宣布自己在对着模拟运行。它错了:束流自始至终都在线,而它说没有连上的那些名字大多是它自己编的。运行日志里这样一句自信的话,是任何工具关口都拦不住的失败,它在我的待办清单上。
数字来自 Badger 的运行存档,运行器日志和会话记录,2026 年 6 月和 7 月。完整的文章正与 ALS 团队一起准备。