01

一次代码修复背后

当你让 OpenHands "修复这个 Bug",30 秒内到底发生了什么?

你用一句话改了代码,但背后是一台机器在运作

OpenHands 是一个开源 AI 编程 Agent 平台,它让 LLM(大语言模型)不只是"说",而是真正地打开文件、运行代码、提交 Git、操作浏览器。GitHub 评分超过 40K 星。

想象你发现了一个网站 Bug:登录按钮在移动端点击没反应。你对 OpenHands 说"帮我修这个 Bug",它不是给你一段代码让你复制粘贴 —— 它打开你的项目文件、定位问题、修改代码、运行测试、提交 PR,一气呵成。

🎯
为什么这对你很重要

理解这套架构,你就能更精准地指挥 AI 做复杂任务、快速定位"为什么 AI 卡住了",以及判断什么任务适合交给 Agent,什么必须自己来。

从你发送消息,到代码被提交 —— 完整旅程

让我们跟着一次真实请求,看每一步发生了什么。

🧑‍💻
你(用户)
🖥️
前端 UI
⚙️
后端 API
🤖
Agent
📦
Sandbox
点击"下一步"开始追踪请求旅程
Step 0 / 8

真实代码长什么样?

这是 OpenHands 启动一个 Agent 会话的核心代码。左边是源码,右边是白话翻译:

openhands/app_server/ async def create_conversation( request: Request, session_id: str = uuid4(), ): # 创建隔离的代码运行环境 runtime = await create_runtime(config) # 启动 Agent Controller 管理对话 controller = AgentController( agent=agent, runtime=runtime, event_stream=stream, ) return {"session_id": session_id}
白话翻译
用 uuid4 给这次对话分配一个唯一 ID,就像给每位客人分一个房间号
创建一个隔离的"工作间"(runtime),里面有文件、终端、浏览器,但与外界隔开
启动一个"项目经理"(AgentController),负责调度 LLM 推理和工具执行
把事件流(event_stream)交给 Controller,这样前端能实时看到每一步进展
把 session_id 返回给前端,后续所有消息都用这个 ID 找到对应的对话

快速测验:你理解了多少?

OpenHands Agent 和你平时用的 AI 聊天(比如 ChatGPT)最本质的区别是什么?
在 OpenHands 架构里,Sandbox(沙箱)的主要作用是什么?
02

Agent 的演员阵容

OpenHands 里有哪些"角色"?它们各自负责什么?

把 OpenHands 想象成一个工程师团队

一个软件公司要修复 Bug 需要多个角色:有人理解需求、有人写代码、有人运行测试、有人管理代码仓库。OpenHands 的架构也是这样 —— 不同组件扮演不同角色,通过明确接口协作。

这不只是一个"AI 聊天机器人",而是一套精心设计的多层架构,每个组件都有精确的职责边界。

用户界面层 (Frontend)
🖥️ React 前端
🔌 VS Code 插件
应用服务层 (Backend)
⚙️ FastAPI Server
🎮 AgentController
📡 EventStream
AI 推理层 (Agent)
🤖 CodeActAgent
🧠 Memory
执行隔离层 (Runtime)
🐳 Docker Runtime
📁 File Store
点击任意组件查看详情

组件之间是怎么"说话"的?

当你发一条消息,这些组件的一次真实内部对话看起来是这样的:

0 / 7 messages

源码在哪里找?

如果你想深入某个组件,这是项目关键文件的地图:

openhands/
app_server/
app.pyFastAPI 应用入口,路由注册
app_conversation/对话创建、WebSocket 管理
core/
config/配置加载(TOML / 环境变量)
server/
event/EventStream 事件总线
sandbox/Runtime 接口定义
frontend/src/
routes/页面路由(对话页、设置页)
services/API 调用、WebSocket 客户端
💡
指挥 AI 的实用技巧

当你让 AI 修改 OpenHands 的某个功能时,用组件名称精确描述位置:"在 AgentController 里加一个超时限制" 比 "让 AI 不要一直转圈" 能让 AI 更准确理解你的意图。

拖拽练习:为每个组件匹配职责

调度 Agent 状态机
组件间通信总线
隔离执行代码
渲染用户界面
🖥️ React Frontend
Drop here
🎮 AgentController
Drop here
📡 EventStream
Drop here
🐳 Docker Runtime
Drop here
03

信息如何流动

EventStream:OpenHands 的神经系统

像电台广播一样的事件总线

想象一个城市的电台系统:交通台、新闻台、音乐台各自播出内容,司机只收听自己感兴趣的频道,无需电台知道谁在听。这就是 EventStream(事件总线) 的工作方式。

在 OpenHands 里,每次 Agent 思考、执行、报错、完成,都会向 EventStream 发布一个事件。前端、后端、其他 Agent 各自订阅自己关心的事件,无需任何直接连接。

🔑
这个设计的关键好处

EventStream 让整个对话变成了可重放的历史记录。断线重连?从最后一个事件继续。调试 Agent 行为?查看完整事件序列。这是生产级 AI 系统的必备设计。

