资讯动态

拆解 OpenHands(3)--- 启动:从 run_controller 到 CodeActAgent 的 EventStream 初始化链路

发布时间:2026/10/8 23:38:06 来源:尧图企业网站定制
1. 从 run_controller 到 CodeActAgentOpenHands 启动链路到底在装配什么如果你正在读 OpenHands 源码大概率会卡在同一个地方命令行敲下去之后日志刷了一屏但你不清楚run_controller到底先干了什么、后干了什么CodeActAgent是什么时候被 new 出来的EventStream又是在哪一步挂上订阅的。这篇就沿着 OpenHands 启动阶段的真实调用链走一遍把「配置加载 → 注册中心 → Agent 创建 → Runtime 连接 → EventStream 初始化 → 首条事件注入」这条线拆开让你能对着日志断点定位启动卡点。OpenHands 是一个开源的软件工程 Agent 运行时它把「用户消息」抽象成事件把「Agent 决策」抽象成 Action把「环境执行结果」抽象成 Observation三者全部通过 EventStream 流转。run_controller就是单个会话的核心入口协程负责把 LLM 注册表、CodeActAgent、Runtime、Memory、Controller 这些模块装配起来并让它们各自订阅事件流。适合谁读想二次开发 OpenHands、想接自己的模型、或者启动时遇到local proxy failed、reading choices这类报错想快速定位的开发者。我试过直接打断点跟一遍最直观的感受是启动阶段 80% 的坑都不在 Agent 逻辑里而在「配置装配」和「事件流订阅顺序」上。下面按可跟做的顺序展开每一步都给出可复制的配置片段和验证动作。2. 前置准备模型接入配置与本地启动环境在跟源码之前先把模型接入这一层配好否则run_controller跑到创建 LLM 那一步就会因为拿不到可用 endpoint 而失败。OpenHands 的 LLM 配置走的是config.toml模型侧需要一个兼容 OpenAI 协议、能返回标准choices结构的服务端点。这里我用 TaoToken 的 API 作为示例端点它的 Base URL 是https://taotoken.net/api模型 ID 按你实际要用的填。先建配置目录OpenHands 默认读取~/.openhands/config.tomlmkdir -p ~/.openhands touch ~/.openhands/config.toml然后写入下面这段可复制的 TOML。注意base_url结尾不要带/v1之外的路径model字段要和你在控制台看到的模型 ID 完全一致[core] default_agent CodeActAgent max_iterations 30 max_budget_per_task 2.0 [llm] model claude-sonnet-4-20250514 base_url https://taotoken.net/api api_key sk-你的Key temperature 0.2 timeout 120 [agent] enable_prompt_extensions true enable_browsing false [runtime] runtime_type localKey 的获取路径是控制台里的 API Keys 页面创建后复制一次即可页面不会再次明文展示。如果你还没建 Key先去 https://taotoken.net/api-keys 生成一个再回来填进api_key。环境变量方式也可以适合 CI 或容器里跑export LLM_MODELclaude-sonnet-4-20250514 export LLM_BASE_URLhttps://taotoken.net/api export LLM_API_KEYsk-你的Key配好之后先别急着跑 Agent用一条最小请求验证端点通不通curl -s https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $LLM_API_KEY \ -d { model: claude-sonnet-4-20250514, messages: [{role: user, content: reply with ok}], max_tokens: 16 }返回体里能看到choices[0].message.content就说明模型侧没问题可以进入源码链路了。这一步很关键因为后面run_controller里LLMRegistry初始化时会做一次模型可用性检查端点不通会直接抛异常日志里往往只显示一句模糊的LLM init failed容易误判成 Agent 的问题。3. run_controller 装配链路注册中心、Agent 与 EventStream 初始化这一节是全文核心。run_controller位于openhands/core/main.py它的职责可以概括成一句话把配置变成一堆互相订阅的运行时对象然后启动事件循环。下面按真实执行顺序拆。第一步是生成会话 ID 并创建注册中心。sid是整条链路的身份标识后面所有事件、日志、轨迹文件都带这个 IDsid sid or generate_sid(config) llm_registry, conversation_stats, config create_registry_and_conversation_stats( config, sid, None, )create_registry_and_conversation_stats内部做了四件事用user_settings覆盖基础配置、初始化LLMRegistry、初始化FileStore、创建ConversationStats并把两者订阅起来。关键点是最后一行llm_registry.subscribe(conversation_stats.register_llm)——每当注册表里新增一个 LLM 实例统计器就自动记录这是 OpenHands 可观测性设计的一个缩影模块之间不直接调用而是通过订阅解耦。第二步是创建 Agent。默认配置里default_agent CodeActAgent所以create_agent会走到 CodeActAgent 的构造函数agent create_agent(config, llm_registry)create_agent的逻辑是从Agent.get_cls(config.default_agent)拿到类从配置里取出该 Agent 的专属配置把主配置里的 runtime 信息透传进去最后实例化。CodeActAgent 的__init__里做了几件影响启动成败的事调用父类完成 LLM 注册和 prompt manager 初始化、reset()清空行动历史、_get_tools()拉取工具集、创建ConversationMemory、根据配置创建Condenser上下文压缩器、最后用llm_registry.get_router(config)覆盖self.llm。如果你在启动日志里看到Condenser相关报错基本就是这一步配置里的condenser字段写错了。第三步是创建 Runtime 和 Controller。Runtime 负责真正执行 bash 命令和 Python 代码Controller 负责驱动 Agent 的 step 循环。Controller 创建时会完成 EventStream 的初始化并把 Agent、Runtime、Memory 依次订阅上去。订阅顺序很重要Memory 要先于 Runtime 订阅否则首条MessageAction注入时 Memory 可能还没准备好接收RecallAction。第四步是注入首条用户事件并进入循环state await run_controller(configconfig, initial_user_actionaction)initial_user_action是一条MessageAction它被注入 EventStream 后所有订阅者收到通知Controller 调用agent.step()Agent 向 LLM 发起请求生成 Action 再注入事件流Runtime 执行后回传 Observation形成闭环。max_iterations和max_budget_per_task是两道硬闸前者限制循环次数后者限制累计花费防止任务跑飞。把这条链路画成时序就是run_controller → create_registry → create_agent(CodeActAgent) → create_runtime → create_controller → EventStream.subscribe(agent/runtime/memory) → inject(MessageAction) → loop。你可以在create_controller返回处打一个断点检查event_stream._subscribers的长度正常应该是 3 以上。4. 验证启动成功日志断点与事件流观测配好之后跑一次最小任务验证整条链路是否走通。用官方 CLI 入口python -m openhands.core.main \ -t Write a hello world program in Python \ -d ./workspace \ --config-file ~/.openhands/config.toml启动过程中重点盯这几行日志。第一行是会话 ID 生成格式类似sidabc123记下它后面轨迹文件按这个命名。第二行是LLMRegistry initialized如果这行没出现说明模型配置有问题回到第 2 节检查base_url和api_key。第三行是Agent created: CodeActAgent出现这行说明 Agent 装配成功。第四行是EventStream initialized with N subscribersN 应该是 3 或更多。最后是Injecting initial action之后就开始刷 step 日志了。想更细地观测事件流可以在EventStream的add_event方法里临时加一行打印def add_event(self, event: Event) - None: logger.debug(f[EventStream] {event.__class__.__name__} id{event.id}) # ... 原有逻辑把日志级别调到 DEBUG 再跑你就能看到事件按MessageAction → RecallAction → RecallObservation → AgentStateChanged → Action → Observation的顺序流动。这个顺序就是 OpenHands 的核心交互模型看懂它基本就理解了整个系统。任务跑完后轨迹文件会落在./workspace/sessions/sid/下里面是 JSON 格式的事件序列可以直接用jq过滤jq .[] | select(.type Action) | .action \ ./workspace/sessions/sid/trajectory.json如果任务正常结束你会看到 Agent 生成的 bash 命令和最终的 Python 文件内容。到这一步从run_controller到CodeActAgent再到EventStream的整条启动链路就算验证通过了。5. 启动阶段常见报错排查401、local proxy failed 与 reading choices启动卡点大多集中在三类报错逐个对照。第一类是401 Unauthorized。日志通常长这样LLM request failed: 401 Client Error。原因基本是api_key无效或没带上。检查config.toml里api_key字段是否为空、是否有多余空格、是否用了过期的 Key。用第 2 节的 curl 命令单独验证一次如果 curl 也 401就是 Key 本身的问题去控制台重新生成。第二类是local proxy failed或Connection refused。这类报错说明请求根本没发出去通常是base_url写错比如多写了/v1/chat/completions后缀或者把https写成了http。OpenHands 内部会自己拼接/v1/chat/completions所以base_url只写到https://taotoken.net/api即可。另外检查本机是否有环境变量HTTP_PROXY干扰有的话临时 unset 再跑。第三类是Error reading choices或KeyError: choices。这个报错说明请求发出去了、也返回了但返回体结构不符合 OpenAI 协议。常见原因是模型 ID 写错服务端返回了一个错误对象而不是标准的 chat completion 结构。检查model字段是否和控制台里的模型 ID 完全一致大小写和连字符都不能差。还有一种情况是max_tokens设得过大导致服务端截断把max_tokens降到 4096 以内再试。第四类是启动卡在Initializing runtime不动。这通常是 Runtime 类型配置问题runtime_type local时会在本地起一个执行环境如果端口被占用或权限不足就会卡住。换成runtime_type docker并确保 Docker 在运行或者检查本地端口占用。第五类是OAuth相关报错如果你用的是需要 OAuth 的模型服务检查 token 是否过期。OpenHands 的 LLM 配置支持api_key直填也支持从环境变量读取优先用环境变量方式避免配置文件泄露。排查时记住一个原则先验证模型端点通不通再验证 Agent 装配成不成功最后验证事件流订阅数量对不对。这三步分别对应第 2、3、4 节按顺序走一遍90% 的启动问题都能定位。6. 继续深入从启动链路到长期编码 Agent把启动链路跑通之后下一步通常是让 OpenHands 承接更长期的任务比如多轮代码修改、跨文件重构、或者接上 MCP 工具做自动化。这时候单次run_controller的max_iterations就不够用了需要走 Coding Plan 这类长期编码方案把会话状态持久化、支持断点续跑。如果你想把模型接入换成更稳定的端点或者需要看完整的接入参数说明可以从这几个入口进模型对话调试用 https://taotoken.net/models 接入文档在 https://taotoken.net/doc API Keys 管理在 https://taotoken.net/api-keys 长期编码和 Agent 场景看 https://taotoken.net/coding-plan 。Claude Code 相关的接入配置在 https://taotoken.net/claude-code 。最后留一个实用技巧调试启动链路时把config.toml里的max_iterations临时设成 1这样跑一次就停日志干净方便你逐行对照事件流顺序。等确认装配没问题了再调回正常值。这个习惯能帮你省掉大量翻日志的时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