Agent 工程思考:从 ReAct 到 Agent Harness

作者:vivo 互联网项目团队- Ding Junjie

从 ReAct 出发,文章讨论 Agent 工程如何从"模型循环"走向 Harness:通过前后端共享的 State Schema、运行事实和 UI 边界,让模型行为变成可交互、可恢复、可控制、可追溯的产品能力。

1分钟看图掌握核心要点👇

大模型不是马,是大脑,而且是一颗刚刚觉醒的大脑。

一、Agent 不止是 model+loop

早期我理解的Agent 工程,可以被写成一段伪代码:

bash 复制代码
whilenot done:
  reason
  act
  observe

用户输入一句话,模型思考一下,决定调用工具。工具返回结果,模型再思考,再决定下一步。

这也是 ReAct 的基本形态。Thought、Action、Observation 交替出现,模型在生成内容的过程中调用工具,再把工具结果放回下一步推理。

早期Agent概念刚出,我做了一个 demo ,这个循环完全够用了。命令行里输出 token,中间插入 tool call,再把 observation 塞回 prompt,最后模型给出结论。

但最近开始深入去做Agent 产品,过程中疯狂调研codex、lobehub、goose、opencode、PI、Flue等优秀Agent产品,发现远远不止这段伪代码所表达的。

真正的产品还会遇到一组运行时问题:

  • 用户刷新页面之后,刚才的工具审批还在吗?
  • 工具执行到一半,后端进程重启了,下一次从哪里恢复?
  • 子 Agent 在后台跑,主线程要显示什么?
  • 生成了一个 artifact,内容本身不塞进上下文,那它的引用、状态、归属在哪里?
  • 用户点了 stop,哪些东西应该取消,哪些东西应该保留?

ReAct 不回答这些问题。

ReAct 解释的是模型怎么思考和行动。Agent 产品还需要一层工程系统:把模型做过的事,落成可恢复、可控制、可展示、可验证的软件事实。

下面我把这层系统叫做 Agent Harness。

二、ReAct 的解释边界

ReAct 的最小单元是:

rust 复制代码
Thought -> Action -> Observation

这个抽象用来描述模型行为:模型先想,再行动,再观察结果。

到了工程系统里,单位会换成 event、state、checkpoint、control。

同样一次工具调用,放在真实的Agent产品工程里,大概会拆成这些事件:

arduino 复制代码
run.started
message.created
assistant.text.delta
tool.call.created
tool.approval_required
tool.call.running
tool.call.completed
artifact.created
run.finished

这里需要先区分 Observation 的身份。

在 ReAct 里,Observation 是给模型看的。工具返回了什么,就把这段结果塞回上下文,让模型继续想。这个层面上,它当然可以是一段文本。

但产品系统不能只停在这里。同一次工具调用,用户关心它还在不在跑,前端展示关心该不该显示审批按钮,存储层关心能不能恢复,artifact 面板关心这个结果属于哪次运行。这些消费方需要同一组可以被系统引用的事实。

如果 Observation 只剩文本,这些事实就没有权威来源。前端展示要从文本猜状态,后端要靠临时字段补状态,adapter 要把旧数据翻译成新 UI。问题会从运行时边界,转移到多处推导逻辑的一致性。

所以 ReAct 解释的是执行微循环:模型看到什么,下一步做什么。它没有定义产品系统里的事实边界:哪些事情已经发生,哪些状态可以恢复,哪些动作可以控制,哪些结果可以检查。

这里说的 Agent Harness,先落在一个具体职责上:为 Agent 产品定义事实协议。

三、事实从哪里来

最近看了很多开源高star的项目,会发现他们的整体设计都会解释一件事:模型做出的动作,怎么变成软件系统里的事实?

一种做法是让 ReAct loop 原样跑完,外面再写一层 UI adapter。它读 message,读 observation,读 tool result,尽量拼出 isBusy、pending-Approval、artifactRefs。

这种做法改动小:模型继续想,工具继续跑,前端也能先画出来。

问题是,这些事实只在事后出现。

等 adapter 看到 observation 时,审批可能只是一句话,artifact 可能只是模型提到过的一个路径,stop 也只剩一个按钮状态。系统还可以继续补字段、补判断、补同步逻辑,但这些逻辑都在追认已经发生过的事。

如果说,Agent = Harness + model ,那么Harness中最需要搞清楚的问题绝对不是skill怎么安装,mcp怎么加载,记忆是如何设计。而是更加工程的底气:事实应该在哪里产生。

当 tool call 需要审批,runtime 应该直接产生 tool.approval_required。当文件或文档被生成,runtime 应该写出 artifact.created 和可定位的 ref。用户点 stop,control 应该回到对应的 run,而不是让组件自己把按钮置灰。

