找回密码
 立即注册
首页 业界区 业界 AI Agent框架探秘:拆解 OpenHands(8)--- CodeActAge ...

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

倡粤 前天 22:30
AI Agent框架探秘:拆解 OpenHands(8)---  CodeActAgent


目录

  • AI Agent框架探秘:拆解 OpenHands(8)---  CodeActAgent

    • 0x00 摘要
    • 0x01 背景

      • 1.1 Agent的核心能力
      • 1.2 Agent设计原则
      • 1.3 Agent in OpenHands

        • 1.3.1. 事件驱动执行
        • 1.3.2. 基于技能与提示的智能体上下文定制
        • 1.3.3. 子智能体委托机制

      • 1.4 CodeAct

        • 1.4.1 理念
        • 1.4.2 模式
        • 1.4.3 特色


    • 0x02 定义

      • 2.1 可配置性
      • 2.2 插件系统
      • 2.3 工具系统Tools

        • 2.3.1 工具集
        • 2.3.2 BrowserTool

      • 2.4 上下文

        • 2.4.1 需求
        • 2.4.2 核心特色
        • 2.4.3 流程图
        • 2.4.4 代码

      • 2.5 提示词(prompt)
      • 2.6 迭代修改

    • 0x03 工作流程

      • 3.1 决策流程
      • 3.2 消息处理
      • 3.3 历史压缩(Condensation)
      • 3.4 内存管理

    • 0xFF 参考


0x00 摘要

大模型是不可控的。不是‘给LLM一堆工具让它自由发挥’,而是大部分由确定性代码构成,在关键决策点巧妙地融入LLM能力。好的 Agent 应用,是工程设计与 AI 能力的精妙结合,而不是对 AI 的盲目放权。
在 OpenHands 智能框架的生态中,CodeActAgent 占据着核心地位,它是基于 CodeAct 理念构建的核心代理模块。其设计初衷极具巧思:将各类复杂任务统一转化为 “代码执行” 的形式来完成,同时兼顾自然语言对话的交互特性。这一设计既保障了任务执行的精准性与高效性,又为人类与智能代理的协作提供了灵活空间,使其成为框架中处理自动化编程、数据处理等复杂场景的核心载体。
因为本系列借鉴的文章过多,可能在参考文献中有遗漏的文章,如果有,还请大家指出。
0x01 背景

1.1 Agent的核心能力

根据Google电子书的定义,一个真正的 AI 智能体拥有四项核心能力:
1.png


  • 做出动态决策 (Make dynamic decisions):它们不是遵循预定的路径,而是根据所学到的东西决定下一步做什么。
  • 跨交互保持状态 (Maintain state across multiple interactions):它们能记住自己做过什么,并利用这些历史来为未来的决策提供信息。
  • 自适应地使用工具 (Use tools adaptively):它们可以从可用工具中进行选择,并以非预先编程的方式组合它们。
  • 根据结果修正方法 (Modify their approach based on results):当一种策略不起作用时,它们可以尝试不同的方法。
1.2 Agent设计原则

如何设计Agent?不同人有不同的理解,这可能是一个哲学问题。
我们使用 https://github.com/humanlayer/12-factor-agents 来切入,具体如下:

  • 原则一:组织规划、工具调用,复杂Agent的工作范式
  • 原则二:工具执行器,Agent“思考”和“行动”分离解耦、独立进化
  • 原则三:通过工具调用来联系人类,Agent中的“人机协同”
  • 原则四:提示词可调试、可迭代、可回滚、面向场景设计、A/B测试
  • 原则五:建设上下文评估与仲裁,突破上下文有限束缚,提升“生成质量”
  • 原则六:将错误压缩到上下文窗口,让Agent从错误中学习并尝试自我纠正
  • 原则七:统一执行状态和业务状态,为Agent赋予了“自主恢复”的能力
  • 原则八:使用简单的API来完成启动、暂停和恢复,实现“任务全生命周期”管理
  • 原则九:有限状态机,拥有Agent的自主控制流
  • 原则十:多个小而专注的Agent,共同组成“智能组织”
  • 原则十一:从任何地方触发Agent,构建“无处不在的智能生产力”
  • 原则十二:将Agent看做是一个“无状态的归约器”
论文 From Storage to Experience: A Survey on the Evolution of LLM Agent Memory Mechanisms 也给出了Agent的设计原则:
设计原则1:Memory as Experience, Not Storage

  • 错误方向:追求更大的存储容量、更快的检索速度
  • 正确方向:追求更深的经验抽象、更强的模式迁移
设计原则2:Proactive over Reactive

  • 错误方向:优化响应速度和准确度
  • 正确方向:增强主动探索和自我完善能力
设计原则3:Cross-trajectory Learning

  • 错误方向:单任务优化、垂直领域深耕
  • 正确方向:跨任务抽象、通用能力泛化
设计原则4:Continual Learning without Catastrophic Forgetting

  • 错误方向:定期重训、版本迭代
  • 正确方向:增量更新、经验积累
1.3 Agent in OpenHands

智能体抽象将配置与执行状态分离,智能体被定义为无状态、不可变的规格对象,包含 LLM 设置、工具规格、安全策略与智能体核心逻辑,可序列化并跨进程传输。
1.3.1. 事件驱动执行

智能体通过事件驱动循环逐步处理对话状态,不直接返回结果,而是通过回调函数 on_event (event: Event) -> None 输出结构化事件(如消息、动作、观察结果),实现事件生成与执行控制的分离。该设计支持:

  • 安全介入 —— 基于风险分析在执行前审核或拦截动作;
  • 增量执行 —— 智能体分步推进任务,支持暂停 / 恢复、上下文溢出恢复与长对话压缩;
  • 事件流传输 —— 实时输出中间结果(如观察数据、推理轨迹),用于界面更新与监控。
