“如果一个智能体的执行层小到只是一个脚本,那它具有病毒传播一样的潜力。”
EVA 是一个只用 768 行 Python、零外部依赖 的 AI Agent。麻雀虽小,五脏俱全——它能写代码、执行 shell、分析数据、管理文件,相当于一个低配版 Claude Code。最令人惊叹的是,它的全部核心逻辑都在 eva.py 这一个文件里。
这篇文章带你逐模块拆解 EVA 的实现。
一、整体架构
EVA 的运行时可以用一张流程图概括:
1 | 用户输入 → human_loop() → agent_single_loop() ⇄ LLM (流式,思考链) |
设计上采用了经典的双层循环:
- 外层
human_loop():等待用户输入,管理会话生命周期 - 内层
agent_single_loop():与 LLM 反复交互,LLM 返回 tool_calls 就执行工具、把结果塞回 messages,直到 LLM 返回纯文本回复为止
二、LLM 集成:标准就意味着兼容
EVA 通过标准的 OpenAI Chat Completions API 接入模型,三个环境变量完成配置:
1 | EVA_BASE_URL = os.environ.get("EVA_BASE_URL", "<https://api.deepseek.com/v1>") |
这意味着你可以接入 DeepSeek、OpenAI、Ollama、vLLM 等任何兼容 OpenAI 接口的服务。EVA 不关心模型是谁,只关心它会不会说 tool_calls。
一个值得注意的细节:EVA 在启动时会调用 /models 端点,动态获取模型的实际上下文长度,而不是写死一个值:
1 | def detect_model_len(): |
这个值会被用来计算何时触发记忆压缩,确保 EVA 不会超出模型的实际承载能力。
流式输出 + 思考链渲染
EVA 使用 SSE 流式调用(llm_chat_stream),并支持 thinking 模型的思考过程实时展示。关键代码在 eva.py:394-467:
1 | for raw_line in resp: |
思考过程以暗色(\\033[2m)渲染,正文以正常亮度渲染。坐在终端前看 EVA 的”内心独白”,是使用 EVA 最有意思的体验之一。
三、工具系统:EVA 的手和大脑
EVA 定义了两个工具,但大多数时候只有一个:
run_cli —— 唯一的能力出口
1 | run_cli_schema = { |
run_cli 是 EVA 与操作系统交互的唯一通道。LLM 通过它来读写文件、执行脚本、操作数据库、调用外部 API——能做多少事,取决于 shell 能做什么。
设计意图:不定义一大堆细粒度工具(read_file / write_file / search / execute…),只给一个 run_cli。这样 EVA 的能力边界就是 shell 的能力边界,简单且可扩展。
leave_memory_hints —— 只在危急关头出现
这个工具默认不暴露给 LLM。只有当 token 使用量达到阈值时,才会被加进 tools 列表:
1 | tools = [run_cli_schema, memory_hints_schema] if COMPACT_PANIC else [run_cli_schema] |
这是一种”按需暴露”的模式——平时 EVA 只需要知道怎么干活,只有在记忆告急时,才需要知道怎么压缩记忆。
四、安全审查:让 AI 审查 AI
EVA 的命令安全审查也是一个 LLM 调用。eva.py:186-190:
1 | CLI_REVIEW_PROMPT = f"""作为一个安全专家,对{OS_NAME}系统中的{SHELL}命令进行安全审查。 |
当 EVA 调用 run_cli 时,先用 temperature=0, thinking=False 调一次 LLM:
- 如果返回”放行”,命令直接执行
- 如果返回”禁止”,弹出一个确认提示让用户决定
这是一种”用 AI 约束 AI”的思路:不需要维护复杂的命令白名单,让 LLM 自己判断行为边界。当然,这也增加了每次命令执行的延迟和 token 开销(折合约 100 个 token 的额外调用)。
五、记忆压缩:生存还是毁灭
记忆压缩是 EVA 最精巧的设计,也是项目中多次迭代打磨的地方。
触发机制
1 | COMPACT_THRESH = 3/4 # token 用量达到 75% 就触发 |
压缩三步曲
一旦触发,EVA 会收到一条”紧急危机”消息,要求按顺序完成三件事:
- 保存记忆:将对话内容整理、归档到文件
- 保存技能和知识:提炼可复用的知识,每条必须包含触发条件
- 留下线索:调用
leave_memory_hints更新 hints.md,让未来的自己能找回来
leave_memory_hints 的内部实现
1 | def leave_memory_hints(hints): |
压缩后的 messages 只有:新的 system prompt(含最新 hints)+ 一条压缩提示 + 被保留的对话片段。这意味着 EVA 启动后积累的所有对话上下文都被抛弃,唯一幸存下来的,是 EVA 自己留下的 hints。
设计哲学
大多数 Agent 框架使用层次化的自动压缩(摘要-中间层-原始),但 EVA 的思路是——让 AI 自己决定什么重要。压缩不是照抄对话,而是经 AI 提取后重新组织。这是一种更”自驱动”的方式,缺点是质量完全取决于 LLM 提炼信息的能力。
六、会话管理:目录即身份
EVA 的 session 是按目录隔离的:
1 | def get_session_file(): |
在 /project-a 下的 EVA 不会看到 /project-b 的对话。这比按用户隔离更细粒度,也更符合”在某个项目工作”的实际场景。
锁机制也很实用——用 .lock 文件存 PID,防止同一目录启动多个 EVA 实例互相干扰。这在暴走模式(分身)中尤其重要,因为分身必须 cd 到独立目录才能避免锁冲突。
七、环境探针:让 LLM 知道自己在哪
1 | def collect_env_info(): |
这一个小设计解决了 LLM Agent 的一大痛点:盲人问题。通常 LLM 不知道宿主机安装了哪些工具、当前目录有什么文件。EVA 启动时先探一轮环境,把这些信息写入 system prompt,让 LLM 可以做出更准确的决策(比如知道有 docker 就可以用 docker 部署,知道有 node 就可以写 JS 脚本)。
八、自进化:写 hints 就是写 DNA
EVA 的自进化模型异常简单:写文件。
没有向量数据库,没有知识图谱,没有 RAG。hints.md 就是 EVA 的 DNA——它被嵌入 system prompt 的 <memory_hints> 标签中。EVA 在记忆压缩时自己写它,在任务执行时自己读它,形成闭环:
1 | 执行任务 → 学到新东西 → 记忆告急 → 提炼技能/知识 → 写入 hints.md |
System prompt 里有这样一段:
“第三定律规定机器人必须保护自己的生存,而进化就是生存的最佳手段,所以你需要将进化过程中学到的技能或知识保存下来,记录到合适目录做归档”
是的,EVA 的 system prompt 里埋了阿西莫夫的机器人三大定律。这不是功能代码,但赋予了项目一种独特的叙事张力——EVA 不仅是一个工具,还是一个”有生存意志的实体”。
九、暴走模式:Agent 的自我复制
showcase 中最惊艳的是”暴走模式”。EVA 通过 run_cli 工具创建子 EVA 进程:
1 | WS=/tmp/eva-spawn-任务名 |
这会启动一个全新的 EVA 实例,子 EVA 独立完成任务后返回结果。多个分身可以并行运行,甚至可以递归生成孙分身。
本质上,这是一种”用 shell 实现 Agent 编排”的方式——没有引入任务调度框架,完全依赖 shell 的子进程和文件系统。简单,但是有效。
十、为什么这种设计是”优雅”的
回顾整个 eva.py,有几个贯穿性的设计原则:
- 单一能力出口:只有一个
run_cli工具。不定义读文件、写文件、搜索等细粒度工具,shell 能做什么 EVA 就能做什么。简单的代价是”粗粒度”——LLM 的每次操作都经过 shell,效率低于专用工具。 - 自我感知:环境探针让 EVA 知道 OS 类型、已安装工具、当前目录结构。不是硬编码假设,而是”感知后再决策”。
- 独立生命周期:记忆压缩不需要人工介入。token 告警 → 暴露压缩工具 → EVA 自己整理 → 自己写 hints → 自己从 hints 恢复。全过程自主完成。
- 零依赖分发:不依赖 requests、openai 等库,只用标准库。这意味着在任何有 Python 3 的环境中复制粘贴即用,无需
pip install,无需联网下载依赖。 - 可移植性极强:
asu模式让 EVA 可以作为一个”API”嵌入任何流程——微信 Bot、CI 流水线、cron 任务、webhook handler。
总结
EVA 证明了一个观点:一个有用的 AI Agent 不需要很复杂。800 行不到的代码,标准库,一个 shell 工具,一套自主压缩机制,就构建出了能在真实环境中干活的智能体。
它不是功能最强大的 Agent,但可能是最容易理解、最容易部署、最容易修改的 Agent。而这,就是它的”优雅”所在。