首页 / 资讯中心 / 文章详情

Emacs AI 工作台:agent-shell 与 ACP 协议深度集成实战

Emacs AI 工作台:agent-shell 与 ACP 协议深度集成实战 ★ FEATURED ARTICLE
1. 为什么我要把 AI 工作台塞进 Emacs第一次听说 agent-shell 这个概念时我的反应和大多数人一样Emacs 里跑 AI图什么VS Code 的 Copilot 不香吗Cursor 不香吗但用了两周之后我改主意了。原因很简单——当你的整个工作流都在 Emacs 里时任何需要切窗口的操作都是对心流的谋杀。agent-shell 的核心思路是把 AI 代理agent变成一个可交互的 shell 进程直接跑在 Emacs 的 buffer 里。它不是简单的“在 Emacs 里调用 API”而是通过 ACPAgent Client Protocol协议让 AI 代理以子进程的形式与 Emacs 通信。这意味着什么意味着你可以像操作 bash 一样操作 AI可以管道、可以重定向、可以在 org-mode 里直接嵌入 agent 的输出、可以用 Lisp 写钩子函数在特定事件触发时自动调用 agent。我最初的需求很朴素写代码时不想离开 Emacs 去浏览器里问 AI。但用着用着发现真正的价值不在于“少切一次窗口”而在于AI 变成了 Emacs 的一个原生组件。你可以用completing-read选择 agent用eldoc显示 agent 的实时状态用org-babel把 agent 的对话记录导出成可复现的文档。这种深度集成带来的效率提升是任何外部工具都给不了的。这篇文章适合三类人一是已经在 Emacs 里生活了几年、想试试 AI 集成但不知道从哪下手的老用户二是对 ACP 协议和 agent 架构感兴趣、想自己写一个 shell 集成的开发者三是单纯好奇“Emacs 还能这么玩”的技术爱好者。我会从架构原理讲到实操配置从踩坑记录讲到进阶玩法尽量把每个决策背后的“为什么”说清楚。提示本文假设你有基本的 Emacs 使用经验知道什么是 buffer、mode、hook。如果这些概念对你来说完全陌生建议先花半小时过一遍 Emacs 自带的教程C-h t。2. agent-shell 到底解决了什么问题2.1 传统 AI 编程助手的三个断点在 agent-shell 出现之前我在 Emacs 里用 AI 的方式无非这几种开个终端跑aider或cursor的 CLI、用gptel之类的包调 API、或者干脆切到浏览器。这三种方式各有各的断点。终端方案的问题在于上下文隔离。aider 跑在终端里它看不到我当前编辑的 buffer不知道我的光标在哪不知道我最近改了哪些文件。我得手动把代码复制过去或者用aider --file指定文件。每次对话都是一次“重新介绍背景”的过程效率极低。API 方案比如 gptel解决了上下文问题但它本质上是请求-响应模式。你发一段文字它回一段文字没有“代理”的概念。代理意味着它能主动执行操作——读文件、跑命令、改代码、根据结果决定下一步。gptel 做不到这些它只是一个更聪明的补全工具。浏览器方案的问题更明显状态丢失。你在浏览器里和 AI 聊了半小时关掉标签页就全没了。想找回之前的对话翻历史记录吧。想把对话内容整理成文档手动复制粘贴吧。2.2 ACP 协议带来的范式转变agent-shell 背后的 ACPAgent Client Protocol协议是我认为最值得关注的技术点。它定义了一套标准的通信格式让客户端Emacs和代理AI 进程之间可以双向通信。这不是简单的 stdin/stdout 管道而是一个结构化的消息协议。具体来说ACP 定义了这几种消息类型消息类型方向用途initialize客户端→代理握手交换能力信息prompt客户端→代理发送用户输入stream代理→客户端流式返回生成内容tool_call代理→客户端请求执行工具读文件、跑命令等tool_result客户端→代理返回工具执行结果shutdown双向优雅关闭这套协议的关键在于tool_call 机制。当 AI 代理需要读取一个文件时它不会直接去读因为它可能没有文件系统权限而是发一个tool_call消息给 Emacs由 Emacs 来执行读取操作并返回结果。这意味着Emacs 始终掌握控制权——你可以审查每一个工具调用可以拒绝危险的命令可以记录所有的操作日志。这种设计哲学和传统的“AI 直接操作文件系统”方案有本质区别。后者虽然方便但风险极高——你永远不知道 AI 会不会突然rm -rf你的项目目录。ACP 的方案把执行权留在客户端代理只负责“决定做什么”不负责“实际去做”。2.3 为什么是 Emacs 而不是其他编辑器这个问题我被问过很多次。VS Code 的扩展生态更丰富为什么非要在 Emacs 里折腾答案在于Emacs 的可编程性。VS Code 的扩展是用 TypeScript 写的跑在 Node.js 里和编辑器的核心是隔离的。你可以调用 VS Code 的 API但你不能改变 VS Code 本身的行为。Emacs 不一样Lisp 是它的灵魂你可以用 Lisp 重写任何东西——包括 agent-shell 本身。举个例子我想让 agent 在每次我保存 Python 文件时自动检查代码风格。在 VS Code 里我需要找一个支持这个功能的扩展或者自己写一个。在 Emacs 里我只需要在after-save-hook里加一行(add-hook python-mode-hook (lambda () (add-hook after-save-hook (lambda () (when (derived-mode-p python-mode) (agent-shell-send 检查当前文件的代码风格问题))))))这种“编辑器即平台”的能力是 Emacs 独有的。agent-shell 之所以能在 Emacs 里跑得这么自然正是因为 Emacs 本身就是为这种深度定制而设计的。3. 从零搭建 agent-shell 的完整过程3.1 环境准备那些文档里不会写的细节官方文档会告诉你“安装 Emacs 28、安装 agent-shell 包、配置 API key”但实际操作中你会遇到一堆文档没提的问题。我把我踩过的坑按顺序列出来。第一坑Emacs 版本和 JSON 解析。agent-shell 依赖 Emacs 内置的json-parse-string函数这个函数在 Emacs 27 里性能很差处理大消息时会卡顿。如果你用的是 Emacs 27 或更早版本建议升级到 28.1 以上。我实测下来Emacs 29 的 JSON 解析速度比 27 快了将近三倍。第二坑子进程编码问题。agent-shell 通过子进程和 AI 代理通信如果编码设置不对中文会变成乱码。你需要在配置里显式设置(setq default-process-coding-system (utf-8-unix . utf-8-unix)) (setq locale-coding-system utf-8)第三坑代理可执行文件的路径。agent-shell 需要知道 AI 代理的可执行文件在哪。如果你用npm install -g装的代理可能需要手动指定路径(setq agent-shell-agent-command /usr/local/bin/your-agent)用which your-agent确认路径别想当然。3.2 最小可用配置先跑起来再说我不建议一上来就搞复杂配置。先用最小配置跑通确认基本功能正常再逐步加东西。这是我的最小配置(use-package agent-shell :ensure t :config (setq agent-shell-agent-command your-agent-command agent-shell-default-model your-model-name agent-shell-streaming t) :bind (C-c a s . agent-shell) (C-c a p . agent-shell-send-prompt))agent-shell-streaming设为t是关键。不开流式输出的话你要等 AI 生成完整个回复才能看到内容体验极差。开了之后文字会像打字机一样逐字出现虽然本质上等待时间一样但心理感受完全不同。配置好之后M-x agent-shell应该能打开一个 shell buffer。如果报错先检查这三件事代理可执行文件是否存在、API key 是否设置、网络是否能通。我见过太多人卡在 API key 没设对上面——注意有些代理要求 key 放在环境变量里有些要求放在配置文件里看文档。3.3 和 org-mode 的集成让对话变成可复现的文档agent-shell 最让我惊喜的功能是和 org-mode 的集成。你可以把 agent 的对话直接插入 org 文件变成可执行的代码块#BEGIN_SRC agent :session my-agent 帮我写一个 Python 函数计算斐波那契数列 #END_SRCC-c C-c执行这个代码块agent 的回复会直接插入到文件里。下次打开这个 org 文件你可以重新执行得到新的回复。这意味着AI 对话变成了可版本控制的文档——你可以用 git 管理它可以 diff 不同版本的回复可以在团队里分享。我现在的习惯是每个项目建一个agent-notes.org把重要的 AI 对话都记在里面。三个月后回头看能清楚看到当时的决策过程。这比翻聊天记录强太多了。3.4 多代理协作的配置方式agent-shell 支持同时运行多个代理每个代理可以有不同的模型、不同的系统提示词。配置方式是在agent-shell-agents里定义多个条目(setq agent-shell-agents ((coder . (:command agent-coder :model claude-sonnet :system-prompt 你是一个资深 Python 开发者专注于代码质量和性能优化。)) (writer . (:command agent-writer :model gpt-4 :system-prompt 你是一个技术文档写作者擅长把复杂概念用通俗语言解释清楚。)) (reviewer . (:command agent-reviewer :model claude-opus :system-prompt 你是一个代码审查专家专注于发现边界条件问题和安全隐患。))))用M-x agent-shell时会弹出选择界面让你选哪个代理。我通常开三个 buffer一个 coder 写代码一个 reviewer 审查一个 writer 写文档。三个代理各司其职互不干扰。注意同时跑多个代理会消耗更多 token。如果你的 API 是按量计费的建议只在需要时启动对应代理用完就关。4. 深入 ACP 协议消息流与工具调用机制4.1 一次完整对话的消息流转过程理解 ACP 协议的消息流对排查问题和优化性能很有帮助。我抓包分析过一次完整的对话过程大致是这样的Emacs 启动代理子进程发送initialize消息包含客户端能力信息支持哪些工具、支持流式输出等。代理回复initialize_result包含代理能力信息支持哪些模型、最大上下文长度等。用户输入 promptEmacs 发送prompt消息。代理开始生成通过stream消息逐块返回内容。每个stream消息包含一个 delta增量文本。如果代理需要读取文件发送tool_call消息包含工具名称和参数。Emacs 执行工具调用读文件发送tool_result消息返回结果。代理继续生成直到完成发送stream_end消息。用户关闭 buffer 时Emacs 发送shutdown消息代理优雅退出。这个流程里最容易出问题的是第 5 步。如果工具调用的参数格式不对或者 Emacs 没有实现对应的工具代理会卡住等待。我遇到过代理请求read_file但 agent-shell 只实现了read-file下划线 vs 连字符的情况结果代理等了 30 秒超时。排查这种问题需要打开agent-shell-debug开关看原始消息日志。4.2 工具调用的安全边界设计ACP 协议的工具调用机制有一个很重要的设计客户端可以拒绝任何工具调用。这意味着你可以在 Emacs 里加一层审查逻辑比如(defun my-agent-shell-tool-call-filter (tool-name args) 审查工具调用返回 nil 表示拒绝。 (cond ((string tool-name run_command) (let ((cmd (alist-get command args))) (if (string-match-p \\(rm\\|delete\\|drop\\) cmd) (progn (message 拒绝执行危险命令: %s cmd) nil) t))) (t t))) (setq agent-shell-tool-call-filter #my-agent-shell-tool-call-filter)这段代码会拦截所有包含rm、delete、drop的命令。你可以根据自己的需求定制过滤规则。我建议至少加上对文件删除和数据库操作的拦截除非你完全信任你用的代理。这种安全设计是 agent-shell 相比其他方案的一大优势。大多数 AI 编程工具要么完全信任 AI危险要么完全禁止 AI 执行操作无用。agent-shell 给了你中间地带——你可以精确控制 AI 能做什么、不能做什么。4.3 流式输出的实现细节与性能调优流式输出看起来简单实现起来有不少细节。agent-shell 的做法是每收到一个stream消息就在 buffer 末尾插入文本然后调用redisplay刷新屏幕。问题是如果每个字符都触发一次redisplayEmacs 会卡成幻灯片。agent-shell 的优化策略是批量刷新积累一定数量的字符默认 50 个或等待一定时间默认 100ms后再刷新。这个策略在agent-shell-stream-batch-size和agent-shell-stream-flush-interval两个变量里配置。我实测下来默认值在大多数情况下够用。但如果你用的是性能较弱的机器或者代理返回速度特别快可以调大 batch size(setq agent-shell-stream-batch-size 200 agent-shell-stream-flush-interval 0.2)代价是输出的“打字机效果”会变弱文字会成块出现。看你更在意流畅度还是响应感。5. 用 Lisp 扩展 agent-shell 的实战案例5.1 自动生成 commit message这是我用得最多的一个扩展。每次git commit时自动调用 agent 根据 diff 生成 commit message(defun my-agent-generate-commit-message () 根据 staged diff 生成 commit message。 (interactive) (let ((diff (shell-command-to-string git diff --cached))) (if (string-empty-p diff) (message 没有 staged 的改动) (agent-shell-send (format 根据以下 diff 生成一条简洁的 commit message遵循 Conventional Commits 规范\n\n%s diff) :callback (lambda (response) (let ((msg (string-trim response))) (kill-new msg) (message Commit message 已复制到剪贴板: %s msg)))))))绑定到C-c g c写代码时随手就能生成 commit message。比手动写快多了而且格式统一。5.2 代码审查钩子在保存文件时自动触发代码审查(defun my-agent-review-on-save () 保存时自动审查当前文件。 (when (and (derived-mode-p prog-mode) (buffer-file-name) (not (string-match-p test (buffer-file-name)))) (agent-shell-send (format 审查以下代码指出潜在问题只列出问题不要解释\n\n%s (buffer-substring-no-properties (point-min) (point-max))) :callback (lambda (response) (when (string-match-p 问题\\|bug\\|错误 response) (message 代码审查发现问题\n%s response)))))) (add-hook after-save-hook #my-agent-review-on-save)这个钩子会在每次保存时触发。注意我加了not (string-match-p test ...)的条件跳过测试文件——测试文件通常不需要这么严格的审查。提示这个钩子会频繁调用 API如果你的 API 有速率限制建议加一个冷却时间比如两次调用之间至少间隔 30 秒。5.3 用 transient 做交互式菜单Emacs 的 transient 库可以做出很漂亮的交互式菜单。我给 agent-shell 做了一个(transient-define-prefix my-agent-shell-menu () Agent Shell 快捷菜单 [[发送 (p 发送 prompt agent-shell-send-prompt) (r 发送 region agent-shell-send-region) (b 发送 buffer agent-shell-send-buffer)] [代理 (s 切换代理 agent-shell-switch-agent) (m 切换模型 agent-shell-switch-model)] [工具 (c 生成 commit my-agent-generate-commit-message) (v 审查代码 my-agent-review-code)]]) (global-set-key (kbd C-c a) #my-agent-shell-menu)按C-c a弹出菜单按对应字母执行操作。比记一堆快捷键舒服多了。6. 踩坑记录那些让我抓狂的瞬间6.1 代理进程僵死与超时处理最让人抓狂的问题是代理进程僵死。表现是你发了 promptbuffer 里显示“等待响应”然后就没有然后了。等多久都没用只能手动kill-process。这个问题通常有三个原因一是网络问题代理连不上 API二是代理内部错误但没有正确返回错误消息三是 ACP 消息格式不对代理解析失败后卡住。排查方法打开agent-shell-debug看*agent-shell-debug*buffer 里的原始消息。如果看到tool_call发出去了但没有tool_result返回说明是工具调用的问题。如果看到prompt发出去了但没有stream返回说明是代理的问题。我现在的做法是加一个超时机制(setq agent-shell-response-timeout 60) (add-hook agent-shell-timeout-hook (lambda () (message 代理响应超时已自动终止) (agent-shell-restart)))超时后自动重启代理比手动 kill 省事。6.2 上下文窗口溢出的处理策略AI 模型的上下文窗口是有限的。当你和代理聊了很久历史消息会占满上下文导致新的 prompt 被截断或者代理报错。agent-shell 的处理策略是自动摘要当历史消息超过一定长度时调用代理自己生成一个摘要用摘要替换原始历史。这个策略在agent-shell-auto-summarize开启时生效。但自动摘要有个问题摘要可能丢失重要细节。我遇到过摘要把关键的文件路径省略了导致后续对话中代理找不到文件。我的建议是对于重要的对话手动管理上下文定期用agent-shell-clear-history清空历史只保留当前任务相关的消息。6.3 多代理切换时的状态污染同时跑多个代理时容易出现状态污染。比如你在 coder 代理里让它读了一个文件然后切到 reviewer 代理reviewer 可能“以为”自己已经读过那个文件了。这个问题的根源是 agent-shell 默认共享工具调用缓存。解决办法是给每个代理独立的缓存(setq agent-shell-tool-cache-per-agent t)开启后每个代理有自己的工具调用缓存互不干扰。代价是内存占用会增加但为了状态清晰这点代价值得。7. 进阶玩法让 agent-shell 真正融入工作流7.1 用 agent-shell 做项目级代码生成单个文件的代码生成已经不够用了。我现在的做法是用 agent-shell 做项目级的代码生成。比如我要新建一个 Python 包我会给 agent 一个详细的规格说明让它生成整个包的结构(defun my-agent-generate-package (package-name description) 生成一个完整的 Python 包结构。 (interactive s包名: \ns描述: ) (agent-shell-send (format 生成一个 Python 包名为 %s功能是%s。\ 要求1. 包含 setup.py 或 pyproject.toml2. 包含 README.md\ 3. 包含 tests 目录和至少一个测试文件4. 代码遵循 PEP 8。\ 请以文件树的形式输出每个文件的内容用代码块包裹。 package-name description) :callback (lambda (response) (my-agent-write-files-from-response response))))my-agent-write-files-from-response是我写的一个函数解析 agent 返回的文件树自动创建目录和文件。这样从零到一个可运行的包只需要几分钟。7.2 把 agent 输出接入 CI 流程agent-shell 不只能在交互式环境里用还可以在批处理模式下跑。我把它接入了 CI 流程每次 push 时自动做代码审查emacs --batch -l ~/.emacs.d/init.el \ --eval (agent-shell-batch-review src/ review-report.org)agent-shell-batch-review是我写的一个函数遍历指定目录下的所有源文件逐个发给 agent 审查把结果汇总到一个 org 文件里。CI 跑完后review-report.org 会作为 artifact 上传团队成员可以下载查看。这个方案的好处是审查标准统一。人工审查难免有疏漏agent 审查每次都按同样的标准来。当然agent 审查不能完全替代人工审查但可以作为第一道防线。7.3 自定义代理用 Lisp 写一个专属 agentagent-shell 支持自定义代理。你可以用任何语言写一个符合 ACP 协议的可执行文件然后在 Emacs 里配置它。我用 Lisp 写了一个简单的代理专门用来查询我的 org-mode 笔记(defun my-notes-agent-handler (message) 处理来自 Emacs 的消息。 (let ((type (alist-get type message))) (cond ((string type prompt) (let ((query (alist-get content message))) (my-notes-search query))) ((string type initialize) (my-notes-agent-initialize)) (t nil)))) (defun my-notes-search (query) 在 org 笔记里搜索 query。 (let ((results (shell-command-to-string (format rg -l %s ~/org/notes/ query)))) (agent-shell-send-response (format 找到以下相关笔记\n%s results))))这个代理不调用任何外部 AI 服务纯粹在本地搜索。虽然“智能”程度不高但胜在快和私密。对于“我上次记的那个配置放在哪了”这类问题比调用大模型快得多。8. 我对 agent-shell 未来的一些个人判断用了几个月 agent-shell我最大的感受是AI 集成的关键不在于模型有多强而在于集成有多深。一个中等能力的模型如果深度集成到你的工作流里带来的效率提升可能超过一个顶级模型但需要你切窗口去用。agent-shell 目前还有一些不完善的地方。比如多代理协作的调度还不够智能工具调用的错误处理还不够健壮和 org-mode 的集成还有提升空间。但它的架构方向是对的——把控制权留给用户把执行权留给客户端把决策权留给代理。我接下来想尝试的方向是把 agent-shell 和我的 org-roam 笔记系统打通让 agent 能直接查询我的知识库以及做一个基于 agent-shell 的自动化测试工具用 AI 生成测试用例并自动执行。这些想法还在验证阶段等跑通了再写出来分享。如果你也在用 agent-shell或者有更好的 AI 工作流方案欢迎交流。这个领域变化太快一个人摸索容易走弯路多交流才能少踩坑。
阅读完成 · 觉得有帮助?
咨询建站