所以 Harness 必须在运行路径上。tool call 开始、等待审批、执行完成、生成 artifact、写入 checkpoint,这些节点都应该由 runtime 产生 event 和 state。用户的 approve、stop、resume,也应该进入 runtime 的 control,而不是停在组件自己的状态里。

一个 Agent Harness 至少要回答这些问题:

复制代码
现在谁在运行?
运行卡在哪里?
哪个状态可以恢复?
哪个动作需要用户审批?
哪个结果可以被检查?
哪个 artifact 属于哪次运行?
哪个子 Agent 是谁派出去的?
用户可以发送哪些命令?
刷新、重连、进程重启之后,系统如何回到同一个现场?

如果这些问题没有被 Harness 统一回答,实现里仍然要找地方放它们:前端组件、工具回调、消息渲染、数据库字段、临时缓存,或者某个"先这样"的判断。

判断标准可以直接写出来:同一个运行事实,不应该从多个来源拼出来。

pendingApproval、artifactRef、canStop 这类状态应该有明确归属。它们要么是 runtime state,要么是从 runtime state 派生的 view,不应该同时散在 message 文本、工具结果和前端本地状态里。

问题到这里,下一步就要设计系统怎么记、怎么算、怎么控制。

四、从 Loop 到协议

一个能产品化的 Agent 系统,数据流应该更像这样:

rust 复制代码
runtime event
  -> agent.state
  -> agent.view
  -> UI

user action
  -> agent.control
  -> runtime event

这里有三个词。state 是事实。它应该由 runtime 和明确的业务边界生产。比如:

复制代码
messages
activeRun
checkpoint
pendingApproval
todos
subagents
artifactRefs
workspaceContext

这些状态影响任务能不能继续、能不能恢复、用户能不能检查结果。它们不属于前端展示缓存,而属于 Agent 的运行状态。view 是派生。比如:

复制代码
isBusy
canStop
waitingForFirstToken
approvalBanner
toolBadges
subagentGroups
messageProjection

这些东西应该从 state 算出来。activeRun 存在,所以可以显示 busy。pendingApproval 存在,所以显示审批条。run 已经开始但第一个 assistant part 还没有出现,所以显示 first-token waiting。

control 是命令。比如:

scss 复制代码
invoke(input, stateSnapshot)
resume(approvalDecision)
stop(runId)
updateState(patch)
reload(threadId)

control 只负责发命令,不顺手改展示状态。用户点批准,前端发 resume。审批条什么时候消失,取决于 runtime 是否继续执行并写回新的 state。UI 只从新的 state 派生出来。

边界在这里:runtime 负责写事实,UI 负责读事实。

Agent Harness 把这条边界固定成协议。

五、State Schema 定义事实边界

开发一个Agent时,不管是work Agent还是业务垂类Agent,都应该先设计好state schema ,只有这个清晰了,Agent产品的定位,工程的架构就清晰了。

如果先设计 prompt、tool、模型选择,最后才整理状态,很多运行事实会被迫挂在 message、工具结果或前端缓存上。只要这个 Agent 要进入用户工作流,就应该提前问:

sql 复制代码
哪些事实必须恢复?
哪些事实必须跨端一致?
哪些事实只是 view?
哪些事实是用户现场?
哪些事实可以从 messages 推导?
哪些事实必须成为一等状态?

审批在 UI 上是按钮,在 runtime 里是可恢复暂停点。

如果模型要执行一个危险命令,系统不能只在前端弹一个 modal。因为用户刷新页面之后,这个 modal 会丢。后端也不知道自己停在哪里。

可以把审批写成 state:

ini 复制代码
agent.state.pendingApproval = {
  id,
  runId,
  turnId,
  toolCallId,
  toolName,
  arguments,
  policy
}

用户点击批准:

css 复制代码
agent.control.resume({
  approvalId,
  decision: "allow"
})

runtime 从 checkpoint 继续,继续之后再写回新的 state。前端只消费 state。

artifact 的内容本体不一定要进 state。一个文档、一张图、一个大 JSON、一个外部系统连接,可能属于文件系统、数据库或对象存储。但 artifact 的引用、状态、归属应该进 state:

bash 复制代码
artifactRef:
  id
  type
  title
  status
  ownerRunId
  createdByToolCallId
  contentRef

这样消息列表、右侧面板、历史记录、恢复流程都能通过同一个引用定位 artifact。内容可以懒加载,引用必须是事实。

子 Agent 也不应该只出现在文本里。

子 Agent 的运行关系也应该进入 state。如果主 Agent 只是输出一句:

复制代码
Started subagent abc123

前端想画子任务面板,就只能从文本里抠 ID。后续要做状态同步、跳转、恢复、错误展示时,这个 ID 仍然没有明确归属。

