Agent 产品真正困难的地方,不只是“让模型调用工具”。
更底层的问题是:
你如何定义 Agent 与世界交互的基本单位?
这个问题会决定很多后续设计:
- 工具调用结果是否可审计;
- 执行历史是否可回放;
- 状态是否可以恢复;
- 多环境执行是否可替换;
- 控制流由框架、代码还是模型决定;
- Agent 出错后能否解释原因;
- 产品是否能从 demo 走向稳定系统。
我选择从 OpenHands 出发,是因为它把 Agent 抽象问题问得很清楚:Agent 想让世界发生什么,以及世界返回了什么。
这就是它的核心二元模型:
Action = Agent 或 User 想让世界发生什么
Observation = 世界反馈回来的状态
这篇文章不是要证明 OpenHands 一定最好,而是借它作为基准,比较几类主流 Agent 抽象模型的边界和取舍。
评价维度
比较 Agent 框架时,我最关心六个维度。
| 维度 | 问题 |
|---|---|
| Action 表达 | Agent 的行动是 JSON schema、消息、代码、图节点,还是专用命令? |
| Observation 地位 | 工具结果是一等公民,还是普通文本消息? |
| 状态模型 | 历史是 EventStream、State、Message Log,还是由调用方自己维护? |
| 控制流位置 | 决策由 LLM、图结构、代码、planner,还是多 Agent 对话决定? |
| Runtime 边界 | 执行环境是否从 Agent 核心中解耦? |
| 可审计性 | 能否 replay、debug、追踪因果链、恢复会话? |
这六个问题比“支持多少工具”“接了多少模型”更底层。
工具和模型可以后补,但抽象边界一旦选错,后面会很难改。
OpenHands:Action / Observation 作为世界模型
OpenHands 的核心世界观非常直接:
Action = 想让环境发生变化
Observation = 环境对这个动作的反馈
Action 是强类型的。
CmdRunAction(command="pytest tests/")
FileWriteAction(path="src/agent.py", content="...")
BrowseInteractiveAction(browser_actions=...)
AgentFinishAction(outputs={...})
Observation 也是强类型的。
CmdOutputObservation(exit_code=0, content="...stdout...")
FileReadObservation(path="...", content="...")
BrowserObservation(screenshot=..., dom=...)
ErrorObservation(message="...")
这不是简单的 API 设计,而是一种事件词汇表。每一种 Action 都对应一种或多种可能的 Observation,Agent 的执行历史可以被建模成一条不断追加的事件流。
OpenHands 这个模型背后有两个关键支柱。
EventStream
所有 Action 和 Observation 都被追加写入 EventStream。
Agent 在推理时看到的不是一个不断被覆盖的状态对象,而是一条有因果顺序的执行历史。
这带来几个能力:
- replay;
- debug;
- audit;
- resume;
- 多 Agent 协作的事件基础;
- 成本和工具调用追踪。
对于 Agent 产品来说,这一点非常重要。
如果执行历史只是 prompt 里的文本,系统很难知道哪些内容是用户输入、哪些是工具结果、哪些是模型推理、哪些是环境错误。
Runtime 解耦
OpenHands 的另一个关键设计是 Runtime。
Agent 核心不应该关心命令到底在本地进程、Docker 容器、远程沙箱还是云端环境里执行。
CmdRunAction 只是表达“我要执行命令”。至于执行发生在哪里,应该由 Runtime 负责。
这让 Agent 核心和执行环境解耦:
Agent Core
emits Action
↓
Runtime
executes Action
↓
Observation
returns to Agent
这个边界决定了系统能不能多环境部署、能不能沙箱隔离、能不能测试 mock、能不能远程执行。
我的判断是:
OpenHands 最值得学习的不是某个工具实现,而是它把 Observation 和 Runtime 都提升到了架构一等公民。
ReAct:正确范式,但不是完整架构
ReAct 的核心是:
Thought → Action → Observation
它的重要贡献是把中间推理步骤显式化。模型先思考,再行动,再根据观察继续思考。
这个范式非常重要,因为复杂任务确实需要 reasoning 和 acting 交替推进。
但 ReAct 本身不是完整产品架构。
它没有回答:
- Action 类型怎么定义;
- Observation 是否结构化;
- 工具结果怎么持久化;
- Runtime 在哪里;
- 会话怎么恢复;
- 控制流如何被审计;
- 多步执行如何防止历史漂移。
所以我会把 ReAct 看作 Agent 思维模式,而不是系统架构。
OpenHands 基本继承了 ReAct 的后两步,但把 Thought 内化到 LLM token 流里,没有把 Thought 作为 EventStream 的一等事件长期保存。
这带来一个取舍:
- 工程更干净;
- 但推理过程不完全可 replay。
LangGraph:状态机务实,但限制自主性
LangGraph 的核心抽象是:
Node → Edge → State
开发者定义节点、边和条件跳转,Agent 在图里流转。
这是一种很工程化的思路。
它的优点是:
- 控制流清楚;
- 可视化友好;
- 易测试;
- 适合企业流程;
- 适合有明确步骤和审批节点的任务。
但它也有一个根本张力:
如果图结构是预定义的,Agent 的自主性会被拓扑限制;如果图结构高度动态,图模型的可视化和确定性优势又会下降。
LangGraph 是 State first,OpenHands 是 Event first。
这两种模型适合不同场景。
| 场景 | 更适合 |
|---|---|
| 固定业务流程、审批、条件分支 | LangGraph |
| 开放式软件工程任务、动态工具使用 | OpenHands |
| 强可视化、强流程编排 | LangGraph |
| 强审计、强回放、强执行环境隔离 | OpenHands |
我的判断是:LangGraph 很适合 Workflow Agent,但不一定适合所有 Execution Agent。
AutoGen:多 Agent 对话正确,工具交互偏弱
AutoGen 的核心抽象是:
Agent ↔ Agent Message
它把多个 Agent 之间互相发消息作为一等公民。
这个模型对多 Agent 协作很自然。Planner、Coder、Reviewer、User Proxy 之间确实可以用 message passing 组织。
但问题在于,当这个模型也被用于单 Agent 与工具环境交互时,Observation 容易退化成普通消息。
工具结果如果只是另一条 message,就会丢失很多结构信息:
- exit code;
- file path;
- raw ref;
- stdout / stderr;
- error type;
- tool call id;
- observation source;
- retry metadata。
对多 Agent 协调来说,消息模型很自然。
对工具调用和环境反馈来说,消息模型不够精确。
所以我的判断是:
AutoGen 的多 Agent 直觉是对的,但 Observation 不应该被降格成普通消息。
Claude Code / Aider:极简直接,但平台化能力有限
Claude Code 和 Aider 这类工具的抽象更接近底层模型 API:
tool_use → tool_result
这和 OpenHands 的 Action / Observation 映射很直接。
tool_use 等价于 Action,tool_result 等价于 Observation。
这种设计的优点是:
- 简单;
- 贴近模型 API;
- 调试直接;
- 小团队上手快;
- 个人工具体验好。
但它的上限也很明显:
- Runtime 解耦不足;
- EventStream 不完整;
- 状态管理通常由调用方维护;
- replay 和审计能力弱;
- 多环境部署需要额外封装。
这类设计很适合个人 CLI、IDE 插件、小规模工具。
但如果要做平台级 Agent 系统,只贴着 API message 格式是不够的。
smolagents:Code Action 很激进,也很有启发
smolagents 的核心思路是:
让 LLM 直接输出 Python 代码作为 Action。
这非常激进。
传统工具调用是有限词汇表:模型只能调用系统预定义的工具和参数 schema。
Code Action 则把行动语言变成图灵完备的代码。
例如,模型不必连续调用多个工具:
result = read_file("agent.py")
lines = result.split("\n")
error_line = next(line for line in lines if "Error" in line)
print(error_line)
它可以在一次代码块里组合读取、过滤、聚合、计算。
这个洞察非常深:
JSON schema 是有限行动语言,代码是可组合行动语言。
但代价也很明显:
- 安全沙箱要求极高;
- 错误通常表现为运行时异常;
- Observation 不够结构化;
- 自我修复需要解析异常文本;
- 审计粒度可能变粗。
我的判断是,Code Action 不应该替代所有结构化 Action,但可以作为复杂临时组合能力。
更现实的架构是:
常规操作
使用结构化 Action
复杂组合操作
使用受限 Code Action
所有执行结果
仍然转成结构化 Observation
SWE-agent / ACI:工具接口本身也需要为 Agent 设计
SWE-agent 的重点不完全在 Action / Observation 建模,而在 ACI:
Agent-Computer Interface
它强调工具接口应该为 LLM 使用而设计,而不是直接暴露人类开发者使用的底层命令。
这个观点很重要。
Agent 面对 shell、grep、sed、编辑器、浏览器时,并不一定应该使用人类一样的接口。它需要更适合模型理解和恢复的操作:
- 明确的文件编辑接口;
- 可解析的搜索结果;
- 有上下文的错误输出;
- 适合小步验证的测试反馈;
- 低歧义的 patch 应用方式。
这和 OpenHands 的 Runtime 是同一层面的思考:
Agent 不只是需要工具,还需要适合 Agent 的计算机接口。
横向比较
把几个模型放在一起,可以得到这个对比:
| 模型 | 核心抽象 | 优点 | 风险 |
|---|---|---|---|
| ReAct | Thought / Action / Observation | 推理范式清楚 | 不是完整架构 |
| LangGraph | Node / Edge / State | 流程清楚,可测试 | 自主性受预定义图限制 |
| AutoGen | Agent / Message | 多 Agent 协作自然 | 工具 Observation 易退化成普通消息 |
| Claude Code / Aider | tool_use / tool_result | 简单直接,贴近 API | Runtime、EventStream、审计较弱 |
| smolagents | Code as Action | 行动表达能力强 | 安全和结构化 Observation 难 |
| SWE-agent / ACI | Plan / Edit / Verify + ACI | 工具接口适合 LLM | 流程相对线性 |
| OpenHands | Action / Observation + EventStream + Runtime | 审计、回放、环境解耦强 | 体系偏重,原型成本高 |
这里没有绝对赢家。
不同抽象对应不同产品目标。
如果是固定流程企业 Agent,LangGraph 的确定性更有价值。
如果是多 Agent 协作实验,AutoGen 的 message passing 很自然。
如果是个人 coding assistant,Claude Code / Aider 的极简模型更实用。
如果是开放式软件工程 Agent 平台,OpenHands 的 Action / Observation / Runtime 更稳。
我的判断
我倾向于认同 OpenHands 的方向,但不会完整照搬它的复杂度。
原因有三个。
第一,Observation 应该是一等公民
很多 Agent 框架重视 Action,却低估 Observation。
但 Agent 真正学习下一步怎么做,依赖的不是“它调用了什么工具”,而是“世界怎么回应了它”。
一个命令失败、一个文件不存在、一次浏览器点击无效、一次测试报错、一次用户拒绝权限,这些都不应该只是自然语言字符串。
它们应该是结构化 Observation。
结构化 Observation 的价值包括:
- 减少模型重复解析;
- 支持错误分类;
- 支持重试策略;
- 支持上下文压缩;
- 支持审计和 replay;
- 支持 UI 展示;
- 支持后续训练数据构造。
第二,Runtime 必须解耦
Agent 不能默认绑定本机 shell。
一个成熟 Agent 产品需要面对:
- 本地执行;
- Docker 沙箱;
- 远程 workspace;
- 云端 runtime;
- 只读 reviewer;
- 隔离子任务;
- 测试环境 mock。
如果 Runtime 不抽象,产品后期会被执行环境绑死。
OpenHands 在这一点上是对的。
第三,EventStream 是审计和恢复的基础
Agent 产品要建立信任,不能只展示最终回答。
它需要回答:
- 用户说了什么;
- Agent 做了什么;
- 工具返回了什么;
- 哪一步失败了;
- 哪个 observation 导致了下一步 action;
- 哪些内容被压缩;
- 哪些权限被确认;
- 如何恢复到某个 checkpoint。
这些问题都需要事件流。
仍未解决的问题
即使 OpenHands 的方向更完整,Agent 抽象仍然有几个开放问题。
Observation 是原始事实,还是解释后信息?
exit_code=1 加 300 行错误日志是原始事实。
“第 47 行类型不匹配”是解释后信息。
Agent 每轮都重新解释原始日志会浪费 token,也不稳定。
但如果框架提前解释 Observation,又可能引入新的模型推断。
更合理的方式可能是同时保留:
raw observation
interpreted observation
confidence
raw_ref
控制流应该在哪里?
LangGraph 说控制流在图里。
OpenHands 说控制流主要由 LLM 动态决策。
smolagents 说控制流可以在代码里。
这三个答案都对,但适用场景不同。
我更倾向于:
业务流程控制
放在图或 workflow 中
开放式执行控制
交给 Agent 动态决策
局部复杂组合
允许受限代码表达
Thought 是否应该持久化?
OpenHands 不持久化完整 Thought,这在工程上简洁,也避免保存敏感推理链。
但代价是推理过程不完全可 replay。
未来可能需要一种折中:
- 不保存完整 chain-of-thought;
- 保存结构化 reasoning summary;
- 保存 decision rationale;
- 保存 action selection reason;
- 保存 confidence 和 uncertainty。
这比完整 Thought 更安全,也比完全不记录更可解释。
对 Agent 产品的启发
从产品经理视角,这篇比较最重要的启发有五点。
第一,Agent 产品不是“模型 + 工具列表”。
真正的产品能力来自一套稳定的执行抽象:Action、Observation、Runtime、EventStream、Context、Permission、Checkpoint。
第二,不同 Agent 产品应该选择不同控制流。
业务流程 Agent 需要可视化 workflow;软件工程 Agent 需要开放式执行;代码编辑 Agent 需要强 ACI;多 Agent 系统需要 message passing。
第三,工具结果要产品化。
Observation 不是日志文本,而是可以被 UI 展示、被上下文治理、被错误恢复、被审计系统使用的结构化产品对象。
第四,执行环境是产品边界。
本地、云端、沙箱、远程 workspace、只读 reviewer,不同执行环境对应不同安全、成本、体验和商业模式。
第五,抽象越底层,越要克制。
OpenHands 的完整模型很强,但并不适合所有原型。早期产品可以先实现最小 Action / Observation / EventLog,再逐步引入 Runtime、Condenser、Permission、Checkpoint。
结论
OpenHands 的 Action / Observation 模型之所以值得研究,是因为它把 Agent 与世界交互的基本单位定义得足够清楚。
它提醒我们:
Agent 的核心不是聊天,而是行动、观察、记忆、恢复和审计。
ReAct 提供了推理范式,LangGraph 提供了流程控制,AutoGen 提供了多 Agent 对话模型,Claude Code / Aider 展示了极简工具调用体验,smolagents 提醒我们代码可以成为行动语言,SWE-agent 强调工具接口要为 Agent 设计。
但如果目标是构建可持续迭代的 Execution Agent 产品,我会优先保留 OpenHands 的三个核心判断:
- Observation 是一等公民;
- Runtime 必须解耦;
- EventStream 是审计和恢复的基础。
在这个基础上,再根据具体产品场景选择图、消息、代码或专用 ACI。