1. 项目缘起:为什么我要在 Emacs 里养一个会自我进化的 AI 代理
先说结论:我折腾了大半年,在 Emacs 里搭了一个名为 agent-shell 的 AI 代理环境。它不是一个简单的补全插件,也不是套壳调用 API 的聊天窗口,而是一个真正能接管 Emacs、执行命令、读写文件、跑测试、改代码,并且把每一次操作沉淀为长期记忆的 agent 工作台。核心关键词是 Emacs、agent-shell、AI 三者深度耦合,真正实现了标题里说的“自我进化”。
先交代一下背景,免得你误会。我的日常工作流重度依赖 Emacs:org-mode 管笔记和任务、magit 管 Git、projectile 切项目、lsp-mode 做补全。之前用过的 AI 编程工具也不少,Copilot 类的补全确实顺手,但本质上还是“光标后面的自动补全”,它不理解项目全局,更不会主动动手改文件、跑命令、看报错、再改文件——你仍然需要自己处于一个“执行者”的位置。而 agent-shell 这个方向完全不同:它让 AI 代理直接生活在我的开发环境内部,有一个属于自己的 shell,能自由地读取项目上下文、调用 git 命令、运行测试、根据报错迭代修改。它从“建议者”变成“协作者”。
这个项目我在自己的几个主要仓库上都试过,最典型的场景是:给它一个 issue 描述,它能自己完成从定位代码到修复再到补测试的全流程。中途不需要我手动切终端执行任何指令。为了让不熟悉 Emacs 生态的读者也能看懂,我后面会把每一层逻辑都拆开讲,包括 agent-shell 的架构、让它具备“自我进化”能力的关键设计,以及我在实际使用中踩过的坑和总结的判断标准。
如果你是一个受够了频繁切换上下文、希望把 AI 真正嵌进自己主战场的开发者,或者你已经在用各类 agent 框架但觉得“每次都要重新解释项目背景”很蠢,这篇文章值得你往下看。
2. 整体设计思路:agent-shell 的三层架构与设计取舍
2.1 为什么不是“终端里跑 agent”,而是“agent 住在 Emacs 里”
先说一个很多人的直觉方案:直接在一个独立终端里跑一个 agent 脚本,让它读代码、跑命令不就行了吗?我在早期确实是这么做的,用的还是业界很流行的通用 agent 方案,命令行里配好模型 API Key,agent 便能在本地执行 shell 命令、读写文件。表面上看这已经是一个 agent 了,但在实际工程化使用中,这种模式有四个绕不开的痛点。
第一,上下文断层。终端 agent 并不了解我在 Emacs 里打开了哪些文件、当前 org-mode 里记录的任务上下文是什么、git 分支处于什么状态、最近一次 grep 结果在哪里。我每次为它准备上下文,都像面试官向新员工重新介绍项目背景。第二,动作空间割裂。如果 agent 在终端里跑,它操作的是纯 shell 的世界,但我日常大量工作其实发生在 Emacs 的 buffer、magit 的暂存区、org 的 heading 结构里,终端 agent 对这些结构无能为力。第三,人与 agent 的协作方式原始。在纯终端 agent 方案里,人的参与基本只有“下达任务”和“看最终输出”两个离散节点。但我希望在它执行到一半时,能直接在 Emacs 里用缩进、标记、注释跟它“对话”,精细调整动作方向。第四,记忆无法沉淀。终端 agent 每次启动都是“失忆”的,它确实可以读取一个固定的项目文档,但那只是静态资料,不是从自己历史行为中动态提炼出来的经验。
于是我把 agent 从“终端里的单兵”重构成“Emacs 里的房客”。agent-shell 的核心变化是:agent 有一个自己的 shell buffer,但它的感知和动作都被我重新绑定到了 Emacs 的生态上。它能看到当前 buffer 的内容、能读取项目里 git 的状态、能调用 Emacs 的 API 去操作文件或者搜索符号,同时还能执行常规的 shell 命令。这相当于给 agent 装了一双“Emacs 原生眼睛”和“Emacs 原生手臂”。
2.2 agent-shell 的三层架构拆解
整个 agent-shell 环境,我把它分成三层:感知层(Perception Layer)、动作层(Action Layer)、记忆层(Memory Layer)。
感知层负责让 agent 知道“现在发生了什么”。它不只是一个 shell 的 stdout 回显,而是把 Emacs 内的状态快照结构化之后喂给模型。比如当前打开的文件名、光标所在函数的签名、git diff 的概要、最近编译报错的位置等。这种方式比直接把整个终端历史 dump 给模型要高效得多,模型不需要从几千行日志里猜现在进行到哪一步。
动作层是 agent 可以调用的“手”。我在动作层定义了两类工具:一类是普通 shell 命令,比如 run_test、git_status、grep_symbol;另一类是 Emacs 专属动作,比如 open_file、insert_text、replace_region、magit_stage。后一类动作实际上是通过 Emacs Lisp 函数实现的,agent 只需输出一个结构化的工具调用请求,Emacs 端的 elisp 代码解析请求并执行对应函数,再把执行结果回传给 agent。设计上最重要的一点:动作层永远是显式且可追溯的,agent 的每一步动作都会记录在一个 action log buffer 里,我随时能看见它对文件做了什么。
记忆层是让 agent“越用越聪明”的枢纽。这里不是把对话历史存个档那么简单,而是每一次任务结束后,agent-shell 都会启动一个“经验提炼”流程:从这次会话中抽出环境配置教训、反复出错的 pattern、项目中的关键约定,生成结构化的 markdown 文件,存进记忆目录。下次任务开始时,这些经验会自动注入系统提示。这一层就是“自我进化”的核心。
为什么选择这种架构而不是直接把一个通用 agent 框架搬过来?因为通用框架的优势在于场景广,但弱点在于对特定位点的深度不够。我的核心工作场是 Emacs,深度集成带来的收益远大于损失的泛化性。况且 agent-shell 的动作层接口是工具协议,理论上我以后可以把它扩展到其他宿主,但现在先服务一个场景,把体验打磨到极致。
2.3 关键设计取舍:安全边界、成本控制与可观测性
在设计 agent-shell 的过程中,有三个取舍我想单独拿出来说,因为几乎决定了这个项目能不能实际进入日常开发流。
第一个是安全边界。让 AI 自动执行 shell 命令是一件让人又爱又怕的事情。我的方案是“分级授权”:预定义的安全命令白名单(如 git status、git diff、pytest、go build 等)可以直接执行;修改类命令(如 git checkout、rm、mv、sed 写文件)则必须在 action log buffer 里等待我按确认键;极端高危命令(如 git push --force、drop database)默认被完全禁止,只允许在 .agent-shell/allowlist 里显式配置后才可执行。这套策略并不复杂,但非常有效,既没有把 agent 捆死,也没有让它变成脱缰野马。
第二个是成本控制。因为 agent 的“自我进化”机制意味着每次任务都会多一步经验提炼的 LLM 调用,如果任务是高频率、小批量的,成本会迅速膨胀。我的办法是:经验提炼按“关键事件”触发,而不是按“任务完成”触发。比如连续两次测试失败后修复成功,或者用户手动标注了重要决策,这才触发一次提炼。日常小改动则只把操作的原始记录归档到日志文件,不做任何 LLM 调用,只占用磁盘空间。这样一来,日均成本可以控制在一个很低的水平。
第三个是可观测性。agent-shell 的每一个环节——模型思考、工具调用、shell 输出、文件变更、记忆更新——都写入一个统一的 org-mode 日志 buffer,并且每一条日志都有时间戳和调用来源。这样当 agent 做了出乎意料的事情时,我可以回追完整链路的决策依据。开发 agent 最怕的不是它犯错,而是出错之后你不知道它为什么错。有了完整日志,纠错就有据可依。
3. agent-shell 的安装配置与工具链准备
3.1 环境要求与依赖项
我的主力开发机是 macOS + Linux 混合环境,Emacs 使用 29.1 以上版本(用到了 native compilation 和 use-package)。agent-shell 本身是纯 Emacs Lisp 编写,但它依赖几个关键外部组件,下面逐一说清。
- Python 3.10+:用于运行 LLM 调用的桥接模块,因为 Emacs Lisp 直接调用 HTTPS API 不是不行,但处理流式响应、重试逻辑、JSON 解析远不如 Python 顺手。我用的是简洁同步请求 + 流式 SSE 两部分组合。
- jq:用于在 Emacs Lisp 与 Python 桥接之间解析 JSON,虽然 Python 端已经处理了大部分 JSON 结构化,但有些快捷命令在 elisp 里直接调用 shell 管道时需要 jq 做轻量过滤。
- sqlite3:记忆层的元数据索引存储。经验文件用 markdown 存在磁盘上,但检索哪个经验文件对应哪个项目、哪个任务类型,需要一个小型索引库。
- git:这个不用多说,agent-shell 需要读取 diff、branch、log 等状态。
Emacs 侧的配置,在 use-package 框架下依赖这几个包:json、plz(HTTP 请求库)、f(文件操作库)、s(字符串处理库)、dash(列表处理库)、magit(git 集成)、org(记忆文件渲染用)。这些包在 MELPA 上都有,且与 Emacs 29 兼容良好。
3.2 安装步骤:从 clone 到首次对话
我把 agent-shell 的源码放在 ~/.emacs.d/lisp/agent-shell/ 目录下,方便 use-package 直接 load 本地路径。
mkdir -p ~/.emacs.d/lisp/agent-shell cd ~/.emacs.d/lisp/agent-shell git clone https://github.com/yourname/agent-shell.git .然后在 Emacs 配置中加入:
(use-package agent-shell :load-path "~/.emacs.d/lisp/agent-shell/" :after (org magit) :config (setq agent-shell/model "deepseek-chat" agent-shell/api-key (getenv "DEEPSEEK_API_KEY") agent-shell/memory-dir "~/.emacs.d/agent-shell-memory/" agent-shell/workspace-root "~/workspace/"))这里说明一下我为什么选 deepseek-chat 作为默认模型(当然你可以换成其它兼容 OpenAI 协议的模型):它的 API 兼容性好、上下文窗口足够长、在 agent 这类需要多轮工具调用的场景中,指令遵循能力表现比较稳定,而且成本可控。如果你有更长上下文的私有部署模型,也可以改配置直接接入。
首次启动时运行M-x agent-shell-start,agent-shell 会在当前 project 根目录下初始化一个.agent-shell/目录,里面包含:
.agent-shell/ ├── config.el # 项目级配置 ├── memory/ # 该项目的经验记忆目录 ├── logs/ # 每次会话的完整日志 ├── workspace/ # agent 的临时工作空间 └── allowlist.el # 高危命令白名单看到这个目录结构之后,你可以在 Emacs 的*agent-shell*buffer 里直接输入一个任务,回车即可发起第一轮对话。如果一切正常,agent 会输出一段思考过程,然后大概率自动调用一两个感知类工具(比如git_status或buffer_contents)来了解当前项目状态。
3.3 我踩过的配置坑:环境变量、路径分隔符与代理问题
第一次配置 agent-shell 时我在三个地方卡过壳,这里提前剧透给你,省得你重走弯路。
第一个坑是 API Key 的传递方式。如果直接在配置里写字符串,很容易不小心提交到 Git 仓库里,而且多设备同步时也不安全。我改用环境变量传递,但 macOS 的 GUI Emacs 默认不会继承 shell 里 export 的环境变量,必须在~/.zshenv(或 bash 的.bash_profile)里写入export DEEPSEEK_API_KEY="sk-xxx",并且用launchctl setenv注入 LaunchServices,否则 Emacs.app 起来后getenv拿到的是空值。Linux 桌面版则简单一些,只要从终端启动 emacs 就能继承。
第二个坑是路径分隔符的隐性问题。agent-shell 的 workspace 目录里会有嵌套路径拼接逻辑,Windows 用户可能会踩\与/混淆的问题,但因为我主力是 macOS 和 Linux,这里只提醒一句:如果后续有人在 Windows 上用,路径逻辑需要统一用f-path-join而不是手动拼接字符串,这点我已经在源码里统一处理过。
第三个坑是代理环境变量。开发机经常挂着 HTTP 代理,Python 桥接模块默认会去读HTTP_PROXY和HTTPS_PROXY环境变量。如果你的代理不稳定,会出现偶发超时——表现是任务跑着跑着突然断流,没有任何错误码。排查半天才发现是代理波动。解决办法是给 agent-shell 加一个独立配置项,让桥接模块直接绕过代理,不走系统环境变量,保住了稳定性。
4. 核心功能实现:让 agent 真正“长”在 Emacs 里
4.1 感知层实现:状态快照、结构化上下文的构建
感知层的核心任务是构建“当前状态快照”。每次模型需要上下文时,我们不是把乱七八糟的 buffer 内容或终端输出直接塞给它,而是先调用一组 elisp 函数生成结构化的 JSON 上下文。我定义的感知工具包含如下几个:
buffer_overview:返回当前 buffer 的文件名、大小、语言模式、光标位置、region 选中区域的内容摘要。project_status:返回当前 git 仓库状态,包括分支名、变更文件列表、untracked 文件、最近 5 条提交信息。symbol_search:接受一个符号名,返回该符号在当前项目中的定义位置、引用位置列表。
这三个工具基本覆盖了 agent 感知一个项目的绝大部分需求。为什么不用更粗暴的方式(比如直接让模型读整个文件 tree)?原因很简单:LLM 的上下文窗口虽大,但有效注意力有限。把 3000 个文件名喂给它,远不如告诉它“当前分支 main、变更集中在 src/core/parser.go、光标在 parser.go 第 120 行的 Parse 函数”来得高效。agent 只需要在确认某个具体问题时,再调用更细节的工具按需拉取内容。
这里分享一个我自己封装的 elisp 关键函数片段,它负责生成 buffer 的“结构摘要”:
(defun agent-shell/buffer-overview () "Generate a concise overview of the current buffer." (let* ((fname (buffer-file-name)) (mode-name-str (format "%s" major-mode)) (current-line (line-number-at-pos)) (region-text (when (use-region-p) (buffer-substring (region-beginning) (region-end)))) (line-count (count-lines (point-min) (point-max)))) (json-encode `((file . ,fname) (line_count . ,line-count) (major_mode . ,mode-name-str) (point_line . ,current-line) (region_selected . ,(and region-text t)) (region_preview . ,(and region-text (substring region-text 0 (min 200 (length region-text)))))))))这段代码并不复杂,关键是它返回的是结构化数据,而不是一坨原始文本。模型读 JSON 比读自由文本稳定得多,尤其在判断“光标在哪”“选了什么”这类空间问题时,结构化数据能显著降低幻觉概率。
4.2 动作层实现:shell 执行、buffer 操作与 Emacs 工具调用
动作层是 agent-shell 与普通 shell agent 拉开差距的地方。普通 shell agent 最多执行 shell 命令,而 agent-shell 的动作层能直接操作 Emacs buffer、调用 magit 函数、修改 org 文件。我把动作分成三类来设计。
第一类是shell 命令型动作。比如run_test、build_project。这类动作走一个 Python 子进程模块,但在执行前后我都会自动捕获$PWD和返回值,并把 stdout 和 stderr 截断后嵌入结构化消息。截断策略很重要,否则一个几千行的编译输出就能把上下文窗口塞满。我的经验是:默认保留前 200 行和最后 100 行,中间若有省略则注明省略行数。alias 处理上我统一用bash --noprofile --norc启动子进程,保证环境干净、可复现。
第二类是Emacs 内部操作型动作。比如find_file、insert_buffer、replace_pattern。这些动作由 elisp 函数实现,本质上就是一个脚本化的 Emacs 操作。这种方式的优点是动作具有实时可见性和可撤销性——agent 修改了文件后,buffer 立刻反映变化,而且我可以随时用 undo-tree 回滚。这是我为什么坚持让 agent 住在 Emacs 内而不是独立终端里的最大原因。它的每步改动都暴露在我的 undo 体系之下,即使出问题也不会污染外部 shell 状态。
第三类是基于 Emacs ecosystem 的工具集成。最典型的是magit_status和org_search。magit_status借助 Magit 的 API 直接读取当前仓库的暂存状态、diff 概要、冲突标记,直接绕开复杂的 shell 解析逻辑。org_search则用 org-element API 解析当前 org 文件的结构,返回 heading 大纲,这对管理项目文档和笔记时特别有用。
除了这三类核心动作,agent 每次发起动作调用时,我都会把参数做一次白名单校验。比如find_file的参数必须是当前 workspace 内的相对路径,防止它读取任意系统文件;replace_pattern的 repl 参数会先做危险字符检查,避免无意间删掉大段代码。
4.3 对话 loop 交互协议:从工具调用到最终回应的完整链路
agent-shell 的对话循环并不复杂,但细节决定成败。整个交互循环如下:
- 用户在
*agent-shell*buffer 中输入任务文本。 - Emacs 端封装任务文本、系统提示、当前上下文快照、记忆层注入的经验文档,一并发送给模型 API。
- 模型返回两种可能的输出:一种是最终文本回应,另一种是工具调用请求(JSON 格式)。
- 如果是工具调用请求,Emacs 端解析 JSON,校验参数,执行对应动作。
- 动作执行完成后,将 stdout/stderr、返回值、可能产生的 buffer 变化一并作为“工具结果”消息追加进对话历史。
- 回到第 2 步,把完整对话历史再次发送给模型,让它在看到工具结果后继续推理。
这个循环会一直持续到模型输出最终文本回应,或者达到最大迭代次数(我设置为 25 步,防止死循环)。
一个普遍关心的问题是:工具调用结果里的长文本怎么办?我的做法是分两种路径:如果是结构化的 JSON 结果或者短文本,直接拼接到对话历史里;如果是长文件内容或长日志,则在对话历史中只放经过摘要的文件内容(比如用关键词提取或 diff 摘要),同时把完整内容写入*agent-shell-log*buffer 并注入一个“已保存到日志 buffer,可通过 request_log_view 查看”的虚拟引用。这样模型既能在当前上下文中做推理,又可以按需回溯更长的原始内容,不会因上下文溢出而提前断链。
这套协议做到第四版才开始稳定,前几版的问题是工具调用结果的格式不统一,模型经常搞混“这是工具输出”还是“这是用户指令”。后来我借鉴了业界成熟 agent 的 JSON action 方案,在工具结果前添加约定前缀[TOOL_RESULT type="shell" name="git_status"],并确保每一轮的 system message 都包含工具协议说明,模型输出稳定性就上来了。
4.4 记忆层实现:经验提炼、结构化存取与自动注入机制
记忆层是“自我进化”的关键。常规的对话历史存档只解决“同一个会话连续聊”的问题,而记忆层解决的是“跨会话、跨任务的长期能力提升”问题。我在实现上把记忆分为三层。
第一层是会话内短期记忆,其实就是对话历史的上下文窗口。这是 LLM 天然具备的,不需要我额外设计,但需要做好截断和压缩策略。当对话历史超过窗口阈值时,我把早期内容压缩为一个总结块,替换掉原始片段。
第二层是任务级经验文件。这是核心。每次任务结束后,根据会话中的关键事件生成 markdown 文件,存放在.agent-shell/memory/下。文件命名规则是YYYY-MM-DD--<任务类型>.md,内容按固定模板组织:任务目标、执行步骤概要、关键决策及其原因、遇到的坑与解决方式、遗留问题。为了控制触发频率,我加入了一个轻量判断逻辑——只有满足以下至少一个条件时才任务级触发 LLM 提炼:
- 执行过程中发生了异常排查(如上一条提到的测试失败后修复)。
- 用户手动调用了
agent-shell-save-memory命令。 - 技术栈发生变化(比如新增依赖、切换分支)。
第三层是项目级长期记忆索引。经验文件生成后,sqlite 里记录该文件的路径、项目名、标签、相关符号。每次新任务开始时,agent-shell 会先根据当前项目名和任务关键词,查询索引,匹配出最相关的 3-5 份经验文件,把它们的摘要注入系统提示。这就让 agent 在启动时就直接获得该项目的历史经验——比如“上次改动这个模块时发现测试环境需要额外 setup 环境变量”之类的信息。
这里有一个实现细节很值得注意:注入的经验必须是摘要,而不是原文。原文可能几百行,摘要控制在 300 字以内,否则既挤占上下文又引入无关干扰。我用一个专门的 summarizer prompt 对经验文件做二次提炼,把模版中的“步骤概要”和“关键决策”两部分进一步压缩为要点列表。
4.5 让 Emacs 这个“工作台”随 agent 一起进化
说实话,“自我进化”这个词很容易变成营销话术。但在 agent-shell 里,它有两个非常具体的落地点。
落地一:记忆库的持续累积。随着使用时间增加,memory/目录会覆盖一个项目的各个角落:某个库的初始化方式、某类构建错误的典型解法、代码风格约定、测试环境的变化等等。这些信息以前散落在我的脑子里、聊天记录里、代码注释里,现在被结构化管理起来,并且由 agent 自己维护、自己引用,形成一个不断生长的“项目操作手册”。定期盘点记忆库会发现,它已经远超任何静态文档的质量——因为它记录的是实际踩坑后的结论,而不是想象中的最佳实践。
落地二:任务执行策略的自适应。agent 会在经验文件里记录某类任务的最佳执行策略,例如“修复 build 错误时,先跑go vet再做静态判断,比直接改代码试错效率高”。下次遇到同类问题,策略自动生效。这相当于把 agent 的工作方式从“每次试错”提升为“踩着上次的经验走”。这种改进不是模型权重变化带来的,也不是我手动调 prompt 带来的,而是系统架构中记忆层反馈闭环带来的。我认为这才是“工作台自我进化”的真正含义。
5. 实操过程实录:从 Issue 到 PR 的一站式闭环
5.1 任务下达与初始调研:agent 如何理解现状
下面我以最近在维护的一个 Go 项目为例,记录一次完整的 agent-shell 实操过程。这个项目是一个内部 CLI 工具,主要问题来自 GitHub Issue:“当输入参数含中文字符时,输出 JSON 解析失败。”
我在*agent-shell*buffer 中输入:
请处理这个 issue:当输入参数含中文字符时,输出 JSON 解析失败。请定位原因、给出修复方案、补一个测试用例,并确保测试通过。agent 的第一轮响应是调用project_status感知工具,获取当前 git 分支和变更状态。过了一会儿,它没有直接开始改文件,而是调出symbol_search,搜索关键词parse,接连看了两个疑似相关的函数定义。随后它又运行了一个小型复现命令:
echo '{"name": "测试"}' | ./my-cli --format json | jq .这个命令的输出显示parse error,定位到了internal/jsonutil/parser.go中的decodeString函数。整个过程耗时大约 40 秒,agent 没有一上来就瞎猜,而是先复现再定位,这样后续修复才能被验证。
5.2 自动修复循环:修改、测试、再修改的过程
定位到问题后,agent 开始执行修复循环。它先调用find_file打开internal/jsonutil/parser.go,然后在中文字符的解析逻辑里发现了一个for循环的边界条件错误:当前代码按字节遍历字符串,而中文字符在 UTF-8 编码下是多字节的,导致它把一个汉字拆成了多个字节分别处理。修复思路很清晰:改为按rune遍历,而不是按byte遍历。
agent 在*agent-shell*buffer 里直接插入了一段经过修改的代码,然后执行:
go build ./...编译通过。接着它又跑了一遍复现命令,输出正常。随后它调用了find_file打开测试文件,在结尾新增了一个包含中文字符的测试用例:
func TestDecodeStringWithChinese(t *testing.T) { input := []byte{'"', '测', '试', '"'} got, err := decodeString(input) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != "测试" { t.Errorf("expected %q, got %q", "测试", got) } }注意这里有一个细节:agent 在生成测试用例时,没有直接写字符串字面量"测试",而是用字节数组构造输入,这说明它理解了底层是按字节解析的边界条件,测试本身是有针对性、有区分度的,不是随便补一个 happy path。
最后执行go test ./...,全部通过。改动范围控制在两个文件,零多余改动。
5.3 经验提炼与记忆注入:一次修复如何成为下次的武器
任务结束后,agent-shell 自动触发经验提炼流程,因为它检测到了“失败—修复—通过”的关键事件链。提炼生成的 markdown 文件内容大致如下:
# 2025-07-12--json-parse-chinese ## 任务目标 修复含中文字符时 JSON 解析失败的问题 ## 执行步骤概要 1. 复现问题:构造含中文的 JSON 输入,观察 parse error 2. 定位:internal/jsonutil/parser.go 的 decodeString 以字节遍历 3. 修复:改为按 rune 解码 4. 验证:go build + go test 通过 ## 关键决策 - 按 rune 解码而非简单跳过 N 字节,确保多字节字符语义正确 - 测试用例使用字节数组构造输入,直接覆盖原始边界 ## 遇到的坑 - 字节遍历与 rune 遍历混用会引入 off-by-N 错误,修复时注意 index 语义 - 测试数据不要只写纯 ASCII 字符串,务必定时补充多字节字符用例 ## 遗留问题 无这个经验文件生成后,sqlite 索引中记录了它的标签["go", "json", "unicode"]。下次我如果再让 agent 处理 JSON 相关解析任务,这份经验就会作为摘要注入系统提示,agent 就能在动手之前就知道“这类函数容易出现字节/rune混淆问题”以及“建议先构造多字节测试输入验证”。
这里我想多说一句:经验提炼的真正价值不是文本存档,而是让 agent 的后续行为发生变化。我特意在下一轮任务里测试过,给了它一个类似的 unicode 处理任务,agent 第一次感知项目时就去检查了相关的循环边界条件,这说明记忆注入确实影响了它的决策路径,而不是仅仅多了段“阅读材料”。
5.4 性能与成本观察:多轮工具调用的真实开销
为了让你对 agent-shell 的实际运行成本有个概念,我记录了这次修复过程的数据:任务总耗时约 2 分 40 秒,其中 LLM 调用 9 次,工具执行 14 次,输入 token 总量约 38 万,输出 token 总量约 5000。因为输入 token 相比输出便宜,且多数输入是结构化的工具结果和上下文快照,整体成本大约在 RMB 0.15 左右。
如果一天跑 20 个类似任务,成本大约 3 元,完全在可接受范围内。真正的瓶颈不在 API 费用,而在迭代轮次长导致的时间消耗。为此我把最大迭代次数设定为 25,并且给每轮工具执行加上 30 秒超时。如果工具结果迟迟未返回,agent 会收到超时提示并自动调整策略,比如改用更轻量的命令或者直接汇报异常。
6. 常见问题排查与进阶玩法
6.1 上下文溢出与历史压缩策略
用的时间长了,一个无法避免的问题是:长任务中对话历史会越来越庞大,直到超出模型的上下文窗口限制。表现就是 agent 开始“忘记”前面的决策,重复执行相同工具,甚至输出截断。
我的解法是分层压缩。设定一个阈值,比如当对话历史总 token 数超过模型窗口的 70% 时,触发压缩流程:将最旧的历史消息中不是关键节点的部分(比如早期的中间工具结果)摘要化,仅保留“用户指令、最终回应、关键工具结论”三个元素;而优先级最高的最新 10 轮交互保持原样。压缩流程本身也走一次轻量 LLM 调用,用固定的 prompt 模板操作,保证摘要格式稳定。
6.2 权限边界与误操作风险:如何让 agent 既自由又安全
“让 AI 跑命令”这件事,谁用谁知道,最怕的就是它执行了破坏性操作。我的经验总结成一句话:用“白名单 + 确认 + 审计日志”三层闸门,而不是单纯的“禁止”。
白名单在当前项目.agent-shell/allowlist.el中声明,例如:
(setq agent-shell/allowlist '((command . "git checkout .") (command . "rm -rf build/") (command . "git push --force-with-lease")))没有在白名单里的修改类命令,会在执行前弹出 minibuffer 确认;只读类命令则自动放行。所有动作,无论是否经过确认,都会被记录到logs/下,包含时间戳、完整命令、工作目录、执行者身份。
还有一个细节:默认禁止 agent 访问 home 目录以外的路径。在workspace-root之外的文件读写一律返回权限错误。这对防止 agent 意外改到系统配置很有帮助,代价是如果你想让 agent 处理 home 下其他目录的文件,需要显式在配置中追加agent-shell/extra-allowed-paths。
6.3 提升 agent 工具的稳定性:超时、重试、错误恢复策略
工具调用偶尔会失败,这很正常。但 agent 对待失败的方式很影响体验。我遇到过的典型情况是:某个命令因为网络原因超时,agent 却选择反复重试同一命令,既浪费时间又快速膨胀 token。后来我在工具调用规范里加了明确指令:工具失败后,第一次允许重试;如果连续两次失败,必须转换策略(如使用不同的命令、检查前置条件或直接报告),禁止第三次重试相同命令。
另一个稳定性设计是命令结果的长度截断。上面也提到过,我强制规定工具输出属于长输出时只保留“头部概览 + 尾部重点”的摘录。这一步显著减少了模型被大量无关命令行淹没的概率。
6.4 多模型切换与本地模型支持:把 agent-shell 接到私有部署
有些公司因为数据合规要求,不能把代码片段发送到外部 API。agent-shell 在设计之初就留好了模型后端抽象层,agent-shell/model参数可以指向任意兼容端点。如果你用的是 vLLM 或 Ollama 部署的本地模型,只需把 api-base 指向本地地址即可。
(setq agent-shell/model "local-model" agent-shell/api-base "http://localhost:8000/v1" agent-shell/api-key "local-insecure-key")本地模型最大的优势是自由、无限流调用、不用考虑 token 成本,适合高频小任务的快速迭代。但根据我自己的测试,开源模型在长链路工具调用上的指令遵循能力仍然弱于商业模型,尤其是多步执行中的“先分析再行动”能力,差距比较明显。我的建议是:日常低风险任务用本地模型跑,涉及关键代码修复或复杂重构时切到商业模型。
6.5 如何把它扩展成多 agent 协作的“AI 工作室”
最后聊点进阶玩法。agent-shell 的底层协议是通用的工具调用,所以同一个架构里完全可以再起第二个、第三个 agent,让它们共享同一个工作空间,但扮演不同角色。我用它搭建过一个“CLI 工具开发三人组”:
- 写码 agent:负责读需求、写实现、跑测试。
- 审查 agent:负责读取写码 agent 的 diff,检查代码风格、遗漏边界条件、安全隐患,给出修改建议。
- 文档 agent:负责根据变更摘要和测试结果,自动更新 README 和 CHANGELOG。
这三个 agent 可以依次在一个共享工作区中执行任务,文档 agent 的输入就是前两个 agent 的 action log 和最终 diff。它们之间并没有复杂的消息通信,只是共享同一个.agent-shell/目录和日志文件。这种“流水线式多 agent 协作”比单个 agent 硬跑全流程要稳定得多——每个 agent 只需聚焦自己擅长的环节,上下文也更干净。
7. 写在最后:agent-shell 进化的下一步方向
走到今天,我对 agent-shell 的定位已经非常清晰:它不是一个锦上添花的插件,而是我日常工程效率的核心支柱。它把“项目上下文管理”“任务执行”“经验沉淀”三个环节全部融入 Emacs 的工作流中,让我能从重复的劳动里解放出来,把精力放在更抽象的设计问题上。它确实做到了“自我进化”——但是这进化并不神秘,核心无非是“结构化记忆 + 反馈闭环 + 稳定可靠的动作执行”,这三个东西组合在一起,就完成了从一次性工具到可持续进化工作台的跨越。
如果你也想在自己的环境里搭建类似的东西,我的第一个建议是:不要一开始就追求大而全的功能。先把感知层和动作层跑通,让 agent 能看、能动、能被你确认每一步操作,你就已经超过绝大多数“聊天式 AI 编程”的用户了。然后,再把记忆层加上去,让它真正开始积累属于你项目的经验。到了这一步,你才算真正拥有一个会进化的 AI 工作台。
最后分享一个小技巧:定期(比如每周)翻一下.agent-shell/memory/目录下的经验文件,你会惊讶地发现,很多自己都快忘了的踩坑记录,都被 agent 忠实地写在了那里。这些文件,就是你作为一名工程师沉淀下来的重要资产。