1.3.2. 基于技能与提示的智能体上下文定制

AgentContext 集中管理所有影响 LLM 行为的输入,包括系统 / 用户消息的前缀 / 后缀、用户定义的 Skill(技能)对象。技能可通过编程方式定义,或从 Markdown 文件(如 .openhands/skills/,及 .cursorrules、agents.md 等兼容格式)加载:

  • 永久激活技能(trigger=None):持续增强系统提示;
  • 条件激活技能:基于用户输入的关键词匹配触发,可包含 MCP 工具。
    该设计支持丰富的上下文与行为定制,无需修改智能体核心逻辑。
1.3.3. 子智能体委托机制

SDK 通过委托工具实现分层智能体协作,充分体现了工具抽象的可扩展性。子智能体作为独立对话存在,继承父智能体的模型配置与工作空间上下文,无需修改核心 SDK 即可实现结构化并行处理与隔离。当前实现提供阻塞式并行执行能力,作为 openhands.tools 包中的标准工具 —— 父智能体创建并监控子智能体,直至所有任务完成。这一模式证明:异步委托、动态调度、容错恢复等复杂协作行为,均可通过用户自定义工具实现,无需修改核心框架,彰显了 SDK “高级智能体编排无需改动核心” 的可扩展设计原则。
1.4 CodeAct

1.4.1 理念

CodeAct 理念的核心突破,在于将智能代理的动作空间提升至通用编程的高度 —— 通过让大语言模型(LLM)直接生成可执行代码,打破了传统工具调用的局限。以往的智能代理往往受困于固定的工具接口,只能机械地调用预设功能,而 CodeAct 赋予代理一个统一的 “可编程” 动作接口,就像为工匠配备了一套可灵活组合的精密工具,为解决复杂任务开辟了全新路径。
这一理念的本质,是深度挖掘 LLM 擅长编写代码的原生能力。它让代理的 “动作” 不再局限于单一的原子 API 调用,而是通过生成一段完整的 Python 代码,交由 Python 解释器执行来完成复杂任务。如此一来,代理能在单个动作中封装完整的逻辑流程:包括调用多个函数或工具、控制执行顺序、处理中间结果并存储,极大地提升了任务处理的连贯性与自主性。
1.4.2 模式

CodeAct Agent 是一个极简主义的智能体,以 ReAct的模式,根据已有的若干 Action-Observation 对的轨迹决定下一步需要采取什么 Action。在每一轮的交互循环中,CodeActAgent 具备两种核心操作模式,二者相辅相成,共同支撑任务推进:

  • 对话模式(Converse):以自然语言为沟通桥梁,实现与人类的高效协作。例如当任务需求模糊时,代理会主动请求用户澄清细节;在执行关键操作前,也会向用户确认以规避风险,充分体现了人机协作的灵活性。
  • 代码行动模式(CodeAct):依托一组标准化工具展开具体操作,覆盖多类任务场景:


  • 调用execute_bash函数执行 Linux 系统的 bash 命令,实现系统级操作;
  • 通过execute_ipython_cell在 IPython 环境中运行 Python 代码,处理数据计算、逻辑执行等核心任务;
  • 借助browser与fetch工具与网页浏览器交互,完成信息爬取、页面操作等需求;
  • 利用str_replace_editor或edit_file工具编辑文件内容,实现文档修改、代码编写等功能。
这种 “对话 + 代码” 的双轨模式,不仅简化了智能代理的操作体系,更在实际应用中显著提升了任务处理性能。
实际上 OpenDevin 中 CodeAct Agent 的实现与原始 CodeAct 并不完全一样,前者在原始 CodeAct 的基础上进行改进,并很大程度上借鉴了 SWE-Agent.
1.4.3 特色

CodeActAgent 的特色如下:

  • 行动空间统一化:打破传统代理多行动类型的碎片化设计,将所有任务(文件操作、数据处理、系统交互等)统一为 “代码执行” 行动,简化架构且提升执行效率。
  • 双模式交互能力:支持 “自然语言对话” 与 “代码行动” 双模式,既可以通过自然语言与人类协作(如请求澄清),也能通过代码自主完成复杂任务,适配多样场景。
  • 插件化沙盒依赖:通过 sandbox_plugins 定义沙盒环境所需插件,按顺序初始化确保依赖正确性,同时支持灵活扩展技能(如通过 AgentSkillsRequirement 新增工具函数)。
  • 完善的记忆与上下文管理:集成 ConversationMemory 管理 “行动 - 观察” 历史,搭配 Condenser 压缩长上下文,平衡上下文相关性与模型输入长度限制。
  • 灵活的模型路由支持:通过 LLMRegistry.get_router 获取路由 LLM,可根据任务复杂度动态选择适配模型,兼顾性能与成本。
  • 极简主义设计:核心逻辑聚焦 “代码执行” 单一行动空间,架构简洁易懂,同时保持高扩展性,便于后续功能迭代与定制化开发。
0x02 定义

