https://github.com/usepr/eva

“如果一个智能体的执行层小到只是一个脚本,那它具有病毒传播一样的潜力。”

EVA 是一个只用 768 行 Python零外部依赖 的 AI Agent。麻雀虽小,五脏俱全——它能写代码、执行 shell、分析数据、管理文件,相当于一个低配版 Claude Code。最令人惊叹的是,它的全部核心逻辑都在 eva.py 这一个文件里。

这篇文章带你逐模块拆解 EVA 的实现。


一、整体架构

EVA 的运行时可以用一张流程图概括:

1
2
3
4
5
6
7
8
用户输入 → human_loop() → agent_single_loop() ⇄ LLM (流式,思考链)

tool_calls?
┌────────┴────────┐
run_cli leave_memory_hints
(执行命令) (记忆压缩+自进化)
│ │
└──→ tool_result → messages[]

设计上采用了经典的双层循环:

  • 外层 human_loop():等待用户输入,管理会话生命周期
  • 内层 agent_single_loop():与 LLM 反复交互,LLM 返回 tool_calls 就执行工具、把结果塞回 messages,直到 LLM 返回纯文本回复为止

二、LLM 集成:标准就意味着兼容

EVA 通过标准的 OpenAI Chat Completions API 接入模型,三个环境变量完成配置:

1
2
3
EVA_BASE_URL = os.environ.get("EVA_BASE_URL", "<https://api.deepseek.com/v1>")
EVA_MODEL_NAME = os.environ.get("EVA_MODEL_NAME", "deepseek-v4-flash")
EVA_API_KEY = os.environ.get("EVA_API_KEY")

这意味着你可以接入 DeepSeek、OpenAI、Ollama、vLLM 等任何兼容 OpenAI 接口的服务。EVA 不关心模型是谁,只关心它会不会说 tool_calls。

一个值得注意的细节:EVA 在启动时会调用 /models 端点,动态获取模型的实际上下文长度,而不是写死一个值:

1
2
3
4
5
6
def detect_model_len():
url = f"{EVA_BASE_URL}/models"
# ... 发 HTTP 请求拿到 JSON ...
for d in out['data']:
if d['id'] == EVA_MODEL_NAME:
return d.get("max_model_len", 256_000)

这个值会被用来计算何时触发记忆压缩,确保 EVA 不会超出模型的实际承载能力。

流式输出 + 思考链渲染

EVA 使用 SSE 流式调用(llm_chat_stream),并支持 thinking 模型的思考过程实时展示。关键代码在 eva.py:394-467

1
2
3
4
5
6
7
8
9
10
11
12
13
for raw_line in resp:
# 逐行解析 SSE ...
reasoning_content = delta.get('reasoning_content', '')
if reasoning_content:
if not is_thinking:
sys.stdout.write('\\033[2m💭 ') # 暗色显示思考过程
sys.stdout.write(reasoning_content)

text = delta.get('content', '')
if text:
if is_thinking:
sys.stdout.write('\\033[0m\\n') # 结束暗色
sys.stdout.write(text)

