--- 啟動(dòng):從 run_controller 到 CodeActAgent 的 EventStream 初始化鏈路)
1. 從 run_controller 到 CodeActAgentOpenHands 啟動(dòng)鏈路到底在裝配什么如果你正在讀 OpenHands 源碼大概率會(huì)卡在同一個(gè)地方命令行敲下去之后日志刷了一屏但你不清楚run_controller到底先干了什么、后干了什么CodeActAgent是什么時(shí)候被 new 出來的EventStream又是在哪一步掛上訂閱的。這篇就沿著 OpenHands 啟動(dòng)階段的真實(shí)調(diào)用鏈走一遍把「配置加載 → 注冊(cè)中心 → Agent 創(chuàng)建 → Runtime 連接 → EventStream 初始化 → 首條事件注入」這條線拆開讓你能對(duì)著日志斷點(diǎn)定位啟動(dòng)卡點(diǎn)。OpenHands 是一個(gè)開源的軟件工程 Agent 運(yùn)行時(shí)它把「用戶消息」抽象成事件把「Agent 決策」抽象成 Action把「環(huán)境執(zhí)行結(jié)果」抽象成 Observation三者全部通過 EventStream 流轉(zhuǎn)。run_controller就是單個(gè)會(huì)話的核心入口協(xié)程負(fù)責(zé)把 LLM 注冊(cè)表、CodeActAgent、Runtime、Memory、Controller 這些模塊裝配起來并讓它們各自訂閱事件流。適合誰(shuí)讀想二次開發(fā) OpenHands、想接自己的模型、或者啟動(dòng)時(shí)遇到local proxy failed、reading choices這類報(bào)錯(cuò)想快速定位的開發(fā)者。我試過直接打斷點(diǎn)跟一遍最直觀的感受是啟動(dòng)階段 80% 的坑都不在 Agent 邏輯里而在「配置裝配」和「事件流訂閱順序」上。下面按可跟做的順序展開每一步都給出可復(fù)制的配置片段和驗(yàn)證動(dòng)作。2. 前置準(zhǔn)備模型接入配置與本地啟動(dòng)環(huán)境在跟源碼之前先把模型接入這一層配好否則run_controller跑到創(chuàng)建 LLM 那一步就會(huì)因?yàn)槟貌坏娇捎?endpoint 而失敗。OpenHands 的 LLM 配置走的是config.toml模型側(cè)需要一個(gè)兼容 OpenAI 協(xié)議、能返回標(biāo)準(zhǔn)choices結(jié)構(gòu)的服務(wù)端點(diǎn)。這里我用 TaoToken 的 API 作為示例端點(diǎn)它的 Base URL 是https://taotoken.net/api模型 ID 按你實(shí)際要用的填。先建配置目錄OpenHands 默認(rèn)讀取~/.openhands/config.tomlmkdir -p ~/.openhands touch ~/.openhands/config.toml然后寫入下面這段可復(fù)制的 TOML。注意base_url結(jié)尾不要帶/v1之外的路徑model字段要和你在控制臺(tái)看到的模型 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 的獲取路徑是控制臺(tái)里的 API Keys 頁(yè)面創(chuàng)建后復(fù)制一次即可頁(yè)面不會(huì)再次明文展示。如果你還沒建 Key先去 https://taotoken.net/api-keys 生成一個(gè)再回來填進(jìn)api_key。環(huán)境變量方式也可以適合 CI 或容器里跑export LLM_MODELclaude-sonnet-4-20250514 export LLM_BASE_URLhttps://taotoken.net/api export LLM_API_KEYsk-你的Key配好之后先別急著跑 Agent用一條最小請(qǐng)求驗(yàn)證端點(diǎn)通不通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就說明模型側(cè)沒問題可以進(jìn)入源碼鏈路了。這一步很關(guān)鍵因?yàn)楹竺鎟un_controller里L(fēng)LMRegistry初始化時(shí)會(huì)做一次模型可用性檢查端點(diǎn)不通會(huì)直接拋異常日志里往往只顯示一句模糊的LLM init failed容易誤判成 Agent 的問題。3. run_controller 裝配鏈路注冊(cè)中心、Agent 與 EventStream 初始化這一節(jié)是全文核心。run_controller位于openhands/core/main.py它的職責(zé)可以概括成一句話把配置變成一堆互相訂閱的運(yùn)行時(shí)對(duì)象然后啟動(dòng)事件循環(huán)。下面按真實(shí)執(zhí)行順序拆。第一步是生成會(huì)話 ID 并創(chuàng)建注冊(cè)中心。sid是整條鏈路的身份標(biāo)識(shí)后面所有事件、日志、軌跡文件都帶這個(gè) IDsid sid or generate_sid(config) llm_registry, conversation_stats, config create_registry_and_conversation_stats( config, sid, None, )create_registry_and_conversation_stats內(nèi)部做了四件事用user_settings覆蓋基礎(chǔ)配置、初始化LLMRegistry、初始化FileStore、創(chuàng)建ConversationStats并把兩者訂閱起來。關(guān)鍵點(diǎn)是最后一行l(wèi)lm_registry.subscribe(conversation_stats.register_llm)——每當(dāng)注冊(cè)表里新增一個(gè) LLM 實(shí)例統(tǒng)計(jì)器就自動(dòng)記錄這是 OpenHands 可觀測(cè)性設(shè)計(jì)的一個(gè)縮影模塊之間不直接調(diào)用而是通過訂閱解耦。第二步是創(chuàng)建 Agent。默認(rèn)配置里default_agent CodeActAgent所以create_agent會(huì)走到 CodeActAgent 的構(gòu)造函數(shù)agent create_agent(config, llm_registry)create_agent的邏輯是從Agent.get_cls(config.default_agent)拿到類從配置里取出該 Agent 的專屬配置把主配置里的 runtime 信息透?jìng)鬟M(jìn)去最后實(shí)例化。CodeActAgent 的__init__里做了幾件影響啟動(dòng)成敗的事調(diào)用父類完成 LLM 注冊(cè)和 prompt manager 初始化、reset()清空行動(dòng)歷史、_get_tools()拉取工具集、創(chuàng)建ConversationMemory、根據(jù)配置創(chuàng)建Condenser上下文壓縮器、最后用llm_registry.get_router(config)覆蓋self.llm。如果你在啟動(dòng)日志里看到Condenser相關(guān)報(bào)錯(cuò)基本就是這一步配置里的condenser字段寫錯(cuò)了。第三步是創(chuàng)建 Runtime 和 Controller。Runtime 負(fù)責(zé)真正執(zhí)行 bash 命令和 Python 代碼Controller 負(fù)責(zé)驅(qū)動(dòng) Agent 的 step 循環(huán)。Controller 創(chuàng)建時(shí)會(huì)完成 EventStream 的初始化并把 Agent、Runtime、Memory 依次訂閱上去。訂閱順序很重要Memory 要先于 Runtime 訂閱否則首條MessageAction注入時(shí) Memory 可能還沒準(zhǔn)備好接收RecallAction。第四步是注入首條用戶事件并進(jìn)入循環(huán)state await run_controller(configconfig, initial_user_actionaction)initial_user_action是一條MessageAction它被注入 EventStream 后所有訂閱者收到通知Controller 調(diào)用agent.step()Agent 向 LLM 發(fā)起請(qǐng)求生成 Action 再注入事件流Runtime 執(zhí)行后回傳 Observation形成閉環(huán)。max_iterations和max_budget_per_task是兩道硬閘前者限制循環(huán)次數(shù)后者限制累計(jì)花費(fèi)防止任務(wù)跑飛。把這條鏈路畫成時(shí)序就是run_controller → create_registry → create_agent(CodeActAgent) → create_runtime → create_controller → EventStream.subscribe(agent/runtime/memory) → inject(MessageAction) → loop。你可以在create_controller返回處打一個(gè)斷點(diǎn)檢查event_stream._subscribers的長(zhǎng)度正常應(yīng)該是 3 以上。4. 驗(yàn)證啟動(dòng)成功日志斷點(diǎn)與事件流觀測(cè)配好之后跑一次最小任務(wù)驗(yàn)證整條鏈路是否走通。用官方 CLI 入口python -m openhands.core.main \ -t Write a hello world program in Python \ -d ./workspace \ --config-file ~/.openhands/config.toml啟動(dòng)過程中重點(diǎn)盯這幾行日志。第一行是會(huì)話 ID 生成格式類似sidabc123記下它后面軌跡文件按這個(gè)命名。第二行是LLMRegistry initialized如果這行沒出現(xiàn)說明模型配置有問題回到第 2 節(jié)檢查base_url和api_key。第三行是Agent created: CodeActAgent出現(xiàn)這行說明 Agent 裝配成功。第四行是EventStream initialized with N subscribersN 應(yīng)該是 3 或更多。最后是Injecting initial action之后就開始刷 step 日志了。想更細(xì)地觀測(cè)事件流可以在EventStream的add_event方法里臨時(shí)加一行打印def add_event(self, event: Event) - None: logger.debug(f[EventStream] {event.__class__.__name__} id{event.id}) # ... 原有邏輯把日志級(jí)別調(diào)到 DEBUG 再跑你就能看到事件按MessageAction → RecallAction → RecallObservation → AgentStateChanged → Action → Observation的順序流動(dòng)。這個(gè)順序就是 OpenHands 的核心交互模型看懂它基本就理解了整個(gè)系統(tǒng)。任務(wù)跑完后軌跡文件會(huì)落在./workspace/sessions/sid/下里面是 JSON 格式的事件序列可以直接用jq過濾jq .[] | select(.type Action) | .action \ ./workspace/sessions/sid/trajectory.json如果任務(wù)正常結(jié)束你會(huì)看到 Agent 生成的 bash 命令和最終的 Python 文件內(nèi)容。到這一步從run_controller到CodeActAgent再到EventStream的整條啟動(dòng)鏈路就算驗(yàn)證通過了。5. 啟動(dòng)階段常見報(bào)錯(cuò)排查401、local proxy failed 與 reading choices啟動(dòng)卡點(diǎn)大多集中在三類報(bào)錯(cuò)逐個(gè)對(duì)照。第一類是401 Unauthorized。日志通常長(zhǎng)這樣LLM request failed: 401 Client Error。原因基本是api_key無效或沒帶上。檢查config.toml里api_key字段是否為空、是否有多余空格、是否用了過期的 Key。用第 2 節(jié)的 curl 命令單獨(dú)驗(yàn)證一次如果 curl 也 401就是 Key 本身的問題去控制臺(tái)重新生成。第二類是local proxy failed或Connection refused。這類報(bào)錯(cuò)說明請(qǐng)求根本沒發(fā)出去通常是base_url寫錯(cuò)比如多寫了/v1/chat/completions后綴或者把https寫成了http。OpenHands 內(nèi)部會(huì)自己拼接/v1/chat/completions所以base_url只寫到https://taotoken.net/api即可。另外檢查本機(jī)是否有環(huán)境變量HTTP_PROXY干擾有的話臨時(shí) unset 再跑。第三類是Error reading choices或KeyError: choices。這個(gè)報(bào)錯(cuò)說明請(qǐng)求發(fā)出去了、也返回了但返回體結(jié)構(gòu)不符合 OpenAI 協(xié)議。常見原因是模型 ID 寫錯(cuò)服務(wù)端返回了一個(gè)錯(cuò)誤對(duì)象而不是標(biāo)準(zhǔn)的 chat completion 結(jié)構(gòu)。檢查model字段是否和控制臺(tái)里的模型 ID 完全一致大小寫和連字符都不能差。還有一種情況是max_tokens設(shè)得過大導(dǎo)致服務(wù)端截?cái)喟裮ax_tokens降到 4096 以內(nèi)再試。第四類是啟動(dòng)卡在Initializing runtime不動(dòng)。這通常是 Runtime 類型配置問題runtime_type local時(shí)會(huì)在本地起一個(gè)執(zhí)行環(huán)境如果端口被占用或權(quán)限不足就會(huì)卡住。換成runtime_type docker并確保 Docker 在運(yùn)行或者檢查本地端口占用。第五類是OAuth相關(guān)報(bào)錯(cuò)如果你用的是需要 OAuth 的模型服務(wù)檢查 token 是否過期。OpenHands 的 LLM 配置支持api_key直填也支持從環(huán)境變量讀取優(yōu)先用環(huán)境變量方式避免配置文件泄露。排查時(shí)記住一個(gè)原則先驗(yàn)證模型端點(diǎn)通不通再驗(yàn)證 Agent 裝配成不成功最后驗(yàn)證事件流訂閱數(shù)量對(duì)不對(duì)。這三步分別對(duì)應(yīng)第 2、3、4 節(jié)按順序走一遍90% 的啟動(dòng)問題都能定位。6. 繼續(xù)深入從啟動(dòng)鏈路到長(zhǎng)期編碼 Agent把啟動(dòng)鏈路跑通之后下一步通常是讓 OpenHands 承接更長(zhǎng)期的任務(wù)比如多輪代碼修改、跨文件重構(gòu)、或者接上 MCP 工具做自動(dòng)化。這時(shí)候單次run_controller的max_iterations就不夠用了需要走 Coding Plan 這類長(zhǎng)期編碼方案把會(huì)話狀態(tài)持久化、支持?jǐn)帱c(diǎn)續(xù)跑。如果你想把模型接入換成更穩(wěn)定的端點(diǎn)或者需要看完整的接入?yún)?shù)說明可以從這幾個(gè)入口進(jìn)模型對(duì)話調(diào)試用 https://taotoken.net/models 接入文檔在 https://taotoken.net/doc API Keys 管理在 https://taotoken.net/api-keys 長(zhǎng)期編碼和 Agent 場(chǎng)景看 https://taotoken.net/coding-plan 。Claude Code 相關(guān)的接入配置在 https://taotoken.net/claude-code 。最后留一個(gè)實(shí)用技巧調(diào)試啟動(dòng)鏈路時(shí)把config.toml里的max_iterations臨時(shí)設(shè)成 1這樣跑一次就停日志干凈方便你逐行對(duì)照事件流順序。等確認(rèn)裝配沒問題了再調(diào)回正常值。這個(gè)習(xí)慣能幫你省掉大量翻日志的時(shí)間。