CodeActAgent的定义如下。
  1. class CodeActAgent(Agent):
  2.     """
  3.     CodeActAgent:极简主义的智能代理,基于 CodeAct 理念实现。
  4.     核心逻辑:将模型的行动统一到“代码执行”这一单一行动空间,通过传递“行动-观察”对列表,
  5.     引导模型决策下一步操作,兼顾简洁性与执行性能。
  6.     核心理念(源自论文:https://arxiv.org/abs/2402.01030):
  7.     打破传统代理多行动类型的复杂设计,用代码执行统一所有行动,既简化架构又提升效率。
  8.     """
  9.     VERSION = '2.2'  # 代理版本号
  10.     # 沙盒环境所需插件依赖(按初始化顺序排列)
  11.     sandbox_plugins: list[PluginRequirement] = [
  12.         # 注意:AgentSkillsRequirement 需在 JupyterRequirement 之前初始化
  13.         # 原因:AgentSkillsRequirement 提供大量 Python 工具函数,
  14.         # Jupyter 环境需要依赖这些函数才能正常工作
  15.         AgentSkillsRequirement(),  # 提供代理核心技能函数的插件
  16.         JupyterRequirement(),      # 提供交互式 Python 执行环境的插件
  17.     ]
  18.     def __init__(self, config: AgentConfig, llm_registry: LLMRegistry) -> None:
  19.         """
  20.         初始化 CodeActAgent 实例。
  21.         参数:
  22.             config (AgentConfig):当前代理的配置对象(包含模型路由、记忆策略等)
  23.             llm_registry (LLMRegistry):LLM 注册表实例,用于获取所需 LLM 或路由 LLM
  24.         """
  25.         # 调用父类 Agent 的初始化方法,完成基础配置(如 LLM 注册、提示词管理器初始化)
  26.         super().__init__(config, llm_registry)
  27.         
  28.         self.pending_actions: deque['Action'] = deque()  # 待执行的行动队列(双端队列,支持高效进出)
  29.         self.reset()  # 重置代理状态(初始化行动历史、观察记录等)
  30.         self.tools = self._get_tools()  # 获取代理可使用的工具集(从插件或配置中提取)
  31.         # 初始化对话记忆实例:存储“行动-观察”对,支持记忆压缩、上下文管理
  32.         self.conversation_memory = ConversationMemory(self.config, self.prompt_manager)
  33.         # 初始化上下文压缩器:根据配置创建 Condenser 实例,用于压缩长对话历史
  34.         self.condenser = Condenser.from_config(self.config.condenser, llm_registry)
  35.         # 覆盖父类的 LLM 实例:如需模型路由,优先使用路由 LLM(根据代理配置动态选择模型)
  36.         self.llm = self.llm_registry.get_router(self.config)
复制代码
2.1 可配置性

CodeActAgent 通过 AgentConfig,可以灵活启用 / 禁用各种功能,可配置的功能如下:
  1. config.enable_cmd # 启用命令执行
  2. config.enable_think # 启用思考功能
  3. config.enable_finish # 启用完成功能
  4. config.enable_browsing # 启用浏览器功能
  5. config.enable_jupyter # 启用 Jupyter
  6. config.enable_editor # 启用文件编辑器
复制代码
2.2 插件系统

CodeActAgent 通过 sandbox_plugins 定义沙盒环境所需插件,按顺序初始化确保依赖正确性,同时支持灵活扩展技能(如通过 AgentSkillsRequirement 新增工具函数)。
  1.     sandbox_plugins: list[PluginRequirement] = [
  2.         # NOTE: AgentSkillsRequirement need to go before JupyterRequirement, since
  3.         # AgentSkillsRequirement provides a lot of Python functions,
  4.         # and it needs to be initialized before Jupyter for Jupyter to use those functions.
  5.         AgentSkillsRequirement(), # 提供Python函数
  6.         JupyterRequirement(), # 提供Jupyter支持
  7.     ]
复制代码
2.3 工具系统Tools

工具是智能代理拓展能力边界的关键,正是工具的存在,让 LLM 从单纯的对话机器人(ChatBot)进化为具备实际执行能力的智能代理(Agent)。CodeAct 的方案却反其道而行之,以极致简洁的思路重构了工具调用逻辑 —— 它将 Python 作为唯一的工具,让 LLM 通过自主编写代码的方式实现各类功能调用,摒弃了传统多工具集成的复杂设计。
传统工具调用模式中,开发者需要在系统提示词(system prompt)中明确告知 LLM 可用的工具接口(Available APIs),LLM 再通过生成工具名和参数列表的方式调用工具,无论输出格式是文本还是 JSON,本质上都受限于预设范围。而 CodeAct 省去了这一繁琐的预定义步骤,将 Python 作为统一接口,LLM 在每一轮交互中直接生成代码并交由解释器执行。这种设计让动作空间更标准化,工具调用过程简洁优雅,充分释放了 LLM 的原生潜力。
2.3.1 工具集

CodeActAgent 则有所不同。CodeActAgent是一个混合型代理,它既允许模型执行任意代码,也提供了一些特定工具供模型使用。具体来说:

  • 允许模型执行任意代码。CodeActAgent的核心理念是让模型能够执行任意代码:

    • 通过create_cmd_run_tool工具,模型可以执行任何有效的Linux bash命令
    • 通过IPythonTool工具,模型可以执行任何有效的Python代码
    • 这符合CodeAct论文中提出的统一代码操作空间的概念,旨在简化和提高代理性能。

  • 提供特定工具集。CodeActAgent 也提供了一些预定义的工具供模型使用:

    • ThinkTool:让模型记录其思考过程
    • FinishTool:结束交互
    • CondensationRequestTool:请求压缩对话历史
    • BrowserTool:与浏览器交互(非 Windows 平台)
    • LLMBasedFileEditTool 或 create_str_replace_editor_tool:编辑文件
    • create_task_tracker_tool:任务管理工具

  • 工具启用的灵活性通过配置,可以控制哪些工具被启用
