首页 / 资讯中心 / 文章详情

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

拆解 OpenHands(3)--- 启动:从 run_controller 到 AgentController 的初始化链路 ★ FEATURED ARTICLE
1. 从 run_controller 进入 OpenHands 启动链路一次会话到底装配了哪些组件如果你正在读 OpenHands 的源码或者想搞清楚「为什么我的会话起不来」那run_controller这个协程基本绕不开。它是 OpenHands 后端单个会话的核心入口负责把配置、LLM 注册中心、Agent、Runtime、Memory、MCP 工具、AgentController 和 EventStream 这一整套东西按顺序装配起来最后把用户的第一条消息注入事件流让整个系统跑起来。简单说run_controller能做的事包括生成会话 IDSID、建立运行时连接、按需克隆代码仓库、把 MCP 工具挂到 Agent 上、创建控制器、订阅事件流、发送启动事件、运行代理直到进入结束状态最后把会话状态和执行轨迹持久化。它适合谁适合正在做 OpenHands 二次开发、排查初始化失败、或者想理解事件驱动架构的工程师。你不需要把每个模块都吃透但至少要能看懂这条链路上每一步在干什么出问题时知道该往哪查。我试过在本地直接调run_controller跑一个最小会话结果第一次就卡在 Runtime 连接上日志里只有一句local proxy failed排查了半天才发现是沙盒环境没起来。所以这篇不会只讲「连上后就能用」而是把每一步的配置、验证动作和常见报错都摊开说让你能自己定位初始化失败点。OpenHands 的整体交互逻辑可以提炼成「初始化 - 事件注入 - 协同处理 - 等待」这个极简流程核心围绕 EventStream 做模块联动。用户创建会话时系统自动完成 Agent、AgentController、Runtime、Memory 等核心模块的初始化每个模块都会订阅 EventStream确保能捕获自己关心的事件。用户发消息本质上是往 EventStream 里注入一条事件这条事件触发所有订阅了相关回调的模块启动协同处理。AgentController 调用Agent.step处理当前事件生成 Action 注入事件流Agent 基于状态向 LLM 发起请求生成下一步行动行动通过 EventStream 传给 Runtime 执行执行结果作为 Observation 回传完成一次闭环。AgentController 根据 Observation 判断任务是否完成没完成就重复需要协同就委派给其他 Agent直到任务结束。这条链路里run_controller是「总装车间」它决定了各个模块的创建顺序和依赖关系。顺序错了或者某个模块订阅事件流的时机不对都会导致会话卡死或者直接报错。下面我按源码里的实际顺序把每一步拆开讲并给出可复制的配置和验证方法。2. TaoToken 前置给 OpenHands 配一个稳定的模型入口在讲run_controller的装配细节之前得先把模型入口这件事说清楚。OpenHands 本身不绑定某一家模型服务它通过 LLM 配置去调用兼容 OpenAI 接口的服务。如果你在本地跑 OpenHands又不想折腾复杂的网络环境可以用 TaoToken 作为模型入口它提供兼容 OpenAI 的 API配置起来比较直接。TaoToken 的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数直接用它作为 Base URL 就行。你需要先去控制台创建一个 API Key控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建好 Key 之后把它填到 OpenHands 的 LLM 配置里。OpenHands 的 LLM 配置通常放在config.toml里路径一般是~/.openhands/config.toml或者项目根目录下的config.toml。你需要设置base_url、api_key和model三个关键字段。模型 ID 要填 TaoToken 支持的模型名比如claude-sonnet-4-20250514或者gpt-4o这类具体以你控制台里可用的模型为准。如果你用的是 Claude Code 相关的接入方式可以参考 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的说明把 Base URL、Key 和 Model ID 三件套配齐。这里要提醒一句OpenHands 的 LLM 注册中心LLMRegistry会在run_controller一开始就被创建它负责管理所有 LLM 实例。如果你在配置里写错了base_url或者api_keyLLMRegistry初始化时可能不会立刻报错但等到 Agent 第一次调用 LLM 时就会抛出 401 或者连接超时。所以配好之后最好先用一个简单的请求验证一下模型入口是否通。你可以用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 快速测一下确认 Key 和模型 ID 都能正常工作再回到 OpenHands 里跑会话。另外如果你打算长期跑编码任务或者 Agent 工作流可以考虑 Coding Plan地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它更适合持续性的编码场景。不过这一节的重点是让你在进入run_controller之前先把模型入口配好避免后面排查问题时把配置错误和代码问题混在一起。3. 可复制配置run_controller 启动参数与 EventStream 订阅示例现在进入正题看run_controller的实际调用和配置。最直接的入口是这样一段代码config load_openhands_config() action MessageAction(contentWrite a hello world program) state await run_controller(configconfig, initial_user_actionaction)这段代码来自 OpenHands 的命令行调用方式load_openhands_config()会读取你的config.tomlMessageAction是初始用户动作run_controller返回最终状态。如果你想在脚本里控制更多参数可以显式传入sid、runtime、headless_mode、fake_user_response_fn等。run_controller的完整签名里关键参数有这些config是应用配置实例initial_user_action是包含初始用户输入的 Action 对象sid是会话 ID非必要不要手动设置错误设置可能导致 RemoteRuntime 异常runtime是可选的运行时环境实例exit_on_message控制代理请求用户消息时是否退出fake_user_response_fn接收当前状态并返回模拟用户响应适合自动化测试headless_mode表示是否以无头模式运行memory是可选的记忆系统实例conversation_instructions是传给代理的对话指令。在config.toml里和启动相关的配置项主要有[core] max_iterations 100 max_budget_per_task 10.0 save_trajectory_path ./trajectories [llm] base_url https://taotoken.net/api api_key sk-你的Key model claude-sonnet-4-20250514 [sandbox] selected_repo your-org/your-repomax_iterations和max_budget_per_task是双重安全管控防止无限循环和费用超额。save_trajectory_path决定执行轨迹保存位置如果配成文件夹会以 SID 为文件名生成 JSON。selected_repo如果填了Runtime 创建后会自动克隆这个仓库。EventStream 的订阅示例可以这样写。在run_controller内部各个模块会分别订阅事件流但如果你想在外部观察事件可以自己订阅from openhands.events.stream import EventStream from openhands.events.event import Event from openhands.events.stream import EventStreamSubscriber def on_event(event: Event) - None: print(f收到事件: {type(event).__name__}, 来源: {event.source}) event_stream runtime.event_stream event_stream.subscribe(EventStreamSubscriber.MAIN, on_event, sid)注意EventStreamSubscriber.MAIN这个订阅者 IDrun_controller自己也会用它来注册用户输入回调。如果你在外部重复订阅同一个 ID可能会覆盖掉内部回调导致代理等待用户输入时没人处理。所以外部订阅建议用自定义的订阅者 ID或者只在调试时临时订阅。AgentController 的创建是通过create_controller完成的它会传入agent、runtime、config、conversation_stats和replay_events。在AgentController.__init__里如果不是委托模式会订阅EventStreamSubscriber.AGENT_CONTROLLER回调是self.on_event。这个订阅是控制器能接收事件的前提如果订阅失败控制器就不会响应任何事件。Runtime 的初始化会注册EventStreamSubscriber.RUNTIME它只处理可运行的 Action 事件执行动作拿到 Observation 后发回事件流。Memory 初始化时注册EventStreamSubscriber.MEMORY只处理RecallAction把工作空间上下文或 microagent knowledge 加到RecallObservation里发回事件流。这几个订阅者 ID 是固定的你在排查问题时可以对照日志看哪个模块没订阅上。4. 验证请求与成功结果确认控制器已就绪配好之后怎么确认run_controller真的把控制器装配好了最直接的方法是跑一个最小会话然后看日志和状态。你可以用下面这段代码做验证import asyncio from openhands.core.main import run_controller from openhands.core.config import load_openhands_config from openhands.events.action import MessageAction async def main(): config load_openhands_config() action MessageAction(contentPrint hello world) state await run_controller(configconfig, initial_user_actionaction) print(f最终状态: {state.agent_state}) print(f会话ID: {state.session_id}) asyncio.run(main())跑起来之后你会在日志里看到几个关键节点。首先是Agent Controller Initialized: Running agent CodeActAgent, model ...这行日志说明控制器已经创建Agent 名称和模型都正确。然后是事件流里出现MessageAction来源是EventSource.USER说明启动事件已经注入。接着 Runtime 会处理 Action返回 ObservationAgentController 调用Agent.stepAgent 向 LLM 发起请求。如果模型入口配置正确你会看到 LLM 返回的响应然后 Agent 生成下一步 Action。成功的结果是代理进入FINISHED状态或者进入AWAITING_USER_INPUT等待你输入。如果配置了save_trajectory_path你会在对应目录下看到以 SID 命名的 JSON 文件里面记录了完整的执行轨迹。如果配置了file_store会话状态也会被保存下次可以用State.restore_from_session恢复。验证控制器是否就绪还可以看 EventStream 的订阅情况。你可以在run_controller返回后打印event_stream._subscribers确认RUNTIME、MEMORY、AGENT_CONTROLLER、MAIN这几个订阅者都在。如果某个订阅者缺失说明对应模块初始化时出了问题。比如AGENT_CONTROLLER缺失可能是create_controller抛了异常但被吞掉了RUNTIME缺失可能是 Runtime 连接失败。还有一个实用的验证动作用fake_user_response_fn做自动化测试。传入一个函数它接收当前状态并返回模拟用户响应这样代理在AWAITING_USER_INPUT时不会真的等你输入而是自动继续。这能帮你快速验证整条链路是否通畅而不用手动交互。def fake_response(state): return 继续执行 state await run_controller( configconfig, initial_user_actionaction, fake_user_response_fnfake_response, )如果这条链路能跑通说明run_controller的装配基本没问题。接下来就是排查那些常见的初始化错误。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth初始化阶段最容易遇到的几个报错我按实际踩过的坑列一下并给出排查方向。401 Unauthorized这个通常出现在 Agent 第一次调用 LLM 时。日志里会看到401或者invalid api key。排查步骤先确认config.toml里的base_url是https://taotoken.net/api注意不要多加路径再确认api_key是完整的没有多余空格最后确认model字段填的模型 ID 在 TaoToken 控制台里可用。如果用的是环境变量检查变量名是否和 OpenHands 读取的一致。你可以先用模型对话页面单独测一下 Key排除 Key 本身的问题。local proxy failed这个报错一般出现在 Runtime 连接阶段。OpenHands 的 Runtime 可能依赖本地沙盒或容器环境如果沙盒没起来或者端口被占用就会报local proxy failed。排查步骤确认 Docker 或沙盒服务在运行检查config.toml里sandbox相关配置比如use_host_network、runtime_container_image是否正确看日志里 Runtime 的connect方法有没有超时。如果是 RemoteRuntime检查runtime参数传入的地址是否可达。reading choices这个报错通常和 LLM 响应解析有关。日志里可能出现Error reading choices或者choices字段为空。原因可能是模型返回格式不符合预期或者base_url指向的服务返回了非标准响应。排查步骤确认base_url是兼容 OpenAI 接口的地址检查模型 ID 是否正确有些模型名不被支持会返回错误结构看 LLM 请求的原始响应可以在LLMRegistry里加日志打印response.json()。如果用的是 TaoToken确认模型 ID 和控制台里的一致。OAuth 相关报错如果你在 OpenHands 里配置了 Git 仓库克隆可能会遇到 OAuth 失败。日志里会出现OAuth或者authentication failed。排查步骤确认git_provider_tokens是否正确传入检查selected_repo是否有权限访问如果是私有仓库确认 token 有repo权限。OAuth 失败不会直接导致run_controller崩溃但会导致仓库克隆失败repo_directory为 None后续 Memory 加载 microagent 时会缺少仓库上下文。除了这些还有一个隐蔽的问题EventStream 订阅顺序。如果某个模块在订阅之前就有事件注入它会错过这些事件。比如run_controller在创建 Memory 之前就发送了启动事件Memory 就收不到这条事件。实际上run_controller的顺序是先创建所有模块并订阅最后才发送启动事件所以正常情况不会错过。但如果你自己改代码把event_stream.add_event提前了就会出现模块收不到事件的问题。排查时建议打开 DEBUG 日志run_controller里有一行logger.debug(fAgent Controller Initialized: ...)能看到 Agent 名称、模型和初始动作。如果这行日志没出现说明create_controller之前就出错了。如果出现了但后续没动静说明事件流订阅或 Agent 执行环节有问题。6. 语义一致 CTA把启动链路跑通之后把run_controller这条链路跑通之后你基本就掌握了 OpenHands 启动阶段的核心。接下来如果要继续深入可以关注 AgentController 的状态机转移、Memory 的 microagent 加载机制、以及 MCP 工具的注入过程。这些都是在run_controller装配完成后才真正开始工作的。如果你在配置模型入口时遇到问题可以先去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 状态再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查 Base URL 和 Model ID 的写法。如果是长期跑编码任务Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 会更合适。验证模型是否可用直接用模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 测一下最快。启动链路里最容易被忽略的是 Runtime 的connect调用它是同步调用异步方法的call_async_from_sync(runtime.connect)如果这一步卡住后面所有模块都创建不了。所以排查时先确认 Runtime 连接成功再看 Agent 和 Controller 的创建日志。控制器就绪的标志是AgentController订阅了EventStreamSubscriber.AGENT_CONTROLLER并且启动事件成功注入事件流。你可以在这两个点加日志快速判断链路走到哪一步了。
阅读完成 · 觉得有帮助?
咨询建站