子 Agent 至少应该有运行事实:

lua 复制代码
subagent:
  id
  parentRunId
  parentTurnId
  title
  status
  startedAt
  completedAt
  resultRef
  error

前端再从 subagent state 派生分组、徽标、进度文案、跳转目标。

state 的判断标准可以直接写成一句话:影响恢复、审批、继续执行、跨端一致、审计和可检查结果的东西,应该进入 state。

只改变展示方式的东西,留在 view。

六、UI 只能消费事实,不能补写事实

UI 可以负责渲染、交互、布局、流式展示和 view state。

runtime fact 不属于 UI。pendingApproval、artifactRef、activeRun 这类事实,应该由 runtime 写入 state。

UI 的位置在 state 下游:从 state 派生 view,再把 view 渲染成可操作界面。

rust 复制代码
agent.state.pendingApproval
  -> agent.view.approvalBanner
  -> UI: 批准 / 拒绝按钮

如果 runtime 没有写入 pendingApproval,UI 不应该从 message 文本创建这个事实:

rust 复制代码
message: "Need approval to run shell command"
  -> UI 判断这句话像审批请求
  -> UI 本地创建 pendingApproval
  -> UI 显示批准 / 拒绝按钮

这条路径的问题很具体:刷新之后这个 approval 还在吗?后端知道自己停在哪个 tool call 吗?用户点批准时,前端要把决定发给哪个 run?

边界规则是:runtime 写入 fact,UI 消费 fact。

七、系统要记得发生过什么

到这里,Harness 的含义可以再落一下。

它不是给 UI 多加一层封装,而是把 Agent 运行中的关键事实放进系统协议里:模型发起了哪个 tool call,当前卡在哪个审批,哪个 run 产生了 artifact,用户的 stop 要终止哪次运行。

Agent 的运行不会总是顺着一条完整的同步调用走完。页面会刷新,进程会重启,工具调用会等待审批,用户可能点 stop,子 Agent 也可能在另一个执行上下文里结束。

这时问题就不是 UI 怎么画,而是这些事实有没有稳定归属,能不能被恢复、继续和追溯。

可以把这个要求写成可检查的行为:

arduino 复制代码
刷新页面后,pendingApproval 仍然存在且指向同一个 toolCall
进程重启后,能从 checkpoint resume 到同一个 run/turn
用户 stop 后,activeRun 终止且后端不会继续执行后续 tool call
artifactRef 存在时,内容可被定位、可追溯到 ownerRunId / toolCallId
子 Agent 完成后,parent turn 能拿到 resultRef 并展示归属

这组检查最后落到同一个问题:运行事实有没有明确归属。pendingApproval、artifactRef、activeRun 这些东西,必须由 runtime 生成并持久化;UI 只能从 state 派生 view,不能替系统补事实。

所以 ReAct 和 Harness 的分工也会变得清楚:

ReAct 解释模型怎样一步步决定下一步。

Harness 保证这些步骤在软件系统里发生过、能恢复、可控制、可审计。

八、最后

Agent 产品的难点,不止是写出一个会调用工具的循环。

那个循环让模型"能动",但用户真正依赖的是系统层面的确定性:刷新不丢现场、重启能恢复、该停就停、结果可定位、责任可追溯。

这就是 Agent Harness 的价值:把一次次"模型行为"落成一组可引用、可恢复、可控制的软件事实。

一句话总结:ReAct 让模型动起来;Harness 让这件事在产品里可被信任。

相关推荐
leeyi3 小时前
流式传输引擎:Eino StreamReader 源码拆解(第61篇-E47)
llm·aigc·agent
John_ToDebug4 小时前
Git Stash 完全指南:临时保存工作区的艺术
人工智能·git·agent
老梁agent4 小时前
告别硬编码 System Prompt:Prompt 六层编译引擎设计与实现
agent
决战灬4 小时前
langgraph之interrupt(事例篇)
人工智能·python·agent
张申傲4 小时前
拆解 harness9(9):Observability 可观测性
人工智能·aigc·agent·deepseek·harness
陳陈陳14 小时前
Workflow vs Agent:别再被“调包侠”忽悠了,一张图看懂AI工程的“骨架”与“大脑”
langchain·agent·workflow
新知图书15 小时前
11.3 详细实现与核心配置(作业批改智能体开发)
人工智能·agent·ai agent·智能体·扣子
小林ixn15 小时前
Workflow 还是 Agent?帮你一次性搞清楚这俩到底有啥不一样
agent·workflow
Darling噜啦啦15 小时前
Workflow 还是 Agent?一文搞懂 AI 工程的两大核心范式,以及它们的未来
agent·workflow