CodeActAgent支持丰富的工具集如下:
  1.     def _get_tools(self) -> list['ChatCompletionToolParam']:
  2.         # For these models, we use short tool descriptions ( < 1024 tokens)
  3.         # to avoid hitting the OpenAI token limit for tool descriptions.
  4.         SHORT_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-4', 'o3', 'o1', 'o4']
  5.         use_short_tool_desc = False
  6.         if self.llm is not None:
  7.             # For historical reasons, previously OpenAI enforces max function description length of 1k characters
  8.             # https://community.openai.com/t/function-call-description-max-length/529902
  9.             # But it no longer seems to be an issue recently
  10.             # https://community.openai.com/t/was-the-character-limit-for-schema-descriptions-upgraded/1225975
  11.             # Tested on GPT-5 and longer description still works. But we still keep the logic to be safe for older models.
  12.             use_short_tool_desc = any(
  13.                 model_substr in self.llm.config.model
  14.                 for model_substr in SHORT_TOOL_DESCRIPTION_LLM_SUBSTRS
  15.             )
  16.         tools = []
  17.         if self.config.enable_cmd: # Bash命令执行工具
  18.             tools.append(create_cmd_run_tool(use_short_description=use_short_tool_desc))
  19.         if self.config.enable_think: # 思考工具,记录推理过程
  20.             tools.append(ThinkTool)
  21.         if self.config.enable_finish: # 完成工具,结束任务
  22.             tools.append(FinishTool)
  23.         if self.config.enable_condensation_request:
  24.             tools.append(CondensationRequestTool)
  25.         if self.config.enable_browsing: # 浏览器工具
  26.             if sys.platform == 'win32':
  27.                 logger.warning('Windows runtime does not support browsing yet')
  28.             else:
  29.                 tools.append(BrowserTool)
  30.         if self.config.enable_jupyter: # IPython工具
  31.             tools.append(IPythonTool)
  32.         if self.config.enable_plan_mode:
  33.             # In plan mode, we use the task_tracker tool for task management
  34.             tools.append(create_task_tracker_tool(use_short_tool_desc))
  35.         if self.config.enable_llm_editor: # 文件编辑工具
  36.             tools.append(LLMBasedFileEditTool)
  37.         elif self.config.enable_editor:
  38.             tools.append(
  39.                 create_str_replace_editor_tool(
  40.                     use_short_description=use_short_tool_desc,
  41.                     runtime_type=self.config.runtime,
  42.                 )
  43.             )
  44.         return tools
复制代码
2.3.2 BrowserTool

BrowserTool 举例如下:
  1. BrowserTool = ChatCompletionToolParam(
  2.     type='function',
  3.     function=ChatCompletionToolParamFunctionChunk(
  4.         name=BROWSER_TOOL_NAME,
  5.         description=_BROWSER_DESCRIPTION,
  6.         parameters={
  7.             'type': 'object',
  8.             'properties': {
  9.                 'code': {
  10.                     'type': 'string',
  11.                     'description': (
  12.                         'The Python code that interacts with the browser.\n'
  13.                         + _BROWSER_TOOL_DESCRIPTION
  14.                     ),
  15.                 },
  16.                 'security_risk': {
  17.                     'type': 'string',
  18.                     'description': SECURITY_RISK_DESC,
  19.                     'enum': RISK_LEVELS,
  20.                 },
  21.             },
  22.             'required': ['code', 'security_risk'],
  23.         },
  24.     ),
  25. )
复制代码
_BROWSER_TOOL_DESCRIPTION 如下。
  1. _BROWSER_TOOL_DESCRIPTION = """
  2. The following 15 functions are available. Nothing else is supported.
  3. goto(url: str)
  4.     Description: Navigate to a url.
  5.     Examples:
  6.         goto('http://www.example.com')
  7. go_back()
  8.     Description: Navigate to the previous page in history.
  9.     Examples:
  10.         go_back()
  11. go_forward()
  12.     Description: Navigate to the next page in history.
  13.     Examples:
  14.         go_forward()
  15. noop(wait_ms: float = 1000)
  16.     Description: Do nothing, and optionally wait for the given time (in milliseconds).
  17.     You can use this to get the current page content and/or wait for the page to load.
  18.     Examples:
  19.         noop()
  20.         noop(500)
  21. scroll(delta_x: float, delta_y: float)
  22.     Description: Scroll horizontally and vertically. Amounts in pixels, positive for right or down scrolling, negative for left or up scrolling. Dispatches a wheel event.
  23.     Examples:
  24.         scroll(0, 200)
  25.         scroll(-50.2, -100.5)
  26. fill(bid: str, value: str)
  27.     Description: Fill out a form field. It focuses the element and triggers an input event with the entered text. It works for <input>, <textarea> and [contenteditable] elements.
  28.     Examples:
  29.         fill('237', 'example value')
  30.         fill('45', 'multi-line\nexample')
  31.         fill('a12', 'example with "quotes"')
  32. select_option(bid: str, options: str | list[str])
  33.     Description: Select one or multiple options in a <select> element. You can specify option value or label to select. Multiple options can be selected.
  34.     Examples:
  35.         select_option('a48', 'blue')
  36.         select_option('c48', ['red', 'green', 'blue'])
  37. click(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']] = [])
  38.     Description: Click an element.
  39.     Examples:
  40.         click('a51')
  41.         click('b22', button='right')
  42.         click('48', button='middle', modifiers=['Shift'])
  43. dblclick(bid: str, button: Literal['left', 'middle', 'right'] = 'left', modifiers: list[typing.Literal['Alt', 'Control', 'ControlOrMeta', 'Meta', 'Shift']] = [])
  44.     Description: Double click an element.
  45.     Examples:
  46.         dblclick('12')
  47.         dblclick('ca42', button='right')
  48.         dblclick('178', button='middle', modifiers=['Shift'])
  49. hover(bid: str)
  50.     Description: Hover over an element.
  51.     Examples:
  52.         hover('b8')
  53. press(bid: str, key_comb: str)
  54.     Description: Focus the matching element and press a combination of keys. It accepts the logical key names that are emitted in the keyboardEvent.key property of the keyboard events: Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp, F1 - F12, Digit0 - Digit9, KeyA - KeyZ, etc. You can alternatively specify a single character you'd like to produce such as "a" or "#". Following modification shortcuts are also supported: Shift, Control, Alt, Meta, ShiftLeft, ControlOrMeta. ControlOrMeta resolves to Control on Windows and Linux and to Meta on macOS.
  55.     Examples:
  56.         press('88', 'Backspace')
  57.         press('a26', 'ControlOrMeta+a')
  58.         press('a61', 'Meta+Shift+t')
  59. focus(bid: str)
  60.     Description: Focus the matching element.
  61.     Examples:
  62.         focus('b455')
  63. clear(bid: str)
  64.     Description: Clear the input field.
  65.     Examples:
  66.         clear('996')
  67. drag_and_drop(from_bid: str, to_bid: str)
  68.     Description: Perform a drag & drop. Hover the element that will be dragged. Press left mouse button. Move mouse to the element that will receive the drop. Release left mouse button.
  69.     Examples:
  70.         drag_and_drop('56', '498')
  71. upload_file(bid: str, file: str | list[str])
  72.     Description: Click an element and wait for a "filechooser" event, then select one or multiple input files for upload. Relative file paths are resolved relative to the current working directory. An empty list clears the selected files.
  73.     Examples:
  74.         upload_file('572', '/home/user/my_receipt.pdf')
  75.         upload_file('63', ['/home/bob/Documents/image.jpg', '/home/bob/Documents/file.zip'])
  76. """
