Emacs AI工作台:基于ACP协议与Lisp的自我进化agent-shell实践

发布时间:2026/10/7 6:49:26
Emacs AI工作台:基于ACP协议与Lisp的自我进化agent-shell实践 1. 从一次“编辑器里长出一个AI工作台”的折腾说起第一次在 Emacs 里敲下M-x agent-shell的时候我其实没抱太大期待。毕竟这些年“把 AI 塞进编辑器”的方案见过太多有的只是个补全插件有的把聊天窗口硬塞进侧边栏用起来总有种“外挂”的感觉——AI 是 AI编辑器是编辑器两边各干各的。但 agent-shell 给我的第一印象不太一样它不是把某个大模型 API 包一层 UI而是试图在 Emacs 内部搭一个能自我进化的 AI 工作台让 agent 真正成为编辑器的一部分而不是一个悬浮的对话框。这篇东西我想聊的就是这个方向。核心关键词是Emacs、agent-shell、AI、ACP、Lisp。如果你是个长期泡在 Emacs 里的老用户同时又对 AI agent 这套东西感兴趣那这篇基本就是写给你的。如果你只是听说过 Emacs 但没真正用过也没关系我会尽量把里面的机制讲成人话——因为 agent-shell 真正有意思的地方不在于它用了哪个模型而在于它怎么把“AI 协作”这件事拆成一套可组合、可扩展、可被 Lisp 改写的协议层。先说清楚它解决的是什么问题。传统的 AI 编程助手交互模型基本是“你问一句、它答一句”上下文靠插件自己维护工具调用靠插件自己实现一旦你想换个模型、加个新工具、改个行为逻辑就得去改插件源码。而 agent-shell 走的是另一条路它把 agent 的能力抽象成一套基于 ACPAgent Client Protocol的通信层Emacs 这边只负责“显示、输入、调度”真正的推理和工具执行交给 agent 进程。这样一来编辑器不用关心你背后接的是哪个模型agent 也不用关心你用的是 Emacs 还是别的客户端。两边通过协议对话各司其职。这个设计思路听起来有点抽象但落到实际使用里好处非常直接你可以在 Emacs 里同时挂多个不同能力的 agent让它们协作你可以用 Lisp 写自己的工具函数注册进 agent 的能力列表你甚至可以让 agent 反过来调用 Emacs 的功能比如操作 buffer、跑 elisp、读写文件。这就是标题里“自我进化”的含义——工作台不是死的它能随着你写的 Lisp 代码不断长出新的能力。下面我会从几个角度把这件事拆开讲agent-shell 到底怎么在 Emacs 里跑起来、ACP 协议层为什么是关键、Lisp 在其中扮演什么角色、多 agent 协作怎么落地、以及我在实际配置里踩过的那些坑。每一部分我都会尽量给出可复现的操作和背后的取舍逻辑而不是只丢结论。2. agent-shell 在 Emacs 里的运行骨架进程、buffer 与消息流2.1 为什么它不是一个普通插件要理解 agent-shell先得理解它和普通 Emacs 插件的根本区别。普通插件是“跑在 Emacs 进程里的 elisp 代码”它的一切行为都受 Emacs 主循环调度。而 agent-shell 的核心是一个独立运行的 agent 进程Emacs 通过管道或 socket 跟它通信。这个区别决定了三件事第一agent 的推理不会阻塞 Emacs。你在大模型那边等一个长回复的时候Emacs 主线程该干嘛干嘛不会卡住。第二agent 进程可以用任何语言写不限于 elisp这就打开了工具生态。第三通信必须走协议不能靠共享内存所以 ACP 这种协议层就成了必需品。我一开始没意识到这个架构差异直接按“装个包、配个 key”的思路去弄结果发现 agent-shell 的配置项里有一大半是在描述“怎么启动 agent 进程”“用什么传输方式”“超时怎么设”而不是“用哪个模型”。这就是架构决定的配置形态。2.2 启动一个 agent 会话的完整链路从操作层面看启动一次会话大致经历这么几步。首先 Emacs 侧读取你的 agent 配置确定要启动哪个 agent 可执行文件、传什么参数、用什么传输通道。然后 agent-shell 拉起子进程建立连接发送初始化握手。握手完成后agent 会回报自己支持的能力列表比如能不能读文件、能不能执行命令、支持哪些工具。Emacs 侧根据这个列表决定 UI 上暴露哪些操作。这里有个容易被忽略的点能力协商是动态的。也就是说同一个 agent-shell 前端接不同 agent 后端时你能用的功能是不一样的。我一开始以为“装了 agent-shell 就有一套固定功能”后来发现不是——功能取决于对面 agent 报了什么。这个设计很聪明因为它把“前端”和“能力”解耦了但也意味着你调试问题时得先确认“是前端没显示还是后端没报这个能力”。2.3 buffer 布局与消息渲染的取舍agent-shell 在 Emacs 里的呈现方式通常是专门的 buffer里面按消息流渲染对话。这里有个设计取舍值得说它没有把每条消息做成独立的 widget而是用类似 comint 的模式管理输出。好处是性能好、滚动流畅、跟 Emacs 原生的 shell 体验一致代价是消息的结构化程度没那么高你想对某条消息做精细操作比如折叠、引用就得自己写 elisp。我实测下来这个取舍是合理的。因为 AI 对话的消息量往往很大如果每条都做成重 widgetbuffer 会变得很卡。用 comint 风格虽然“糙”一点但胜在稳。如果你需要更结构化的展示可以在 agent 侧把输出格式化成特定标记再在 Emacs 侧写 font-lock 规则去高亮这样既保留了性能又能做出层次感。提示如果你发现 agent-shell buffer 输出乱序或者丢消息先检查传输层是不是用了行缓冲。很多 agent 进程默认全缓冲导致消息攒一批才发出来看起来就像卡顿。3. ACP 协议层agent-shell 真正的“神经中枢”3.1 ACP 到底在解决什么问题ACP也就是 Agent Client Protocol是 agent-shell 这套东西里最核心也最容易被忽视的部分。很多人一上来就关心“支持哪些模型”但真正决定这套工作台能不能扩展的是协议层设计得好不好。打个比方如果没有 ACP每个 AI 插件都得自己定义“怎么发请求、怎么收结果、怎么调工具”那结果就是 N 个插件 N 套私有协议互相不通。ACP 的作用就是把这些交互标准化——请求长什么样、响应长什么样、工具调用怎么描述、错误怎么回报、流式输出怎么分片全都有统一格式。这样一来任何实现了 ACP 的 agent 都能接任何实现了 ACP 的客户端。我在配置过程中最大的体会是协议层的存在让“换后端”变成了一件几乎零成本的事。今天接这个 agent明天想试另一个只要它说 ACP前端这边基本不用动。这在传统插件生态里是很难想象的。3.2 一次工具调用的协议往返为了让你对 ACP 有具体感知我拆一次典型的工具调用往返。假设你在对话里让 agent “读一下当前项目的 README”。流程大致是Emacs 侧把用户输入通过 ACP 发给 agent 进程。agent 判断需要调用“读文件”工具于是通过 ACP 发回一个工具调用请求里面带工具名和参数。Emacs 侧或 agent 侧取决于工具注册在哪边执行这个工具把结果通过 ACP 回传。agent 拿到结果继续推理生成自然语言回复流式发回。Emacs 侧渲染回复。这个流程里第 2 步和第 3 步是关键。工具到底在哪边执行决定了架构的灵活性。如果工具在 Emacs 侧执行那 agent 就能操作你的编辑器环境如果工具在 agent 侧执行那 agent 就能访问它自己的沙箱环境。agent-shell 的设计允许两边都注册工具这就为“AI 操作编辑器”和“AI 操作外部系统”同时打开了口子。3.3 流式输出与中断处理流式输出是 AI 交互的标配但它在协议层其实挺麻烦。因为流式意味着一条逻辑消息被切成很多片每片都要带足够的元信息让客户端知道“这属于哪条消息、是不是最后一片”。ACP 在这块的处理方式是给每个消息流分配标识分片带上序号和结束标记。中断处理更微妙。你在 Emacs 里按C-g想取消一次生成这个信号得通过协议传到 agent 进程agent 得能优雅地停下当前推理而不是直接被杀掉。我踩过一次坑早期配置里中断信号没正确传递结果C-g之后 agent 还在后台跑下次发消息时收到了上一次的残留输出整个对话就乱了。后来确认是协议层的中断消息没实现好换成支持中断的 agent 版本就正常了。注意判断一个 agent 实现是否靠谱看它怎么处理中断和错误回报。这两块做不好的用起来迟早出问题。4. Lisp 作为“进化引擎”让工作台长出你自己的能力4.1 为什么是 Lisp而不是配置文件agent-shell 最让我兴奋的一点是它把 Lisp 放在了扩展的核心位置。你可以用 elisp 写工具函数注册进 agent 的能力列表让 AI 在需要的时候调用。这跟“写个 JSON 配置文件声明工具”是完全不同的体验。配置文件是静态的你只能声明“有这么个工具参数是这些”。而 Lisp 是活的你可以在工具函数里写任意逻辑读当前 buffer、调用其他 elisp 函数、访问 Emacs 的内部状态、甚至动态生成新的工具。这意味着工作台的能力边界不是产品经理定的而是你自己写的。举个我实际用的例子。我写了一个 elisp 工具功能是“把当前选中的代码块连同所在文件路径一起返回”。注册进 agent 之后我在对话里说“解释一下我选中的这段”agent 就能拿到精确的上下文而不是靠猜。这个工具本身很简单但它体现的思路很重要把 Emacs 已有的能力暴露给 AI比让 AI 重新实现一遍要高效得多。4.2 注册一个自定义工具的完整过程下面是我注册工具时的大致步骤供你参考。首先定义一个 elisp 函数它接收参数、返回结果。然后把这个函数包装成 agent-shell 能识别的工具描述包括工具名、说明、参数 schema。最后把它加到 agent 的工具列表里重启会话生效。(defun my/agent-tool-get-selection () 返回当前选中的文本及其所在文件路径。 (let ((file (buffer-file-name)) (text (if (region-active-p) (buffer-substring-no-properties (region-beginning) (region-end)) ))) (list :file file :text text))) ;; 注册到 agent-shell 的工具表示意具体 API 以你的版本为准 (agent-shell-register-tool :name get_selection :description 获取当前 Emacs 中选中的文本和文件路径 :handler #my/agent-tool-get-selection)这里的关键不是代码本身而是思路工具的描述要写得让模型能理解“什么时候该用它”。我一开始描述写得太简略模型经常该调用的时候不调用后来把 description 写具体了命中率明显上升。这跟写 prompt 是一个道理——工具描述就是给模型看的 prompt。4.3 工具设计的几个反直觉经验用了一段时间之后我总结出几条工具设计的经验有些跟直觉相反。第一工具不要设计得太“大”。我一开始想做一个“万能工具”一个函数搞定读文件、写文件、搜索、执行命令。结果模型经常搞混参数调用失败率很高。后来拆成几个小工具每个只做一件事成功率反而上去了。模型对“单一职责”的工具理解得更准。第二返回值要结构化。返回一大段自然语言模型还得再解析一遍返回带字段的结构化数据模型直接用。我现在的工具基本都返回 alist 或 plist模型处理起来干净很多。第三错误信息要写清楚。工具执行失败时返回的错误信息是给模型看的不是给人看的。写“操作失败”没用要写“文件不存在路径是 X请确认路径”。模型拿到具体错误才能自我纠正。5. 多 agent 协作从“一个助手”到“一个团队”5.1 为什么要多个 agent单个 agent 再强也有它的能力边界和上下文限制。多 agent 协作的思路是让不同 agent 负责不同的事各司其职通过协议互相调用。这在 agent-shell 的架构下是天然支持的因为每个 agent 都是独立进程前端可以同时挂多个。我实际用下来的典型组合是一个 agent 负责代码理解和生成一个 agent 负责跑测试和读日志一个 agent 负责查文档。它们之间不直接通信而是通过我在 Emacs 侧做调度——我把 A 的输出整理一下作为 B 的输入。这种“人肉编排”听起来笨但可控性极高出问题容易定位。5.2 协作模式与调度策略多 agent 协作大致有几种模式我按复杂度排一下。最简单的是串行接力A 干完活结果交给 B。比如让 A 生成代码让 B 审查代码。这种模式实现简单缺点是慢因为要等前一个完成。进阶一点是并行分工同时让多个 agent 处理不同子任务最后汇总。比如一个 agent 分析前端代码一个分析后端代码我来合并结论。这种模式快但需要任务能干净地拆分。最复杂的是互相评审两个 agent 就同一个问题给出方案然后互相挑毛病。这种模式质量高但 token 消耗大而且容易陷入“互相抬杠”的死循环。我一般只在关键决策上用。协作模式适用场景主要代价串行接力有明确先后依赖的任务延迟叠加并行分工子任务可独立拆分结果合并成本互相评审高风险决策token 消耗大、可能死循环5.3 上下文隔离带来的意外好处多 agent 用久了我发现一个意外的好处上下文隔离反而提升了质量。单个 agent 聊久了上下文里堆满历史消息容易“跑偏”或者被早期错误带节奏。而多个 agent 各自维护独立上下文每个都从干净状态开始反而不容易累积错误。这个观察让我调整了使用习惯不再追求“一个超长会话搞定所有事”而是“每个任务开一个干净会话”。虽然要多敲几次启动命令但结果质量稳定得多。这算是 agent-shell 架构给我的一个使用层面的启发。6. 实战配置里那些文档不会写的坑6.1 进程启动与环境变量第一个大坑是环境变量。agent 进程是 Emacs 拉起来的子进程它继承的是 Emacs 的环境不是你 shell 里的环境。这意味着你在.zshrc里配的 PATH、API key、代理设置agent 进程可能根本看不到。我第一次配置时死活连不上排查半天才发现是 PATH 问题。解决办法有两个要么在 Emacs 启动前把环境变量 export 好要么在 agent-shell 的配置里显式指定环境变量。我推荐后者因为更可控。具体做法是在 agent 的启动配置里加一个 env 字段把需要的变量写进去。(setq agent-shell-agents ((:name my-agent :command my-agent-binary :env ((PATH . /usr/local/bin:/usr/bin) (MY_API_KEY . your-key-here)))))注意不要把密钥硬编码进会提交到版本库的配置文件。用~/.authinfo或者环境变量注入的方式管理。6.2 编码与换行符的隐形问题第二个坑是编码。agent 进程和 Emacs 之间的通信如果编码不一致中文就会变乱码。我遇到过好几次“英文正常、中文乱码”的情况根源都是子进程默认用了非 UTF-8 编码。解决办法是在启动 agent 时显式设置LANG和LC_ALL为 UTF-8并在 Emacs 侧确认coding-system-for-read和coding-system-for-write设置正确。换行符也类似。如果 agent 在 Windows 环境下跑输出可能带\r\nEmacs 侧渲染时就会多出奇怪的符号。这个一般通过设置进程的编码系统就能解决但排查起来很费时间因为症状不明显。6.3 超时、重试与“假死”排查第三个坑是超时。AI 推理有时候很慢如果超时设得太短请求会被中断设得太长真卡住的时候你又不知道。我的经验是设一个中等超时比如 60 秒同时开启流式输出——只要流还在动就说明没死流停了超过一定时间才判定为卡住。排查“假死”有个实用技巧看 agent 进程的 CPU 占用。如果 CPU 是 0说明它在等网络如果 CPU 跑满说明它在本地算。这两种情况的处理方式完全不同。前者查网络和 API 配置后者查是不是任务太重或者陷入循环。6.4 版本兼容性协议演进带来的阵痛最后一个坑是版本兼容。ACP 这类协议还在演进不同版本的 agent 和客户端之间可能有细微不兼容。我遇到过“握手成功但工具调用失败”的情况最后发现是协议版本对不上某些字段的格式变了。这类问题的排查思路是先看双方日志里的协议版本号再看具体哪个字段解析失败。如果文档没写清楚就只能靠抓包或者读源码。我的建议是固定一套验证过能用的版本组合不要盲目追新。等社区反馈稳定了再升级能省掉大量排查时间。7. 我对这套工作台的真实使用体会用 agent-shell 这套东西几个月下来我最大的感受是它代表了一种跟“AI 插件”完全不同的思路。插件是把 AI 当功能塞进编辑器而 agent-shell 是把 AI 当协作者接进工作流。前者你只能用它给的功能后者你能自己定义协作方式。当然它也不是没有代价。配置复杂度比装个普通插件高不少协议层的抽象也意味着出问题时排查链路更长。但如果你本来就熟悉 Emacs 和 Lisp这些成本是值得的——因为你换来的是一个能随你需求不断进化的工具而不是一个功能固定的黑盒。我现在的工作流大致是日常编码用 Emacs 原生功能遇到需要 AI 介入的场景按任务类型选不同的 agent需要操作编辑器状态的时候调用自己写的 elisp 工具。这套组合不是一次配好的而是用着用着慢慢长出来的。这可能就是标题里“自我进化”最真实的含义——工作台的能力边界最终由你自己的使用习惯和 Lisp 代码决定。如果你也想试我的建议是从最小配置开始先跑通一个 agent确认通信正常再慢慢加工具、加 agent。别一上来就追求“全自动多 agent 协作”那玩意儿在配置没稳之前只会让你怀疑人生。先把单 agent 用顺剩下的都是水到渠成的事。