
1. 为什么我要在 Emacs 里折腾一个 agent-shell第一次听说“在 Emacs 里跑 AI Agent”这个想法时我的反应大概是这不是多此一举吗终端里开个窗口、浏览器里开个网页不都能跟大模型对话为什么非要把这东西塞进一个四十多年前诞生的文本编辑器里直到我把这个流程真正跑通、并且连续用了两周之后我才意识到自己之前的判断有多草率。Emacs agent-shell 的核心价值不是“在 Emacs 里聊天”而是把 AI 从一个需要你主动切换过去的外部工具变成一个常驻在你编辑上下文里的协作进程。这个区别听起来很虚但实际用起来差别巨大。举个最直接的场景。我在写一段 Lisp 宏的时候经常需要让模型帮我检查括号匹配、解释某个destructuring-bind的行为、或者把一段 Python 逻辑翻译成 Elisp。传统做法是切到浏览器复制代码粘贴等回复再复制回来切回 Emacs调整缩进。这一套动作下来注意力已经被打断了三四次。而 agent-shell 做的事情是让这个往返过程压缩成一次按键——当前 buffer 的内容、光标位置、甚至选中的 region都可以直接作为上下文喂给 agent回复直接落在同一个 Emacs 会话里。所以这篇内容我想聊的不是“怎么装一个包”这种层面的事而是一个长期使用 Emacs 的人如何把 AI Agent 真正编织进自己的工作流。关键词里的 Emacs、agent-shell、ACP、Lisp 这几个词基本勾勒出了整条技术链路Emacs 是宿主环境agent-shell 是交互界面ACP 是 agent 与编辑器之间的通信协议而 Lisp 既是 Emacs 的母语也是整个配置体系的粘合剂。适合读这篇的人大概分三类一是已经用 Emacs 超过半年、有自己的配置体系、想进一步把 AI 接进来的老用户二是对 AI Agent 架构感兴趣、想看看“编辑器内 agent”这种形态到底怎么落地的开发者三是单纯好奇 ACP 这类协议在真实项目里长什么样的人。如果你连init.el都没打开过那这篇可能会有点劝退但我会尽量把每个概念都讲清楚你至少能理解这套东西的设计思路。需要提前说明的是agent-shell 本身是一个相对年轻的生态不同版本之间接口会有变化我下面讲到的配置和用法都是基于我当前实际跑通的版本你在复现时如果遇到字段对不上优先去看包本身的 README 和 changelog而不是死磕我的配置。2. agent-shell 到底解决了什么问题从“调用工具”到“常驻进程”2.1 传统 AI 辅助的三个断点在 agent-shell 出现之前Emacs 用户想用 AI主流方案无非这么几种gptel、chatgpt-shell、ellama或者干脆自己写个shell-command把请求发给某个 CLI。这些方案我都用过它们能解决问题但都有各自的断点。第一个断点是上下文割裂。大多数方案需要你手动把代码复制到对话 buffer 里模型看不到你的项目结构、看不到你正在编辑的文件、更看不到你光标附近的那几行。你得自己当“上下文搬运工”。第二个断点是会话状态丢失。每次调用都是一次性的请求-响应模型不记得你五分钟前让它改过什么。对于需要多轮迭代的任务比如重构一个函数这意味着你要反复重复背景信息。第三个断点是工具能力缺失。普通对话模型只能“说”不能“做”。你让它帮你改文件它给你一段代码你还得自己粘贴回去。而真正的 agent 应该能读文件、写文件、执行命令、搜索代码库。agent-shell 的设计目标就是同时打掉这三个断点。它通过 ACPAgent Client Protocol把 Emacs 变成一个 agent 的“客户端”agent 进程在后台常驻能感知编辑器状态能调用工具能维持会话。2.2 ACP 协议在中间扮演的角色这里必须把 ACP 单独拎出来讲因为它是整个架构的枢纽。ACP 全称 Agent Client Protocol你可以把它理解成“编辑器与 AI agent 之间的 USB 接口”。它定义了一套消息格式规定了客户端Emacs怎么把上下文发给 agent、agent 怎么把工具调用请求发回来、双方怎么协商能力。为什么要有这么一层协议而不是让 agent-shell 直接调用某个模型的 API原因在于解耦。模型会换、agent 实现会换、编辑器也可能换如果每一对组合都写一套适配代码维护成本会爆炸。ACP 把“谁在跟谁说话”这件事标准化了agent-shell 只需要实现 ACP 客户端任何实现了 ACP 服务端的 agent 都能接进来。从实际使用角度看ACP 带来的最直接好处是工具调用的双向性。当 agent 决定要读一个文件时它不是自己去猜路径而是通过 ACP 向 Emacs 发一个请求“帮我读一下这个 buffer 的内容”。Emacs 执行完再把结果回传。这意味着 agent 操作的是你真实的编辑环境而不是一个隔离的沙箱。2.3 和 gptel、ellama 这类方案的定位差异很多人会问我已经用 gptel 了为什么还要 agent-shell我的回答是它们解决的不是同一层问题。gptel 更像是一个“增强版的聊天窗口”它的核心是让你在 Emacs 里方便地跟模型对话支持多后端、支持 org-mode 集成非常成熟。但它本质上还是“你问它答”的模式模型是被动的。agent-shell 的定位是“agent 运行时”。它假设模型会主动做事——读文件、改代码、跑测试、根据结果再决定下一步。这种模式更接近你在终端里用 Claude Code 或者类似工具时的体验只不过宿主从终端换成了 Emacs。所以我的实际配置是两者共存gptel 用来做快速的问答和文本处理agent-shell 用来做需要多步操作的任务。它们不冲突反而互补。3. 把 agent-shell 跑起来环境、依赖与第一道坎3.1 前置条件盘点在动手之前先把需要的东西列清楚避免中途卡壳。组件作用我的实际选择Emacs宿主环境29.1 及以上建议 30agent-shellACP 客户端包从 MELPA 或源码安装ACP agent服务端进程任意兼容 ACP 的 agent模型后端提供推理能力本地或远程均可Lisp 环境配置与扩展内置 Elisp 即可Emacs 版本这块我要特别强调29 以下不要尝试。agent-shell 用到了一些较新的 API比如改进的json-parse行为和进程通信相关的函数在 28 上会报一些莫名其妙的错。我一开始在 28.2 上折腾了半天最后升级到 29.3 才顺利跑通。3.2 安装 agent-shell 的两种路径第一种是走包管理器。如果你的package-archives里配了 MELPA直接(use-package agent-shell :ensure t :commands (agent-shell) :custom (agent-shell-agent-command your-acp-agent-command))第二种是从源码装适合想跟进最新特性的人git clone agent-shell-repo ~/.emacs.d/site-lisp/agent-shell然后在配置里把路径加进load-path(add-to-list load-path ~/.emacs.d/site-lisp/agent-shell) (require agent-shell)我个人的建议是先用包管理器跑通确认整个链路没问题再考虑切源码。因为源码版本接口变动频繁新手容易被各种 breaking change 搞晕。3.3 配置 agent 命令时最容易踩的坑agent-shell-agent-command这个变量是第一个大坑。它需要的是一个可执行的命令字符串而不是一个函数或者路径片段。我第一次配置的时候写了个相对路径结果 Emacs 启动时找不到报错信息又很含糊排查了快半小时。正确的做法是给绝对路径或者确保这个命令在exec-path里(setq agent-shell-agent-command /usr/local/bin/your-acp-agent --flag value)另一个坑是参数传递。有些 agent 需要额外的启动参数比如指定工作目录、指定模型、指定配置文件。这些参数要跟命令写在同一个字符串里agent-shell 会自己解析。但要注意引号转义路径里有空格的话必须用双引号包起来。提示配置完之后先用M-x agent-shell手动启动一次观察*agent-shell*buffer 里的输出。如果 agent 进程启动失败错误信息会打在这里比 minibuffer 里的提示详细得多。3.4 验证链路是否打通启动之后最简单的验证方式是发一句“你好请告诉我你当前能访问哪些工具”。如果 agent 正常响应并且列出了类似 read_file、write_file、run_command 这样的工具说明 ACP 链路是通的。如果 agent 没反应按这个顺序排查检查 agent 进程是否真的起来了用M-x list-processes看。检查*agent-shell*buffer 里有没有协议层的报错。检查 agent 命令单独在终端里能不能跑起来。检查 Emacs 的exec-path是否包含 agent 所在目录。这四步走下来九成的启动问题都能定位。4. 让 agent 真正“懂”你的项目上下文注入的几种姿势4.1 当前 buffer 自动作为上下文agent-shell 最实用的一个特性是它能自动把当前 buffer 的内容作为上下文传给 agent。这意味着你在编辑某个文件时直接唤起 agent它已经知道你在看什么代码了。但这个“自动”是有边界的。默认情况下它传的是 buffer 全文对于小文件没问题对于几千行的文件就会浪费大量 token。我的做法是在配置里限制上下文范围(setq agent-shell-context-max-lines 200)这样只传光标附近的两百行既保留了足够的上下文又控制了开销。具体数值你可以根据自己的文件大小和模型上下文窗口调整。4.2 用 region 精确投喂比全文更精确的方式是选中一个 region 再唤起 agent。这个操作我绑定到了C-c a r(define-key global-map (kbd C-c a r) (lambda () (interactive) (agent-shell-send-region)))选中一段函数、一个报错信息、一段配置直接发给 agent它的注意力就完全集中在这段内容上。实测下来这种方式得到的回复质量明显高于全文投喂因为模型不会被无关代码干扰。4.3 项目级上下文让 agent 看到目录结构单个文件之外agent 还需要理解项目结构。ACP 协议支持 agent 主动请求读取文件所以理论上你不需要手动喂目录树。但实际使用中我发现在会话开始时主动给一次项目概览能显著提升后续交互的效率。我的做法是写了个小函数把项目根目录下的文件列表排除.git、node_modules这类拼成一段文本在会话初始化时发过去(defun my/agent-shell-project-overview () (interactive) (let ((files (directory-files-recursively (project-root (project-current)) .* nil (lambda (d) (string-match-p \\(?:\\.git\\|node_modules\\|__pycache__\\) d))))) (agent-shell-send-message (format 当前项目文件结构如下\n%s (string-join files \n)))))这段代码不复杂但效果很实在。agent 知道了项目里有哪些文件之后你让它“帮我看看测试文件里对应的用例”它就能直接定位不用你告诉它路径。4.4 上下文注入的取舍原则这里分享一条我踩坑总结出来的原则上下文不是越多越好而是越相关越好。我早期图省事把整个项目所有文件都塞给 agent结果两个问题一是 token 消耗飞快二是模型反而抓不住重点回复变得泛泛而谈。后来改成“当前文件 相关文件 项目结构概览”这个组合效果立刻好转。具体来说我的上下文策略是这样的编辑单个函数时只发 region。调试报错时发报错信息 出错文件的相关部分。做重构时发项目结构 涉及的两三个文件。做架构讨论时发项目结构 README 关键接口定义。这个分层策略让我在 token 成本和回复质量之间找到了一个平衡点。5. 多 agent 协作与工具调用的实战细节5.1 为什么需要多个 agent 实例单个 agent 能做的事有限。当你同时需要“一个负责写代码、一个负责审查、一个负责跑测试”时单实例就会顾此失彼。agent-shell 支持同时运行多个 agent 会话每个会话有独立的上下文和状态。我的配置里常驻三个会话coder负责写和改代码上下文偏向实现细节。reviewer负责审查上下文偏向代码规范和潜在问题。runner负责执行命令、跑测试、看输出。这三个会话通过不同的快捷键唤起互不干扰。写完之后切到 reviewer 让它审一遍审完切到 runner 跑测试整个流程非常顺。5.2 会话隔离与状态管理多会话的关键是隔离。每个 agent 会话有自己的 buffer、自己的上下文历史、自己的工具权限。agent-shell 默认会为每个会话创建独立的 buffer命名规则大概是*agent-shell-name*。我建议在配置里显式给每个会话命名方便切换(defun my/start-coder () (interactive) (agent-shell :name coder :agent-command your-coder-agent)) (defun my/start-reviewer () (interactive) (agent-shell :name reviewer :agent-command your-reviewer-agent))命名之后用M-x agent-shell-switch-to就能快速跳转。5.3 工具调用的权限控制这是安全层面最需要注意的地方。agent 能调用工具意味着它能读写文件、执行命令。如果 agent 判断失误可能会改错文件或者跑错命令。agent-shell 提供了工具调用的确认机制。默认情况下涉及写操作和命令执行时会弹出确认提示。我强烈建议不要关掉这个机制尤其是在你还不完全信任 agent 判断力的时候。如果你确实想减少确认频率可以配置白名单只对特定工具自动放行(setq agent-shell-auto-approve-tools (read_file list_directory search_code))读操作自动放行写操作和命令执行仍然需要确认。这个平衡点我觉得比较合理。5.4 多 agent 协作的一个真实案例上周我遇到一个需求把一个 Python 脚本的核心逻辑翻译成 Elisp并且要保证行为一致。我的操作流程是这样的在 coder 会话里把 Python 脚本的 region 发过去让它翻译。拿到 Elisp 代码后切到 reviewer 会话让它检查翻译是否忠实、有没有遗漏边界条件。reviewer 指出两处问题一个是异常处理没对应上一个是浮点精度处理有差异。回到 coder把 reviewer 的意见发过去让它修正。切到 runner让它跑一段测试代码验证行为。整个过程大概十分钟如果全靠手动在浏览器和编辑器之间来回切我估计要半小时以上而且容易漏掉细节。这就是多 agent 协作的实际价值。6. 用 Lisp 把 agent-shell 改造成自己的形状6.1 为什么 Lisp 在这里是优势而不是负担很多人对 Emacs 的 Lisp 配置有畏难情绪觉得语法古怪、生态封闭。但在 agent-shell 这个场景里Lisp 恰恰是最大的优势。原因很简单agent-shell 的所有行为都是可编程的。你想让 agent 在特定条件下自动触发、想把 agent 的输出自动格式化、想把 agent 和 org-mode 或者 magit 联动这些都能用 Lisp 实现而且不需要改 agent-shell 的源码只需要在配置里加 hook 和 advice。换成其他编辑器你大概率只能等插件作者提供接口或者去提 issue。而在 Emacs 里你自己就是插件作者。6.2 几个我实际写过的扩展扩展一自动把 agent 回复插入 org-mode 笔记(defun my/agent-reply-to-org () (interactive) (let ((reply (agent-shell-last-reply))) (with-current-buffer (find-file-noselect ~/notes/ai-log.org) (goto-char (point-max)) (insert (format \n* %s\n%s\n (format-time-string %Y-%m-%d %H:%M) reply)))))这个函数让我能把有价值的 agent 回复一键归档到笔记里方便以后检索。扩展二根据当前 major mode 自动选择 agent(defun my/agent-for-mode () (pcase major-mode (emacs-lisp-mode coder-elisp) (python-mode coder-python) (org-mode writer) (_ coder)))这样在不同类型的文件里唤起 agent会自动路由到最合适的会话省去了手动切换的麻烦。扩展三agent 输出自动语法高亮agent 返回的代码块默认是纯文本看起来不舒服。我加了一段 advice在插入回复时自动检测代码块并应用对应的高亮(defun my/highlight-agent-codeblocks (orig-fun rest args) (let ((result (apply orig-fun args))) (with-current-buffer (get-buffer *agent-shell*) (my/fontify-code-blocks)) result)) (advice-add agent-shell-insert-reply :around #my/highlight-agent-codeblocks)这段代码不算优雅但能用。核心思路就是拦截插入操作插入完之后再对 buffer 做一次处理。6.3 把 agent 接入 magit 工作流这是我最近在折腾的一个方向让 agent 参与代码审查流程。具体做法是在 magit 的 diff buffer 里加一个快捷键把当前 diff 发给 reviewer agent让它给出审查意见。(with-eval-after-load magit (define-key magit-diff-mode-map (kbd C-c a) (lambda () (interactive) (agent-shell-send-region-to reviewer))))这个功能还在打磨中但已经能用了。它把“提交前审查”这个动作从“手动复制 diff 到浏览器”变成了“在 magit 里按一个键”体验提升很明显。6.4 配置组织的建议随着扩展越来越多init.el会变得臃肿。我的建议是单独建一个agent-shell-config.el把所有跟 agent 相关的配置、函数、hook 都放进去然后在init.el里load它。;; init.el (load ~/.emacs.d/lisp/agent-shell-config.el)这样既保持了主配置的清爽又方便单独维护和版本控制。如果你用use-package也可以用:config块来组织但文件分离的方式在扩展多了之后更好管理。7. 稳定性、性能与那些没人告诉你的坑7.1 进程崩溃与自动重启agent 进程不是永远稳定的。我遇到过几次 agent 在长时间运行后无响应的情况表现是发消息没反应*agent-shell*buffer 里也没有新输出。排查下来原因通常是 agent 进程本身挂了或者 ACP 连接断了。agent-shell 有重连机制但不是万能的。我的做法是加一个 watchdog(defun my/agent-shell-watchdog () (when (and (agent-shell-live-p) (not (agent-shell-responsive-p))) (agent-shell-restart))) (run-with-idle-timer 60 t #my/agent-shell-watchdog)这个定时器每分钟检查一次 agent 是否响应不响应就重启。虽然粗暴但有效。7.2 大文件处理的性能问题前面提到过上下文限制这里再展开说一下性能。当你在一个几千行的文件里唤起 agent如果配置不当Emacs 会卡住好几秒因为它在序列化整个 buffer。解决办法有两个一是前面说的agent-shell-context-max-lines二是用agent-shell-send-region代替全文发送。我现在的习惯是只要文件超过 500 行就一律用 region 方式。7.3 编码与换行符的坑这个坑比较隐蔽。如果你的项目里有 Windows 换行符CRLF的文件agent 读到的内容和实际显示的可能不一致导致它给出的修改建议在应用后出现格式错乱。我的处理方式是在 agent 会话开始时显式声明换行符约定(setq agent-shell-default-eol-type lf)并且在发送 region 之前用delete-trailing-whitespace清理一下。这个习惯帮我避免了好几次莫名其妙的 diff 污染。7.4 会话历史膨胀长时间使用后agent 会话的历史会越来越长导致每次请求都要带上大量历史消息token 消耗和响应时间都会上升。agent-shell 提供了历史清理的接口我配置了一个快捷键在会话变得迟钝时手动清理(defun my/agent-shell-trim-history () (interactive) (agent-shell-trim-history :keep-last 20))保留最近 20 条消息既维持了基本的上下文连贯性又控制了开销。这个操作我大概每天做一两次。7.5 一个关于“信任边界”的经验最后说一个偏理念但很重要的点。agent 能读写文件、执行命令这意味着它有能力造成实际损害。我在早期过于信任 agent 的判断让它自动执行了一批命令结果其中一个命令删掉了一个我还没提交的临时文件。从那以后我给自己定了条规矩任何写操作和命令执行都必须经过我的确认。读操作可以放行因为读不会造成损害。这个边界一旦划清楚用起来反而更安心因为你知道最坏情况不会发生。8. 我现在的日常agent-shell 如何改变了我的工作节奏用了两个月之后我回头看自己的 Emacs 使用习惯变化其实挺大的。以前我打开 Emacs 主要是写代码和记笔记AI 是另一个窗口里的东西。现在 agent-shell 常驻在后台我写代码的时候会习惯性地让它先看一眼、提提意见遇到不熟悉的 API 会直接问它重构之前会先让它评估影响范围。AI 从一个“需要专门去用的工具”变成了“随手可及的第二双眼睛”。这个转变的关键不在于模型有多强而在于摩擦被降到了足够低。低到你不需要做心理建设就能用低到它成为你工作流里自然的一环。agent-shell 加上 ACP 这套架构本质上就是在做这件事把 AI 从外部服务变成编辑器的一部分。当然它现在还不完美。多会话管理有点笨重工具调用的确认机制偶尔会打断思路Lisp 扩展的调试成本也不低。但这些都是可以慢慢打磨的。对我来说方向是对的剩下的就是时间问题。如果你也在用 Emacs并且对 AI 辅助开发有兴趣我建议你至少花一个周末把 agent-shell 跑起来试试。不用一开始就搞多 agent、搞复杂扩展先让它能跟你对话、能读你当前的文件感受一下那种“AI 就在编辑器里”的体验。很多时候工具的价值不在于它多强大而在于它离你多近。