复制代码
2.4 上下文

让 AI 做决策,意味着它需要对环境有深刻的理解,甚至具备一定程度的“常识”,在已知的模型能力下,往往和高质量的prompt和上下文强相关。要让 AI 胜任这个角色,必须给它提供一套明确的行动框架:清晰的工具集、详尽的工具使用场景、固定的工作流,甚至要细化到每个决策节点的触发时机。
2.4.1 需求

在以自然语言为接口、大模型为核心的 Software 3.0 时代,AI Agent 作为上下文驱动的生成式应用,需突破传统上下文窗口的固有局限。传统依赖上下文窗口维持对话状态与任务记忆的方式,存在长度受限、组织无序、知识静态、成本高昂四大痛点 —— 既无法承载超长历史信息,也难以高效检索与动态更新知识,更会因长文本处理消耗大量计算资源。
从本质上讲,上下文(Context)是提供给 LLM 的、用于完成下一步推理或生成任务的全部信息集合,从系统架构视角看,Agentic System 可类比为新型操作系统:LLM 扮演 CPU 角色,上下文窗口则如同容量有限的 RAM,而上下文工程就是核心的 “内存管理器”—— 其核心职责并非简单填充数据,而是通过智能调度算法,动态决定上下文数据的加载与换出,确保系统高效运行与结果精准性。上下文工程具体如下图所示。
2.png

图中所有模块可分为 “上下文输入源” 和 “输出 / 工具支撑” 两类:
分类模块作用说明上下文输入源Instructions / System Prompt模型的 “规则 / 角色定义”,决定模型的行为模式(如 “你是一个严谨的助手”)State / History(Short-term Memory)短期记忆:当前会话的历史交互记录,保证对话连贯性Long-term Memory长期记忆:跨会话的用户 / 任务信息(如用户偏好、历史任务结果)User Prompt用户当前的查询指令,是上下文的核心触发点Retrieved Information(RAG)检索增强生成:从外部知识库(文档、数据库)中调取的相关信息输出 / 工具支撑Available Tools模型可调用的外部工具(如计算器、搜索引擎),扩展模型能力Structured Output模型输出的结构化格式(如 JSON、表格),提升结果的可用性2.4.2 核心特色

AgentContext 是 OpenHands 框架中管理提示词扩展的核心结构,负责整合所有影响系统扩展和解释用户提示的上下文信息。它将静态环境细节(如代码库信息)和动态用户激活的扩展(如技能组件)结合,为大语言模型(LLM)交互提供完整的提示词上下文,是组装、格式化和注入所有与提示相关信息的主要容器。

  • 多维度上下文整合:统一管理代码库上下文、运行时环境、对话指令和知识技能等多类信息,避免上下文分散。
  • 技能扩展机制:支持通过技能(Skill)动态扩展提示词,技能可被用户输入触发,自动注入领域知识或指导信息。
  • 灵活的提示词后缀:提供系统消息后缀和用户消息后缀,可按需附加额外信息(如代码库详情、运行时参数)。
  • 用户技能自动加载:支持从本地目录自动加载用户自定义技能,且避免与显式技能重复。
2.4.3 流程图

3.png

