Agent 工程思考:从 ReAct 到 Agent Harness

发布时间:2026/7/24 2:38:29

Agent 工程思考:从 ReAct 到 Agent Harness 作者vivo 互联网项目团队- Ding Junjie从 ReAct 出发文章讨论 Agent 工程如何从“模型循环”走向 Harness通过前后端共享的 State Schema、运行事实和 UI 边界让模型行为变成可交互、可恢复、可控制、可追溯的产品能力。1分钟看图掌握核心要点大模型不是马是大脑而且是一颗刚刚觉醒的大脑。一、Agent 不止是 modelloop早期我理解的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。用户点 stopcontrol 应该回到对应的 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 会丢。后端也不知道自己停在哪里。可以把审批写成 stateagent.state.pendingApproval { id, runId, turnId, toolCallId, toolName, arguments, policy }用户点击批准agent.control.resume({ approvalId, decision: allow })runtime 从 checkpoint 继续继续之后再写回新的 state。前端只消费 state。artifact 的内容本体不一定要进 state。一个文档、一张图、一个大 JSON、一个外部系统连接可能属于文件系统、数据库或对象存储。但 artifact 的引用、状态、归属应该进 stateartifactRef: 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 没有写入 pendingApprovalUI 不应该从 message 文本创建这个事实message: Need approval to run shell command - UI 判断这句话像审批请求 - UI 本地创建 pendingApproval - UI 显示批准 / 拒绝按钮这条路径的问题很具体刷新之后这个 approval 还在吗后端知道自己停在哪个 tool call 吗用户点批准时前端要把决定发给哪个 run边界规则是runtime 写入 factUI 消费 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 让这件事在产品里可被信任。

相关新闻