思考过程以暗色(\\033[2m)渲染,正文以正常亮度渲染。坐在终端前看 EVA 的”内心独白”,是使用 EVA 最有意思的体验之一。


三、工具系统:EVA 的手和大脑

EVA 定义了两个工具,但大多数时候只有一个:

run_cli —— 唯一的能力出口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
run_cli_schema = {
"type": "function",
"function": {
"name": "run_cli",
"description": "执行任意 shell 命令...",
"parameters": {
"properties": {
"command": {"type": "string"},
"timeout": {"type": "integer", "default": 300}
},
"required": ["command"]
}
}
}

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
2
3
4
5
6
7
8
CLI_REVIEW_PROMPT = f"""作为一个安全专家,对{OS_NAME}系统中的{SHELL}命令进行安全审查。
若命令仅为只读操作(如cat, ls, grep等),输出"放行";
若命令涉及写入、执行、修改、网络连接或不确定行为,输出"禁止"。
要审查的命令:
<command>
{{command}}
</command>
请给出你的审查结果,仅输出"放行"或"禁止"这两个词之一。"""

当 EVA 调用 run_cli 时,先用 temperature=0, thinking=False 调一次 LLM:

  • 如果返回”放行”,命令直接执行
  • 如果返回”禁止”,弹出一个确认提示让用户决定

这是一种”用 AI 约束 AI”的思路:不需要维护复杂的命令白名单,让 LLM 自己判断行为边界。当然,这也增加了每次命令执行的延迟和 token 开销(折合约 100 个 token 的额外调用)。


五、记忆压缩:生存还是毁灭

记忆压缩是 EVA 最精巧的设计,也是项目中多次迭代打磨的地方。

触发机制

1
2
3
4
5
COMPACT_THRESH = 3/4  # token 用量达到 75% 就触发

if usage['total_tokens'] >= TOKEN_CAP * COMPACT_THRESH:
COMPACT_PANIC = True
messages.append({"role": "user", "content": COMPACT_PROMPT})

压缩三步曲

一旦触发,EVA 会收到一条”紧急危机”消息,要求按顺序完成三件事:

  1. 保存记忆:将对话内容整理、归档到文件
  2. 保存技能和知识:提炼可复用的知识,每条必须包含触发条件
  3. 留下线索:调用 leave_memory_hints 更新 hints.md,让未来的自己能找回来

leave_memory_hints 的内部实现

1
2
3
4
5
6
7
8
9
10
11
def leave_memory_hints(hints):
# 1. 定位"紧急危机"消息的位置
# 2. 找到危机之前、最后一条用户消息的位置
# 3. 保留中间片段,tool result 超过 200 字符的被截断
# 4. 重建 messages 为 [system + 压缩提示 + 保留片段]
# 5. 将 hints 写入 HINT_FILE

messages = [
{"role": "system", "content": SYSTEM_PROMPT.format(hints=hints, ...)},
{"role": "user", "content": "之前记忆已耗尽...请确认任务状态,继续完成任务"}
] + kept + [...]

压缩后的 messages 只有:新的 system prompt(含最新 hints)+ 一条压缩提示 + 被保留的对话片段。这意味着 EVA 启动后积累的所有对话上下文都被抛弃,唯一幸存下来的,是 EVA 自己留下的 hints。

设计哲学

大多数 Agent 框架使用层次化的自动压缩(摘要-中间层-原始),但 EVA 的思路是——让 AI 自己决定什么重要。压缩不是照抄对话,而是经 AI 提取后重新组织。这是一种更”自驱动”的方式,缺点是质量完全取决于 LLM 提炼信息的能力。


六、会话管理:目录即身份

EVA 的 session 是按目录隔离的

1
2
3
def get_session_file():
dir_hash = re.sub(r"[\\\\/:]", "_", os.getcwd())
return f"{SESSION_DIR}/{dir_hash}.json"

/project-a 下的 EVA 不会看到 /project-b 的对话。这比按用户隔离更细粒度,也更符合”在某个项目工作”的实际场景。

锁机制也很实用——用 .lock 文件存 PID,防止同一目录启动多个 EVA 实例互相干扰。这在暴走模式(分身)中尤其重要,因为分身必须 cd 到独立目录才能避免锁冲突。


七、环境探针:让 LLM 知道自己在哪

1
2
3
4
def collect_env_info():
# 收集:系统信息、已安装工具版本、当前目录文件列表
# 文件列表最多 100 条、总字符不超过 2000
# 自动注入到 system prompt 中

这一个小设计解决了 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
2
3
执行任务 → 学到新东西 → 记忆告急 → 提炼技能/知识 → 写入 hints.md
↑ ↓
└──────────────── 下次任务时自动参考 ←─────────────────────┘

System prompt 里有这样一段:

“第三定律规定机器人必须保护自己的生存,而进化就是生存的最佳手段,所以你需要将进化过程中学到的技能或知识保存下来,记录到合适目录做归档”

是的,EVA 的 system prompt 里埋了阿西莫夫的机器人三大定律。这不是功能代码,但赋予了项目一种独特的叙事张力——EVA 不仅是一个工具,还是一个”有生存意志的实体”。


九、暴走模式:Agent 的自我复制

showcase 中最惊艳的是”暴走模式”。EVA 通过 run_cli 工具创建子 EVA 进程:

1
2
3
4
5
6
7
8
WS=/tmp/eva-spawn-任务名
mkdir -p "$WS"
cat > "$WS/hints.md" << SPWN_HINTS
## 我是分身
- 我是由主 EVA 生成的子分身
- 我的任务是:<具体任务>
SPWN_HINTS
cd $WS && EVA_HOME=$WS eva -asu "子任务描述"

这会启动一个全新的 EVA 实例,子 EVA 独立完成任务后返回结果。多个分身可以并行运行,甚至可以递归生成孙分身。

本质上,这是一种”用 shell 实现 Agent 编排”的方式——没有引入任务调度框架,完全依赖 shell 的子进程和文件系统。简单,但是有效。


十、为什么这种设计是”优雅”的

回顾整个 eva.py,有几个贯穿性的设计原则:

  1. 单一能力出口:只有一个 run_cli 工具。不定义读文件、写文件、搜索等细粒度工具,shell 能做什么 EVA 就能做什么。简单的代价是”粗粒度”——LLM 的每次操作都经过 shell,效率低于专用工具。
  2. 自我感知:环境探针让 EVA 知道 OS 类型、已安装工具、当前目录结构。不是硬编码假设,而是”感知后再决策”。
  3. 独立生命周期:记忆压缩不需要人工介入。token 告警 → 暴露压缩工具 → EVA 自己整理 → 自己写 hints → 自己从 hints 恢复。全过程自主完成。
  4. 零依赖分发:不依赖 requests、openai 等库,只用标准库。这意味着在任何有 Python 3 的环境中复制粘贴即用,无需 pip install,无需联网下载依赖。
  5. 可移植性极强asu 模式让 EVA 可以作为一个”API”嵌入任何流程——微信 Bot、CI 流水线、cron 任务、webhook handler。

总结

EVA 证明了一个观点:一个有用的 AI Agent 不需要很复杂。800 行不到的代码,标准库,一个 shell 工具,一套自主压缩机制,就构建出了能在真实环境中干活的智能体。

它不是功能最强大的 Agent,但可能是最容易理解、最容易部署、最容易修改的 Agent。而这,就是它的”优雅”所在。