本页还没有译文
本页 21 段还没译成简体中文,下面显示的是英文原文。Human-in-the-Loop
还没翻译译自英文(docs.crewai.com)本页 21 段都还没译
Put the user in the loop
Some steps should not happen without a human saying yes. Human-in-the-loop pauses the agent mid-run, renders an interactive component in the frontend, and waits. The user makes a choice; the agent resumes with that choice and continues.
The mechanism is a tool the frontend registers. When the model calls it, the run halts at that tool call until the user responds. Nothing happens automatically: the agent stays parked until respond() hands control back.
In the example below, the agent proposes a list of task steps. The user enables or disables each step and confirms. The agent then continues, respecting exactly what the user approved.
Note
This pattern works with Flows. It relies on the Flow's chat loop re-entering after respond(): the returned value comes back as a tool result, and the agent's next turn acts on it.
Build it
-
Bind the frontend actions into the model's tools
In your Flow, add the frontend-registered actions to the model's tool list with
*self.state.copilotkit.actions. Those actions are the tools your frontend registered (viauseHumanInTheLoop). Binding them lets the model call them; the run pauses at that tool call until the user responds.# 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): passCopilotKitStatecarries the frontend-registered actions onself.state.copilotkit.actions. When the model calls one, the run pauses there. After the user responds, the returned value lands inself.state.messagesas the tool result, and the Flow loops back throughchatso the model can act on the decision. -
Serve the Flow over AG-UI
Expose the Flow from your FastAPI server with
add_crewai_flow_fastapi_endpoint, the same way as every other agent. See Frontend Overview for the full server, runtime, and provider setup.# 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
useHumanInTheLoopregisters the tool the agent pauses on and gives you arenderfunction to draw the interactive UI. When the agent calls the tool, your component appears; when the user acts, you callrespond()to resume the agent."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)} /> ), });The
renderfunction receives:args— the tool arguments the model produced (here, the proposedsteps). These stream in as the model generates them.status— the tool call's lifecycle. While it is"executing", the agent is paused and waiting on the human.respond(value)— resumes the agent with the user's decision. The agent's next turn sees the returned value and acts on it.
-
Let the user decide, then respond
Your component reads
args.steps, lets the user toggle each one, and callsrespond()with the final selection. That value is what the agent continues with.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> ); }Once the user clicks Confirm,
respond()fires, the run resumes, and the Flow'schatstep runs again with the user's choices in the message history.
Related
Let the agent call functions that run in the browser.
Keep agent state and your app UI in two-way sync.
Render live agent state as custom components.