2.4.4 代码
  1. class AgentContext(BaseModel):
  2.     """管理提示词扩展的核心结构。
  3.     AgentContext 统一了所有影响系统扩展和解释用户提示的上下文输入,
  4.     融合了静态环境细节和来自技能的动态用户激活扩展。
  5.     具体包含:
  6.     - **代码库上下文/代码库技能**:活跃代码库、分支信息及代码库技能提供的特定指令。
  7.     - **运行时上下文**:当前执行环境(主机、工作目录、密钥、日期等)。
  8.     - **对话指令**:约束或指导智能体在会话中行为的任务/渠道特定规则(可选)。
  9.     - **知识技能**:可被用户输入触发的扩展组件,用于注入知识或领域特定指导。
  10.     这些元素共同使 AgentContext 成为负责组装、格式化和注入所有提示相关上下文到
  11.     LLM 交互中的主要容器。
  12.     """  # noqa: E501
  13.     skills: list[Skill] = Field(
  14.         default_factory=list,
  15.         description="List of available skills that can extend the user's input.",
  16.     )
  17.     system_message_suffix: str | None = Field(
  18.         default=None, description="Optional suffix to append to the system prompt."
  19.     )
  20.     user_message_suffix: str | None = Field(
  21.         default=None, description="Optional suffix to append to the user's message."
  22.     )
  23.     load_user_skills: bool = Field(
  24.         default=False,
  25.         description=(
  26.             "Whether to automatically load user skills from ~/.openhands/skills/ "
  27.             "and ~/.openhands/microagents/ (for backward compatibility). "
  28.         ),
  29.     )
  30.     @field_validator("skills")
  31.     @classmethod
  32.     def _validate_skills(cls, v: list[Skill], _info):
  33.         """验证技能列表,确保无重复名称。"""
  34.         if not v:
  35.             return v
  36.         # 检查重复的技能名称
  37.         seen_names = set()
  38.         for skill in v:
  39.             if skill.name in seen_names:
  40.                 raise ValueError(f"Duplicate skill name found: {skill.name}")
  41.             seen_names.add(skill.name)
  42.         return v
  43.     @model_validator(mode="after")
  44.     def _load_user_skills(self):
  45.         """若启用,则从用户主目录加载自定义技能。"""
  46.         if not self.load_user_skills:
  47.             return self
  48.         try:
  49.             # 加载用户技能
  50.             user_skills = load_user_skills()
  51.             # 合并用户技能与显式技能,避免重复
  52.             existing_names = {skill.name for skill in self.skills}
  53.             for user_skill in user_skills:
  54.                 if user_skill.name not in existing_names:
  55.                     self.skills.append(user_skill)
  56.                 else:
  57.                     logger.warning(
  58.                         f"Skipping user skill '{user_skill.name}' "
  59.                         f"(already in explicit skills)"
  60.                     )
  61.         except Exception as e:
  62.             logger.warning(f"Failed to load user skills: {str(e)}")
  63.         return self
  64.     def get_system_message_suffix(self) -> str | None:
  65.         """获取包含代码库技能内容和自定义后缀的系统消息。
  66.         自定义后缀通常包括:
  67.         - 代码库信息(仓库名称、分支名称、PR编号等)
  68.         - 运行时信息(如可用主机、当前日期)
  69.         - 对话指令(如用户偏好、任务详情)
  70.         - 代码库特定指令(从代码库技能收集)
  71.         """
  72.         # 筛选无触发条件的技能(始终激活的代码库技能)
  73.         repo_skills = [s for s in self.skills if s.trigger is None]
  74.         logger.debug(f"Triggered {len(repo_skills)} repository skills: {repo_skills}")
  75.         # 构建工作区上下文信息
  76.         if repo_skills:
  77.             # TODO(test): 添加渲染测试确保功能正常
  78.             formatted_text = render_template(
  79.                 prompt_dir=str(PROMPT_DIR),  # 模板目录
  80.                 template_name="system_message_suffix.j2",  # 系统消息后缀模板
  81.                 repo_skills=repo_skills,  # 代码库技能列表
  82.                 system_message_suffix=self.system_message_suffix or "",  # 自定义系统后缀
  83.             ).strip()
  84.             return formatted_text
  85.         # 若无可激活的代码库技能,直接返回自定义系统后缀(非空时)
  86.         elif self.system_message_suffix and self.system_message_suffix.strip():
  87.             return self.system_message_suffix.strip()
  88.         return None
  89.     def get_user_message_suffix(
  90.         self, user_message: Message, skip_skill_names: list[str]
  91.     ) -> tuple[TextContent, list[str]] | None:
  92.         """通过技能召回的知识增强用户消息。
  93.         流程如下:
  94.         - 提取用户消息的文本内容
  95.         - 匹配查询中的技能触发词
  96.         - 若有相关技能被触发,返回格式化的知识和触发的技能名称
  97.         """  # noqa: E501
  98.         user_message_suffix = None
  99.         # 处理自定义用户消息后缀
  100.         if self.user_message_suffix and self.user_message_suffix.strip():
  101.             user_message_suffix = self.user_message_suffix.strip()
  102.         # 提取用户消息中的纯文本内容
  103.         query = "\n".join(
  104.             c.text for c in user_message.content if isinstance(c, TextContent)
  105.         ).strip()
  106.         recalled_knowledge: list[SkillKnowledge] = []
  107.         # 若查询为空,仅返回自定义用户后缀(如有)
  108.         if not query:
  109.             if user_message_suffix:
  110.                 return TextContent(text=user_message_suffix), []
  111.             return None
  112.         # 在查询中搜索技能触发词
  113.         for skill in self.skills:
  114.             if not isinstance(skill, Skill):
  115.                 continue
  116.             # 匹配技能触发条件
  117.             trigger = skill.match_trigger(query)
  118.             if trigger and skill.name not in skip_skill_names:
  119.                 logger.info(
  120.                     "Skill '%s' triggered by keyword '%s'",
  121.                     skill.name,
  122.                     trigger,
  123.                 )
  124.                 # 收集触发技能的知识
  125.                 recalled_knowledge.append(
  126.                     SkillKnowledge(
  127.                         name=skill.name,
  128.                         trigger=trigger,
  129.                         content=skill.content,
  130.                     )
  131.                 )
  132.         # 若有触发的技能,渲染知识内容
  133.         if recalled_knowledge:
  134.             formatted_skill_text = render_template(
  135.                 prompt_dir=str(PROMPT_DIR),
  136.                 template_name="skill_knowledge_info.j2",  # 技能知识模板
  137.                 triggered_agents=recalled_knowledge,  # 触发的技能知识列表
  138.             )
  139.             # 合并自定义用户后缀
  140.             if user_message_suffix:
  141.                 formatted_skill_text += "\n" + user_message_suffix
  142.             return TextContent(text=formatted_skill_text), [
  143.                 k.name for k in recalled_knowledge
  144.             ]
  145.         # 若无触发技能,仅返回自定义用户后缀(如有)
  146.         if user_message_suffix:
  147.             return TextContent(text=user_message_suffix), []
  148.         return None
