本页还没有译文
本页 31 段还没译成한국어,下面显示的是英文原文。Human-in-the-loop overview
还没翻译译自英文(docs.langchain.com)本页 31 段都还没译
Human-in-the-loop (HITL) pauses an agent before a tool call runs so a person can review it. The reviewer approves the call, edits its arguments, rejects it with feedback, or answers in place of the tool. Use it for actions that are hard to undo or that need sign-off, such as deleting records, sending email, or writing files.
You add review with humanInTheLoopMiddleware in LangChain, or with the interruptOn parameter in Deep Agents. The middleware pauses in one of two modes:
- Per-call: One pause for each tool call that needs review.
- Batched: One pause per model turn, covering every tool call in that turn. Batched mode is the current default.
Note
Per-call mode requires langchain>=1.5.18. Deep Agents uses batched mode.
Follow an approval from pause to resume
Every approval follows the same steps in either mode:
- The model proposes one or more tool calls.
- The middleware checks each call against your
interruptOnpolicy. Calls that need review pause the agent with an interrupt. - The checkpointer saves the agent's state, so the run can stop and resume later, even in another process.
- Your application shows the pending calls to a reviewer and collects a decision for each one.
- You resume the agent with a
new Command({ resume }). The middleware applies each decision, and the agent continues.
A decision is one of four types:
| Decision Type | Description | Example Use Case |
|---|---|---|
✅ approve | Execute the tool with the original arguments as proposed by the agent. | Send an email draft exactly as written |
✏️ edit | Modify the tool arguments before execution. | Change the recipient before sending an email |
❌ reject | Skip executing this tool call entirely and return rejection feedback to the agent. | Deny file deletion and explain why |
💬 respond | Return the human's message directly as a synthetic tool result, skipping execution, for "ask user" style tools. | Answer an "ask_user" prompt with a direct reply |
The examples on this page use an agent with three tools. write_file allows every decision type. execute_sql allows only approve and reject. read_data runs without review. In one turn, the model calls all three.
Review each tool call separately
In per-call mode, each tool call that needs review pauses on its own as it starts:
sequenceDiagram
participant App as Your app
participant Agent
participant Tools
App->>Agent: invoke(user message)
Agent->>Tools: write_file, execute_sql, read_data
Tools-->>Agent: read_data result (not gated, runs right away)
Tools-->>App: interrupt for write_file
Tools-->>App: interrupt for execute_sql
App->>Agent: resume {write_file ID: approve, execute_sql ID: reject}
Note over Agent: Each answer is checked against its response_schema before it is saved
Agent->>Tools: run write_file
Agent-->>App: final resultEach interrupt value describes one call and has type: "tool_approval". The execute_sql interrupt looks like this:
{
type: "tool_approval",
tool_call_id: "call_sql",
name: "execute_sql",
args: { query: "DELETE FROM orders WHERE created_at < '2025-01-01'" },
description: "Tool execution requires approval\n\nTool: execute_sql\nArgs: {\n \"query\": \"DELETE FROM orders WHERE created_at < '2025-01-01'\"\n}",
}Each interrupt also carries a response_schema, a JSON schema of the answers that call accepts. For execute_sql, the schema allows approve and reject. For write_file, it also allows respond and edit. The edit branch includes write_file's own argument schema. Clients can render a form from the schema instead of hardcoding the decision types. The LangChain guide shows the schema for each decision type.
You answer each interrupt with one decision, keyed by the interrupt's ID:
new Command({
resume: {
[writeFileInterrupt.id]: { type: "approve" },
[executeSqlInterrupt.id]: { type: "reject", message: "Archive the rows before deleting them." },
},
});LangGraph rejects an invalid answer before saving it, so the reviewer can correct it and answer again. Interrupts you do not answer stay pending, which lets a reviewer answer them one at a time.
Review a whole turn at once
In batched mode, the middleware pauses once after the model responds, before any tool runs. read_data waits for the review too, even though it is not gated. One interrupt lists every gated call in actionRequests, and each call's allowed decisions in a separate reviewConfigs list:
{
actionRequests: [
{ name: "write_file", args: { path: "orders_archive.csv", content: "id,total\n1,20\n" }, description: "..." },
{ name: "execute_sql", args: { query: "DELETE FROM orders WHERE created_at < '2025-01-01'" }, description: "..." },
],
reviewConfigs: [
{ actionName: "write_file", allowedDecisions: ["approve", "edit", "reject", "respond"] },
{ actionName: "execute_sql", allowedDecisions: ["approve", "reject"] },
],
}You answer with one list of decisions, in the same order as actionRequests:
new Command({
resume: {
decisions: [
{ type: "approve" },
{ type: "reject", message: "Archive the rows before deleting them." },
],
},
});The interrupt has no response_schema. The middleware checks each decision only when it applies it, after LangGraph has saved the answer.
Compare batched-mode problems
Batched mode matches answers to calls by position, and checks each answer only after saving it. Each row shows a problem the example agent hits in batched mode, and how per-call mode avoids it:
| In batched mode | What happens | In per-call mode |
|---|---|---|
| Decisions match calls by position | A reviewer means to approve write_file and reject execute_sql, but sends [reject, approve]. The rejection goes to write_file. | Each decision is keyed by its call's interrupt ID. |
Allowed decisions live in reviewConfigs | Clients pair two lists by position and hardcode a button for each decision type. | Each interrupt's response_schema lists its decisions and edit arguments. |
| An answer is saved before it is checked | One decision for two calls throws Error: Number of human decisions (1) does not match number of hanging tool calls (2). The corrected answer then fails with the same error, so the thread stays stuck. | The answer is checked first. Fix it and answer again. |
| Edited arguments are not checked | An edit to write_file without content still runs the tool, and the model receives an error for a call it never made. | The edit is rejected with edited_action.args.content Invalid input: expected string, received undefined. |
One reject stops the whole turn | Approving write_file and rejecting execute_sql runs neither tool, nor the ungated read_data. The model receives only the rejection. | Each call runs or is rejected on its own. |
Choose an interrupt mode
| Per-call | Batched (default) | |
|---|---|---|
| When it pauses | As each gated tool call starts | After the model responds, before any tool runs |
| Interrupts per turn | One per gated tool call | One for the whole turn |
| Ungated calls in the turn | Run right away | Wait for the review |
| Answer | One decision per interrupt, keyed by interrupt ID | A list of decisions, matched by position |
| Best for | New agents, one approval card or message per call, forms built from the schema | Approval UIs that already read actionRequests |
Use per-call mode for new agents. LangChain plans to make it the default in the next major release.
Keep batched mode while your approval UI reads the batched format, such as one built with the useStream approval pattern.
Per-call mode also changes how middleware order and multi-answer resumes behave. Before you switch, read Order middleware for per-call review and Resume each call by ID.
See also
- Human-in-the-loop in LangChain: configure the middleware and answer each decision type.
- Human-in-the-loop in Deep Agents: approvals for deep agents and their subagents.
- Define an interrupt response schema: how LangGraph checks resume values.