Agent 架构文档 · Markdown

OpenHands Agent 架构全景讲解

以 Agent 的生命脉络 为主线,讲清楚 OpenHands 这套"开源 autonomous coding agent 平台"是怎么把一个 LLM 包成一个能自己写代码、跑 shell、用浏览器、改 PR 的开发者。 所有概念(AgentController / Agent / Action / Observation / Event Stream /

来源文件:openhands-agent-architecture.md · 阅读时间 28 分钟

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.0openhands-sdkopenhands-agent-server 当前复核到 1.26.0。本文主体仍以 v0.55 经典架构 + v1 SDK 迁移说明为主,当前包名/版本以本条和官方发布为准。


#目录

  1. 写在前面:项目身份与核心矛盾 0.5. 一张图看懂:OpenHands = Model + Harness
  2. Agent 是什么 —— Agent 抽象类 + Microagents + System Prompt 三件套
  3. Agent 怎么活起来 —— CLI / WebUI / GitHub Action / Cloud 四种触发
  4. Agent 怎么思考 —— AgentController step loop + Event Stream + CodeAct
  5. Agent 怎么行动 —— Action / Observation 协议 + Sandbox Runtime + Browser
  6. Agent 怎么记忆 —— Event History + Condenser + Microagents 触发性记忆
  7. Agent 怎么开口 —— WebUI 流式 / CLI 输出 / 多端复用同一份 EventStream
  8. Agent 的外脑 —— litellm 抽象 + 模型路由 + cost tracking
  9. 关键设计权衡(架构师视角)
  10. 附录:组件地图、关键概念、源码索引

#零、写在前面:项目身份与核心矛盾

#项目消歧

"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 返回。

为什么这么做?因为:

  1. 表达能力for ... try ... if file.exists() ... 这种组合控制流,tool_call 写起来非常啰嗦, 写代码是 LLM 训练分布里出现频率最高的、最自然的表达。
  2. 组合性:一段代码可以串起多个工具调用 + 中间数据处理,避免 N 轮 round-trip。
  3. 复用 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 ContextAGENTS.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 → 直到 AgentFinishActionmax_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)         │                                │
   │◄─────────────────────────────────│                                │

AgentSessionopenhands/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-resolver action,配 LLM_API_KEY secret
  • 触发:issue 评论里写 @openhands-agent <task>,action 启动一个 headless OpenHands run
  • 产物:一个分支 + 一条 PR + 评论里贴上 trace

链路(关键文件):

链路:.github/workflows/openhands-resolver.ymlopenhands.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

注意几个设计点:

  1. Controller 不直接调 runtime:它 emit action 到 event stream,runtime 是 stream 的另一个 subscriber,通过 stream 解耦。这意味着同一个 controller 可以 接不同 runtime(Docker / Local / Remote),甚至 fan-out 到多个 runtime 做 A/B。
  2. State 是从 event stream 重建的:state 不是 controller 自己存的字段,而是 _build_state() 从 stream history 算出来的。这让 conversation 可以从任意 event id 重放controller/replay.py),调试和 reproducibility 极强。
  3. 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 都有 idsource(USER / AGENT / ENVIRONMENT)、timestamptool_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
E2B / Daytona / Modal / Runloop 外部 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 Serveropenhands/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_filesearch_file_contentscroll_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 kernel
  • vscode:可选,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.0openhands-sdk@1.26.0openhands-agent-server@1.26.0
  • 最不确定的几点
    1. v1 SDK 对 agenthub / controller / runtime 的拆包细节,部分内容来自论文摘要和文档 片段;主仓库 openhands/ 已不见这三个目录,但 v1 各 PyPI 包的 import path 没逐一 验证。(待核实具体包名与版本对应)
    2. v1 文档已把 Skills 重新分类成 Permanent Context / Keyword-Triggered / Organization / Global 四类,与 v0 的 repo / knowledge / task 三分不是 1:1 映射——例如 v0 的 Task Microagent 在 v1 文档里未见对应分类,可能合并进 Keyword Skill 或保留为兼容 特性。(待核实)
    3. OpenHands Cloud 当前的 runtime backend 是否仍走 Daytona / Modal,还是已全部迁到自 研托管 sandbox,企业版代码未完整公开。(待核实)
    4. SWE-bench Verified 的 OpenHands 当前最高分版本归属。文档给的是论文里 CodeAct Python 79.3%(子集),全集分数随提交版本变动,应以 swebench.com 实时排行榜为准。

返回 Agent 资料库