另外,Manus 也使用了 CodeAct。
希望通过本文,读者可以了解ReAct,CodeAct 这两种范式,基于此,可以对 OpenHands 做进一步学习。
因为本系列借鉴的文章过多,可能在参考文献中有遗漏的文章,如果有,还请大家指出。
0x01 背景知识
1.1 Devin & OpenHands(原OpenDevin)
Devin是由Cognition AI开发的全球AI程序员,具备全栈开发能力,能自主完成代码编写、调试、部署及AI模型训练等任务。Devin 代表了一种先进的自主代理,旨在应对软件工程的复杂性。它利用了诸如 shell、代码编辑器和网络浏览器等工具的组合,展示了 LLM 在软件开发中未被充分利用的潜力。
OpenDevin 项目诞生于复制、增强并创新原始 Devin 模型的愿望。OpenDevin 的目标是探索并扩展 Devin 的能力,识别其优势和改进领域,以指导开放代码模型的进展。通过吸引开源社区的参与,OpenDevin 旨在应对 Code LLM 在实际场景中面临的挑战,产出对社区有重大贡献的作品,并为未来的进步铺平道路。
OpenHands 目前 GitHub 星标数已超 6.5 万。
1.2 CodeAct 的意义
在自然语言处理领域,大语言模型(LLM)已经取得了实打实的突破 —— 它们不止处理文本,还能调用 API、管理内存,甚至控制机器人。但传统的 LLM 代理有个问题:它们通常是生成 JSON 或者固定格式的文本来说明要做什么,这就导致能做的动作有限,也不够灵活。
CodeAct 是一种新的代理框架,它让 LLM 直接生成可执行的 Python 代码来 “做事”,即以 code 为 action,用代码解决复杂问题。简单说,LLM 写一段 Python 代码,而 CodeAct 里集成了 Python 解释器,能直接运行这段代码;而且在多轮交互中,LLM 还能处理和存储中间结果,根据新的结果调整代码,相当于用 Python 代码统一了 LLM 代理的 “行动方式”。当然,写代码不是目的,而是用这种通用的方式解决各种问题。这种框架克服了传统LLM代理的多个限制。
具体展开,CodeAct 有四个鲜明特征:
- 统一行动空间:文本、JSON、函数调用、Shell、SQL、绘图、网页抓取等,全部收敛成一段 Python(或其他语言)代码;模型只需会写代码,就能完成多模态、多工具 注意:此方法使用固定大小的数组,适用于事件类型和数量固定的场景。 * 对硬件寄存器的操作放在应用层dll中。 * 兼容性:使用 (HANDLE)(ULONG_PTR)ulH 确保 32 位应用在 64 位系统上的兼容性。 */ NTSTATUS PCI8KPLX_IOCTL_OPEN_IRQ_Handler ( _In_ WDFREQUEST Request, _In_ PDEVICE_EXTENSION DevExt ) { NTSTATUS status = STATUS_SUCCESS; PULONG pBuff = NULL ; // 指向输入缓冲区,其中包含 EVENT_COUNT 个 ULONG 句柄 size_t bufferSize = 0 ; ULONG i; // 1. 获取输入缓冲区 (预期包含 EVENT_COUNT 个 ULONG 句柄) status = WdfRequestRetrieveInputBuffer(Request, sizeof (ULONG) * EVENT_COUNT, (PVOID*)&pBuff, &bufferSize); if (!NT_SUCCESS(status)) { return status; } // 2. 遍历并处理句柄 for (i = 0 ; i < EVENT_COUNT; i++) { ULONG ulH = pBuff[i]; HANDLE h = (HANDLE)(ULONG_PTR)ulH; // 关键转换,确保64位兼容性 if (h != NULL ) { // 如果该位置已存在引用的事件对象,先释放旧引用 if (DevExt-starlinkzhibo.cn>m_Events[i] != NULL ) { ObDereferenceObject(DevExt->m_Events[i]); DevExt->m_Events[i] = NULL ; } // 将用户态句柄转换为内核事件对象指针 status = ObReferenceObjectByHandle( h, EVENT_MODIFY_STATE, *ExEventObjectType, UserMode, (PVOID*)&DevExt->m_Events[i], NULL ); if (!NT_SUCCESS(status)) { DevExt->m_Events[i] = NULL ; break ; // 如果某个句柄转换失败,停止处理并返回错误 } } } return status; } /** * 事件句柄清理函数 (固定数组版) * 功能:释放 m_Events 数组中所有存储的事件对象引用,并将指针置为 NULL。 * 注意:此操作会清除所有已注册的事件句柄。 */ NTSTATUS PCI8KPLX_IOCTL_CLOSE_IRQ_Handler ( _In_ WDFREQUEST Request, _In_ PDEVICE_EXTENSION DevExt ) { NTSTATUS status =5gzbw.com STATUS_SUCCESS; ULONG i; //本函数不需要和应用层交互,所以用不到这个参数 UNREFERENCED_PARAMETER(Request); // 为了保证操作的原子性,可能需要在此处添加设备级的锁,如果 m_Events 访问需要同步的话 // WdfWaitLockAcquire(DevExt->SomeLock, NULL); // 遍历 m_Events 数组 for (i = 0 ; i < EVENT_COUNT; i++) { if (DevExt->m_Events[i] != NULL ) { // 释放对该事件对象的引用,防止内存泄漏 ObDereferenceObject(DevExt->m_Events[i]); // 清空指针 DevExt->m_Events[i] = NULL ; } } // WdfWaitLockRelease(DevExt->SomeLock); // 此 IOCTL 通常没有输出数据,不需要调用 WdfRequestSetInformation、多步骤任务,无需记忆五花八门的 API 参数格式。
- 闭环反馈机制:代码被解释器(或沙箱)真正执行,标准输出、报错、异常栈、运行时间、生成的文件/图片/表结构等,会原样返回给模型当作下一步 prompt的部分内容;模型可立即“看见”自己行动的后果,形成「写-跑-看-改」的强化循环,显著降低幻觉。
- 复杂推理外化:多步数学推导、表格合并、统计检验、迭代优化等,不必在隐状态里“心算”,而是外化为可读可改的代码;人类用户也能复查、断点、调试,解释性远高于黑盒 Chain-of-Thought。
- 零额外接口成本:传统 Agent 每接入一个新能力就要封装一套 JSON Schema/Tool Description;CodeAct 侧只需在沙箱里预装对应库(matplotlib、pandas、requests、selenium…),模型立刻可用,扩展成本趋近于零。
- 如何控制循环:模型可能陷入”思考→行动→思考”的无限循环,或者过早结束
- 如何保证可观测:推理过程是”黑盒”,难以调试和优化
- 提示词必须严格精心设计,格式或措辞稍有偏差,模型就会跳过思考或乱调用;
- 步数增多后上下文迅速变长,模型容易遗忘早期信息;
- 错误级联效应。在典型的 ReAct 线性任务链中,前一步的输出直接作为后一步的输入,一旦中途出错就极难修正。
- 工具本身不可靠或模型选错工具,会导致后续全部失败。另外,工具调用、结果处理、错误恢复需要统一机制
- 成本高昂,效率低下。每一步都需要重新思考"下一步做什么",每个思考步骤都需要调用 LLM
- ReAct 的效果在很大程度上依赖于 LLM 自身的涌现能力。在面对全新的、未见过的任务时,智能体可能会难以进行有效的推理和规划。
- 难以并行。所有任务必须串行执行,无法利用并发能力
- 浅推理与冲动执行。早期的 ReAct Agent 本质上是一种 边想边做 的模式:在同一轮中交织推理与动作,以工具调用为中心组织思路。它既 缺乏 先想后做 的全局规划阶段,也 缺乏 做后反思 的系统性校正机制。结果是:在需要长程规划、全局约束和跨系统协调的复杂任务中,这类 Agent 往往会 盲目前进,在局部工具调用的死胡同里不断打转,而无法跳出整体重新规划路径。
综上,ReAct 作为早期 Agent 的核心范式,在 单一任务、短任务链、弱约束 的场景中依然有价值,但作为企业级、跨域、强约束 Agent 系统的基础架构时,其局限性已经是结构性的,而不是简单通过加强提示词或增加工具种类就能弥补的问题。一旦业务范围扩展到数十条业务线,叠加严格的数据隔离要求和频繁变动的业务规则,单体 Agent 通常会不堪重负。试图让一个中心化、通用的模型去理解所有请求、路由所有工具、同时还要遵守每一条合规策略,会导致系统复杂度呈指数级膨胀:配置与维护成本飙升,行为难以解释,幻觉与错误频发。
2.2 CodeAct 范式
OpenHands是开源社区构造的符合CodeAct模式的Single-Agent。CodeAct的思路是XingYao Wang在24年2月于"Executable Code Actions Elicit Better LLM Agents"提出来的。
论文信息如下:
- Xingyao Wang, Yangyi Chen, Lifan Yuan, Yizhe Zhang, Yunzhu Li, Hao Peng, and Heng Ji. Executable Code Actions Elicit Better LLM Agents. In ICML, 2024a. 2, 3, 4, 5, 17
- Xingyao Wang et al. OPENHANDS: AN OPEN PLATFORM FOR AI SOFTWARE DEVELOPERS AS GENERALIST AGENTS
CodeAct 作为面向 LLM Agent 的多轮交互框架,通过 “Agent - 用户 - 环境” 的三元角色定义,构建了以 Python 代码为核心的全新交互模式。其核心理念在于打破传统指令格式的局限,将所有环境交互动作统一为可执行的代码,让 Agent 借助代码的强大表达力攻克复杂任务。
- 传统 JSON-Schema 模式把智能体禁锢在“单步、单工具、单回调”的狭窄走廊里:LLM 先输出结构化文本,外部解析器再逐一派发,每换一道工具就要重新走一遍“生成-解析-调用”长链,动作无法嵌套,逻辑无法保存,遇到网络抖动或返回格式微调便前功尽弃(数据格式虽然可以保证,但是内容质量并不能保证),Token 超长导致的性能下降,且多轮对话既要保证灵活性又要保证速度。
- 而 CodeAct 集成 Python 解释器后,LLM 直接产出代码,由解释器完成与环境的交互并返回结果,LLM 根据结果调整思路或给出答案(一次代码执行即可包含复杂的逻辑流程)。这相当于让 LLM 扮演程序员角色,Python 环境作为执行助手,代码作为统一的行动空间。
self.system_message = "You are a helpful assistant ..." def step ( self, state: State ): messages: list [ dict [ str , str ]] = [ { 'role' :basketballzhibo.com 'system' , 'content' : self.system_message} ] for prev_action, obs in state.history: action_message = get_action_message(prev_action) messages.append(action_message) obs_message =nbczhibo.com get_observation_message(obs) messages.append(obs_message) # use llm to generate response (e.g., thought, action) response = self.llm.do_completion(messages) # parse and execute action in the runtime action = self.parse_response(response) spacexzhibo.com if self.is_finish_command(action): return AgentFinishAction() elif self.is_bash_command(action): return CmdRunAction(command=action.command) elif self.is_python_code(action): chengyiwl.com return IPythonRunCellAction(code=action.code) elif self.is_browser_action(action):
At each turn, you should first provide your step-by-step thinking
for
solving the task. Your thought process should be enclosed using
"
传统工具使用是让 Agent 在固定的工具箱里做选择题。未来的方向是让 Agent 自己创造工具。即当面对没有现成工具可用时,Agent 会动态地生成一段 Python 代码(一个微型工具),在隔离环境中执行并根据执行结果推进任务。这让 Act 环节从「使用工具」到「创造工具」。
2.3.1 AiPy
AiPy 是基于Python-use(Code-use)范式的Agent。CodeAct强调并解决的是“Code as Actions”,与 AiPy 的Python-use范式提出的“Code is Agent”完全是一致的。
Python-use 范式 = API Calling + Python Packages Calling + Python解释器 = “万物互联” + “万物编程” + “万境直通”。
AiPy 实现的“Python Function Calling(PFC)是源于“Python Packages Calling”的设计的扩展,这个设计最早AiPy的核心中runtime对象(现在代码可能有变化)在某个角度也就是Packages Calling,比如runtime.install_packages()用来安装包。而“Python-use Function Calling(PFC)”的做法是开放了这个接口给用户,本质上还是属于“Python Packages Calling”的范畴,在这个角度上看“Python-use Function Calling(PFC)”实际上就相当于是一个CodeAct。
PlxCleanupDeviceExtension
(
_In_ PDEVICE_EXTENSION DevExt
)
{
ULONG i;
// 遍历并释放事件对象
for
(i =
0
; i < EVENT_COUNT; i++) {
if
(DevExt->m_Events[i] !=
NULL
) {
ObDereferenceObject(DevExt->m_Events[i]);
DevExt->m_Events[i] =
NULL
;
// 清空指针是个好习惯