Agent 工程思考:从 ReAct 到 Agent Harness
作者:vivo 互联网项目团队- Ding Junjie
从 ReAct 出发,文章讨论 Agent 工程如何从“模型循环”走向 Harness:通过前后端共享的 State Schema、运行事实和 UI 边界,让模型行为变成可交互、可恢复、可控制、可追溯的产品能力。
1分钟看图掌握核心要点👇

大模型不是马,是大脑,而且是一颗刚刚觉醒的大脑。
一、Agent 不止是 model+loop
早期我理解的Agent 工程,可以被写成一段伪代码:
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 的最小单元是:
Thought -> Action -> Observation
这个抽象用来描述模型行为:模型先想,再行动,再观察结果。
到了工程系统里,单位会换成 event、state、checkpoint、control。
同样一次工具调用,放在真实的Agent产品工程里,大概会拆成这些事件:
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 系统,数据流应该更像这样:
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 是命令。比如:
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 要进入用户工作流,就应该提前问:
哪些事实必须恢复?
哪些事实必须跨端一致?
哪些事实只是 view?
哪些事实是用户现场?
哪些事实可以从 messages 推导?
哪些事实必须成为一等状态?
审批在 UI 上是按钮,在 runtime 里是可恢复暂停点。
如果模型要执行一个危险命令,系统不能只在前端弹一个 modal。因为用户刷新页面之后,这个 modal 会丢。后端也不知道自己停在哪里。
可以把审批写成 state:
agent.state.pendingApproval = {
id,
runId,
turnId,
toolCallId,
toolName,
arguments,
policy
}
用户点击批准:
agent.control.resume({
approvalId,
decision: "allow"
})
runtime 从 checkpoint 继续,继续之后再写回新的 state。前端只消费 state。
artifact 的内容本体不一定要进 state。一个文档、一张图、一个大 JSON、一个外部系统连接,可能属于文件系统、数据库或对象存储。但 artifact 的引用、状态、归属应该进 state:
artifactRef:
id
type
title
status
ownerRunId
createdByToolCallId
contentRef
这样消息列表、右侧面板、历史记录、恢复流程都能通过同一个引用定位 artifact。内容可以懒加载,引用必须是事实。
子 Agent 也不应该只出现在文本里。
子 Agent 的运行关系也应该进入 state。如果主 Agent 只是输出一句:
Started subagent abc123
前端想画子任务面板,就只能从文本里抠 ID。后续要做状态同步、跳转、恢复、错误展示时,这个 ID 仍然没有明确归属。
子 Agent 至少应该有运行事实:
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 渲染成可操作界面。
agent.state.pendingApproval
-> agent.view.approvalBanner
-> UI: 批准 / 拒绝按钮
如果 runtime 没有写入 pendingApproval,UI 不应该从 message 文本创建这个事实:
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 怎么画,而是这些事实有没有稳定归属,能不能被恢复、继续和追溯。
可以把这个要求写成可检查的行为:
刷新页面后,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 让这件事在产品里可被信任。

浙公网安备 33010602011771号