Agent 架构文档 · Markdown
OpenHands Agent 架构全景讲解
以 Agent 的生命脉络 为主线,讲清楚 OpenHands 这套"开源 autonomous coding agent 平台"是怎么把一个 LLM 包成一个能自己写代码、跑 shell、用浏览器、改 PR 的开发者。 所有概念(AgentController / Agent / Action / Observation / Event Stream /
以 Agent 的生命脉络 为主线,讲清楚 OpenHands 这套"开源 autonomous coding agent 平台"是怎么把一个 LLM 包成一个能自己写代码、跑 shell、用浏览器、改 PR 的开发者。 所有概念(AgentController / Agent / Action / Observation / Event Stream / Runtime / CodeAct / Microagents / Condenser)都从"它支撑了 Agent 的哪个能力"这个角度切入, 而不是按目录清单堆叠。
适合:第一次读 OpenHands 源码 / 想把它当 SDK 集成进自己的产品 / 想拿它和别家 agent 框架对位时心里有数。
2026-06-08 官方复核补丁:GitHub / PyPI
openhands-ai当前复核到1.7.0;openhands-sdk与openhands-agent-server当前复核到1.26.0。本文主体仍以 v0.55 经典架构 + v1 SDK 迁移说明为主,当前包名/版本以本条和官方发布为准。
#目录
- 写在前面:项目身份与核心矛盾 0.5. 一张图看懂:OpenHands = Model + Harness
- Agent 是什么 —— Agent 抽象类 + Microagents + System Prompt 三件套
- Agent 怎么活起来 —— CLI / WebUI / GitHub Action / Cloud 四种触发
- Agent 怎么思考 —— AgentController step loop + Event Stream + CodeAct
- Agent 怎么行动 —— Action / Observation 协议 + Sandbox Runtime + Browser
- Agent 怎么记忆 —— Event History + Condenser + Microagents 触发性记忆
- Agent 怎么开口 —— WebUI 流式 / CLI 输出 / 多端复用同一份 EventStream
- Agent 的外脑 —— litellm 抽象 + 模型路由 + cost tracking
- 关键设计权衡(架构师视角)
- 附录:组件地图、关键概念、源码索引
#零、写在前面:项目身份与核心矛盾
#项目消歧
"OpenHands" 这个名字下面藏着一段简短但容易混淆的历史,先把名字理清:
- OpenHands = 前身 OpenDevin,2024 年 3 月起步,2024 年 9 月正式更名为 OpenHands。
- 不是 Cognition AI 的 Devin(闭源、SaaS、Cognition 公司维护)。
- 维护方:All Hands AI 公司(同名公司,由 OpenDevin 项目核心贡献者成立)。
- 官方仓库:https://github.com/All-Hands-AI/OpenHands(同时镜像到
OpenHands/OpenHands)。 - 官方文档站:https://docs.all-hands.dev/(已重定向到
docs.openhands.dev)。 - 官方云服务:https://app.all-hands.dev/(OpenHands Cloud)。
- 论文:OpenHands: An Open Platform for AI Software Developers as Generalist Agents, ICLR 2025(arXiv:2407.16741)。
- 协议:MIT;主体语言:Python(agent / runtime)+ TypeScript(前端 React)。
它解决的事情,一句话讲清楚:
"给我一个能像人类开发者一样写代码、跑命令、用浏览器、改 PR 的开源 autonomous agent。 不是套了个 LLM 的 IDE 插件,而是一个自己开 shell、自己 git push 的全自动开发者。"
历史上 2024 年那波"autonomous Devin 复刻热潮"里,OpenHands(当时叫 OpenDevin)是 最早跑通端到端 SWE-bench Verified、并把架构开源开放贡献的项目,因此积累了非常厚的 社区基础(论文里写 188+ 贡献者;截至 2026-05 主仓库 6k+ commits、100+ release,最新 版本 1.7.0)。它的 SWE-bench Verified 成绩长期处于开源 agent 的第一梯队 (CodeAct 框架下 Python 子集 79.3% bug 修复率,论文 ICLR 2025)。
#三条核心矛盾
| 矛盾 | OpenHands 的回答 |
|---|---|
| 想让 agent "什么都能干",但 LLM tool_call 表达能力有限 | CodeActAgent:让模型直接写 Python 代码作为 action,命令/编辑/浏览器全统一为代码语义,绕开 tool schema 的表达瓶颈 |
| 多端(CLI / WebUI / GitHub / Cloud)共用一套 agent,又要解耦 | Event Stream 作为唯一总线:所有 Action / Observation 都是 typed event,每端只是 event 的不同消费者 |
| 本地、云、SaaS 三种执行场景都要 sandbox | Runtime 抽象:Docker / E2B / Daytona / Modal / Local / Remote 全是同一个 Action Execution Server 接口的实现,agent 完全不感知 |
后面每一节都会反复回到这三条矛盾,看具体的设计是怎么回应的。
#跟 Claude Code / Devin / Cursor / Aider 的差异(一句话)
| 对比项 | OpenHands |
|---|---|
| Claude Code | OpenHands 提供完整 web UI + 远端 sandbox + 多端,CC 是 CLI-first;OpenHands 默认让模型写 Python 代码(CodeAct),CC 走标准 tool_call |
| Cognition Devin | OpenHands 是 Devin 的开源对标,架构透明、可自部署、可改 agent;Devin 闭源 SaaS |
| Cursor / Windsurf | OpenHands 不绑 IDE,跑的是 sandbox 里的 shell + 浏览器,是"无 IDE 的全自动开发者";Cursor 是 IDE-first 的人机协同 |
| Aider | Aider 走 git diff + repl 的极简路线,OpenHands 多了完整 runtime sandbox + 浏览器 + 多 agent delegation |
#零·五、一张图看懂:OpenHands = Model + Harness
业界视角:Anthropic / Cursor / Codex / Aider / Cline 用的是同一批底层模型, 实际表现差距却很大。差的不是模型,是"模型之上的外壳(Harness)"——指令、工具、 基础设施、可观测性这四层共同决定了 agent 的真实能力上限。
套到 OpenHands 上:Microagents 是指令层的资产化,CodeAct + Action/Observation 是能力层,Sandbox Runtime + Event Stream 是基础设施,litellm cost tracking + Cloud 平台是可观测层。下文按"4 层 + 8 支柱"反向索引本文档每个章节。
#0.5.1 四层 Harness 结构
┌──────────────────────────────────────────────────────────────────────┐
│ Agent = Model (LLM) + Harness (OpenHands 四层外壳) │
│ └ litellm 适配几十家 └ Event Stream 把四端粘成一台 │
└──────────────────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ ① 指令层 │ Agent 子类自带 system prompt(agenthub/*/prompts) → §1.1 │
│ │ Repository Microagent / AGENTS.md(永久 prepend) → §1.3 │
│ │ Knowledge Microagent / Keyword Skill(关键词触发) → §5.4 │
│ │ Task Microagent(slash-style 工作流模板) → §1.3 │
├──────────────────────────────────────────────────────────────────────┤
│ ② 能力层 │ Action / Observation typed 协议(一一映射) → §4.1 │
│ │ CodeAct: <execute_ipython> / <execute_bash> → §1.2 │
│ │ AgentSkills plugin(open_file/scroll_down/...) → §4.3 │
│ │ MCP client(McpAction / McpObservation) → §4.6 │
│ │ AgentDelegateAction → 子 controller + 子 stream → §3.5 │
├──────────────────────────────────────────────────────────────────────┤
│ ③ 基础设施│ Runtime 抽象(Docker / Local / Remote) → §4.2 │
│ │ ActionExecutionServer(sandbox 内 HTTP 服务) → §4.3 │
│ │ BashSession(长生命周期 shell,保留 cwd/env) → §4.3 │
│ │ Browser (BrowserGym + chromium + playwright) → §4.5 │
│ │ EventStream 持久化(mem / file / PG / Redis) → §3.3 │
├──────────────────────────────────────────────────────────────────────┤
│ ④ 可观测 │ litellm Metrics(每次 completion 累计 token/cost) → §7.4 │
│ │ state.metrics 暴露给 UI/CLI 实时看花费 → §7.4 │
│ │ Replay(从任意 event_id 重放,deterministic state) → §3.1 │
│ │ Stuck Detector(循环模式识别,自动 delegate/abort) → §3.1 │
│ │ Cloud 平台审计日志 + 计费 → §2.5 │
└──────────────────────────────────────────────────────────────────────┘
#0.5.2 八支柱对照表
| # | Harness 支柱 | 一句话定义 | OpenHands 的实现 | 对应章节 |
|---|---|---|---|---|
| 01 | FS + Git | state 必须活在 context 之外 | EventStream 持久化(~/.openhands/sessions/<id>/events/)+ Repository Microagent / AGENTS.md 跟仓库走 git 提交;GitHub Resolver 直接产出 commit + PR |
§3.3 / §1.3 / §2.4 |
| 02 | Bash 通用工具 | "随用随装",避免 token 爆炸 | CodeAct 让模型直接写 Python/bash 代码,组合控制流不用拆 N 个 tool_call;AgentSkills plugin 提供 open_file/scroll_down 等 helper,进 IPython kernel 即可调 |
§1.2 / §4.3 |
| 03 | 沙箱 + 子工具 | 行动可隔离 + 可自检 | Runtime 抽象 + ActionExecutionServer 统一 HTTP 协议;Docker / Local / Remote 三种实现,E2B/Daytona/Modal 通过 RemoteRuntime 协议接入;Security Analyzer 做 action 风控(仅 source=AGENT) | §4.2 / §4.3 / §3.2 |
| 04 | 记忆 + 搜索 | 跨 turn / 跨 session 拿历史 | EventStream 是真相之源(可重放、可分叉、可审计);ConversationMemory 把 typed event 翻译成 LLM messages;Knowledge Microagent 用关键词触发外部记忆补丁注入 | §5.1 / §5.2 / §5.4 |
| 05 | 对抗 Context Rot | 长对话也别糊 | LLMSummarizingCondenser:保头 K + LLM 摘要中段 + 保尾 M,把 long-session cost 从 二次方降到线性;多个 Condenser 策略可选(NoOp / RecentEvents / ObservationMasking / AmortizedForgetting) | §5.3 |
| 06 | 长程执行 | 多 phase 任务不跑偏 | AgentController step loop + max_iterations;AgentDelegateAction 把子任务交给专用 agent(BrowsingAgent / LocAgent)避免主 context 被巨大 BrowserGym observation 污染 | §3.1 / §3.5 |
| 07 | Hooks 强制层 | 失败转成永久规则 | Stuck Detector 在 event history 里找循环模式(同 action 重复 N 次、同 error M 次),命中后强制 delegate 或 abort;Repository Microagent 把"项目级硬规则"以 markdown 形式 prepend 到 system prompt | §3.1 / §1.3 |
| 08 | 规划 + 工具选择 | 别什么都试,先想再调 | CodeAct 用 Python 代码代替 tool_call schema,一段代码可串多个工具 + 中间处理,避免 N 轮 round-trip(论文实测 SWE-bench Python 子集多 ~10pp 通过率);fn_call_converter 在原生 tool_call 模型上反向用结构化协议 | §1.2 / §3.4 / §7.3 |
#0.5.3 当前架构的短板(按 Harness 视角看出来的)
OpenHands 是这三家里 Harness 最全的一个(毕竟它就是为了拿 SWE-bench 设计的),但仍有 四条值得关注的真实短板:
- ❌ Browser action 慢且不稳:BrowsingAgent 跑的是 BrowserGym 抽象的 axtree + screenshot 观察空间,单次 BrowseInteractiveAction 的 RTT 远高于 bash/file action;DOM 抓取异常时 observation 容易夹带乱码或巨大 HTML 把 context 灌爆,VisualBrowsingAgent 加上 screenshot 后 更吃 token。
- ❌ Microagents 触发机制隐式:Knowledge Microagent / Keyword-Triggered Skill 的
triggers: [react, hooks]关键词匹配是字面 substring,没有语义匹配;用户消息里出现 "react to this" 也会被命中 React 知识包。Repository Microagent /AGENTS.md是永久 prepend,跟 prompt cache 友好;但 Keyword Skill 是 RecallAction 触发后才注入, 触发时机本身会破坏 cache。v1 SDK 把 frontmatter 简化到只保留triggers,但 匹配仍是关键词字面匹配,没有 embeddings。 - ❌ Condenser 不够智能:LLMSummarizingCondenser 简单地"保头 K + 摘要中段 + 保尾 M",
没有按事件重要性加权(一次关键的
FileEditAction跟一次平凡的CmdRunAction同等对待), 社区 issue 里反复报告"压缩后 agent 忘了之前已经改过哪个文件"。AmortizedForgettingCondenser 靠 step 比例舍弃更粗暴。 - ❌ Cloud vs Local 体验割裂:OpenHands Cloud(
enterprise/)做了多租户、托管 runtime、 PostgreSQL 持久化、计费审计——但这些没有反向回流到开源版。开源用户跑长 session 想看 cost 趋势、想做 conversation 跨设备同步、想 RBAC,要么自己搭,要么上 Cloud。Harness 视角 下"可观测层"在开源核心只有 litellm Metrics 这一个粗粒度信号。
值得肯定的是:EventStream + deterministic replay 是这一支柱里做得最漂亮的设计之一, 配合 Stuck Detector 已经能解决多数"agent 卡死"问题——只是 cost / failure 聚合还差临门一脚。
#一、Agent 是什么 —— Agent 抽象类 + Microagents + System Prompt 三件套
一个 OpenHands Agent ≠ 一段代码,而是「Agent 子类的 step() 实现 + 仓库里的 microagents + 注入 system prompt 的指令」三者的组合。同一个引擎,挂上不同的 Agent 子类、不同的 microagents, 就是不同的 agent 人格。
#1.1 Agent 抽象基类
OpenHands 的核心不是某个具体 agent,而是一个 Agent 抽象基类,定义在
openhands/controller/agent.py(v0 架构)。它的契约非常简单:
class Agent(ABC):
@abstractmethod
def step(self, state: State) -> Action:
"""读 state(含 event history),返回下一个 Action。"""
也就是说,一个 agent 的全部职责就是:给我历史事件,我告诉你下一步动作。
所有控制流(什么时候停、要不要 delegate、出错怎么办)都不在 agent 里,而是在
AgentController 里。这是 OpenHands 最干净的一层抽象,也是它跟很多"agent 框架自己
内置 ReAct 循环"的设计差异。
具体的 agent 实现都放在 openhands/agenthub/:
| Agent | 路径 | 用途 |
|---|---|---|
| CodeActAgent | openhands/agenthub/codeact_agent/ |
默认通用 agent,让模型直接写 Python 代码作为 action |
| BrowsingAgent | openhands/agenthub/browsing_agent/ |
专门跑 web 浏览任务(基于 BrowserGym 观察空间) |
| VisualBrowsingAgent | openhands/agenthub/visualbrowsing_agent/ |
多模态浏览(screenshot + DOM) |
| DummyAgent | openhands/agenthub/dummy_agent/ |
测试用,按预设脚本 step |
| ReadOnlyAgent | openhands/agenthub/readonly_agent/ |
只读 action 集(grep / read),用于代码审查类任务 |
| LocAgent | openhands/agenthub/loc_agent/ |
代码定位类专用 agent |
注意:在 v1 SDK 重写(2025 H2 起,对应 openhands-sdk 1.x;最新 1.7.0,2026-05)里,
agenthub 被进一步拆出去成了独立 PyPI 包(仓库 All-Hands-AI/software-agent-sdk),
openhands/agenthub/ 在主仓库 main 分支已经被精简,但 step(state) → action 这个核心
抽象不变;v1 文档里也新增了 "File-Based Agents"(markdown 定义 sub-agent,无需 Python
代码)和 "ACP Agent"(接 ACP 协议外部 agent server)两类入口。
#1.2 CodeActAgent:默认人格、最值得讲的那个
CodeActAgent 是 OpenHands 的默认 agent,也是它跟其他 agent 框架最不一样的设计。 它的核心思路来自 CodeAct 论文(Wang et al., ICML 2024):
与其让 LLM 输出 JSON 形式的 tool call,不如让它直接写一段 Python 代码作为 action。 这段代码会丢进 sandbox 的 IPython kernel 里执行,结果作为 observation 返回。
为什么这么做?因为:
- 表达能力:
for ... try ... if file.exists() ...这种组合控制流,tool_call 写起来非常啰嗦, 写代码是 LLM 训练分布里出现频率最高的、最自然的表达。 - 组合性:一段代码可以串起多个工具调用 + 中间数据处理,避免 N 轮 round-trip。
- 复用 sandbox:所有 action(命令 / 文件 / 浏览器)都可以包成 Python wrapper 进同一个 IPython 进程,状态自然延续。
CodeActAgent 的 system prompt(位于
openhands/agenthub/codeact_agent/prompts/)大致约束:
- 输出形如
<execute_ipython>...</execute_ipython>/<execute_bash>...</execute_bash>/<execute_browse>...</execute_browse>的 XML 标签包起来的代码块。 - AgentController 收到 LLM 输出后,由
action_parser.py把这些 tag 解析成对应的IPythonRunCellAction/CmdRunAction/BrowseInteractiveAction。 - 每跑完一个 action 把 stdout/stderr/return_value 包成 observation 塞回 history。
LLM 输出:
"我需要看下 main.py。"
<execute_bash>cat main.py</execute_bash>
action_parser 解析 → CmdRunAction(command="cat main.py")
runtime 执行 → CmdOutputObservation(content="...", exit_code=0)
观测进 event stream → 下一轮 LLM 看到完整文本继续推理
#1.3 Microagents → Skills:随仓库走的"提示词补丁"
OpenHands 的另一大特色是 Microagents:一组 markdown 文件,里面写"遇到 X 时该 怎么做",由 agent 在合适的时机自动注入到 prompt。在 v1 SDK 里这套机制更名为 Skills / AgentSkills standard,但概念结构基本继承,下文先按 v0 三分法讲清楚 (仍然是最广为流传的心智模型),再说 v1 的差异。
v0 Microagents 三分类(.openhands/microagents/,现已 deprecated 但仍兼容):
| 类型 | 触发方式 | 典型用途 |
|---|---|---|
| Repository Microagent | 一旦 agent 进入某个仓库就总是加载 | 项目级约定:编码规范、测试命令、CI 流程 |
| Knowledge Microagent | 关键词触发(frontmatter triggers: [react, hooks]) |
领域知识:碰到 React 才注入 React 规范 |
| Task Microagent | 用户显式调用(slash-command 形式) | 常见工作流:发布、回滚、复现 bug |
文件位置(v0):
- 公共 microagents(社区共享):
microagents/仓库根目录,所有用户可见。 - 仓库级 microagents:
<your-repo>/.openhands/microagents/,进入该仓库时被发现并加载。repo.md→ Repository 类型knowledge/*.md→ Knowledge 类型tasks/*.md→ Task 类型
v0 frontmatter 示例(YAML):
---
name: react-best-practices
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- react
- hooks
- useState
---
# 当用户提到 React 时...
- 函数组件优先
- hook 顺序不能动
- ...
加载链路(v0):
AgentController.start
└→ Memory 模块(openhands/memory/memory.py)扫描 .openhands/microagents/
├→ repo.md → 直接 prepend 到 system prompt
└→ knowledge/*.md → 注册成 trigger,等 user message 命中关键词时再 inject
(以 RecallAction → RecallObservation 的形式进 event stream)
v1 SDK 的演化(Skills / AgentSkills standard):
- 路径迁移到
.agents/skills/(推荐);.openhands/skills/与.openhands/microagents/标记为 deprecated 但仍兼容 - 仓库级永久注入文件从
repo.md改为根目录的AGENTS.md(与 Cursor / Aider / Claude Code 等趋同的"通用 agent context 文件"约定) - v0 的 repo / knowledge / task 三分被重写成四类:Permanent Context(
AGENTS.md等永久注入)/ Keyword-Triggered Skills(关键词触发,对应原 knowledge)/ Organization Skills(团队级共享)/ Global Skills(社区注册表,从公共 registry 拉) - frontmatter 大幅精简,Keyword-Triggered Skills 仅
triggers字段是必填,其他如name/type/version/agent在 v1 已非强约束;General Skills 干脆不需要 frontmatter - 文档站:https://docs.openhands.dev(原
docs.all-hands.dev已 308 重定向到此)
下文若不特别说明,"microagent" 与 v1 的 "skill" 在本文档语义上可互换。
#1.4 三件套合起来定义"一个 agent"
[Agent 子类 step()] 决定 ReAct / CodeAct / Plan 风格
↓ 调 LLM 时
[System Prompt] agenthub/<agent>/prompts/*.j2
↓ 启动期叠加
[Repository Microagent: repo.md] 仓库级永久约定
↓ 运行期触发性叠加
[Knowledge Microagents] 关键词命中 → RecallObservation 注入
这种"分层定义 agent 人格"的好处:换仓库不换 agent 引擎,agent 引擎升级不破坏仓库习惯。
#二、Agent 怎么活起来 —— CLI / WebUI / GitHub Action / Cloud 四种触发
同一个 AgentController,被四种不同的入口包了不同的脸:CLI 是单进程同步 stdin/stdout, WebUI 是 socket.io 长连接,GitHub Action 是 issue/PR 触发的一次性 headless run, Cloud 是托管在 app.all-hands.dev 的多租户 SaaS。看上去四套,其实只是 EventStream 的 四种不同的 driver。
#2.1 入口全景图
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ ┌──────────────┐
│ CLI │ │ Local GUI │ │ GitHub Action │ │ OpenHands │
│ (headless) │ │ (React) │ │ (Resolver) │ │ Cloud (SaaS) │
└──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ └──────┬───────┘
│ │ │ │
│ stdin/stdout │ WebSocket │ GitHub webhook │ HTTPS / WS
│ │ /socket.io │ │
↓ ↓ ↓ ↓
┌─────────────────────────────────────────────────────────────────────────┐
│ openhands/server/ + openhands/core/ │
│ │
│ server/listen.py / listen_socket.py:FastAPI + SocketIO 入口 │
│ server/session/agent_session.py:AgentSession 生命周期 │
│ │
│ ┌──────────── AgentController ───────────────┐ │
│ │ loop: read EventStream → step() → emit │ │
│ └────────────────────────────────────────────┘ │
│ ↑ ↓ │
│ EventStream(订阅/发布) Action / Observation │
│ ↑ ↓ │
│ ┌────────── Runtime(Docker / E2B / Local / Remote)────────────┐ │
│ │ action_execution_server.py:sandbox 内的 HTTP server │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
#2.2 CLI / Headless 模式
CLI 是 OpenHands 最轻量的入口,对应 openhands/cli/ 目录(v0)。一条命令:
poetry run python -m openhands.cli.main \
--task "帮我把 main 分支的 TypeError 修了,跑通测试" \
--runtime docker \
--max-iterations 50
链路:__main__.py → 加载 toml/yaml config(openhands/core/config/)→ 选 Agent
子类(默认 CodeActAgent)→ 起 Runtime(默认 docker)→ 建内存 EventStream → 创建
AgentController(agent, event_stream, runtime) → controller.run_until_done(),
循环:pop user message / observation → agent.step(state) → emit action → runtime
执行 → emit observation → 直到 AgentFinishAction 或 max_iterations。
Headless 模式是 CLI 的一个变种:不接 stdin,直接吃一条 task 跑到结束,常用于 CI、
benchmark(SWE-bench evaluation harness 入口在 evaluation/benchmarks/)。
#2.3 WebUI / Local GUI
WebUI 由 openhands/server/ 起 FastAPI,前端在 frontend/(React + Vite)。
通信走 socket.io(双向流式 event):
Browser server/listen_socket.py AgentSession + Controller
│ │ │
│ sio.emit("oh_user_action",msg) │ │
│─────────────────────────────────►│ │
│ │ event_stream.add_event(...) │
│ │───────────────────────────────►│
│ │ │ step
│ │ event_stream subscriber │
│ │◄───── Action/Observation ──────│
│ sio.emit("oh_event", ev) │ │
│◄─────────────────────────────────│ │
AgentSession(openhands/server/session/agent_session.py)维持一个 conversation 的
完整生命周期:runtime container 起停、event stream 订阅、token 计费、状态持久化。
#2.4 GitHub Resolver / GitHub Action
OpenHands 提供一个开箱即用的 GitHub Resolver,让 agent 直接接住 GitHub issue / PR 评论,自动 fork、写代码、push 分支、回 PR 评论:
- 入口代码:
openhands/resolver/(v0) - GitHub Action:用户在自己仓库装上
openhands-resolveraction,配LLM_API_KEYsecret - 触发:issue 评论里写
@openhands-agent <task>,action 启动一个 headless OpenHands run - 产物:一个分支 + 一条 PR + 评论里贴上 trace
链路(关键文件):
链路:.github/workflows/openhands-resolver.yml → openhands.resolver.resolve_issue
→ 拉 issue 上下文 → git clone 到 sandbox → 跑 CodeActAgent(max_iterations 50) →
git push HEAD:openhands-fix-<issue-id> → gh pr create + 回 issue 评论。
#2.5 OpenHands Cloud(app.all-hands.dev)
托管 SaaS 版,对应仓库里 enterprise/ 目录(source-available,单独 license)。Cloud 在开源版本之上做了:
- 多租户(org / user / RBAC)
- 托管的 runtime(Daytona / Modal 后端)
- GitHub / GitLab / Bitbucket / Slack / Jira / Linear 集成
- conversation 持久化到 PostgreSQL
- Web Console + 计费 + 审计日志
但 agent 引擎本体跟开源版完全一致 —— 这是 OpenHands 项目治理上一个很重要的承诺。
#三、Agent 怎么思考 —— AgentController step loop + Event Stream + CodeAct
AgentController 是 OpenHands 唯一的"调度中枢",但它做的事情比想象中少:它不解释 tool call、不做 ReAct 决策,它只做三件事 —— 把 event 喂给 agent.step()、把返回的 action 推给 runtime、捕获 observation 重新进 stream。所有"智能"都在 agent.step() 里,所有"传输"都在 EventStream 里。
#3.1 AgentController 的 step loop
定义在 openhands/controller/agent_controller.py(v0)。简化版本:
class AgentController:
async def _step(self):
state = self._build_state() # 从 stream 重建 state
action = self.agent.step(state) # agent 想下一步
await self.event_stream.add_event(action, EventSource.AGENT)
# runtime 订阅 stream 接 action → 执行 → emit observation 进 stream
async def run(self):
while not self.is_done():
await self._step()
if self._is_stuck(): # stuck.py 检测循环
self.delegate_or_abort()
if self.iteration > self.max_iterations: break
注意几个设计点:
- Controller 不直接调 runtime:它 emit action 到 event stream,runtime 是 stream 的另一个 subscriber,通过 stream 解耦。这意味着同一个 controller 可以 接不同 runtime(Docker / Local / Remote),甚至 fan-out 到多个 runtime 做 A/B。
- State 是从 event stream 重建的:state 不是 controller 自己存的字段,而是
_build_state()从 stream history 算出来的。这让 conversation 可以从任意 event id 重放(controller/replay.py),调试和 reproducibility 极强。 - Stuck 检测:
controller/stuck.py在 history 里找循环模式(同一个 action 重复 N 次、同一个 error observation 出现 M 次),命中后触发 delegation 或 abort,避免 agent 把 max_iterations 烧光。
#3.2 Event Stream:典型事件总线,但 typed
openhands/events/stream.py 实现了一个内存事件总线,对外暴露:
class EventStream:
def add_event(self, event: Event, source: EventSource) -> None: ...
def subscribe(self, callback) -> Subscriber: ...
def get_events(self, start_id=None, end_id=None) -> Iterator[Event]: ...
def filtered_history(self, filter: EventFilter) -> List[Event]: ...
Event 是个抽象基类,下面分两个大族:
Event
├── Action (openhands/events/action/)
│ ├── CmdRunAction # 跑 bash
│ ├── IPythonRunCellAction # 跑 IPython cell
│ ├── FileReadAction
│ ├── FileWriteAction
│ ├── FileEditAction # str_replace 风格
│ ├── BrowseURLAction
│ ├── BrowseInteractiveAction
│ ├── MessageAction # agent 直接说话
│ ├── AgentDelegateAction # 把任务委给子 agent
│ ├── AgentFinishAction # 任务结束
│ ├── McpAction # 调 MCP 工具
│ └── RecallAction # 触发 microagent 注入
└── Observation (openhands/events/observation/)
├── CmdOutputObservation
├── IPythonRunCellObservation
├── FileReadObservation
├── FileWriteObservation / FileEditObservation
├── BrowserOutputObservation
├── ErrorObservation
├── AgentDelegateObservation
├── McpObservation
└── RecallObservation # microagent 内容
每个 event 都有 id、source(USER / AGENT / ENVIRONMENT)、timestamp、tool_call_metadata。
Source 字段在权限控制和回放时很关键 —— 例如 security_analyzer 只检查 source=AGENT 的
action,user 主动塞的 message 不走风控。
v0 → v1 事件命名差异:v0 是
CmdRunAction/CmdOutputObservation这样按工具 一一命名的 typed Action / Observation;v1 SDK 进一步泛化成统一的ActionEvent/ObservationEvent/MessageEvent/Condensation/CondensationRequest等少数 几类基础事件,工具差异下沉到事件 payload。本文表格沿用 v0 命名(语义更直观), 读 v1 源码时把它们对应到ActionEvent的不同 payload 即可。
#3.3 EventStream 的存储后端
EventStream 不只是内存的 publish/subscribe,它还有持久化:
- 内存模式(CLI、benchmark):
event_store.py直接拿 dict 存 - 文件模式(默认 server):写到
~/.openhands/sessions/<id>/events/<event_id>.json - 远端模式(Cloud):
storage/抽象到 PostgreSQL / Redis(在enterprise/)
event_store_abc.py 定义抽象接口,nested_event_store.py 支持子 conversation
(delegation 场景下子 agent 有独立 stream)。
v1 SDK 把这块进一步泛化为 event-sourced state with deterministic replay (论文 The OpenHands Software Agent SDK arXiv:2511.03690):state 完全由 event log 决定,给定 event log 跑出来的 state 一定一样 —— 见 §8.2。
#3.4 CodeAct 路径:从 LLM 输出到 IPython kernel
CodeActAgent 一个完整 step 的内部流程:
agent.step(state)
├─ ConversationMemory.process_events(state.history) # events → list[Message]
├─ Microagent.recall(state.last_user_message) # 关键词触发
├─ Condenser.condense(messages) if exceeds threshold # 见 §5
├─ llm.completion(messages, tools=...) # litellm 包过的 OpenAI 协议
└─ action_parser.parse(llm_response)
匹配 <execute_ipython> / <execute_bash> / <execute_browse> / <finish>
↓
返回 IPythonRunCellAction / CmdRunAction / BrowseInteractiveAction / AgentFinishAction
action_parser(controller/action_parser.py)是 LLM 文本与 typed Action 之间的桥。
注意这里有一个微妙的设计选择:CodeActAgent 走的是 prompt-only 协议(自定义 XML 标签),
而不是 OpenAI tool_calling。原因:
| 维度 | XML 风格(CodeAct) | OpenAI tool_call |
|---|---|---|
| 跨模型可移植 | 强(任何能写代码的模型都行) | 弱(需要 provider 支持 tool_call) |
| 可组合性 | 强(一段代码可调多个工具) | 弱(一次只能 emit 几个 tool_call) |
| 训练分布友好度 | 高(代码) | 中(tool_call schema 不够通用) |
| 解析鲁棒性 | 中(要处理转义/嵌套) | 高(结构化 JSON) |
OpenHands 主体倒向了 CodeAct,但保留了"也能用 tool_call"的备选 —— litellm 抽象
让模型如果原生支持 tool_call,可以走 openhands/llm/fn_call_converter.py 来把
function call 反向翻译成 Action。
#3.5 多 agent / Delegation
OpenHands 支持多 agent 协作,机制是 Delegation:一个 agent 在 step 里返回
AgentDelegateAction(agent="BrowsingAgent", inputs={...}),AgentController 收到
后会启动一个子 controller + 子 stream,跑完把 AgentDelegateObservation 推回
父 stream。
父 Controller (CodeActAgent) → AgentDelegateAction(agent=BrowsingAgent)
↓
子 Controller (BrowsingAgent) ← 独立 EventStream
step × N ... AgentFinishAction
↓
父 Controller 收到 AgentDelegateObservation(content=summary) → 继续 step
典型用法:CodeActAgent 把"看官方文档"委派给 BrowsingAgent,自己只接 summary, 避免把巨大的 BrowserGym observation 灌进主 agent 的 context。DelegatorAgent (部分版本里出现)是更高级的纯路由 agent —— hierarchical agent 的雏形。
#四、Agent 怎么行动 —— Action / Observation 协议 + Sandbox Runtime + Browser
OpenHands 把"动作"抽象成 typed Action,把"环境反馈"抽象成 typed Observation, 中间夹一层 Runtime —— 这个 Runtime 可以是本地 Docker、可以是 E2B / Daytona / Modal 的远端 sandbox,也可以是裸 Local(用户机器)或 Remote API。Agent 永远不知道底下 跑的是哪一个,只看到 action 进、observation 出。
#4.1 Action / Observation 一一对应
openhands/events/action/ 下面的 Action 类,对应 openhands/events/observation/ 下面的
Observation 类。一一映射关系:
| Action | Observation | 场景 |
|---|---|---|
CmdRunAction(command) |
CmdOutputObservation(content, exit_code) |
bash 命令 |
IPythonRunCellAction(code) |
IPythonRunCellObservation(content) |
IPython cell(CodeAct 主力) |
FileReadAction(path, start, end) |
FileReadObservation(content) |
读文件(带 line range) |
FileWriteAction(path, content) |
FileWriteObservation |
写文件 |
FileEditAction(path, old_str, new_str) |
FileEditObservation |
str_replace 风格编辑(避免重写整文件) |
BrowseURLAction(url) |
BrowserOutputObservation(html, screenshot) |
浏览器导航 |
BrowseInteractiveAction(action) |
BrowserOutputObservation |
浏览器交互(基于 BrowserGym 操作集) |
McpAction(name, args) |
McpObservation(content) |
调 MCP 工具 |
RecallAction(query) |
RecallObservation(microagents) |
触发 microagent 注入 |
AgentDelegateAction(...) |
AgentDelegateObservation(...) |
委派子 agent |
MessageAction(content) |
(无配对,直接面向用户) | agent 跟用户说话 |
AgentFinishAction() |
(终止) | 任务完成 |
这个表是 OpenHands 整个能力面的全景 —— 一个 OpenHands agent 能干的所有事,本质就是 emit 这些 Action 中的一个。
#4.2 Runtime 抽象:从 base.py 到具体实现
openhands/runtime/base.py 定义抽象基类:
class Runtime(ABC):
@abstractmethod
async def run(self, action: CmdRunAction) -> CmdOutputObservation: ...
@abstractmethod
async def run_ipython(self, action: IPythonRunCellAction) -> IPythonRunCellObservation: ...
@abstractmethod
async def read(self, action: FileReadAction) -> FileReadObservation: ...
@abstractmethod
async def write(self, action: FileWriteAction) -> FileWriteObservation: ...
@abstractmethod
async def browse(self, action: BrowseURLAction) -> BrowserOutputObservation: ...
...
所有具体 runtime 都在 openhands/runtime/impl/:
| 实现 | 用途 | 特点 |
|---|---|---|
| DockerRuntime | 默认本地 sandbox | 起一个 docker container,container 内跑 action_execution_server |
| LocalRuntime | 直接在 host 跑(不 sandbox) | 风险大但快,CI/调试用 |
| RemoteRuntime | HTTP API 接外部 runtime | 最通用的远端入口,第三方 sandbox(E2B/Daytona/Modal/Runloop)均通过此协议接入 |
| CLIRuntime | CLI 模式专用,复用 Docker 或 Local | headless |
| 外部 sandbox 服务 | 2025 年中(v0.x 后期)主代码库精简,移除原生 impl,统一退到 RemoteRuntime 协议接入 |
2025 年中的精简("Removal of E2B, Modal, Daytona, and Runloop runtimes from the main codebase")反映了一个治理选择:核心仓库只维护 Docker / Local / Remote, 第三方 runtime 通过 RemoteRuntime 协议接,避免主仓库被多家 SDK 拖累。
#4.3 Action Execution Server:sandbox 内的 HTTP 服务
不管底下 runtime 是 Docker 还是 E2B,sandbox 容器里都跑一个 Action Execution Server
(openhands/runtime/action_execution_server.py):
host sandbox container
┌─────────────────────────┐
AgentController │ action_execution_server │
│ HTTP POST /execute_action │ │
│ { type: "CmdRunAction", │ switch on action type: │
│ command: "ls -la" } │ ├─ bash session │
│ ─────────────────────────────────► │ ├─ IPython kernel │
│ │ ├─ file ops │
│ │ └─ chromium/playwright│
│ { type: "CmdOutputObservation", │ │
│ content: "...", exit_code: 0 } │ │
│ ◄────────────────────────────────── │ │
└─────────────────────────┘
server 的内部结构(关键模块):
BashSession:维护一个长生命周期 bash 进程,保留 cwd、env、shell 状态,避免每次 CmdRunAction 重启 shell 丢上下文- IPython kernel:通过
runtime/plugins/jupyter/起 jupyter kernel gateway,CodeAct 的 Python 代码就在这里执行,全程持有变量 - Browser:
runtime/browser/里启 chromium + playwright,BrowserGym 观察空间通过 HTTP 暴露 - AgentSkills 插件:
runtime/plugins/agent_skills/提供一组 Python helper (open_file、search_file_content、scroll_down等),CodeAct 调它们写代码 - VS Code 插件(部分版本):暴露一个 web 化的 VS Code,方便用户接管
#4.4 Plugin 机制
runtime/plugins/ 是轻量级插件系统,由 Agent.sandbox_plugins 字段声明、runtime
启动时按 agent 类型挂载。ALL_PLUGINS 注册表里主要有:
agent_skills:Python helper 库(open_file/search_file_content等)jupyter:IPython kernelvscode:可选,web 版 VS Code(方便用户接管)
#4.5 Browser:BrowserGym 集成
openhands/runtime/browser/ 对接 BrowserGym(把浏览器操作抽象成 RL 风格 obs/action
集的库)。BrowsingAgent / VisualBrowsingAgent 跑的就是 BrowserGym 观察空间:
DOM accessibility tree、screenshot、URL、focus state 等。例:
BrowseInteractiveAction(action="click(123)") # 123 = BrowserGym 元素 id
→ playwright in sandbox
→ BrowserOutputObservation { axtree, url, screenshot? }
#4.6 MCP 集成
openhands/mcp/ + openhands/runtime/mcp/ 提供 Model Context Protocol 客户端:
agent 通过 McpAction(name, args) 调挂载到 OpenHands 的任意 MCP server。v1 SDK
把这个抽象统一进 typed tool system,让 MCP tool 跟 native action 在 agent 视角无差别。
#五、Agent 怎么记忆 —— Event History + Condenser + Microagents 触发性记忆
OpenHands 没有传统意义上的"长期记忆向量库",它的记忆模型是「事件流就是记忆, 能压缩就压缩,能召回就召回」。三层结构:完整 event history 是真相之源, ConversationMemory 把它转成 LLM 消息,Condenser 在超长时压缩历史, Microagents 是触发性的外部记忆补丁。
#五层记忆结构
┌────────────────────────────────────────────────────────────────────┐
│ L1: EventStream history(持久化的事实 ground truth) │
│ 所有 Action / Observation 一条不丢,文件 or DB 后端 │
│ ↓ ConversationMemory.process_events() │
│ L2: list[Message](LLM 看的那一份) │
│ 把 event 翻译成 user/assistant/tool message,处理截断、合并 │
│ ↓ Condenser.condense() if too long │
│ L3: 压缩后的 messages(LLM 实际收到的) │
│ LLMSummarizingCondenser / RecentEventsCondenser 等 │
│ ↑ inject │
│ L4: Microagents(触发性外部记忆,repo.md / knowledge / tasks) │
│ 由 RecallAction → RecallObservation 进 stream │
│ ↑ inject 到 system 区 │
│ L5: System prompt(agent 子类自带 + microagent repo 永久注入) │
└────────────────────────────────────────────────────────────────────┘
#5.1 L1:EventStream 是真相
每个 conversation 的所有 event 按顺序持久化。这是 OpenHands 跟很多 agent 框架最不一样的 地方 —— 它把 conversation = ordered event log 当作核心抽象,state 是 derived value。 好处:
- 完全可重放:
controller/replay.py可以从任意 event_id 起跑,调试无敌 - 可审计:合规要查"这个 action 之前 agent 看到了什么",直接 cut event log
- 可分叉:从某个 event_id fork 出新 conversation,调"如果当初我做了不同选择"
#5.2 L2:ConversationMemory
openhands/memory/conversation_memory.py 负责把 typed event 翻译成 LLM 消息列表:
process_events(events, max_message_chars) → list[Message]
for ev in events:
match ev:
case CmdRunAction: → assistant message with bash block
case CmdOutputObservation: → tool message with truncated stdout
case FileEditAction: → assistant message with patch
case RecallObservation: → system message (or wrapped in user)
case ...
关键工程点:
- 截断:单个 observation content 超过
max_message_chars(默认 30k)会被尾部截断 + "... [N chars truncated] ..." 提示 - 图像处理:BrowserOutputObservation 的 screenshot 走多模态 message
- tool_call 兼容:如果模型用 tool_call 协议,这里把 Action 转成 tool_call_id 绑定的 message 序列
#5.3 L3:Condenser —— 超长时把历史压成摘要
openhands/memory/condenser/ 是 OpenHands 处理"上下文越来越长"问题的核心。Condenser
是一个抽象:
class Condenser(ABC):
@abstractmethod
def condense(self, messages: list[Message]) -> list[Message]: ...
具体实现:
| Condenser | 策略 |
|---|---|
| NoOpCondenser | 不压缩(默认 short conversation) |
| RecentEventsCondenser | 只保留最近 N 条 event |
| LLMSummarizingCondenser | 关键设计:超过阈值时,把中间段事件用 LLM 总结成一段,前 K 条(system + 初始 user)保留,后 M 条详细保留 |
| ObservationMaskingCondenser | 把老的大 observation 内容 mask 掉只留摘要 |
| AmortizedForgettingCondenser | 滚动遗忘,每 step 按比例舍弃 |
| RollingCondenser (基类) | 维护一个 rolling 窗口的接口约定 |
LLMSummarizingCondenser 的工作方式(简化):
if len(events) > max_size:
keep_first = events[:keep_first_n] # 系统消息 + 初始 user 任务
keep_last = events[-keep_last_n:] # 最近一段详细历史
middle = events[keep_first_n:-keep_last_n]
summary = llm.summarize(middle) # 单次 LLM 调用产生摘要
return keep_first + [SummaryEvent(summary)] + keep_last
OpenHands 团队 2025 年发的 Context Condensation for More Efficient AI Agents 博客里给了实测:开启 LLMSummarizingCondenser 后,"average cost per-turn" 从随轮次 二次方增长变成线性增长,长 session 可以省下一半以上的成本。
#5.4 L4:Microagents 是"触发性外部记忆"
§1.3 已经介绍过 microagents 的分类。这里看记忆视角:
- Repo microagent = 永久注入的项目记忆(你不必告诉 agent "本项目用 pnpm 不用 npm", repo.md 写一次就行)
- Knowledge microagent = 关键词触发的领域记忆(用户消息里出现 "react", RecallAction 触发,注入 React 知识包)
- Task microagent = 用户显式调用的工作流模板(slash 命令式)
触发链路:
user: "我想加个 useEffect 管下副作用"
│ event_stream.add_event(MessageAction)
↓
AgentController step
↓
agent.step:
├ 检查 user message keywords → 命中 "useEffect"
├ emit RecallAction(query="useEffect")
↓
Memory.recall (openhands/memory/memory.py)
├ 扫已注册的 knowledge microagents triggers
├ 命中 react-best-practices.md
└ emit RecallObservation(content=microagent_text)
↓
event_stream 收到 RecallObservation
↓ 下一轮 step
ConversationMemory 把 RecallObservation 转成 system 区注入
LLM 看到 React 最佳实践 → 输出更准
#5.5 记忆坐标小结
| 维度 | OpenHands |
|---|---|
| 长期记忆 | 不主推向量库;Cloud 版可挂 vector store,开源核心是 event log |
| 工作记忆 | LLMSummarizingCondenser 滚动压缩 |
| 外部记忆 | Microagents(trigger / repo / task) |
| 真相之源 | EventStream history(确定性回放) |
#六、Agent 怎么开口 —— WebUI 流式 / CLI 输出 / 多端复用同一份 EventStream
Agent "开口"在 OpenHands 里不是单独一条流式通道,而是 EventStream 上的 event 被 多个订阅者各自渲染:CLI 订阅者把 event 打成终端文本,WebUI 订阅者把 event 转 JSON 发到 socket.io,GitHub Resolver 订阅者把关键 event 写到 PR 评论。一份 stream,多面输出。
#6.1 输出全景
┌────────────── EventStream ──────────────┐
│ Action / Observation / MessageAction │
└────────────┬─────────────────┬──────────┘
│ │
┌────────────────┴──┐ ┌─────────┴────────┐ ┌──────────────┐
↓ ↓ ↓ ↓ ↓ ↓
CLI subscriber SocketIO subscr. Resolver subscr. Telemetry subscr.
(rich terminal) (oh_event emit) (PR comment) (Prometheus)
订阅者实现都很薄,只做"event → 目标格式"的翻译。
#6.2 CLI 输出
openhands/cli/ 用 rich 做终端美化,每个 event 类型对应一个渲染:
[user] 修一下 main.py 的 TypeError
[agent] 我先看看 main.py
$ cat main.py
[obs] Traceback (most recent call last):
File "main.py", line 5...
[agent] 发现是 None.split() 引起的,加个判断...
$ python -c "..." <CmdRunAction>
[obs] OK
[finish] 修好了,已 commit。
#6.3 WebUI 流式输出
WebUI 通过 socket.io(openhands/server/listen_socket.py)的 oh_event 事件订阅
EventStream,每个 event 转 JSON 发到前端。前端用 React 渲染成"对话流 + 命令面板 +
文件树 + 终端 + 浏览器视图"。
LLM token 流式输出走 两条独立通道:
- EventStream 通道:agent.step 内部
llm.completion是整体调用(CodeActAgent 要等 完整文本才能 parse XML 标签),完成后 emit Action 进 stream。 - token streaming 通道:litellm 的 stream callback 把 LLM token 实时往 UI 推 ("agent 正在打字"),用于打字机效果,跟 EventStream 解耦。
#6.4 GitHub Resolver 的"开口"
Resolver 不向用户说话,而是把关键 event 翻译成 PR 评论 / commit message:
MessageAction → PR 描述;CmdRunAction + CmdOutputObservation → 折叠成
markdown details;AgentFinishAction → 在 issue 上 @ 用户"已完成,请 review"。
#七、Agent 的外脑 —— litellm 抽象 + 模型路由 + cost tracking
OpenHands 不绑定任何 LLM 厂商,它通过 litellm 这一层把 OpenAI / Anthropic / Google / 本地 Ollama 等几十家 provider 统一成同一套 OpenAI 协议接口。Agent 永远写的是
self.llm.completion(...),换模型只改一行model="anthropic/claude-sonnet-4"。
#7.1 LLM 抽象
openhands/llm/llm.py 是核心包装:
class LLM:
def __init__(self, config: LLMConfig):
self.model = config.model # "openai/gpt-4o" / "anthropic/..." / "ollama/..."
self.api_key = config.api_key
self.base_url = config.base_url # 走 OpenRouter / 自部署 vLLM 都行
self.metrics = Metrics() # token / cost 累计
def completion(self, messages, tools=None, stream=False, **kw):
return litellm.completion(model=self.model, messages=messages,
tools=tools, stream=stream, api_key=self.api_key,
base_url=self.base_url, **kw)
litellm 帮 OpenHands 解决了:
- provider 协议统一(Anthropic 的
messages跟 OpenAI 的chat.completions自动转换) - tool_call 协议归一化(不同 provider 的 function calling 格式差异)
- 流式支持
- cost calculation(按模型查表算 token 价钱)
- retries / fallback
#7.2 模型路由 / 多模型
OpenHands 通过 LLMRegistry(openhands/llm/)支持给不同 agent / 不同子任务挂不同模型:
# config.toml
[llm]
model = "anthropic/claude-sonnet-4"
api_key = "..."
[llm.condenser]
model = "openai/gpt-4o-mini" # 摘要走便宜模型
api_key = "..."
[agent.CodeActAgent]
llm_config = "llm"
[agent.BrowsingAgent]
llm_config = "llm"
典型实践:
- 主 agent 用强模型(Claude Sonnet / GPT-4 class)
- Condenser 摘要用便宜模型(gpt-4o-mini / claude-haiku)
- Stuck detector 用 cheap model 做"是不是循环"判断
- Sub-agent delegation 看任务复杂度选模型
#7.3 Function Call Converter
openhands/llm/fn_call_converter.py 处理"模型原生支持 tool_call vs CodeAct XML"
之间的 fallback:
- 如果模型支持原生 tool_call(OpenAI / Anthropic / 部分 OS 模型),CodeActAgent 可以 把 IPythonRunCellAction 等 emit 成 tool_call schema,让模型用结构化方式调用
- 如果模型不支持(早期开源模型),fallback 到 XML 标签提示
#7.4 Cost / Telemetry
LLM 包装内置 Metrics,每次 completion 自动累加 input tokens / output tokens / cost。
AgentController 通过 state.metrics 暴露给 UI / CLI,可以实时看"这个 task 花了多少钱"。
Cloud 版本基于这个做计费。
#八、关键设计权衡(架构师视角)
下面挑六条最值得记住的取舍,每条用"问题 / 回答 / 启发"三段式说清。
#8.1 CodeAct vs tool_call:让模型直接写 Python
问题:传统 agent 框架让 LLM 走 tool_calls: [{name, arguments}],但碰到"读完这个
JSON 把里面 status 不是 200 的全 retry"这种组合逻辑就要拆好几轮 round-trip,每轮
都把全部 history 灌一次,token 烧得快、推理也容易跑偏。
回答:CodeActAgent 让模型直接输出一段 Python(用 <execute_ipython> 包),整段
丢进 sandbox 的 IPython kernel 跑,return 值/副作用通过 IPythonRunCellObservation 回流。
组合控制流、错误处理、数据后处理全在一段代码里完成,单轮抵 N 轮 tool_call。
论文里的实测:CodeAct 比 JSON tool_call 在 SWE-bench-style 任务上多 ~10pp 通过率。
启发:当任务的"动作组合性"高时,让模型说它最熟的那种语言(代码) 比强行把 能力切成一堆 schema 出 tool_call 更有效。代价:parser 要 robust(XML 标签嵌套、 转义陷阱),sandbox 要够强,agent 抓不住的副作用风险变大。
#8.2 Event Stream 作为唯一总线:解耦的代价是"event 也要持久化所有副作用"
问题:CLI / WebUI / GitHub Action / Cloud 四种入口都要复用一套 agent,怎么避免 四套通信代码?
回答:所有 Action / Observation 都是 typed event,所有组件(agent、runtime、UI、 resolver、telemetry)都是 EventStream 的 subscriber 或 publisher。换入口只换 stream 的 driver,不动 agent 逻辑。这套设计还顺带送了一个超强属性:deterministic replay —— 给定 event log,state 完全可重建(v1 SDK 把这个特性做成了 first-class)。
启发:event-sourced 是 agent 系统天然契合的范式,但要付出代价:副作用必须 emit 成 event 才"算数",agent 不能在 step 内偷偷改外部状态而不留 event。这对工程纪律 要求很高,但给 debug、合规、A/B、回放带来质变。
#8.3 Runtime 抽象:Docker 不是唯一答案
问题:本地开发要 Docker,CI 要 Local,Cloud 要 E2B/Daytona/Modal,企业要自部署 sandbox —— agent 怎么不被 runtime 绑死?
回答:抽象出 Runtime 基类 + 统一的 action_execution_server HTTP 协议。
不管底下是 Docker 容器还是 E2B 远端 sandbox,agent 看到的都是同一个 runtime.run(action)。
2025 年中砍掉 E2B/Modal/Daytona/Runloop 的具体实现、统一走 RemoteRuntime 协议,
进一步减负 —— 不让外部 SDK 拖累主仓库的演化速度,是一个值得学习的边界划定。
启发:sandbox 这种"低层基础设施"特别容易出多个供应商百花齐放的局面,agent 框架 应该早早抽出协议层(HTTP / gRPC),让具体实现以独立包/外部服务形式存在。
#8.4 Microagents:repo.md 是"长在仓库里的 prompt 补丁"
问题:agent 进了不同仓库要遵守不同规范(这个项目用 pnpm、那个项目测试要先 docker compose up),怎么避免每次都让用户重新讲?
回答:.openhands/microagents/repo.md 跟随仓库走,agent 第一次进仓库就把它
prepend 进 system prompt。Knowledge microagent 用 triggers: [...] 关键词触发性
注入,避免无脑灌爆 context。Task microagent 是"slash 命令模板",常用工作流(发布、
回滚)一键调起。
启发:把 prompt 这一层资产跟着代码仓库走(而不是存在 agent 平台后台), 给团队协作和版本控制带来天然便利 —— 改了规范走 PR 走 review 走 git history。 这是 OpenHands 比"中央化的 agent 配置后台"更工程师友好的地方。
#8.5 Condenser:上下文管理是个"agent 框架的核心义务"
问题:autonomous agent 跑长 task 时 context 必然爆炸,单纯靠模型自己长 context 撑不住成本,怎么办?
回答:Condenser 是 agent 框架的一等公民,不是用户自己写 callback。 LLMSummarizingCondenser 在超过阈值时:保留前 K(系统 + 初始任务)、把中间段 LLM 总结 成一句话、保留后 M(最近详细动作)。OpenHands 团队的实测:长 session cost 从 二次方增长变成线性增长。
启发:上下文压缩不应该是用户自选 plugin,应该是 agent runtime 默认开启、用便宜 模型做摘要的标配。设计时要保证摘要本身也是 event,能审计能回放 —— 不能把摘要藏在 agent 内部状态里。
#8.6 多 agent delegation:不要一开始就堆 hierarchical
问题:很多 agent 框架一开 Day 1 就堆出 supervisor / worker / planner 一堆角色, 但实际效果常常不如单 agent + 好 prompt。
回答:OpenHands 主推单 agent(CodeActAgent),delegation 作为专门子任务的 逃生口(把"跑浏览器"委派给 BrowsingAgent,把"找文件位置"委派给 LocAgent),而不是 默认架构。DelegatorAgent 这种纯路由 agent 存在但不主推。
启发:多 agent 不是越多越好,当 sub-task 的 observation space 跟主任务差异大 (比如浏览器 axtree vs 代码)时才值得拆。否则收益不抵协调成本。
#九、附录:组件地图、关键概念、源码索引
#9.1 组件全景图
用户入口(四种)
CLI / Headless React WebUI GitHub Resolver OH Cloud
│ stdin/out │ socket.io │ webhook │ HTTPS
└────────┬────────┴────────┬───────┴────────┬───────┘
↓ ↓ ↓
┌─────────────────────────────────────────────────────────┐
│ server / cli / core │
│ AgentSession ←→ AgentController │
│ │ step() │
│ ↓ │
│ Agent (CodeActAgent / ...) │
│ │ │
│ ┌────────── EventStream ─────────┐ │
│ │ Action / Observation events │ storage: │
│ │ │ file/DB/mem │
│ └──┬────────┬──────────┬─────────┘ │
│ │ │ │ │
│ Memory Runtime Security │
│ Condenser base+ Analyzer │
│ Microagent action_exec_server │
│ │ │
│ Sandbox (Docker / Local / Remote / ...) │
│ [BashSession + IPython + Browser + Plugins] │
│ │
│ LLM 抽象 → litellm → OpenAI / Anthropic / ... │
└─────────────────────────────────────────────────────────┘
#9.2 关键概念对照表
| 概念 | 一句话定义 | 源码位置 |
|---|---|---|
| Agent | 抽象基类,唯一方法 step(state) → action |
openhands/controller/agent.py |
| AgentController | 调度循环,把 step 串成 conversation | openhands/controller/agent_controller.py |
| Action | typed 事件,agent 想做的动作 | openhands/events/action/ |
| Observation | typed 事件,环境的反馈 | openhands/events/observation/ |
| EventStream | 事件总线(发布订阅 + 持久化) | openhands/events/stream.py, event_store*.py |
| Runtime | sandbox 抽象 | openhands/runtime/base.py + impl/ |
| ActionExecutionServer | sandbox 内的 HTTP 服务 | openhands/runtime/action_execution_server.py |
| CodeActAgent | 默认 agent,写 Python 作为 action | openhands/agenthub/codeact_agent/ |
| ActionParser | LLM 文本 → Action 的解析器 | openhands/controller/action_parser.py |
| Microagent / Skill | v0 repo / knowledge / task 三类 markdown prompt;v1 重命名为 Skills(Permanent/Keyword/Org/Global 四类) | openhands/microagent/, .openhands/microagents/(deprecated)→ .agents/skills/ + 根 AGENTS.md |
| Memory | 处理 event → message 的转换层 | openhands/memory/conversation_memory.py |
| Condenser | 上下文压缩抽象 | openhands/memory/condenser/ |
| LLM | litellm 包装,统一 provider | openhands/llm/llm.py |
| AgentSession | conversation 生命周期管理 | openhands/server/session/agent_session.py |
| Stuck Detector | 循环检测 | openhands/controller/stuck.py |
| Replay | 从 event log 重放 | openhands/controller/replay.py |
| Security Analyzer | action 安全审查(可选) | openhands/security/ |
| Resolver | GitHub issue/PR 自动处理 | openhands/resolver/ |
| MCP Client | Model Context Protocol 集成 | openhands/mcp/, openhands/runtime/mcp/ |
#9.3 仓库目录索引
github.com/All-Hands-AI/OpenHands (v0.55 历史 layout,最经典):
OpenHands/
├── openhands/ # 核心 Python 库
│ ├── agenthub/ # 具体 Agent: codeact_agent / browsing_agent /
│ │ # visualbrowsing_agent / readonly_agent /
│ │ # loc_agent / dummy_agent
│ ├── controller/ # agent.py / agent_controller.py /
│ │ # action_parser.py / stuck.py / replay.py / state/
│ ├── events/ # action/ + observation/ + stream.py +
│ │ # event_store*.py + serialization/
│ ├── memory/ # memory.py + conversation_memory.py + condenser/
│ ├── microagent/ # microagent 加载器(microagent.py / types.py)
│ ├── runtime/ # base.py + action_execution_server.py +
│ │ # impl/ (Docker/Local/Remote/...) +
│ │ # browser/ + plugins/ (agent_skills/jupyter/vscode)
│ │ # + builder/ + mcp/
│ ├── llm/ # llm.py(litellm 包装)+ fn_call_converter.py
│ ├── server/ # FastAPI + SocketIO;listen.py / listen_socket.py /
│ │ # session/agent_session.py / routes/ / services/
│ ├── cli/ # CLI 入口
│ ├── resolver/ # GitHub issue/PR resolver
│ ├── mcp/ # MCP 客户端
│ ├── core/ # config / message / schema
│ ├── security/ # LLMRiskAnalyzer / invariant
│ ├── storage/ # 持久化抽象(local / S3 / DB)
│ └── integrations/ # 第三方 git provider 等
├── frontend/ # React + Vite WebUI
├── containers/ microagents/ # Docker 镜像 / 公共 microagents
├── enterprise/ # 商业版(source-available)
├── evaluation/ # SWE-bench / GAIA / WebArena harness
└── tests/ docs/
v1 SDK(2025 H2 起,对应 main 分支当前布局;当前最新 release 1.7.0,2026-05)做了如下 重构(参考 arXiv:2511.03690):
agenthub/controller/runtime/等被拆出去成独立 PyPI 包(openhands-sdk/openhands-agent-server/openhands-workspace等,仓库All-Hands-AI/software-agent-sdk), 主仓库openhands/收敛成app_server/core/events/llm/mcp/server/storage/等服务编排层- Action / Observation 进一步泛化为统一的
ActionEvent/ObservationEvent/MessageEvent/Condensation/CondensationRequest等少数几类 typed event, 工具差异下沉到 payload;原生融合 MCP(typed tool system) - Microagents 重写为 Skills / AgentSkills standard,路径迁移到
.agents/skills/(兼容旧.openhands/microagents/、.openhands/skills/);仓库级永久注入文件从repo.md改为根目录AGENTS.md - frontmatter 大幅简化:Keyword-Triggered Skills 仅要求
triggers字段,General Skills 可不带 frontmatter - Workspace 抽象(
LocalWorkspace/DockerWorkspace/RemoteAPIWorkspace)替代 原 Runtime 三分;外部 sandbox 一律走RemoteAPIWorkspace - event-sourced state model with deterministic replay 成为 first-class
读老论文(ICLR 2025)/ 老博客 / 老教程时,对应的还是 v0 layout;想跟踪最新架构走 v1 SDK。
#9.4 评测成绩定位(仅作能力坐标,数据可能随版本变动)
| 数据集 | OpenHands 大致区间 | 来源 |
|---|---|---|
| SWE-bench Verified | 50%–60% 区间长期处于开源 agent 第一梯队 | swebench.com 排行榜(多版本提交) |
| SWE-bench Python (CodeAct) | ~79.3% 修复率 | OpenHands 论文(ICLR 2025) |
| GAIA / WebArena | 论文中均有评测但分数随评测细节变化 | OpenHands 论文 |
注:具体数值版本依赖性强,截至 2026-05 排行榜上 Salesforce SAGE(OpenHands 衍生)等 版本仍持续刷榜,请以官方 leaderboard 为准。
#9.5 一句话总结
OpenHands = 「一个能写代码、跑 shell、用浏览器的开源 autonomous agent,把 agent 抽象 成 step()、把 IO 抽象成 typed event、把 sandbox 抽象成 Runtime,再用 Event Stream 串 起 CLI / WebUI / GitHub / Cloud 四端。CodeAct 让 LLM 写代码代替 tool_call, Microagents 让仓库自带 prompt 补丁,Condenser 让长 session cost 线性而非二次方。」
#文档元信息
- 作者视角:架构走读,非源码逐行解读
- 覆盖版本:以 v0.55 经典架构为主线(心智模型最容易讲清楚),v1 SDK 重写处(最新 release 1.7.0,2026-05)单独标注;文档站迁移后以 https://docs.openhands.dev 为准
- 撰写时间:2026-04-25(首版) / 2026-05-21(核对 v1 命名与版本号)
- 2026-06-08 复核:
openhands-ai@1.7.0,openhands-sdk@1.26.0,openhands-agent-server@1.26.0 - 最不确定的几点:
- v1 SDK 对 agenthub / controller / runtime 的拆包细节,部分内容来自论文摘要和文档
片段;主仓库
openhands/已不见这三个目录,但 v1 各 PyPI 包的 import path 没逐一 验证。(待核实具体包名与版本对应) - v1 文档已把 Skills 重新分类成 Permanent Context / Keyword-Triggered / Organization / Global 四类,与 v0 的 repo / knowledge / task 三分不是 1:1 映射——例如 v0 的 Task Microagent 在 v1 文档里未见对应分类,可能合并进 Keyword Skill 或保留为兼容 特性。(待核实)
- OpenHands Cloud 当前的 runtime backend 是否仍走 Daytona / Modal,还是已全部迁到自 研托管 sandbox,企业版代码未完整公开。(待核实)
- SWE-bench Verified 的 OpenHands 当前最高分版本归属。文档给的是论文里 CodeAct Python 79.3%(子集),全集分数随提交版本变动,应以 swebench.com 实时排行榜为准。
- v1 SDK 对 agenthub / controller / runtime 的拆包细节,部分内容来自论文摘要和文档
片段;主仓库