Chuyển tới nội dung chính

Luồng Phê Duyệt (Human-in-the-Loop)

Tổng Quan

Một số tool calls yêu cầu xác nhận của người dùng trước khi thực thi. Khi đó agent sẽ:

  1. Dừng lại (pause)
  2. Gửi block approval_request với danh sách actions cần phê duyệt
  3. Chờ client gửi lệnh approval với quyết định
  4. Tiếp tục hoặc dừng tùy theo quyết định

Luồng Đầy Đủ

Server                                    Client
│ │
│── content_block_start (approval_request)▶│
│── content_block_stop ───────────────────▶│
│ │
│ (agent đang chờ...) │ ← hiển thị UI confirm
│ │
│◀── approval {decisions: [...]} ──────────│
│ │
│── content_block_start (approval_result) ▶│
│── content_block_delta {decisions} ──────▶│
│── content_block_stop ───────────────────▶│
│ │
│── (tiếp tục chạy agent nếu approved) ───▶│

Block approval_request

Ví Dụ Đầy Đủ

{
"type": "content_block_start",
"index": 4,
"content_block": {
"type": "approval_request",
"approval_key": "abc-123_1",
"actions": [
{
"name": "execute_trade",
"args": {
"symbol": "VNM",
"quantity": 100,
"side": "buy",
"price": 82000
},
"tool_use_id": "toolu_01XyzAbc"
}
],
"review_configs": [
{
"require_approval": true,
"timeout": 300
}
]
},
"message_id": "msg-abc"
}
{
"type": "content_block_stop",
"index": 4
}

Giải Thích Trường

TrườngKiểuMô tả
approval_keystringKey duy nhất, format {session_id}_{interrupt_count}
actionsarrayDanh sách actions cần phê duyệt
actions[].namestringTên tool
actions[].argsobjectArguments đầy đủ của tool
actions[].tool_use_idstringID của block tool_use tương ứng
review_configs[].timeoutnumberGiây chờ trước khi auto-reject (mặc định 300)

Lệnh approval — Phản Hồi Quyết Định

Phê Duyệt

{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "approve"
}
]
}

Từ Chối

{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "reject"
}
]
}

Sửa Arguments Trước Khi Thực Thi

{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "edit",
"edited_action": {
"name": "execute_trade",
"args": {
"symbol": "VNM",
"quantity": 50,
"side": "buy",
"price": 82000
}
}
}
]
}

Kèm Nội Dung Text Từ Người Dùng

{
"type": "approval",
"session_id": "abc-123",
"approval_key": "abc-123_1",
"decisions": [
{
"type": "approve"
}
],
"user_edit_content": "OK nhưng chỉ mua 50 cổ thôi"
}

Nhiều Actions

Nếu actions có 2 phần tử, decisions cần đủ 2 phần tử:

{
"decisions": [
{
"type": "approve"
},
{
"type": "reject"
}
]
}

Nếu decisions ít hơn actions, phần còn thiếu tự động fill bằng decision đầu tiên.


Block approval_result — Kết Quả Sau Quyết Định

{
"type": "content_block_start",
"index": 5,
"content_block": {
"type": "approval_result",
"approval_key": "abc-123_1"
}
}
{
"type": "content_block_delta",
"index": 5,
"delta": {
"decisions": [
{ "type": "approve" }
]
}
}
{ "type": "content_block_stop", "index": 5 }

Block approval_timeout — Hết Thời Gian

Nếu user không phản hồi trong timeout giây, tất cả actions bị auto-reject:

{ "type": "content_block_start", "index": 5, "content_block": { "type": "approval_timeout", "approval_key": "abc-123_1" } }
{ "type": "content_block_stop", "index": 5 }

Ví Dụ: React.js

function ApprovalModal({ request, sessionId, onSend }) {
const [decisions, setDecisions] = useState(
request.actions.map(() => 'approve')
)

const submit = () => {
onSend({
type: 'approval',
session_id: sessionId,
approval_key: request.approval_key,
decisions: decisions.map(type => ({ type })),
})
}

return (
<div className="modal">
<h3>Xác nhận hành động</h3>

{request.actions.map((action, i) => (
<div key={i} className="action-item">
<h4>{action.name}</h4>
<pre>{JSON.stringify(action.args, null, 2)}</pre>

<div className="decision-buttons">
<button
className={decisions[i] === 'approve' ? 'active' : ''}
onClick={() => {
const next = [...decisions]
next[i] = 'approve'
setDecisions(next)
}}
>
Phê duyệt
</button>
<button
className={decisions[i] === 'reject' ? 'active' : ''}
onClick={() => {
const next = [...decisions]
next[i] = 'reject'
setDecisions(next)
}}
>
Từ chối
</button>
</div>
</div>
))}

<button onClick={submit}>Xác nhận</button>
</div>
)
}

// Trong event handler
function handleEvent(msg) {
if (msg.type === 'content_block_start' &&
msg.content_block.type === 'approval_request') {
setApprovalRequest(msg.content_block)
}
}

Ví Dụ: Flutter (Dart)

void showApprovalDialog(
BuildContext context,
Map<String, dynamic> approvalBlock,
String sessionId,
Function(Map<String, dynamic>) sendApproval,
) {
final actions = (approvalBlock['actions'] as List).cast<Map<String, dynamic>>();
final decisions = List<String>.filled(actions.length, 'approve');

showDialog(
context: context,
barrierDismissible: false,
builder: (_) => StatefulBuilder(
builder: (context, setState) => AlertDialog(
title: const Text('Xác nhận hành động'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: actions.asMap().entries.map((e) {
final i = e.key;
final action = e.value;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(action['name'] as String,
style: const TextStyle(fontWeight: FontWeight.bold)),
Text(const JsonEncoder.withIndent(' ')
.convert(action['args']),
style: const TextStyle(fontFamily: 'monospace', fontSize: 12)),
Row(children: [
ChoiceChip(
label: const Text('Phê duyệt'),
selected: decisions[i] == 'approve',
onSelected: (_) => setState(() => decisions[i] = 'approve'),
),
const SizedBox(width: 8),
ChoiceChip(
label: const Text('Từ chối'),
selected: decisions[i] == 'reject',
onSelected: (_) => setState(() => decisions[i] = 'reject'),
),
]),
],
);
}).toList(),
),
actions: [
ElevatedButton(
onPressed: () {
sendApproval({
'type': 'approval',
'session_id': sessionId,
'approval_key': approvalBlock['approval_key'],
'decisions': decisions.map((t) => {'type': t}).toList(),
});
Navigator.of(context).pop();
},
child: const Text('Xác nhận'),
),
],
),
),
);
}