OpenHands 里有哪些事件类型?

事件分两大类:Action(动作)是 Agent 决定要做什么,Observation(观察)是执行后得到的反馈。

🗣️
MessageAction

Agent 向用户说一句话,或用户发了新消息。对话的起点。

⌨️
CmdRunAction

运行一条 Shell 命令,比如 `git diff`、`pytest tests/`、`npm install`。

📝
FileWriteAction

写入文件内容。Agent 修复完代码后,把新内容写回磁盘。

📖
FileReadAction

读取文件内容。Agent 理解问题前,通常先读相关文件。

💥
ErrorObservation

执行某个动作时出错。Agent 会把错误信息纳入下一轮推理。

AgentFinishAction

Agent 认为任务完成,给出最终答案。触发前端显示结果。

事件如何被记录和传播?

openhands/server/event/ class EventStream: def add_event( self, event: Event, source: EventSource, ): # 给事件分配递增的 ID event._id = self._cur_id self._cur_id += 1 # 持久化存储 self.file_store.write(event) # 通知所有订阅者 for listener in self._subscribers: listener(event)
白话翻译
add_event 是整个系统的"收发室",所有事件必须经过这里
给每个事件分配递增 ID(0, 1, 2...),像日记编页码,确保顺序
先把事件写入磁盘(file_store),断电也不丢失
然后通知所有"订阅者"(前端、AgentController 等),用的是回调函数模式
这种设计叫"发布-订阅",添加新订阅者不需要改这段代码

场景判断:这是 Action 还是 Observation?

用户在聊天框输入"帮我写一个冒泡排序"并按下发送,这在 EventStream 里对应什么事件?
Agent 执行了 `pytest tests/` 并收到测试失败信息。这个"测试失败信息"对应什么事件?
04

沙箱安全机制

为什么 AI 写的代码可以安全执行?Docker Runtime 揭秘

如果没有沙箱,会发生什么?

假设你让 AI 帮你"清理无用文件",它写出了 rm -rf /(删除全部文件),然后直接在你的电脑上执行。结果就是… 什么都没了。

这不是假设 —— 在 AI 真正获得"执行权"之前,这是一个必须解决的核心安全问题。OpenHands 的答案是:所有代码执行都发生在 Docker 容器内部,容器就是一个"一次性工作间"。

1
完全隔离

容器内的文件系统、网络、进程与宿主机完全隔离。Agent 删光容器内的文件,你的电脑丝毫无损。

2
受控权限

你的项目文件通过"挂载"方式映射到容器,Agent 只能访问你授权的目录,无法触碰其他地方。

3
彻底清理

对话结束后容器销毁,临时文件、安装的软件包、环境变量全部消失,下次任务从干净状态开始。

4
网络策略

可以配置容器的网络访问权限:只允许访问特定域名,或完全隔离网络防止数据泄露。

Agent 在沙箱里能做什么?

OpenHands 给 Agent 提供了一套"工具箱",每个工具都对应一种能力。这些工具通过 MCP(Model Context Protocol) 进行标准化描述。

bash 执行 Shell 命令:git、npm、python、curl 等任何命令行工具
read_file 读取文件内容,支持指定行范围,节省 Token
write_file 写入文件,支持整文件替换或按行号精确修改
browser 控制 Chromium 浏览器:导航、点击、填表单、截图
list_files 列出目录结构,Agent 借此快速了解项目布局
search_files 在代码库里搜索特定模式,类似 grep 但 AI 友好
🛠️
MCP 让工具可扩展

你可以给 OpenHands 添加自定义 MCP 工具,比如接入内部数据库、私有 API、或者特殊的代码分析工具。Agent 会自动发现并使用这些工具。

Agent 如何在沙箱里执行代码?

Runtime 执行流程 # Agent 生成的行动 action = CmdRunAction( command="pytest tests/ -v", timeout=60, ) # DockerRuntime 在容器里执行 obs = await runtime.run_action(action) # obs.output = "FAILED test_login" # obs.exit_code = 1 # Observation 放入 EventStream event_stream.add_event(obs)
白话翻译
Agent 把"我想运行 pytest"打包成 CmdRunAction 对象
设置 timeout=60:超过 60 秒就强制终止,防止 Agent 陷入无限循环
DockerRuntime 把命令发进容器,等待执行,收集输出
返回 Observation:测试失败了,退出码是 1(非零 = 有错误)
把这个结果扔进 EventStream,Agent 看到后会尝试修复代码

安全判断:哪个操作需要沙箱隔离?

你正在让 AI 帮你处理一批任务。哪个操作最需要在 Docker 沙箱里安全执行?
05

LLM 与工具调用

大脑是如何和手连接起来的?

LLM 的"思考-行动"循环

LLM 本质上只能生成文字。那它是怎么执行代码的?答案是 工具调用(Tool Use):LLM 生成"我要执行 X 命令"的指令,由 OpenHands 的 Runtime 真正去执行,再把结果返回给 LLM。

