Agent Framework 中构建Human-in-the-Loop工作流 目录示例场景核心代码实现RequestPortWorkflow 如何等待外部输入外部如何让 Workflow 继续执行JudgeExecutor 如何决定下一步RequestPort 与普通 Executor 的区别小结在前面的文章中我们介绍了顺序执行、条件边、Switch、Fan-Out 以及 Loop 等多种 Workflow 编排模式。这些 Workflow 有一个共同特点整个执行过程都发生在 Workflow 内部节点之间通过消息不断传递直到流程结束。但在真实业务系统中很多流程并不能完全依赖系统自动完成而是需要等待用户或者外部系统参与。对于这类场景Workflow 需要具备一种能力执行过程中暂停等待外部输入然后继续执行。Agent Framework 为此提供了RequestPort。RequestPort 可以把 Workflow 与外部世界连接起来使 Workflow 在需要时主动向外发起请求并在收到响应后继续执行后续流程。示例场景我们继续沿用上一篇文章中的例子来演示 RequestPort 的工作方式。系统预先保存了一个目标数字42Workflow 并不会自动生成猜测结果而是把猜数字这个动作交给外部用户完成。整个交互过程如下如果用户猜测过大Workflow 会提示重新输入更小的数字如果猜测过小则提示输入更大的数字。整个过程会不断重复直到用户猜中目标数字。与上一篇 Loop Workflow 最大的区别在于上一篇文章中循环是由 Workflow 内部两个 Executor 自动完成的。而本示例中每一次循环都需要等待外部用户参与因此 Workflow 会在每一轮判断结束后暂停直到收到新的输入后再继续执行。核心代码实现整个 Workflow 的定义非常简单RequestPort numberRequestPort RequestPort.CreateNumberSignal, int(GuessNumber); JudgeExecutor judgeExecutor new(42); return new WorkflowBuilder(numberRequestPort) .AddEdge(numberRequestPort, judgeExecutor) .AddEdge(judgeExecutor, numberRequestPort) .WithOutputFrom(judgeExecutor) .Build();虽然只有几行代码却构建了一个完整的人机交互工作流。RequestPort首先创建的是RequestPort.CreateNumberSignal, int(GuessNumber)这里创建了一个名为GuessNumber的 RequestPort。与普通 Executor 不同它本身并不会执行业务逻辑而是作为 Workflow 的输入端口。泛型参数表示Workflow 向外发送NumberSignal外部返回int也就是说当 Workflow 执行到 RequestPort 时它并不会继续向下执行而是主动向外发起一次请求。Workflow 如何等待外部输入在前面的示例中Workflow 的所有节点都会自动执行。一个 Executor 执行完成后消息会继续传递给下一个 Executor直到整个 Workflow 结束。而本示例最大的不同在于Workflow 并不知道用户会输入什么数字因此执行到 RequestPort 时无法继续向下执行。此时RequestPort 会主动向外部发起一次输入请求。Workflow 启动后await using StreamingRun handle await InProcessExecution.RunStreamingAsync(workflow, NumberSignal.Init);程序随后开始持续监听 Workflow 产生的各种事件await foreach (WorkflowEvent evt in handle.WatchStreamAsync()) { ... }当 Workflow 执行到 RequestPort 时并不会立即进入下一个节点而是产生一个RequestInfoEvent。可以把它理解成Workflow 正在向外部发送一个请求“我现在需要用户输入请获取数据后再继续执行。”例如第一次运行时就会产生如下请求请输入一个数字作为初始猜测此时Workflow 会暂停执行并等待外部返回结果。这里需要注意的是Workflow 并没有结束而只是进入了等待状态。只有收到外部输入之后它才会继续向下执行。整个过程可以简单理解为Workflow ↓ RequestPort 发起请求 ↓ 等待外部输入外部如何让 Workflow 继续执行当监听到RequestInfoEvent后程序会进入下面这段代码case RequestInfoEvent requestInputEvt: ExternalResponse response HandleExternalRequest(requestInputEvt.Request); await handle.SendResponseAsync(response); break;这里的HandleExternalRequest就代表了外部世界。在当前示例中它只是简单地读取控制台输入ReadIntegerFromConsole(...)随后通过request.CreateResponse(...)将用户输入封装成ExternalResponse再发送回 Workflowawait handle.SendResponseAsync(response);Workflow 收到ExternalResponse后就会从刚才暂停的位置继续执行。整个交互过程如下Workflow │ ▼ RequestPort 发起请求 │ ▼ 用户输入数字 │ ▼ 创建 ExternalResponse │ ▼ Workflow 恢复执行虽然本示例使用的是控制台输入但在真实项目中这里的数据完全可以来自浏览器页面、移动端 App、企业审批系统、Teams、Slack甚至其他微服务。对于 Workflow 来说它并不关心数据来自哪里只要最终收到一个ExternalResponse就会继续执行后续流程。JudgeExecutor 如何决定下一步当 Workflow 收到用户输入后请求的数据会传递给 JudgeExecutor。它负责判断当前猜测是否正确if (message this._targetNumber) { await context.YieldOutputAsync(...); } else if (message this._targetNumber) { await context.SendMessageAsync(NumberSignal.Below); } else { await context.SendMessageAsync(NumberSignal.Above); }如果猜中了目标数字Workflow 会调用YieldOutputAsync输出最终结果整个流程结束。如果数字偏小则发送Below如果数字偏大则发送Above。收到这两个信号后Workflow 会再次回到 RequestPort等待用户输入新的数字。因此整个 Workflow 实际形成了下面这样的执行过程RequestPort │ ▼ 等待用户输入 │ ▼ JudgeExecutor │ ├── 猜中 │ │ │ ▼ │ 输出结果并结束 │ └── 未猜中 │ ▼ 返回 RequestPort可以看到这个 Workflow 同样形成了一个循环。不同的是上一篇文章中的 Loop 是 Workflow 内部两个 Executor 自动循环执行而本示例中的循环则需要等待外部用户参与每完成一次判断Workflow 都会暂停直到收到新的输入后再继续执行。RequestPort 与普通 Executor 的区别很多开发者第一次接触 RequestPort 时容易把它理解成一个特殊的 Executor。实际上两者承担的职责完全不同。普通 Executor 负责处理业务逻辑输入消息后立即执行并返回结果。而 RequestPort 并不会处理任何业务它只是 Workflow 与外部世界之间的一座桥梁。当 Workflow 执行到 RequestPort 时它负责向外发起请求收到外部响应后再把响应重新送回 Workflow继续后续执行。因此可以简单理解为Executor 负责执行业务 RequestPort 负责等待外部输入小结本示例介绍了 Agent Framework 中 RequestPort 的使用方式。通过 RequestPortWorkflow 可以在执行过程中主动向外请求数据并等待外部返回结果后继续执行。整个过程中Workflow 并没有因为等待用户输入而结束而是在 RequestPort 暂停在收到 ExternalResponse 后恢复执行。这种模式使 Workflow 不再局限于系统内部自动流转而能够与用户、第三方系统以及各种外部服务进行交互。在人机协同、审批流程、客服系统以及 AI Agent 等场景中RequestPort 往往都是构建 Human-in-the-Loop 工作流的核心能力。源代码地址https://github.com/bingbing-gui/dotnet-agent-playbook/tree/master/src/ai-agent/Agent-Framework/44-Human-in-the-Loop引入地址