复制代码
2.5 提示词(prompt)

CodeActAgent的提示词(prompt)是通过PromptManager从文件中加载的。具体来说:

  • 提示词位置:提示词文件位于openhands/agenthub/codeact_agent/prompts/目录下,主提示词文件名为codeact_agent_system_prompt.hbs。
  • 加载机制:通过PromptManager类管理提示词,默认系统提示词文件名由AgentConfig的resolved_system_prompt_filename属性决定。
2.6 迭代修改

CodeActAgent 被设计为可以并且应该继续修改自己生成的代码。系统提供了多种工具和明确的指导原则来支持迭代开发过程。代理被鼓励通过多次迭代来完善其解决方案,包括修改、测试和重新修改代码,直到达到满意的结果。这种设计符合 CodeAct 论文的理念,即将所有操作统一到代码执行空间中,从而简化和提高代理性能。

  • 系统设计系统设计明确鼓励 CodeActAgent 迭代修改代码:

    • 在系统提示中,有明确的指导原则:"当探索代码库时,使用高效的工具如 find、grep 和 git 命令,并在适当时候使用过滤器来最小化不必要的操作"
    • 提示中还强调:"在实现任何更改之前,首先通过探索彻底理解代码库"
    • "在 reproducing bugs 或 implementing fixes 时,使用单个文件而不是创建具有不同版本的多个文件"

  • 系统提示中的文件系统指南明确支持修改现有代码:

    • "如果被要求编辑文件,直接编辑文件,而不是创建具有不同文件名的新文件"
    • "对于全局搜索和替换操作,考虑使用 sed 而不是多次打开文件编辑器"
    • "永远不要为同一文件创建多个版本"

  • 系统提示中定义的问题解决工作流程明确支持迭代修改:

    • 探索:彻底探索相关文件并理解上下文
    • 分析:考虑多种方法并选择最有希望的一种
    • 测试:为 bug 修复创建测试以在实施修复之前验证问题
    • 实施:进行有针对性的、最小的更改以解决问题
    • 验证:如果环境设置为运行测试,则彻底测试实现,包括边缘情况

  • CodeActAgent 拥有多种工具来支持迭代修改代码:

    • 文件编辑工具(create_str_replace_editor_tool 和 LLMBasedFileEditTool)允许它修改现有文件
    • Bash 命令执行工具允许它运行测试、编译代码、安装依赖等
    • IPython 执行工具允许它测试代码片段

0x03 工作流程

我们接下来看看 CodeActAgent 的工作流程。
3.1 决策流程

step方法是决策过程,返回各种Action:

  • CmdRunAction(command) - 要运行的bash命令
  • IPythonRunCellAction(code) - 要运行的IPython代码
  • AgentDelegateAction(agent, inputs) - 用于(子)任务的委托操作
  • MessageAction(content) - 要运行的消息操作(例如,请求澄清)
  • AgentFinishAction() - 结束交互
  • CondensationAction(...) - 通过遗忘指定事件并可选地提供摘要来压缩对话历史
  • FileReadAction(path, ...) - 从指定路径读取文件内容
  • FileEditAction(path, ...) - 使用基于LLM(已弃用)或基于ACI的编辑方式编辑文件
  • AgentThinkAction(thought) - 记录代理的思考/推理过程
  • CondensationRequestAction() - 请求压缩对话历史
  • BrowseInteractiveAction(browser_actions) - 使用指定操作与浏览器交互
  • MCPAction(name, arguments) - 与MCP服务器工具交互
具体代码如下:
  1.     def step(self, state: State) -> 'Action':
  2.         """使用CodeAct Agent执行一步操作。
  3.         
  4.         包括收集先前步骤的信息,并提示模型生成要执行的命令。
  5.         
  6.         参数:
  7.         - state (State): 用于获取更新的信息
  8.         """
  9.         # 处理待处理操作(如果有)
  10.         if self.pending_actions:
  11.             # 返回并移除队列中的第一个待处理操作
  12.             return self.pending_actions.popleft()
  13.         # 如果任务已完成,退出
  14.         # 获取最新的用户消息
  15.         latest_user_message = state.get_last_user_message()
  16.         # 若用户输入"/exit",则返回结束操作
  17.         if latest_user_message and latest_user_message.content.strip() == '/exit':
  18.             return AgentFinishAction()
  19.         # 压缩状态中的事件。如果获得视图,将其传递给对话管理器处理;
  20.         # 如果获得压缩事件,则返回该事件而非操作。控制器将立即要求代理使用新视图再次执行步骤
  21.         condensed_history: list[Event] = []
  22.         # 匹配压缩器返回的结果类型
  23.         match self.condenser.condensed_history(state):
  24.             # 若为View类型,提取事件列表作为压缩历史
  25.             case View(events=events):
  26.                 condensed_history = events
  27.             # 若为Condensation类型,返回其包含的压缩操作
  28.             case Condensation(action=condensation_action):
  29.                 return condensation_action
  30.         # 打印调试日志:显示处理的压缩事件数量和总事件数量
  31.         logger.debug(
  32.             f'从共{len(state.history)}个事件中处理{len(condensed_history)}个压缩事件'
  33.         )
  34.         # 获取初始用户消息(从状态历史中)
  35.         initial_user_message = self._get_initial_user_message(state.history)
  36.         # 构建用于LLM的消息列表(基于压缩历史和初始用户消息)
  37.         messages = self._get_messages(condensed_history, initial_user_message)
  38.         # 构建LLM调用参数
  39.         params: dict = {
  40.             'messages': messages,  # 消息列表
  41.         }
  42.         # 检查并添加可用工具(根据LLM配置过滤)
  43.         params['tools'] = check_tools(self.tools, self.llm.config)
  44.         # 添加额外元数据(从状态中提取,适配LLM格式)
  45.         params['extra_body'] = {
  46.             'metadata': state.to_llm_metadata(
  47.                 model_name=self.llm.config.model, agent_name=self.name
  48.             )
  49.         }
  50.         # 调用LLM获取响应
  51.         response = self.llm.completion(** params)
  52.         # 打印调试日志:显示LLM返回的响应
  53.         logger.debug(f'LLM返回的响应: {response}')
  54.         # 将LLM响应转换为具体操作列表
  55.         actions = self.response_to_actions(response)
  56.         # 打印调试日志:显示转换后的操作
  57.         logger.debug(f'response_to_actions转换后的操作: {actions}')
  58.         # 将所有操作添加到待处理队列
  59.         for action in actions:
  60.             self.pending_actions.append(action)
  61.         # 返回并移除队列中的第一个操作
  62.         return self.pending_actions.popleft()
