1. 为什么要在 Emacs 里折腾一个 AI 工作台我第一次在终端里跑起 agent-shell 的时候心里其实没抱太大期望。毕竟这些年各种 AI 编程工具轮番上阵从浏览器插件到独立 IDE用了一圈下来最深的感受是工具越换越勤思路反而越来越碎。代码在编辑器里对话在网页里上下文在剪贴板里来回切换的损耗比写代码本身还累。Emacs 用户大概都有这种执念——既然 Emacs 号称是“伪装成编辑器的操作系统”那 AI 能力为什么不能长在它里面而是要我跳出去agent-shell 这个项目解决的正是这个痛点。它把 AI agent 的交互能力直接嵌进 Emacs 的 buffer 体系里用 ACPAgent Client Protocol作为通信层让 AI 会话变成一个个可编辑、可保存、可版本管理的文本缓冲区。换句话说你不再是在“用”一个 AI 工具而是在“编辑”一段与 AI 的协作记录。这个定位听起来有点抽象但用起来之后你会发现它和传统聊天窗口的体验差异就像用 org-mode 记笔记和用记事本记笔记的差异一样大。这篇文章适合三类人看一是长期泡在 Emacs 里、想把手头的 AI 工作流收拢进来的老用户二是对 Lisp 配置有一定了解、愿意折腾但不想从零造轮子的中级玩家三是虽然不用 Emacs但想理解“AI 工作台”这个思路、看看能不能迁移到自己工具链里的开发者。我会从设计思路讲到具体配置从核心机制讲到踩坑记录尽量把每个选择背后的理由说清楚而不是只丢一段配置让你抄。需要提前说明的是agent-shell 本身还在快速演进我写下的这些经验基于我实际使用的版本和场景你在复现时可能会遇到接口微调。但底层的那套设计逻辑是稳定的理解了它你就能自己判断该怎么改。2. 整体设计思路为什么是 ACP为什么是 buffer2.1 把 AI 会话当成文本而不是当成窗口传统 AI 编程工具的交互模型是“窗口式”的你打开一个面板输入问题AI 流式返回你复制结果关掉面板。这个模型的问题在于会话是瞬态的、不可寻址的、难以复用的。你想回头找三天前那段关于某个函数重构的讨论只能靠翻聊天记录而聊天记录往往和代码是割裂的。agent-shell 的设计选择是把每一次 AI 会话落成一个 Emacs buffer。这个决定看似简单但它带来的连锁反应很关键。buffer 意味着你可以用 Emacs 的一切能力去操作它用occur搜索历史对话用narrow-to-region聚焦某一段用org-mode把会话导出成结构化笔记甚至用 git 对会话文件做版本管理。AI 的输出不再是“看完就没了”的东西而是变成了你知识库的一部分。这个思路和 ACP 协议是配套的。ACP 定义了一套客户端与 agent 之间的消息格式把“请求—响应—流式输出—工具调用”这些环节标准化。agent-shell 作为客户端负责把 ACP 消息渲染成 buffer 里的文本同时把你在 buffer 里的输入编码成 ACP 请求发出去。中间这层协议的存在意味着后端 agent 是可替换的——今天接这个模型明天换那个 agent只要它说 ACP前端体验就是一致的。2.2 为什么不用现成的 HTTP 直连有人会问直接调模型的 HTTP API 不就行了为什么要引入 ACP 这层我一开始也有这个疑问实际用下来才明白差别在哪。HTTP 直连的模型是“一问一答”你发一个请求等一个响应。但真正的 agent 工作流不是这样的。agent 可能需要多轮工具调用先读文件再搜索再执行命令再根据结果决定下一步。这些中间状态如果全靠你自己在客户端拼代码会迅速膨胀成一团乱麻。ACP 把这些状态机逻辑收进了协议层客户端只需要处理“收到一段流式文本就渲染”“收到工具调用请求就展示”这类简单事件。另一个原因是可替换性。HTTP 直连意味着你的配置和某个具体厂商的 API 格式绑死换模型就要改代码。ACP 把这一层抽象掉了agent-shell 的配置里你只需要指定启动哪个 agent 进程剩下的协议细节由双方协商。这个设计在长期维护上的价值用过被 API 变更搞崩配置的人都懂。2.3 Lisp 在这里扮演什么角色Emacs 的配置语言是 Emacs Lispagent-shell 的集成自然也是 Lisp 代码。但这里有个容易被忽略的点Lisp 不只是配置语言它是这个工作台的“胶水层”。你可以用 Lisp 写 hook在 AI 返回特定内容时触发动作可以用 Lisp 写函数把当前 buffer 的上下文自动塞进请求可以用 Lisp 把 AI 的输出解析后写入其他 buffer。举个例子我写了一个小函数当 agent-shell 的会话里出现代码块时按一个快捷键就能把代码块内容提取到临时 buffer 里单独编辑编辑完再塞回去。这种“AI 输出—人工干预—回填”的循环用其他工具做要么做不到要么要写一堆插件。在 Emacs 里它就是几十行 Lisp 的事。这也是为什么我说agent-shell 的价值不只是“在 Emacs 里聊天”而是“让 AI 会话变成可编程的对象”。3. 核心机制拆解会话、上下文与工具调用3.1 会话生命周期与 buffer 结构agent-shell 启动一个会话时会创建一个主 buffer通常命名为类似*agent-shell*的形式。这个 buffer 里发生的事情按时间顺序排列你的输入、agent 的流式回复、工具调用的请求与结果、错误信息。每一类内容用不同的 faceEmacs 的样式机制区分视觉上能一眼看出哪段是你说的话哪段是 AI 说的哪段是工具执行的输出。会话不是一次性的。你可以把 buffer 保存成文件下次用find-file打开agent-shell 会识别出这是历史会话并恢复上下文。这个能力在实际使用中比想象中重要。我经常把一次复杂的重构讨论保存下来过几天接着聊agent 能记得之前的决策不用我重新解释一遍背景。这里有个细节值得说会话文件的格式是纯文本不是某种二进制序列化。这意味着你可以用任何文本工具去处理它包括 diff、grep、sed。我有一次不小心让 agent 生成了错误的配置直接用git diff对比历史版本一眼就看出改错了哪里。这种“会话即文件”的设计是它区别于封闭式 AI 工具的根本。3.2 上下文注入的几种方式AI 要帮上忙前提是它知道你在干什么。agent-shell 提供了几种把上下文喂给 agent 的方式各有适用场景。最直接的是手动选中区域发送。你在代码 buffer 里选中一段函数调用 agent-shell 的发送命令选中的内容会作为请求的一部分发出去。这种方式精确但需要你主动操作。进阶一点的是自动注入当前文件或项目信息。你可以配置一个函数在每次发送请求前自动把当前 buffer 的文件名、光标位置附近的代码、甚至整个项目的目录结构附加到请求里。这个能力要慎用因为上下文越长模型的响应越慢、成本越高。我的做法是只注入文件名和光标所在函数的签名需要更多细节时再手动补充。还有一种是被动监听模式。你可以让 agent-shell 监听某个 buffer 的变化当文件被修改时自动通知 agent。这个模式适合“结对编程”场景agent 能实时看到你的改动并给出反馈。但它也容易造成干扰我一般只在专门的重构会话里开。3.3 工具调用的展示与确认agent 不只是聊天它还能调用工具——读文件、写文件、执行命令、搜索代码库。这些工具调用在 agent-shell 的 buffer 里会以结构化的形式展示调用了什么工具、传了什么参数、返回了什么结果。关键在于确认机制。默认情况下涉及写操作或命令执行的工具调用会暂停等你确认后才继续。这个设计非常重要因为 AI 的判断不是永远可靠让它未经审查就改你的文件或跑命令风险太大。确认界面会展示具体的操作内容你可以选择允许、拒绝或者修改参数后再允许。我踩过的一个坑是早期版本里确认提示不够醒目我习惯性地按了回车结果让 agent 执行了一条我没仔细看的命令。后来我改配置把确认提示做成了需要显式输入yes才通过的形式虽然麻烦一点但安全。这个经验分享出来是想说工具调用的便利性和安全性需要你自己权衡默认配置不一定适合你的风险偏好。4. 从零配置一个可用的 agent-shell 环境4.1 前置依赖与版本确认在动手之前先确认你的环境。Emacs 版本建议 28 以上因为 agent-shell 用到了一些较新的 buffer 和进程管理特性。我实测在 27 上也能跑但偶尔会遇到流式渲染的卡顿升级到 29 之后顺畅很多。除了 Emacs 本身你还需要一个支持 ACP 的 agent 后端。这个后端是一个独立的进程agent-shell 负责启动它并通过标准输入输出通信。后端的安装方式取决于你选哪个常见的是通过包管理器或直接从源码构建。我建议先用官方文档里推荐的默认后端跑通流程再考虑替换。Lisp 侧没有额外的包依赖agent-shell 是自包含的。但如果你想像我一样做深度定制建议同时装好use-package和straight.el或elpaca这类包管理器方便管理配置。4.2 最小可用配置下面是我实际使用的最小配置去掉了我个人的定制部分保留核心。你可以直接放进init.el或单独的配置文件里。(use-package agent-shell :ensure t :bind ((C-c a s . agent-shell-start) (C-c a i . agent-shell-send-input) (C-c a r . agent-shell-send-region)) :custom (agent-shell-backend-command your-agent-binary) (agent-shell-default-directory ~/projects) (agent-shell-stream-render-delay 0.05))这里几个参数值得解释。agent-shell-backend-command指向你的 agent 可执行文件路径具体填什么取决于你装的后端。agent-shell-default-directory是 agent 启动时的工作目录影响它能访问哪些文件建议设成你常用的项目根目录。agent-shell-stream-render-delay控制流式输出的渲染频率值太小会导致频繁重绘卡顿太大则看起来一顿一顿的0.05 秒是我试下来比较平衡的值。绑定按键这块我用了C-c a作为前缀避免和 Emacs 默认键位冲突。agent-shell-send-region是我用得最多的命令选中代码直接发比复制粘贴快得多。4.3 后端进程的启动与连接排查配置写好后第一次启动可能会遇到后端连不上的问题。排查思路是这样的先确认后端可执行文件路径正确在终端里手动跑一下看能不能正常启动然后看 agent-shell 的日志 buffer里面会记录它尝试启动进程的完整命令和输出。我遇到过一次启动失败日志显示后端进程立刻退出了。原因是后端需要的某个环境变量在我的 Emacs 启动环境里没有而终端里有。解决办法是在配置里显式设置环境变量或者用exec-path-from-shell这类包把 shell 环境同步进 Emacs。这个问题在 macOS 上尤其常见因为图形界面启动的 Emacs 不继承终端的环境变量。连接成功后你会在 buffer 里看到后端的握手信息通常包含版本号和可用工具列表。如果这一步卡住多半是协议版本不匹配检查一下 agent-shell 和后端是不是都更新到了兼容的版本。5. 实操把 agent-shell 接入日常开发流5.1 场景一让 AI 读懂当前函数再改最常见的用法是重构。假设光标停在一个函数里我想让 AI 帮我优化它。操作流程是选中整个函数按C-c a r发送然后在输入框里写清楚要求比如“这个函数嵌套太深帮我拆成几个小函数保持行为不变”。这里有个技巧不要只发函数本身把函数的调用点也一起发过去。AI 看到调用方式才能判断哪些参数是必需的、哪些可以内联。我一般会用mark-defun选中函数然后手动扩展选区把附近的调用也带上。多花几秒钟AI 给出的重构方案质量会明显不同。发送后agent 的回复会流式出现在 buffer 里。如果它调用了写文件的工具会先弹出确认。我建议第一次让它改的时候选择“展示 diff”而不是直接写入这样你能看到具体改了什么再决定。确认无误后再允许写入或者手动把 diff 应用到文件里。5.2 场景二跨文件搜索与理解另一个高频场景是理解陌生代码库。我会在 agent-shell 里直接问“这个项目里处理用户认证的逻辑在哪些文件”agent 会调用搜索工具把相关文件路径和关键代码片段列出来。这比我自己用grep再逐个打开文件快得多。这个场景下上下文注入的配置很关键。我配置了让 agent 启动时自动读取项目根目录的说明文件如果有的话以及package.json或Cargo.toml这类依赖清单。这样它一开始就知道项目用什么技术栈搜索时能更精准。需要注意的是agent 的搜索能力受限于它被允许访问的目录。如果你的项目有多个仓库记得在配置里把父目录加进允许列表否则它会告诉你“找不到文件”。5.3 场景三把会话导出成可复用的笔记前面提到会话是纯文本这就带来一个实用玩法把有价值的会话导出成 org 文件作为项目文档的一部分。我写了一个小函数把当前 agent-shell buffer 的内容按角色分段转成 org 的 headline 结构然后保存到项目的docs/ai-sessions/目录下。(defun my/export-agent-session-to-org () 把当前 agent-shell 会话导出为 org 文件。 (interactive) (let ((content (buffer-substring-no-properties (point-min) (point-max))) (filename (format docs/ai-sessions/%s.org (format-time-string %Y%m%d-%H%M%S)))) (with-temp-file filename (insert #TITLE: AI Session\n) (insert content)) (message 已导出到 %s filename)))这个函数很粗糙但够用。导出的文件可以进 git团队成员能看到某段代码是怎么讨论出来的。这比口头交接或者散落的聊天截图靠谱得多。6. 常见问题与排查实录6.1 流式输出卡顿或乱码这是最常见的问题表现是 AI 的回复一顿一顿地出现或者中文显示成方块。卡顿多半是渲染频率设置不当调大agent-shell-stream-render-delay试试。乱码则是编码问题检查你的 Emacs 是否默认用 UTF-8以及后端进程的输出编码是否一致。我在 Windows 上遇到过更麻烦的情况后端输出的换行符是\r\n而 agent-shell 按\n解析导致每行末尾多一个^M。解决办法是在配置里加一个输出过滤器把\r去掉。这个坑在跨平台使用时很典型值得留意。6.2 工具调用被拒绝后会话卡住有时候你拒绝了 agent 的某个工具调用但会话没有继续而是一直等待。这通常是协议层面的状态没有正确重置。我的处理方式是手动发送一个空输入或者中断信号让 agent 重新进入等待输入的状态。如果频繁出现检查后端版本早期版本在这块的处理确实不够健壮。6.3 上下文过长导致响应变慢当你注入的上下文太多模型的响应时间会显著增加甚至超出后端的处理上限。我的经验是给上下文设一个软上限比如只注入光标前后各 50 行超出部分截断并提示。这个逻辑可以用 Lisp 写在发送前的 hook 里自动处理不用每次手动控制。6.4 会话文件损坏或无法恢复会话文件是纯文本理论上不容易损坏但如果你在写入过程中强制退出 Emacs可能会留下不完整的文件。恢复时 agent-shell 会报解析错误。我的做法是定期用 git 提交会话目录出问题就回滚。另外养成用save-buffer显式保存的习惯不要依赖自动保存。问题现象可能原因排查方向流式输出卡顿渲染频率过高调大 stream-render-delay中文乱码编码不一致检查 UTF-8 设置工具调用后卡住协议状态未重置发送中断信号检查后端版本响应变慢上下文过长限制注入行数会话无法恢复文件写入不完整用 git 管理会话目录7. 我踩过的坑和几条实用建议第一个坑是关于确认机制的。前面提过我因为确认提示不够醒目而误执行了命令。后来我把所有写操作和命令执行的确认都改成了必须输入完整单词的形式虽然每次多打几个字但再也没有误操作过。如果你也在用 agent 做涉及文件系统的操作强烈建议把确认门槛调高。第二个坑是关于上下文注入的边界。我一开始图省事配置了自动注入整个项目目录树。结果 agent 每次响应都要先处理一大堆无关信息慢不说还经常被无关文件干扰给出不相关的建议。后来改成只注入当前文件和直接依赖质量立刻上来了。上下文不是越多越好精准比海量重要。第三个坑是关于会话管理的。我早期把所有会话都堆在一个目录里时间一长找东西很痛苦。后来改成按项目分目录文件名带上日期和简短描述再用consult或helm做模糊搜索效率高了很多。这个习惯建议你一开始就养成不然积累几百个会话后再整理会很痛苦。最后一个建议是关于 Lisp 定制的。agent-shell 的默认功能已经够用但它的真正威力在于你可以用 Lisp 把它改造成适合自己工作流的样子。不要一上来就写一大堆定制代码先用默认配置跑一段时间记录下哪些操作让你觉得别扭再针对性地写函数解决。我现在的配置里大部分定制都是用了几个月之后才加上的每一条都对应一个真实痛点而不是为了定制而定制。这套工作流我用了大半年最大的感受是AI 工具的价值不在于它多聪明而在于它是否无缝地融入了你原本的工作方式。agent-shell 把 AI 会话变成 Emacs buffer 这个设计看似只是形式上的变化但它让 AI 的输出变得可搜索、可编辑、可版本管理、可编程这才是它和普通聊天窗口的本质区别。如果你也是那种“不想离开编辑器”的人值得花一个周末把它配起来试试。