这个循环就像一个外科医生(LLM)和一个手术助理(Runtime)的配合:医生说"给我那把钳子",助理取来,医生再说下一步。

1

LLM 看到任务描述和工具列表

2

LLM 决定调用哪个工具、传什么参数

3

Runtime 在沙箱里执行工具调用

4

结果回传 LLM,进入下一轮推理

CodeAct:用代码作为行动语言

OpenHands 采用了一种叫 CodeAct 的范式:LLM 直接生成 Python 代码作为"行动",代码在沙箱里执行,结果返回给 LLM。

这比"用 JSON 描述工具调用"更强大:Python 代码可以包含逻辑判断、循环、复杂数据处理,把多步操作合并成一次执行。

CodeAct 示例 # LLM 生成这段 Python 代码 import os # 找到所有 .py 文件里的 TODO todos = [] for root, dirs, files in os.walk("."): for f in files: if f.endswith(".py"): content = open(f).read() if "TODO" in content: todos.append(f) print(todos)
白话翻译
LLM 不是说"请帮我搜索 TODO",而是直接写出搜索代码
os.walk 遍历所有目录和文件 —— 这是一次"文件系统探索"
只处理 .py 文件,逻辑判断直接写在代码里,无需额外工具
把所有包含 TODO 的文件名收集到列表里
打印结果 —— 这个输出会被 Runtime 捕获,返回给 LLM
🧠
为什么 CodeAct 比 JSON 工具调用更强?

JSON 工具调用每次只能调用一个工具,复杂任务需要多轮往返。Python 代码可以在一次执行里完成多步逻辑,还能使用条件判断和循环。同等任务,CodeAct 通常减少 50% 以上的 LLM 调用次数。

LLM 的"内心独白"是什么样的?

OpenHands 用 Chain of Thought 让 LLM 先"思考"再"行动"。这是一次修复 Bug 时的内心独白:

0 / 5 messages

测验:你的 AI 指令够精确吗?

你想让 OpenHands 给你的 API 添加错误处理。哪个指令会让 AI 生成更好的 CodeAct 代码?
06

当 Agent 卡住了

如何读懂 OpenHands 的行为,打破 AI 的 Bug 循环

Agent 为什么会"转圈"?

OpenHands Agent 有时会陷入循环:反复尝试同一种修复方法、不断读取同一个文件、或者做出超出能力范围的承诺然后卡死。理解为什么会发生这些,你就能有效干预。

🔄
上下文窗口溢出

对话历史太长,超过 LLM 的 Token 限制。Memory 模块压缩历史时可能丢失关键信息,导致 Agent 忘记之前的结论。

🌀
工具调用失败死循环

某个工具调用总是失败(比如权限问题),但 Agent 不断重试相同的方式。ErrorObservation 没有被正确利用。

🎯
任务描述不够具体

你说"帮我优化性能",Agent 不知道从哪入手,在多个方向间摇摆,什么都做了一点,什么都没做完。

🔀
依赖项问题

安装的包版本冲突、系统依赖缺失,Agent 尝试修复但沙箱里缺少必要工具,陷入"修了这里、坏了那里"的循环。

如何读懂 Agent 的行为日志?

OpenHands 把每一步都写入 EventStream,这是你诊断问题的最好工具。学会看这些关键信号:

找出让 Agent 卡死的那行"事件"

下面是一段真实的 EventStream 日志。点击你认为导致 Agent 循环的那一行:

1 FileReadAction: path="package.json" → OK (2.1KB)
2 CmdRunAction: "npm install" → exit_code=0
3 ErrorObservation: EACCES: permission denied '/usr/local/lib'
4 CmdRunAction: "npm install -g typescript" → exit_code=1
5 MessageAction: "Retrying installation with elevated permissions..."

打破循环的三种方法

1
直接干预:发新消息打断

直接发消息"停下,忽略之前的思路,改用方法 B"。Agent 会把你的新消息加入上下文,重新推理。

2
提供缺失信息

如果 Agent 在循环猜测某个值(API key、文件路径、权限配置),直接告诉它正确答案,比让它自己试探更高效。

3
拆解复杂任务

一个 3 分钟无进展的大任务,拆成 3 个小任务分别完成。每个步骤有明确的完成标准,AI 不容易偏离。

⚠️
当 Agent 超过 15 步还没完成

在 OpenHands 配置里,max_iterations 控制最大迭代次数(默认 100)。如果一个任务超过 15-20 步还没结束,通常意味着任务太复杂或描述不清,应该手动干预而不是继续等待。

最终测验:综合诊断

你想防止 OpenHands Agent 在卡死时消耗过多资源,应该在 config.toml 里调整哪个参数?
Agent 在帮你"重构整个后端架构",执行了 25 步后还在循环,似乎没有进展。最合理的做法是?
🎓
课程完成!你现在掌握了什么

OpenHands 的完整架构:EventStream 事件总线、Docker 沙箱隔离、CodeAct 工具调用范式、AgentController 状态机,以及如何诊断和打破 Agent 的 Bug 循环。这些知识将帮助你更自信地指挥 AI 完成复杂工程任务。