AI Agent 框架探秘:拆解 OpenHands(7)--- Agent

AI Agent 框架探秘:拆解 OpenHands(7)--- Agent

目录

  • [AI Agent 框架探秘:拆解 OpenHands(7)--- Agent](#AI Agent 框架探秘:拆解 OpenHands(7)--- Agent)
    • [0x00 摘要](#0x00 摘要)
    • [0x01 状态管理](#0x01 状态管理)
      • [1.1 设计要点](#1.1 设计要点)
      • [1.2 State类](#1.2 State类)
    • [0x02 Agent系统](#0x02 Agent系统)
      • [2.1 基类](#2.1 基类)
      • [2.2 Agent 类型](#2.2 Agent 类型)
    • [0x03 State](#0x03 State)
      • [3.1 特色](#3.1 特色)
      • [3.2 State 定义](#3.2 State 定义)
        • [3.2.1 OpenHands 的 State](#3.2.1 OpenHands 的 State)
        • [3.2.2 其他实现](#3.2.2 其他实现)
      • [3.3 生命周期](#3.3 生命周期)
      • [3.4 联系](#3.4 联系)
        • [3.4.1 State 与AgentController的联系](#3.4.1 State 与AgentController的联系)
        • [3.4.2 State 与 Observation/Action 的关系](#3.4.2 State 与 Observation/Action 的关系)
        • [3.4.3 State 的共享](#3.4.3 State 的共享)
      • [3.5 持久化和恢复](#3.5 持久化和恢复)
      • [3.6 小结](#3.6 小结)
    • [0x04 大模型适配层(LLM Adapter)](#0x04 大模型适配层(LLM Adapter))
      • [4.1 LLM](#4.1 LLM)
        • [4.1.1 作用](#4.1.1 作用)
        • [4.1.2 代码](#4.1.2 代码)
      • [4.2 LLMRegistry](#4.2 LLMRegistry)
        • [4.2.1 作用](#4.2.1 作用)
        • [4.2.2 工作流](#4.2.2 工作流)
        • [4.2.3 代码](#4.2.3 代码)
    • [0xFF 参考](#0xFF 参考)

0x00 摘要

An LLM agent runs tools in a loop to achieve a goal.

智能体(Agent)是一种能够感知和理解环境,并使用工具来实现目标的应用程序。LLM能够动态指导自己的过程和工具使用,保持对任务完成方式的控制。Agent的设计旨在更灵活地处理某些任务,其决策由模型决定,而非预定义的规则。

借助 CodeAct 的 LLM 智能体,OpenHands 通过交互式的多轮流程,展现出显著的优势:

  • 智能体能够接收新的观察数据,并据此优化先前的行动方案。这类似于人类在任务执行中,依据新信息灵活调整策略的过程。
  • 依托记忆与反馈机制,智能体可随时间提升自身性能。它能将过往经验铭记于心,并在后续任务中加以运用,不断进步,恰似一名持续学习成长的学生。
  • 此外,智能体还能胜任复杂的流程任务,涵盖模型训练、数据可视化以及自动化决策等。这表明 CodeAct 不仅能处理基础任务,更能驾驭高级且复杂的作业,例如训练机器学习模型、绘制图表以及实施自动决策等。

因为本系列借鉴的文章过多,可能在参考文献中有遗漏的文章,如果有,还请大家指出。

0x01 状态管理

1.1 设计要点

多任务并发执行时,任务状态易出现冲突;长流程任务的中间状态(如已完成子任务、待处理步骤)易丢失;异常中断后难以精准恢复到中断前的状态。因此,需要有一个数据结构来维护Agent的状态,这就是State。

在每个 Session(我们的对话线程)中,state 属性就像智能体专用于该特定交互的草稿板,是智能体存储和更新对话期间所需动态细节的地方。

状态管理的价值在于可追溯与可恢复:任何时刻都能回答"当前在哪一步、为什么这样做、结果如何、接下来做什么"。在出现错误或需要人工介入时,可以精确定位问题并从断点恢复。状态设计最佳实践如下:

  • 最小主义: 仅存储必要的、动态的数据。
  • 序列化: 使用基本的、可序列化的类型。
  • 描述性键和前缀: 使用清晰的名称和适当的前缀(user:app:temp: 或无前缀)。
  • 浅层结构: 尽可能避免深层嵌套。
  • 标准更新流程: 依赖 append_event

1.2 State类

State类作为全面的数据容器,用于跟踪 OpenHands 系统中智能体的运行状态。它维护智能体运行、演进和从会话中恢复所需的所有关键信息,包括对话历史、运行指标、迭代控制和错误记录。save_to_session方法支持智能体状态的持久化存储,允许在不同的执行实例之间恢复会话和保持连续性。

对于需要长时间运行且状态不断变化的 Agent 任务来说,State的作用至关重要。

  • 首先,它是 Agent 决策的核心依据,尤其是完整的历史事件记录,为 Agent 提供了不可或缺的上下文信息,让决策不再盲目;
  • 其次,外部系统(比如用户界面或控制器)可以通过调整State来管理 Agent 的生命周期,实现暂停、恢复、终止等操作;
  • 更重要的是,State可以被序列化存储,当任务因意外中断时,系统能通过加载存储的State,让任务从断点处精确恢复,完美解决了长周期任务的连续性问题。

0x02 Agent系统

2.1 基类

实际上,我们要实现一个 AI Agent,最简单的就是以 ReAct 为基础,去构建一个不断循环的推理(Reason),行动(Act)和观察(Observe)。

而在 OpenHands 的技术体系中,Agent 系统凭借高度灵活的模块化架构,实现了多样化专业 Agent 的开发与适配。这一设计的根基,是一个名为Agent的抽象基类 ------ 它就像所有 Agent 的 "通用模板",不仅规定了必须实现的核心接口,还封装了各类 Agent 都需要的基础功能,确保了不同 Agent 在系统中的兼容性。

所有 Agent 都必须遵循Agent基类的规范,其中最核心的就是step()方法。这个方法如同 Agent 的 "决策入口",接收State作为输入,经过内部逻辑处理后输出Action。这种清晰简洁的接口设计,让系统能轻松接入新的 Agent 实现,或是在不同 Agent 之间切换,大大提升了扩展性。

python 复制代码
class Agent(ABC):
    DEPRECATED = False
    """
    This abstract base class is an general interface for an agent dedicated to
    executing a specific instruction and allowing human interaction with the
    agent during execution.
    It tracks the execution status and maintains a history of interactions.
    """

    _registry: dict[str, type['Agent']] = {}
    sandbox_plugins: list[PluginRequirement] = []

    config_model: type[AgentConfig] = AgentConfig
    """Class field that specifies the config model to use for the agent. Subclasses may override with a derived config model if needed."""

    def __init__(
        self,
        config: AgentConfig,
        llm_registry: LLMRegistry,
    ):
        self.llm = llm_registry.get_llm_from_agent_config('agent', config)
        self.llm_registry = llm_registry
        self.config = config
        self._complete = False
        self._prompt_manager: 'PromptManager' | None = None
        self.mcp_tools: dict[str, ChatCompletionToolParam] = {}
        self.tools: list = []
        
    @abstractmethod
    def step(self, state: 'State') -> 'Action':
        """Starts the execution of the assigned instruction. This method should
        be implemented by subclasses to define the specific execution logic.
        """
        pass        

2.2 Agent 类型

在 OpenHands 的智能体系中,针对不同任务场景设计了多种专业化 Agent,它们如同分工明确的 "岗位专员",各自承载着独特的功能使命,共同支撑起系统的多样化能力。

  1. CodeActAgent。作为系统的核心力量,CodeActAgent 践行了 CodeAct 的核心理念,将所有行动统一到代码层面,具备极强的通用性。它主要负责处理各类代码相关任务,既能执行 bash 命令,也能运行 Python 代码。其工作原理很巧妙:先向大语言模型提供文件读写、命令执行等 "工具" 的详细定义,再借助模型的函数调用或工具调用能力,让模型根据任务需求自主选择合适的工具完成操作,堪称系统中无所不能的 "技术骨干"。
  2. BrowsingAgent。专注于网页交互任务的 BrowsingAgent,就像一位专业的网页操作专员。它会把网页的无障碍树作为上下文信息传递给大语言模型,帮助模型理解网页的结构布局。同时,它还提供了点击、填写表单、滚动页面等一系列网页交互动作,模型通过分析无障碍树制定操作策略,由它来精准执行,高效完成网页相关的任务。
  3. ReadOnlyAgent。这是一位坚守 "不修改原则" 的特殊 Agent。它的核心特点是只读不写,只能进行查看类操作,不会执行任何可能改变系统状态或修改数据的动作,在需要保障系统安全、避免数据被误改的场景中发挥着重要作用。
  4. VisualBrowsingAgent。作为 BrowsingAgent 的 "视觉增强版",VisualBrowsingAgent 具备处理视觉信息的能力。它不仅能理解网页的结构,还能识别网页中的图像等视觉内容,针对需要分析视觉元素的网页任务,比如识别图片中的信息、基于视觉布局进行操作等,它能展现出独特的优势。
  5. DummyAgent。DummyAgent 是一个结构简单的 Agent,主要承担测试任务。它就像系统的 "测试道具",开发者可以通过它验证系统的基础功能和交互逻辑,为其他 Agent 的开发和调试提供支持,是保障系统稳定性的重要辅助角色。
  6. LocAgent。LocAgent 实现了LocAgent: Graph-Guided LLM Agents for Code Localization 的Agent。LocAgent 首先将代码库解析为一个异构图表示,其中包含了多种类型的代码实体及其依赖关系。在此基础上,系统构建了分层稀疏索引,这不仅支持了高效的内容检索,还使得结构化的探索成为可能。借助这些索引,LocAgent 能够结合图结构与工具接口,执行由 Agent 驱动的逐步搜索过程,从而精准地完成代码定位任务。这种多跳推理的方式,使得 LocAgent 能够逐步接近目标代码,实现高效的代码定位。

作为系统的 "智能决策者",每个 Agent 的核心动力都来自大型语言模型(LLM)。它的工作目标十分明确:基于当前任务的完整上下文信息(也就是State对象),判断并输出下一步该执行的具体操作(即Action)。这种将决策逻辑完全 "打包" 在 Agent 内部的设计,是实现系统模块化的关键 ------ 就像不同的专业工具各司其职,Agent 只需专注于自己的决策任务,无需干扰其他组件。而agenthub的存在更让这套体系如虎添翼,它如同一个 "Agent 人才库",汇集了具备不同专业技能的 Agent,系统可根据具体任务需求灵活选择或委托相应的 Agent 来处理。

每个 Agent 被设计成一个循环,在每次迭代中,通过调用 agent.step() 方法,以状态 (State)作为输入,输出动作 (Actions)来执行操作或命令,在执行动作的后可能接收到的观察 (Observations)结果。

在实现的过程中,每个 Agent 类都必须实现 step 和 search_memory 方法,以便执行指令和从记忆中查询信息。该抽象类还提供了一些辅助方法,如 reset、register、get_cls、list_agents,帮助管理 Agent 的状态及其注册信息。

而一个Agent最简驱动流程如下。

python 复制代码
while True:
  prompt = agent.generate_prompt(state)
  response = llm.completion(prompt)
  action = agent.parse_response(response)
  observation = runtime.run(action)
  state = state.update(action, observation)

0x03 State

State 对象是 Agent 执行任务时所依赖的关键信息的集合。它包括以下内容:

  • Agent 采取的动作的历史记录,以及这些动作产生的观察结果(例如文件内容、命令输出)。
  • 自最近一步以来发生的一系列动作和观察的轨迹。
  • 一个 plan 对象,包含主要目标。Agent 可以通过 AddTaskAction 和 ModifyTaskAction 来添加和修改子任务。

3.1 特色

OpenHands State的主要特色如下:

  1. 全面的状态跟踪:捕获智能体操作的所有方面,从当前状态和对话历史到性能指标和错误记录。
  2. 多智能体支持 :通过delegate_level和父指标快照包含委托层级跟踪,用于协调多智能体操作。
  3. 持久化机制:通过 pickle 序列化和 base64 编码提供可靠的会话保存,并处理遗留状态文件的向后兼容性。
  4. 运行控制:整合迭代和预算控制标志,管理资源使用并防止无限循环。
  5. 可扩展性 :包含extra_data字段用于特定任务信息,使类适用于不同的使用场景。
  6. 状态转换:跟踪当前状态和恢复状态,管理智能体生命周期(LOADING、RUNNING、PAUSED 等)。
  7. 历史管理:通过起始 / 结束索引维护事件历史,跟踪相关对话片段。

3.2 State 定义

3.2.1 OpenHands 的 State

State表示 OpenHands 系统中代理的运行状态,保存其操作和记忆的数据,实际上聚合了Agent做出决策所需要的所有信息:

  • 多代理/委托状态:
    • 存储任务(代理与用户之间的对话)
    • 子任务(代理与用户或其他代理之间的对话)
    • 全局和局部迭代次数
    • 多代理交互的委托层级数
    • 几乎卡住的状态
  • 代理的运行状态:
    • 当前代理状态(例如,加载中、运行中、已暂停)
    • 流量控制状态,用于速率限制
    • 确认模式
    • 遇到的最新错误
  • 保存和恢复代理的数据:
    • 保存和从会话中恢复
    • 使用 pickle 和 base64 序列化
  • 保存/恢复关于消息历史的数据:
    • 代理历史中事件的开始和结束 ID
    • 摘要和委托摘要
  • 指标:
    • 当前任务的全局指标
    • 当前子任务的局部指标
  • 额外数据:
    • 额外的任务特定数据"

具体代码如下。

python 复制代码
@dataclass
class State:
    """表示OpenHands系统中智能体的运行状态,保存其操作和内存数据。"""

    session_id: str = ''  # 当前会话的唯一标识符
    user_id: Optional[str] = None  # 与会话相关联的用户标识符
    iteration_flag: IterationControlFlag = field(  # 控制迭代限制和进度
        default_factory=lambda: IterationControlFlag(
            limit_increase_amount=100, current_value=0, max_value=100
        )
    )
    conversation_stats: Optional[ConversationStats] = None  # 关于对话历史的统计信息
    budget_flag: Optional[BudgetControlFlag] = None  # 控制资源预算限制
    confirmation_mode: bool = False  # 智能体在执行操作前是否需要确认
    history: List[Event] = field(default_factory=list)  # 智能体操作中的事件记录
    inputs: Dict = field(default_factory=dict)  # 存储智能体的输入参数
    outputs: Dict = field(default_factory=dict)  # 存储智能体生成的输出结果
    agent_state: AgentState = AgentState.LOADING  # 智能体当前的运行状态
    resume_state: Optional[AgentState] = None  # 暂停后要返回的状态

    # 根智能体的层级为0,每个委托层级增加1
    delegate_level: int = 0  # 多智能体委托中的层级结构
    # start_id和end_id跟踪历史中事件的范围
    start_id: int = -1  # 历史中相关事件的起始索引
    end_id: int = -1  # 历史中相关事件的结束索引

    parent_metrics_snapshot: Optional[Metrics] = None  # 父智能体指标的快照
    parent_iteration: int = 100  # 来自父智能体的迭代计数

    # 注意:控制器使用此字段跟踪委托前父级的指标快照
    # 评估任务存储跟踪任务进度/状态所需的额外数据
    extra_data: Dict[str, Any] = field(default_factory=dict)  # 特定于任务的附加数据
    last_error: str = ''  # 最近遇到的错误记录

    # 注意:已弃用的参数,暂时保留以确保向后兼容性
    # 将在30天后移除
    iteration: Optional[int] = None  # 已弃用:使用iteration_flag替代
    local_iteration: Optional[int] = None  # 已弃用:本地迭代计数器
    max_iterations: Optional[int] = None  # 已弃用:最大迭代限制
    traffic_control_state: Optional[TrafficControlState] = None  # 已弃用:速率限制状态
    local_metrics: Optional[Metrics] = None  # 已弃用:使用metrics替代
    delegates: Optional[Dict[Tuple[int, int], Tuple[str, str]]] = None  # 已弃用:委托跟踪

    metrics: Metrics = field(default_factory=Metrics)  # 当前任务的性能指标

    def save_to_session(
        self, sid: str, file_store: FileStore, user_id: Optional[str]
    ) -> None:
        """将当前状态保存到持久存储中,以便以后检索。
        
        参数:
            sid: 与此状态相关联的会话ID
            file_store: 用于保存的存储系统
            user_id: 与此会话相关联的用户ID
        """
        # 暂时移除对话统计信息,因为它们自行处理持久性
        conversation_stats = self.conversation_stats
        self.conversation_stats = None  

        # 序列化状态对象
        pickled = pickle.dumps(self)
        logger.debug(f'Saving state to session {sid}:{self.agent_state}')
        encoded = base64.b64encode(pickled).decode('utf-8')
        
        try:
            # 将编码后的状态写入文件存储
            file_store.write(
                get_conversation_agent_state_filename(sid, user_id), encoded
            )

            # 在SaaS/远程环境中清理旧的状态文件
            if user_id:
                old_filename = get_conversation_agent_state_filename(sid)
                try:
                    file_store.delete(old_filename)
                except Exception:
                    pass  # 删除旧文件时忽略错误
        except Exception as e:
            logger.error(f'Failed to save state to session: {e}')
            raise e
        finally:
            # 恢复对话统计信息引用
            self.conversation_stats = conversation_stats

3.2.2 其他实现

在其他的Agent系统中,也可以用一个保存键值对的集合(字典或 Map)来实现state。它用于存放智能体为让当前对话顺利进行需要记住或追踪的信息:

  • 个性化交互: 记住之前提到的用户偏好(例如,'user_preference_theme': 'dark')。
  • 跟踪任务进度: 在多轮过程中跟踪步骤(例如,'booking_step': 'confirm_payment')。
  • 积累信息: 构建列表或摘要(例如,'shopping_cart_items': ['book', 'pen'])。
  • 做出明智决策: 存储影响下一个响应的标志或值(例如,'user_is_authenticated': True)。

状态键上的前缀定义了它们的作用域和持久性行为,特别是对于持久性服务:

  • 无前缀(会话状态):
    • 作用域: 特定于当前 会话(id)。
    • 持久性: 仅在 SessionService 是持久性的(DatabaseVertexAI)时才持久化。
    • 使用案例: 跟踪当前任务中的进度(例如,'current_booking_step')、此次交互的临时标志(例如,'needs_clarification')。
    • 示例: session.state['current_intent'] = 'book_flight'
  • user: 前缀(用户状态):
    • 作用域: 绑定到 user_id,在该用户的所有 会话中共享(在同一个 app_name 内)。
    • 持久性:DatabaseVertexAI 中持久化。(由 InMemory 存储但在重启时丢失)。
    • 使用案例: 用户偏好(例如,'user:theme')、个人资料详情(例如,'user:name')。
    • 示例: session.state['user:preferred_language'] = 'fr'
  • app: 前缀(应用状态):
    • 作用域: 绑定到 app_name,在该应用程序的所有用户和会话中共享。
    • 持久性:DatabaseVertexAI 中持久化。(由 InMemory 存储但在重启时丢失)。
    • 使用案例: 全局设置(例如,'app:api_endpoint')、共享模板。
    • 示例: session.state['app:global_discount_code'] = 'SAVE10'
  • temp: 前缀(临时会话状态):
    • 作用域: 特定于当前会话处理轮次。
    • 持久性: 从不持久化。 保证被丢弃,即使使用持久性服务。
    • 使用案例: 仅在立即需要的中间结果、你明确不想存储的数据。
    • 示例: session.state['temp:raw_api_response'] = {...}

智能体代码通过单一的 session.state 集合(dict/Map)与合并后的 状态交互。SessionService 会根据前缀从正确的底层存储获取/合并状态。

3.3 生命周期

State类的生命周期如下:

3.4 联系

state和其他组件或者数据结构的联系如下。

3.4.1 State 与AgentController的联系

在AgentController._step()中,Agent通过State获取信息。

python 复制代码
    async def _step(self) -> None:
        """Executes a single step of the parent or delegate agent. Detects stuck agents and limits on the number of iterations and the task budget."""
        if self.get_agent_state() != AgentState.RUNNING:
            self.log(
                'debug',
                f'Agent not stepping because state is {self.get_agent_state()} (not RUNNING)',
                extra={'msg_type': 'STEP_BLOCKED_STATE'},
            )
            return

3.4.2 State 与 Observation/Action 的关系

Observation 更新 State。当 Environment 返回 Observation 时,Observation 被添加到 State 历史中。

python 复制代码
    def add_history(self, event: Event):
        # if the event is not filtered out, add it to the history
        if self.agent_history_filter.include(event):
            self.state.history.append(event)

Agent 基于当前 State 生成 Action。

python 复制代码
action = self.agent.step(self.state)

另外,虽然前端的 FooterContent 组件不直接使用 State,但整个前端界面的状态管理依赖于后端 State 的同步Backend State -> WebSocket -> Frontend State -> UI Updates。前端组件根据接收到的 State 信息更新界面状态和可用操作。

3.4.3 State 的共享

State 分为全局状态和局部状态。全局指标在委托间共享,比如。

python 复制代码
    async def start_delegate(self, action: AgentDelegateAction) -> None:
        """启动一个委托智能体来处理子任务。

        OpenHands 是一个多智能体系统。`任务(task)` 指 OpenHands(整个系统)与用户之间的对话,
        可能包含用户的一个或多个输入。它始于用户的初始输入(通常是任务说明),结束于以下三种情况:
        智能体发起的 `AgentFinishAction`、用户发起的停止操作,或出现错误。

        `子任务(subtask)` 指智能体与用户之间,或智能体与其他智能体之间的对话。如果一个 `任务`
        由单个智能体执行,那么它同时也是一个 `子任务`。否则,一个 `任务` 由多个 `子任务` 组成,
        每个子任务由一个智能体执行。

        参数:
            action (AgentDelegateAction):包含要启动的委托智能体信息的动作对象
        """
        # 根据动作中指定的智能体类型获取对应的智能体类
        agent_cls: type[Agent] = Agent.get_cls(action.agent)
        # 获取该智能体的配置(优先使用动作指定的配置,否则使用当前智能体的配置)
        agent_config = self.agent_configs.get(action.agent, self.agent.config)
        # 确保父智能体与子智能体共享指标,以实现全局累积
        delegate_agent = agent_cls(
            config=agent_config, llm_registry=self.agent.llm_registry
        )

        # 在启动委托智能体前,对当前指标进行快照
        state = State(
            session_id=self.id.removesuffix('-delegate'),  # 会话ID(移除委托后缀)
            user_id=self.user_id,  # 关联的用户ID
            inputs=action.inputs or {},  # 子任务的输入参数(默认为空字典)
            iteration_flag=self.state.iteration_flag,  # 继承迭代控制标志
            budget_flag=self.state.budget_flag,  # 继承预算控制标志
            delegate_level=self.state.delegate_level + 1,  # 委托层级在父级基础上加1
            # 全局指标在父智能体与子智能体间共享
            metrics=self.state.metrics,
            # 从事件流的最新位置开始记录新事件
            start_id=self.event_stream.get_latest_event_id() + 1,
            # 记录委托前父智能体的指标快照
            parent_metrics_snapshot=self.state_tracker.get_metrics_snapshot(),
            # 记录父智能体当前的迭代次数
            parent_iteration=self.state.iteration_flag.current_value,
        )

不同层级的控制标志不同,比如在上面代码中也有体现:

python 复制代码
self.state.iteration_flag # 全局迭代控制

self.state.budget_flag # 全局预算控制

3.5 持久化和恢复

OpenHands 通过StateTracker管理状态的持久化,支持会话中断后的状态恢复,确保任务连续性。比如,save_state保存状态到存储。

python 复制代码
class StateTracker:
    """管理并同步智能体在其生命周期内的状态。

    它负责:
    1. 维持智能体状态在多个会话间的持久性
    2. 通过过滤和跟踪相关事件来管理智能体历史(以前由智能体控制器执行)
    3. 在控制器和LLM组件之间同步指标
    4. 更新预算和迭代限制的控制标志
    """

    def __init__(
        self, sid: str | None, file_store: FileStore | None, user_id: str | None
    ):
        self.sid = sid  # 会话ID,用于标识当前会话
        self.file_store = file_store  # 文件存储对象,用于持久化状态
        self.user_id = user_id  # 用户ID,关联到特定用户

        # 过滤掉与智能体无关的事件
        # 这些事件将不被包含在智能体历史中
        self.agent_history_filter = EventFilter(
            exclude_types=(
                NullAction,  # 排除空动作事件
                NullObservation,  # 排除空观察事件
                ChangeAgentStateAction,  # 排除更改智能体状态的动作事件
                AgentStateChangedObservation,  # 排除智能体状态已更改的观察事件
            ),
            exclude_hidden=True,  # 排除隐藏事件
        )

3.6 小结

State 在 OpenHands 系统中起到以下关键作用:

  • 信息中枢:聚合所有决策所需信息

  • 控制中心:管理迭代、预算等控制流程

  • 记忆载体:维护交互历史和上下文

  • 协调机制:支持多 Agent 委托和状态共享

  • 持久化基础:支持会话恢复和状态保存

  • 接口桥梁:连接后端逻辑和前端展示

State 是 OpenHands 系统的 "大脑",确保了整个智能体系统的连贯性和智能决策能力。

0x04 大模型适配层(LLM Adapter)

回归 AI Agent 的根本,其实就是 Loop+Tokens,我们拆解来看看:

  1. Loop:其实也就是循环,类比人类解决一个问题,就是不断去尝试,直到解决,这就是一个循环,只不过循环长短不同。
  2. Tokens:Loop 中不断的去让大模型思考决策,行动,和收集反馈信息继续下次的计划和执行。

因此,大语言模型是 OpenHands 的 "智能核心",将 LLM 作为动态调度器的设计,是当前 AI Agent 领域的核心实现范式。即,将LLM做为一个主动的任务规划与函数调用引擎。这种模式带来了两大优势:

  • 能力的涌现:由于执行计划是LLM动态生成的,agent能够执行开发者从未明确编码过的行为,甚至自主决定编写并执行一个脚本来完成任务。
  • **业务逻辑复杂度降低:**开发者只需不断增加原子能力工具,复杂的业务编排逻辑交给 LLM 来决定,整体代码复杂度得以降低。

此架构范式让应用的能力上限不再受限于开发者预设的控制流,而是取决于 AI 在运行时对可用工具的动态组合与调用,为实现能处理复杂、多步任务,并具备一定自主性的通用 AI agent 提供了可参考的实现路径。

OpenHands 框架通过模块化的设计,实现了对主流 LLM 的无缝集成,既支持云端模型的便捷调用,也兼容本地部署的隐私化需求。

在云端集成方面,OpenHands 系统提供了统一的接口层,封装了 OpenAI、Azure、Mistral AI 等平台的 API 差异。开发者只需配置相应的 API 密钥和模型参数,框架就能自动适配不同模型的输入输出格式,实现 "一键切换"。这种设计的优势在于:当某一模型因负载过高响应缓慢时,系统可自动切换到备用模型,确保任务不中断;同时,也允许开发者根据任务特性选择最适合的模型(例如用代码生成能力突出的模型处理编程任务,用多模态模型处理包含图文的需求)。

对于对数据隐私有严格要求的场景(如医疗、金融领域),OpenHands 支持通过 Ollama 部署本地大语言模型。只需一台配备 GPU 的服务器,开发者就能将模型运行在私有环境中,所有数据处理均在本地完成,避免了敏感信息上传至云端的风险。框架会自动检测本地 GPU 的算力,推荐适配的模型版本,并优化推理参数以平衡速度与精度。

4.1 LLM

4.1.1 作用

LLM 类是 OpenHands 框架中语言模型的核心封装类,继承自重试混入类(RetryMixin)和调试混入类(DebugMixin),提供了统一的大语言模型调用接口。其核心职责是整合 LiteLLM 工具的多模型适配能力,处理模型配置解析、请求参数格式化、函数调用模拟、重试机制、日志记录、成本与延迟统计等全流程逻辑。

LLM 类为各类语言模型提供了统一接口,通过 LiteLLM 支持 100 余种模型提供商,并提供两大 API:一是保证广泛兼容性的标准对话补全 API(Chat Completions API),二是适配最新推理模型的 OpenAI 响应 API(Responses API)。

  • 原生支持推理 / 扩展思考能力:SDK 能够捕获并处理前沿模型的高级原生推理字段 ------ 例如 Anthropic 模型的扩展思考字段 ThinkingBlock、OpenAI 模型的推理字段 ReasoningItemModel。SDK 为智能体透明化支持 OpenAI 响应 API,使客户端开发者可直接使用仅在该 API 开放的先进推理模型(如 GPT-5-Codex)。
  • 内置非函数调用模型支持:针对不原生支持函数调用的模型,SDK 实现了 NonNativeToolCallingMixin 混合类 ------ 将工具 schema 转换为基于文本的提示指令,并通过结构化提示与正则提取技术,从模型输出中解析工具调用指令。这一设计使无函数调用能力的模型也能胜任智能体任务,大幅拓展了可用模型范围。
  • 多 LLM 路由支持 :SDK 内置 RouterLLM(LLM 子类),允许智能体为不同的 LLM 请求匹配不同模型。开发者可通过自定义扩展 RouterLLM 并实现 select_llm () 方法,基于输入内容动态选择适配模型,其适配标准如下。
    • **性能:**某些模型在特定任务(例如,编程、推理、创意写作)方面表现出色。
    • **成本:**不同模型具有不同的价格点。
    • **功能:**模型提供多样化的功能、上下文窗口大小和微调选项。
    • **可用性/冗余:**拥有替代方案可以确保即使一个提供商出现问题,应用程序仍能正常运行。
  • 完善的工程化能力:内置重试机制(支持失败重试、延迟策略)、请求 / 响应日志记录(可持久化到文件)、性能指标统计(延迟、成本),同时支持安全设置、缓存提示词等实用功能。
  • 函数调用灵活支持:对不原生支持函数调用的模型,提供基于提示词的模拟转换能力;对支持函数调用的模型,自动适配其参数格式,无需开发者关注底层差异。
  • 配置化驱动 :通过 LLMConfig 统一管理模型参数(温度系数、最大输出 token 数、API 密钥等),支持动态调整模型配置,适配不同场景需求。

4.1.2 代码

代码如下。

python 复制代码
class LLM(RetryMixin, DebugMixin):
    """语言模型(LLM)实例的封装类,提供统一的模型调用接口。

    属性:
        config: LLMConfig 对象,存储模型的配置参数(如模型名称、API密钥、温度系数等)。
    """

    def __init__(
        self,
        config: LLMConfig,
        service_id: str,
        metrics: Metrics | None = None,
        retry_listener: Callable[[int, int], None] | None = None,
    ) -> None:
        """初始化 LLM 实例。若传入 LLMConfig,其参数将作为默认值;
        直接传入的简单参数会覆盖 config 中的对应配置。

        参数:
            config: 模型配置对象,包含模型调用所需的所有参数。
            service_id: 服务标识,用于关联当前 LLM 实例所属的服务。
            metrics: 指标统计对象,用于记录模型调用的延迟、成本等信息(可选)。
            retry_listener: 重试回调函数,每次重试时触发,接收(当前重试次数,总重试次数)作为参数(可选)。
        """
        # 标记是否已尝试获取模型信息
        self._tried_model_info = False
        # 标记是否支持成本统计指标
        self.cost_metric_supported: bool = True
        # 深拷贝配置对象,避免外部修改影响内部状态
        self.config: LLMConfig = copy.deepcopy(config)
        # 服务标识赋值
        self.service_id = service_id
        # 初始化指标统计对象(若未传入则创建默认实例)
        self.metrics: Metrics = (
            metrics if metrics is not None else Metrics(model_name=config.model)
        )

        # 模型信息(如支持的功能、参数限制等,后续通过 init_model_info 初始化)
        self.model_info: ModelInfo | None = None
        # 标记是否启用函数调用功能
        self._function_calling_active: bool = False
        # 重试回调函数赋值
        self.retry_listener = retry_listener

        # 处理日志记录配置:若启用日志记录,需确保日志文件夹存在
        if self.config.log_completions:
            if self.config.log_completions_folder is None:
                raise RuntimeError(
                    'log_completions_folder is required when log_completions is enabled'
                )
            # 创建日志文件夹(若已存在则不报错)
            os.makedirs(self.config.log_completions_folder, exist_ok=True)

        # 调用 init_model_info 初始化模型信息,核心是获取 config.max_output_tokens(后续函数调用需用到)
        # 忽略初始化过程中的警告信息
        with warnings.catch_warnings():
            warnings.simplefilter('ignore')
            self.init_model_info()

        # 打印调试日志:模型是否支持视觉能力
        if self.vision_is_active():
            logger.debug('LLM: model has vision enabled')
        # 打印调试日志:是否启用提示词缓存
        if self.is_caching_prompt_active():
            logger.debug('LLM: caching prompt enabled')
        # 打印调试日志:模型是否支持函数调用
        if self.is_function_calling_active():
            logger.debug('LLM: model supports function calling')

        # 处理自定义分词器:若配置了自定义分词器,按指定路径加载
        if self.config.custom_tokenizer is not None:
            self.tokenizer = create_pretrained_tokenizer(self.config.custom_tokenizer)
        else:
            self.tokenizer = None

        # 初始化模型调用的基础参数
        kwargs: dict[str, Any] = {
            'temperature': self.config.temperature,  # 温度系数,控制输出随机性
            'max_completion_tokens': self.config.max_output_tokens,  # 最大输出token数
        }

        # 若配置了 top_k,添加到参数中(OpenAI 不支持该参数,LiteLLM 会特殊处理)
        if self.config.top_k is not None:
            kwargs['top_k'] = self.config.top_k
        # 若配置了 top_p,添加到参数中(OpenAI 不支持该参数,但 LiteLLM 支持)
        if self.config.top_p is not None:
            kwargs['top_p'] = self.config.top_p

        # 处理 OpenHands 专属模型:重写为 LiteLLM 代理格式
        if self.config.model.startswith('openhands/'):
            model_name = self.config.model.removeprefix('openhands/')
            self.config.model = f'litellm_proxy/{model_name}'
            self.config.base_url = 'https://llm-proxy.app.all-hands.dev/'
            logger.debug(
                f'Rewrote openhands/{model_name} to {self.config.model} with base URL {self.config.base_url}'
            )

        # 获取当前模型支持的功能特性
        features = get_features(self.config.model)
        # 处理支持推理努力度(reasoning_effort)的模型
        if features.supports_reasoning_effort:
            # Gemini 模型特殊处理:仅将 'low'/'none' 映射为优化的思考预算
            if 'gemini-2.5-pro' in self.config.model:
                logger.debug(
                    f'Gemini model {self.config.model} with reasoning_effort {self.config.reasoning_effort}'
                )
                if self.config.reasoning_effort in {None, 'low', 'none'}:
                    kwargs['thinking'] = {'budget_tokens': 128}  # 思考预算设为 128 token
                    kwargs['allowed_openai_params'] = ['thinking']  # 允许传递 thinking 参数
                    kwargs.pop('reasoning_effort', None)  # 移除原 reasoning_effort 参数
                else:
                    kwargs['reasoning_effort'] = self.config.reasoning_effort
                logger.debug(
                    f'Gemini model {self.config.model} with reasoning_effort {self.config.reasoning_effort} mapped to thinking {kwargs.get("thinking")}'
                )
            # Claude Sonnet 4.5 不支持 reasoning_effort,直接移除该参数
            elif 'claude-sonnet-4-5' in self.config.model:
                kwargs.pop('reasoning_effort', None)
            # 其他支持的模型,直接传递 reasoning_effort 参数
            else:
                kwargs['reasoning_effort'] = self.config.reasoning_effort
            # 推理类模型不支持 temperature 和 top_p,移除这两个参数
            kwargs.pop('temperature')
            kwargs.pop('top_p')

        # 处理 Azure 模型的参数兼容问题(参考:https://github.com/All-Hands-AI/OpenHands/issues/6777)
        if self.config.model.startswith('azure'):
            kwargs['max_tokens'] = self.config.max_output_tokens  # Azure 用 max_tokens 而非 max_completion_tokens
            kwargs.pop('max_completion_tokens')

        # 为支持安全设置的模型添加安全配置
        if 'mistral' in self.config.model.lower() and self.config.safety_settings:
            kwargs['safety_settings'] = self.config.safety_settings
        elif 'gemini' in self.config.model.lower() and self.config.safety_settings:
            kwargs['safety_settings'] = self.config.safety_settings

        # 支持 AWS Bedrock 模型:添加 AWS 相关配置参数
        kwargs['aws_region_name'] = self.config.aws_region_name
        if self.config.aws_access_key_id:
            # 从密钥管理中获取 AWS 访问密钥
            kwargs['aws_access_key_id'] = (
                self.config.aws_access_key_id.get_secret_value()
            )
        if self.config.aws_secret_access_key:
            # 从密钥管理中获取 AWS 密钥
            kwargs['aws_secret_access_key'] = (
                self.config.aws_secret_access_key.get_secret_value()
            )

        # 禁用 Claude Opus 4.1 的 Anthropic 扩展思考功能(避免需要 'thinking' 内容块,参考:#10510)
        if 'claude-opus-4-1' in self.config.model.lower():
            kwargs['thinking'] = {'type': 'disabled'}

        # Anthropic 约束:Opus 4.1 不能同时接受 temperature 和 top_p,若两者都存在则优先保留 temperature
        _model_lower = self.config.model.lower()
        if ('claude-opus-4-1' in _model_lower) and (
            'temperature' in kwargs and 'top_p' in kwargs
        ):
            kwargs.pop('top_p', None)

        # 绑定 LiteLLM 完成函数,预设固定参数(通过 partial 固化模型配置)
        self._completion = partial(
            litellm.completion,  # LiteLLM 的核心完成函数
            model=self.config.model,  # 模型名称
            # API 密钥(若配置则从密钥管理中获取)
            api_key=self.config.api_key.get_secret_value()
            if self.config.api_key
            else None,
            base_url=self.config.base_url,  # 模型服务基础 URL
            api_version=self.config.api_version,  # API 版本(如 Azure 需指定)
            custom_llm_provider=self.config.custom_llm_provider,  # 自定义 LLM 提供商
            timeout=self.config.timeout,  # 超时时间
            drop_params=self.config.drop_params,  # 是否允许 LiteLLM 丢弃不支持的参数
            seed=self.config.seed,  # 随机种子(保证输出可复现)
            **kwargs,  # 上述拼接的动态参数
        )

        # 保存未包装的原始 completion 函数(用于内部调用)
        self._completion_unwrapped = self._completion

        # 为 completion 函数添加重试装饰器(继承自 RetryMixin)
        @self.retry_decorator(
            num_retries=self.config.num_retries,  # 最大重试次数
            retry_exceptions=LLM_RETRY_EXCEPTIONS,  # 触发重试的异常类型
            retry_min_wait=self.config.retry_min_wait,  # 最小重试等待时间
            retry_max_wait=self.config.retry_max_wait,  # 最大重试等待时间
            retry_multiplier=self.config.retry_multiplier,  # 重试等待时间倍增系数
            retry_listener=self.retry_listener,  # 重试回调函数
        )
        def wrapper(*args: Any, **kwargs: Any) -> Any:
            """LiteLLM 完成函数的包装器,负责:
            1. 解析输入消息(处理 Message 对象与字典格式的转换)
            2. 模拟函数调用(对不支持函数调用的模型)
            3. 日志记录(输入提示词、输出响应)
            4. 性能指标统计(延迟、成本)
            5. 响应格式转换与异常处理
            """
            # 延迟导入以避免循环依赖
            from openhands.io import json

            # 初始化消息参数(存储用户传入的消息)
            messages_kwarg: (
                dict[str, Any] | Message | list[dict[str, Any]] | list[Message]
            ) = []
            # 标记是否需要模拟函数调用(模型不支持原生函数调用时为 True)
            mock_function_calling = not self.is_function_calling_active()

            # 处理位置参数:部分调用者可能直接传入 (model, messages, **kwargs)
            if len(args) > 1:
                # 忽略第一个参数(模型名称,已通过 partial 固化)
                # 设计原则:不允许覆盖已配置的模型参数
                messages_kwarg = args[1] if len(args) > 1 else args[0]
                kwargs['messages'] = messages_kwarg

                # 移除前两个位置参数(已转换为关键字参数)
                args = args[2:]
            # 处理关键字参数:若已传入 messages 则直接赋值
            elif 'messages' in kwargs:
                messages_kwarg = kwargs['messages']

            # 确保消息为列表格式(统一处理单条/多条消息)
            messages_list = (
                messages_kwarg if isinstance(messages_kwarg, list) else [messages_kwarg]
            )
            # 格式化消息:将 Message 对象转换为模型可识别的字典格式
            messages: list[dict] = []
            if messages_list and isinstance(messages_list[0], Message):
                messages = self.format_messages_for_llm(
                    cast(list[Message], messages_list)
                )
            else:
                messages = cast(list[dict[str, Any]], messages_list)

            # 更新 kwargs 中的 messages 为格式化后的结果
            kwargs['messages'] = messages

            # 保存原始函数调用相关消息(用于后续日志记录)
            original_fncall_messages = copy.deepcopy(messages)
            mock_fncall_tools = None
            # 若需要模拟函数调用且传入了工具配置,转换消息格式
            if mock_function_calling and 'tools' in kwargs:
                # 标记是否添加上下文学习示例(部分模型不需要)
                add_in_context_learning_example = True
                if (
                    'openhands-lm' in self.config.model
                    or 'devstral' in self.config.model
                ):
                    add_in_context_learning_example = False

                # 将函数调用格式的消息转换为普通文本提示(模拟函数调用)
                messages = convert_fncall_messages_to_non_fncall_messages(
                    messages,
                    kwargs['tools'],
                    add_in_context_learning_example=add_in_context_learning_example,
                )
                kwargs['messages'] = messages

                # 若模型支持停止词且未禁用,添加默认停止词
                if (
                    get_features(self.config.model).supports_stop_words
                    and not self.config.disable_stop_word
                ):
                    kwargs['stop'] = STOP_WORDS

                # 移除 tools 参数(模拟调用时不需要传递)
                mock_fncall_tools = kwargs.pop('tools')
                # OpenHands 自研模型特殊处理:禁用工具调用
                if 'openhands-lm' in self.config.model:
                    kwargs['tool_choice'] = 'none'
                else:
                    # 其他模型:移除 tool_choice 参数(模拟调用时不支持)
                    kwargs.pop('tool_choice', None)

            # 校验消息非空:无消息则抛出异常
            if not messages:
                raise ValueError(
                    'The messages list is empty. At least one message is required.'
                )

            # 记录 LLM 输入提示词日志
            self.log_prompt(messages)

            # 设置 LiteLLM 是否允许修改参数(默认允许,如为空消息添加默认内容)
            # 注意:该设置为全局,无法通过 partial 覆盖
            litellm.modify_params = self.config.modify_params

            # 非 LiteLLM 代理模型:移除 extra_body 参数(仅代理模型支持)
            if 'litellm_proxy' not in self.config.model:
                kwargs.pop('extra_body', None)

            # 记录调用开始时间(用于计算延迟)
            start_time = time.time()

            # 抑制 LiteLLM 调用过程中 httpx 库的弃用警告
            # 避免出现 "Use 'content=<...>' to upload raw bytes/text content" 警告
            with warnings.catch_warnings():
                warnings.filterwarnings(
                    'ignore', category=DeprecationWarning, module='httpx.*'
                )
                warnings.filterwarnings(
                    'ignore',
                    message=r'.*content=.*upload.*',
                    category=DeprecationWarning,
                )
                # 调用原始 completion 函数(非流式,返回 ModelResponse 对象)
                resp: ModelResponse = self._completion_unwrapped(*args, **kwargs)

            # 计算调用延迟并记录到指标中
            latency = time.time() - start_time
            response_id = resp.get('id', 'unknown')  # 获取响应 ID(无则设为 'unknown')
            self.metrics.add_response_latency(latency, response_id)  # 记录延迟指标

            # 深拷贝原始响应(用于模拟函数调用场景的日志记录)
            non_fncall_response = copy.deepcopy(resp)

            # 若启用了函数调用模拟且存在工具配置,将响应转换回函数调用格式
            if mock_function_calling and mock_fncall_tools is not None:
                # 校验响应是否包含有效选项(Gemini 模型曾出现无选项的情况)
                if len(resp.choices) < 1:
                    raise LLMNoResponseError(
                        'Response choices is less than 1 - This is only seen in Gemini models so far. Response: '
                        + str(resp)
                    )

                # 获取非函数调用格式的响应消息
                non_fncall_response_message = resp.choices[0].message
                # 将 "原始消息 + 非函数调用响应" 转换为函数调用格式的响应
                fn_call_messages_with_response = (
                    convert_non_fncall_messages_to_fncall_messages(
                        messages + [non_fncall_response_message], mock_fncall_tools
                    )
                )
                # 提取转换后的函数调用响应消息
                fn_call_response_message = fn_call_messages_with_response[-1]
                # 确保响应消息为 LiteLLMMessage 类型(若为字典则转换)
                if not isinstance(fn_call_response_message, LiteLLMMessage):
                    fn_call_response_message = LiteLLMMessage(
                        **fn_call_response_message
                    )
                # 更新响应中的消息为函数调用格式
                resp.choices[0].message = fn_call_response_message

            # 二次校验响应有效性:确保 choices 非空且至少有一个选项
            if not resp.get('choices') or len(resp['choices']) < 1:
                raise LLMNoResponseError(
                    'Response choices is less than 1 - This is only seen in Gemini models so far. Response: '
                    + str(resp)
                )

            # 记录 LLM 响应日志
            self.log_response(resp)

            # 响应后处理:计算调用成本(如 token 消耗对应的费用)
            cost = self._post_completion(resp)

            # 若启用响应日志持久化,将请求/响应数据写入文件
            if self.config.log_completions:
                # 断言日志文件夹已配置(初始化时已校验,此处防止异常)
                assert self.config.log_completions_folder is not None
                # 构造日志文件名:模型名_时间戳.json(替换 '/' 为 '__' 避免路径错误)
                log_file = os.path.join(
                    self.config.log_completions_folder,
                    f'{self.config.model.replace("/", "__")}-{time.time()}.json',
                )

                # 构造日志数据字典
                _d = {
                    'messages': messages,  # 实际发送给模型的消息(可能是模拟函数调用格式)
                    'response': resp,      # 模型响应(可能是转换后的函数调用格式)
                    'args': args,          # 调用时的位置参数
                    'kwargs': {            # 调用时的关键字参数(过滤敏感/冗余字段)
                        k: v
                        for k, v in kwargs.items()
                        if k not in ('messages', 'client')
                    },
                    'timestamp': time.time(),  # 调用时间戳
                    'cost': cost,              # 调用成本(token 费用)
                }

                # 若启用了函数调用模拟,额外记录原始函数调用格式的消息和响应
                if mock_function_calling:
                    # 覆盖 response 为非函数调用格式(与 messages 保持一致)
                    _d['response'] = non_fncall_response
                    # 新增字段记录原始函数调用格式数据
                    _d['fncall_messages'] = original_fncall_messages
                    _d['fncall_response'] = resp

                # 将日志数据写入文件(JSON 格式)
                with open(log_file, 'w') as f:
                    f.write(json.dumps(_d))

            # 返回最终处理后的模型响应
            return resp

        # 将包装后的函数赋值给 _completion,后续通过该属性调用模型
        self._completion = wrapper

4.2 LLMRegistry

4.2.1 作用

LLMRegistry 是 OpenHands 框架中管理 LLM 实例的核心组件,负责集中创建、复用和监控所有 LLM 资源。配合 create_registry_and_conversation_stats 函数,它构建了从配置解析到实例管理的完整链路,确保 LLM 资源高效利用且配置一致。LLMRegistry的功能如下。

  1. 集中化实例管理 :通过 service_to_llm 映射表跟踪所有 LLM 实例,避免重复创建,降低资源消耗;严格检查同一服务 ID 的配置一致性,防止冲突。
  2. 灵活的实例获取机制
    • 支持通过服务 ID 直接获取或创建 LLM(get_llm
    • 支持从代理配置自动推导 LLM 配置(get_llm_from_agent_config
    • 支持临时补充生成需求(request_extraneous_completion
  3. 多模型路由支持 :通过 get_router 方法集成 RouterLLM,实现基于代理配置的动态模型选择,灵活应对复杂任务对不同模型的需求。
  4. 事件驱动的可扩展性 :提供 subscribenotify 机制,允许外部组件(如 ConversationStats)订阅 LLM 注册事件,轻松扩展统计、监控等功能。
  5. 配置隔离与安全性:通过深拷贝配置和严格的实例创建逻辑,确保不同服务的 LLM 配置相互隔离,避免外部修改干扰内部状态。
  6. 适配用户个性化设置 :结合 create_registry_and_conversation_stats 函数,支持通过用户设置覆盖默认配置,兼顾通用性与个性化需求。

4.2.2 工作流

4.2.3 代码

LLMRegistry 的代码如下:

python 复制代码
class LLMRegistry:
    """
    LLM注册表:管理所有LLM实例的生命周期、配置和事件通知的核心组件。
    
    作用:
    - 集中管理多个LLM实例,避免重复创建
    - 确保同一服务ID的LLM配置一致性
    - 支持事件订阅(如统计、监控)
    - 提供路由LLM(RouterLLM)的创建能力
    """
    def __init__(
        self,
        config: OpenHandsConfig,
        agent_cls: Optional[str] = None,
        retry_listener: Optional[Callable[[int, int], None]] = None,
    ):
        self.registry_id = str(uuid4())  # 注册表唯一标识
        self.config = copy.deepcopy(config)  # 深拷贝配置,避免外部修改影响
        self.retry_listener = retry_listener  # 重试事件监听器(可选)
        # 构建代理到LLM配置的映射(从全局配置中提取)
        self.agent_to_llm_config = self.config.get_agent_to_llm_config_map()
        self.service_to_llm: dict[str, LLM] = {}  # 服务ID到LLM实例的映射
        self.subscriber: Optional[Callable[[Any], None]] = None  # 事件订阅者(如统计器)

        # 确定当前激活的代理类型(用户指定优先,否则使用默认)
        selected_agent_cls = self.config.default_agent
        if agent_cls:
            selected_agent_cls = agent_cls

        # 基于代理类型获取对应的LLM配置
        agent_name = selected_agent_cls if selected_agent_cls is not None else 'agent'
        llm_config = self.config.get_llm_config_from_agent(agent_name)
        # 初始化并激活代理的主LLM实例
        self.active_agent_llm: LLM = self.get_llm('agent', llm_config)

    def _create_new_llm(
        self, service_id: str, config: LLMConfig, with_listener: bool = True
    ) -> LLM:
        """
        内部方法:创建新的LLM实例并注册到注册表中。
        
        参数:
            service_id: 服务唯一标识(用于区分不同LLM实例)
            config: LLM配置
            with_listener: 是否绑定重试监听器
        
        返回:
            新创建的LLM实例
        """
        # 根据是否需要监听器,初始化LLM
        if with_listener:
            llm = LLM(
                service_id=service_id, config=config, retry_listener=self.retry_listener
            )
        else:
            llm = LLM(service_id=service_id, config=config)
        
        # 记录到映射表中
        self.service_to_llm[service_id] = llm
        # 通知订阅者(如统计器)有新LLM注册
        self.notify(RegistryEvent(llm=llm, service_id=service_id))
        return llm

    def request_extraneous_completion(
        self, service_id: str, llm_config: LLMConfig, messages: list[dict[str, str]]
    ) -> str:
        """
        请求额外的LLM生成(用于非主流程的补充生成需求)。
        
        参数:
            service_id: 服务ID
            llm_config: 对应的LLM配置
            messages: 输入消息列表(格式:[{role: ..., content: ...}, ...])
        
        返回:
            LLM生成的文本内容(去除首尾空白)
        """
        # 若服务ID未注册,则创建新LLM(不绑定监听器,适用于临时任务)
        if service_id not in self.service_to_llm:
            self._create_new_llm(
                config=llm_config, service_id=service_id, with_listener=False
            )

        # 获取LLM实例并执行生成
        llm = self.service_to_llm[service_id]
        response = llm.completion(messages=messages)
        return response.choices[0].message.content.strip()

    def get_llm_from_agent_config(self, service_id: str, agent_config: AgentConfig):
        """
        根据代理配置获取对应的LLM实例(支持复用已有实例)。
        
        参数:
            service_id: 服务ID
            agent_config: 代理配置对象
        
        返回:
            匹配的LLM实例
        """
        # 从代理配置中提取LLM配置
        llm_config = self.config.get_llm_config_from_agent_config(agent_config)
        # 若实例已存在,直接返回(配置不一致时暂不处理,预留更新逻辑)
        if service_id in self.service_to_llm:
            if self.service_to_llm[service_id].config != llm_config:
                # TODO: 未来支持动态更新LLM配置
                # 当代理委托的配置不同时,应复用现有LLM
                pass
            return self.service_to_llm[service_id]

        # 实例不存在则创建新的
        return self._create_new_llm(config=llm_config, service_id=service_id)

    def get_llm(
        self,
        service_id: str,
        config: Optional[LLMConfig] = None,
    ) -> LLM:
        """
        获取或创建指定服务ID的LLM实例(核心方法)。
        
        参数:
            service_id: 服务唯一标识
            config: LLM配置(新实例必需)
        
        返回:
            对应的LLM实例
        
        异常:
            ValueError: 同一服务ID配置不一致,或创建新实例时无配置
        """
        # 检查同一服务ID的配置是否一致(防止冲突)
        if (
            service_id in self.service_to_llm
            and self.service_to_llm[service_id].config != config
        ):
            raise ValueError(
                f"Service ID {service_id} requested with different config. Use a new service ID."
            )

        # 实例已存在则直接返回
        if service_id in self.service_to_llm:
            return self.service_to_llm[service_id]

        # 新实例必须提供配置
        if not config:
            raise ValueError("Cannot create new LLM without specifying config.")

        # 创建并返回新实例
        return self._create_new_llm(config=config, service_id=service_id)

    def get_active_llm(self) -> LLM:
        """返回当前激活的代理主LLM实例"""
        return self.active_agent_llm

    def get_router(self, agent_config: AgentConfig) -> LLM:
        """
        获取路由LLM实例(用于多模型路由选择)。
        
        参数:
            agent_config: 代理配置(包含路由规则)
        
        返回:
            路由LLM实例(RouterLLM)或主LLM(当路由为noop时)
        """
        # 从代理配置中获取路由名称
        router_name = agent_config.model_routing.router_name

        # 若为"noop_router"(无操作路由),直接返回主LLM
        if router_name == 'noop_router':
            return self.get_llm_from_agent_config('agent', agent_config)

        # 否则创建并返回路由LLM实例
        return RouterLLM.from_config(
            agent_config=agent_config,
            llm_registry=self,
            retry_listener=self.retry_listener,
        )

    def subscribe(self, callback: Callable[[RegistryEvent], None]) -> None:
        """
        订阅注册表事件(如新LLM创建)。
        
        参数:
            callback: 事件触发时的回调函数
        """
        self.subscriber = callback

        # 订阅后,立即通知已存在的主LLM实例(补报历史事件)
        self.notify(
            RegistryEvent(
                llm=self.active_agent_llm,
                service_id=self.active_agent_llm.service_id
            )
        )

    def notify(self, event: RegistryEvent) -> None:
        """
        通知订阅者事件发生(如LLM注册)。
        
        参数:
            event: 注册表事件对象
        """
        if self.subscriber:
            try:
                self.subscriber(event)
            except Exception as e:
                logger.warning(f"Failed to notify subscriber of event: {e}")

0xFF 参考

https://docs.all-hands.dev/openhands/usage/architecture/backend

当AI Agent从"玩具"走向"工具",我们该关注什么?Openhands架构解析【第二篇:Agent 相关核心概念】 克里

当AI Agent从"玩具"走向"工具",我们该关注什么?Openhands架构解析【第一篇:系列导读】 克里

Coding Agent之Openhands解析(含代码) Arrow

OpenHands 源码解读 一力辉

Google Agent 白皮书解析

Agent开发实践:从想法到产品------SSE、上下文工程与流式解析关键技术攻坚

Agent开发实践:从想法到产品------系统架构设计实践