主题
人在回路
在运行中途暂停你的 CrewAI 智能体以收集用户决策,然后用用户的答案恢复智能体运行。
将用户纳入回路
有些步骤必须得到人的确认才能发生。人在回路会在运行中途暂停智能体,在前端渲染一个交互式组件,然后等待。用户做出选择;智能体带着该选择恢复运行并继续。
其机制是前端注册的一个工具。当模型调用它时,运行会在该工具调用处停下,直到用户响应。不会有任何事情自动发生:智能体会一直停在那里,直到 respond() 把控制权交还。
在下面的示例中,智能体提出一组任务步骤。用户启用或禁用每个步骤并确认。随后智能体继续运行,严格遵照用户所批准的内容。
NOTE
该模式适用于 Flow。它依赖 Flow 的 chat 循环在 respond() 之后重新进入:返回的值作为工具结果传回,智能体的下一轮据此行动。
动手构建
Bind the frontend actions into the model's tools
在你的 Flow 中,用 *self.state.copilotkit.actions 把前端注册的 actions 加入模型的工具列表。这些 action 就是前端(通过 useHumanInTheLoop)注册的工具。绑定它们,模型才能调用它们;运行会在该工具调用处暂停,直到用户响应。
python
# human_in_the_loop_flow.py
from crewai.flow.flow import Flow, start, router, listen
from litellm import acompletion
from ag_ui_crewai.sdk import copilotkit_stream, CopilotKitState
class HumanInTheLoopFlow(Flow[CopilotKitState]):
@start()
@listen("route_follow_up")
async def start_flow(self):
pass
@router(start_flow)
async def chat(self):
system_prompt = (
"You perform tasks for the user. When asked to do a task, call the "
"tool the frontend provides so the user can approve or adjust the steps "
"before you continue."
)
response = await copilotkit_stream(
await acompletion(
model="openai/gpt-4o",
messages=[
{"role": "system", "content": system_prompt},
*self.state.messages,
],
tools=[*self.state.copilotkit.actions], # tools registered by the frontend
parallel_tool_calls=False,
stream=True,
)
)
message = response.choices[0].message
self.state.messages.append(message)
return "route_end"
@listen("route_end")
async def end(self):
passCopilotKitState 在 self.state.copilotkit.actions 上承载前端注册的 actions。当模型调用其中之一时,运行会在那里暂停。用户响应后,返回的值会作为工具结果写入 self.state.messages,Flow 随后回到 chat 循环,使模型能够根据该决策行动。
Serve the Flow over AG-UI
用 add_crewai_flow_fastapi_endpoint 从 FastAPI 服务器暴露该 Flow,与其他任何智能体的方式相同。完整的服务器、运行时和服务商配置参见前端概览。
python
# server.py
from fastapi import FastAPI
from ag_ui_crewai.endpoint import add_crewai_flow_fastapi_endpoint
from my_agents.human_in_the_loop_flow import HumanInTheLoopFlow
app = FastAPI(title="CrewAI Agent Server")
add_crewai_flow_fastapi_endpoint(
app=app,
flow=HumanInTheLoopFlow(),
path="/human_in_the_loop",
)Register the interactive tool on the frontend
useHumanInTheLoop 注册智能体暂停等待的那个工具,并提供一个 render 函数来绘制交互式 UI。当智能体调用该工具时,你的组件出现;当用户做出操作时,你调用 respond() 来恢复智能体。
tsx
"use client";
import { useHumanInTheLoop } from "@copilotkit/react-core/v2";
import { z } from "zod";
useHumanInTheLoop({
agentId: "human_in_the_loop",
name: "generate_task_steps",
parameters: z.object({
steps: z.array(
z.object({
description: z.string(),
status: z.enum(["enabled", "disabled", "executing"]),
})
),
}),
render: ({ args, respond, status }) => (
<StepReview
steps={args.steps ?? []}
// `status === "executing"` means the agent is waiting for the user
waiting={status === "executing"}
onConfirm={(chosen) => respond?.(chosen)}
/>
),
});render 函数接收:
args—— 模型生成的工具参数(这里是提出的steps)。它们会随着模型生成而流式输出。status—— 工具调用的生命周期。当它为"executing"时,智能体已暂停,正在等待人。respond(value)—— 用用户的决策恢复智能体。智能体的下一轮会看到返回的值并据此行动。
Let the user decide, then respond
你的组件读取 args.steps,让用户逐个切换,并用最终选择调用 respond()。智能体正是带着这个值继续运行。
tsx
function StepReview({ steps, waiting, onConfirm }) {
const [choices, setChoices] = useState(steps);
const toggle = (i) =>
setChoices((prev) =>
prev.map((s, idx) =>
idx === i
? { ...s, status: s.status === "enabled" ? "disabled" : "enabled" }
: s
)
);
return (
<div>
{choices.map((step, i) => (
<label key={i}>
<input
type="checkbox"
checked={step.status === "enabled"}
disabled={!waiting}
onChange={() => toggle(i)}
/>
{step.description}
</label>
))}
<button disabled={!waiting} onClick={() => onConfirm(choices)}>
Confirm
</button>
</div>
);
}用户点击确认后,respond() 被触发,运行恢复,Flow 的 chat 步骤带着消息历史中的用户选择再次执行。