复制代码
3.2 消息处理

_get_messages方法负责处理消息。该方法执行以下步骤:

  • 检查事件中是否有SystemMessageAction,若缺失则添加(为了向后兼容)
  • 将事件(操作和观察结果)处理为消息,包括SystemMessageAction
  • 在函数调用模式下处理工具调用及其响应
  • 管理消息角色交替(用户/助手/工具)
  • 为特定LLM提供商(如Anthropic)应用缓存
  • 在非函数调用模式下添加环境提醒
  1.     def _get_messages(
  2.         self, events: list[Event], initial_user_message: MessageAction
  3.     ) -> list[Message]:
  4.         """为LLM对话构建消息历史。
  5.         
  6.         该方法通过处理状态中的事件并将其格式化为LLM可理解的消息,构建结构化的对话历史。
  7.         它处理常规消息流和函数调用场景。
  8.                
  9.         参数:
  10.             events: 要转换为消息的事件列表
  11.         
  12.         返回:
  13.             list[Message]: 格式化的消息列表,可直接供LLM使用,包括:
  14.                 - 带提示的系统消息(来自SystemMessageAction)
  15.                 - 操作消息(来自用户和助手)
  16.                 - 观察消息(包括工具响应)
  17.                 - 环境提醒(在非函数调用模式下)
  18.         
  19.         注意:
  20.             - 在函数调用模式下,工具调用及其响应会被仔细跟踪以维持正确的对话流程
  21.             - 同一角色的消息会被合并,以避免连续出现相同角色的消息
  22.             - 对于Anthropic模型,会根据其文档对特定消息进行缓存
  23.         """
  24.         # 若未实例化提示管理器,抛出异常
  25.         if not self.prompt_manager:
  26.             raise Exception('提示管理器未实例化。')
  27.         # 使用对话内存处理事件(包括SystemMessageAction)
  28.         messages = self.conversation_memory.process_events(
  29.             condensed_history=events,  # 压缩后的事件历史
  30.             initial_user_action=initial_user_message,  # 初始用户消息
  31.             max_message_chars=self.llm.config.max_message_chars,  # 消息最大字符数限制
  32.             vision_is_active=self.llm.vision_is_active(),  # 是否启用视觉功能
  33.         )
  34.         # 若LLM启用了提示缓存,应用缓存机制
  35.         if self.llm.is_caching_prompt_active():
  36.             self.conversation_memory.apply_prompt_caching(messages)
  37.         # 返回构建的消息列表
  38.         return messages
复制代码
3.3 历史压缩(Condensation)

在step函数中,会通过Condensor压缩对话历史,避免上下文过长。
  1.         # Condense the events from the state. If we get a view we'll pass those
  2.         # to the conversation manager for processing, but if we get a condensation
  3.         # event we'll just return that instead of an action. The controller will
  4.         # immediately ask the agent to step again with the new view.
  5.         condensed_history: list[Event] = []
  6.         match self.condenser.condensed_history(state):
  7.             case View(events=events):
  8.                 condensed_history = events
  9.             case Condensation(action=condensation_action):
  10.                 return condensation_action
复制代码
3.4 内存管理

成员变量conversation_memory会进行会话内存管理。
  1. self.conversation_memory = ConversationMemory(self.config, self.prompt_manager)
复制代码
具体代码参见:
  1.     def _get_messages(
  2.         self, events: list[Event], initial_user_message: MessageAction
  3.     ) -> list[Message]:
  4.         # Use ConversationMemory to process events (including SystemMessageAction)
  5.         messages = self.conversation_memory.process_events(
  6.             condensed_history=events,
  7.             initial_user_action=initial_user_message,
  8.             max_message_chars=self.llm.config.max_message_chars,
  9.             vision_is_active=self.llm.vision_is_active(),
  10.         )
  11.         if self.llm.is_caching_prompt_active():
  12.             self.conversation_memory.apply_prompt_caching(messages)
  13.         return messages
复制代码
0xFF 参考

https://docs.all-hands.dev/openhands/usage/architecture/backend
当AI Agent从“玩具”走向“工具”,我们该关注什么?Openhands架构解析【第二篇:Agent 相关核心概念】  克里
当AI Agent从“玩具”走向“工具”,我们该关注什么?Openhands架构解析【第一篇:系列导读】 克里
Coding Agent之Openhands解析(含代码)  Arrow
OpenHands 源码解读  一力辉
SWE-agent 详解:打造大模型与计算机的编程界面 mannaandpoem
https://swe-agent.com/paper.pdf
浅读 OpenDevin  mannaandpoem
深度拆解 Claude 的 Agent 架构:MCP + PTC、Skills 与 Subagents 的三维协同
这是一本40页的上下文工程ebook
人与Language Agent交互的下一个范式:从Attention到Memory的Agree-on

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

昨天 16:07

举报

您需要登录后才可以回帖 登录 | 立即注册