Trong khi làm việc, Claude thỉnh thoảng cần hỏi ý kiến người dùng. Nó có thể cần quyền trước khi xoá file, hoặc cần hỏi nên dùng database nào cho một dự án mới. Ứng dụng của bạn cần đưa các yêu cầu này ra cho người dùng để Claude tiếp tục với input của họ.
Claude yêu cầu input người dùng trong hai tình huống: khi nó cần quyền dùng một tool (như xoá file hay chạy lệnh), và khi nó có câu hỏi làm rõ (qua tool AskUserQuestion). Cả hai đều kích hoạt callback canUseTool của bạn, callback này tạm dừng thực thi cho tới khi bạn trả về phản hồi. Điều này khác với các lượt hội thoại thông thường, nơi Claude hoàn tất và chờ message tiếp theo của bạn.
Với câu hỏi làm rõ, Claude tự sinh câu hỏi và các lựa chọn. Vai trò của bạn là hiển thị chúng cho người dùng và trả về lựa chọn của họ. Bạn không thể thêm câu hỏi riêng của mình vào luồng này; nếu cần hỏi người dùng điều gì đó, hãy làm việc đó riêng trong logic ứng dụng của bạn.
Callback có thể ở trạng thái chờ vô thời hạn. Việc thực thi vẫn tạm dừng cho tới khi callback của bạn trả về, và SDK chỉ huỷ chờ khi chính query bị huỷ. Nếu người dùng có thể mất nhiều thời gian hơn để phản hồi so với thời gian process của bạn có thể chạy hợp lý, hãy trả về quyết định hook defer, giúp process thoát và tiếp tục sau đó từ session đã được lưu lại.
Hướng dẫn này chỉ cách phát hiện mỗi loại yêu cầu và phản hồi phù hợp.
Phát hiện khi Claude cần input
Phần tiêu đề “Phát hiện khi Claude cần input”Truyền một callback canUseTool trong tùy chọn query của bạn. Callback này kích hoạt bất cứ khi nào Claude cần input người dùng, nhận tên tool và input làm tham số:
from claude_agent_sdk import ClaudeAgentOptions
async def handle_tool_request(tool_name, input_data, context): # Hỏi người dùng và trả về allow hoặc deny ...
options = ClaudeAgentOptions(can_use_tool=handle_tool_request)async function handleToolRequest(toolName, input, options) { // options gồm { signal: AbortSignal, suggestions?: PermissionUpdate[] } // Hỏi người dùng và trả về allow hoặc deny}
const options = { canUseTool: handleToolRequest };Callback kích hoạt trong hai trường hợp:
- Tool cần phê duyệt: Claude muốn dùng một tool chưa được tự động duyệt bởi một quy tắc quyền hay permission mode. Kiểm tra
tool_namecho tool (ví dụ:"Bash","Write"). - Claude hỏi một câu hỏi: Claude gọi tool
AskUserQuestion. Kiểm tratool_name == "AskUserQuestion"để xử lý khác đi. Nếu bạn chỉ định một mảngtools, hãy đưaAskUserQuestionvào để cái này hoạt động. Xem Xử lý câu hỏi làm rõ để biết chi tiết.
Bạn cũng có thể dùng PermissionRequest hook để gửi thông báo bên ngoài (Slack, email, push) khi Claude đang chờ phê duyệt.
Xử lý yêu cầu phê duyệt tool
Phần tiêu đề “Xử lý yêu cầu phê duyệt tool”Khi bạn đã truyền callback canUseTool trong tùy chọn query, nó kích hoạt khi Claude muốn dùng một tool mà không có gì trước đó trong luồng quyền phê duyệt. Callback của bạn nhận ba tham số:
| Tham số | Mô tả |
|---|---|
toolName | Tên tool Claude muốn dùng (ví dụ: "Bash", "Write", "Edit") |
input | Các tham số Claude đang truyền cho tool. Nội dung tùy theo tool. |
options (TS) / context (Python) | Ngữ cảnh bổ sung gồm suggestions (tùy chọn, các PermissionUpdate được đề xuất sẵn để tránh hỏi lại) và một cancellation signal. Trong TypeScript, signal là một AbortSignal; trong Python, trường signal được dành cho tương lai. Xem ToolPermissionContext cho Python. |
Object input chứa các tham số riêng của từng tool. Vài ví dụ phổ biến:
| Tool | Trường input |
|---|---|
Bash | command, description, timeout |
Write | file_path, content |
Edit | file_path, old_string, new_string |
Read | file_path, offset, limit |
Xem SDK reference để biết schema input đầy đủ: Python | TypeScript.
Bạn có thể hiển thị thông tin này cho người dùng để họ quyết định có cho phép hay từ chối hành động, rồi trả về phản hồi tương ứng.
Ví dụ sau yêu cầu Claude tạo và xoá một file test. Khi Claude thực hiện mỗi thao tác, callback in yêu cầu tool ra terminal và hỏi phê duyệt y/n.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, queryfrom claude_agent_sdk.types import ( HookMatcher, PermissionResultAllow, PermissionResultDeny, ToolPermissionContext,)
async def can_use_tool( tool_name: str, input_data: dict, context: ToolPermissionContext) -> PermissionResultAllow | PermissionResultDeny: # Hiển thị yêu cầu tool print(f"\nTool: {tool_name}") if tool_name == "Bash": print(f"Command: {input_data.get('command')}") if input_data.get("description"): print(f"Description: {input_data.get('description')}") else: print(f"Input: {input_data}")
# Lấy phê duyệt từ người dùng response = input("Allow this action? (y/n): ")
# Trả về allow hoặc deny dựa trên phản hồi của người dùng if response.lower() == "y": # Allow: tool chạy với input gốc (hoặc đã sửa) return PermissionResultAllow(updated_input=input_data) else: # Deny: tool không chạy, Claude thấy message return PermissionResultDeny(message="User denied this action")
# Workaround bắt buộc: dummy hook giữ stream mở cho can_use_toolasync def dummy_hook(input_data, tool_use_id, context): return {"continue_": True}
async def prompt_stream(): yield { "type": "user", "message": { "role": "user", "content": "Create a test file in /tmp and then delete it", }, }
async def main(): async for message in query( prompt=prompt_stream(), options=ClaudeAgentOptions( can_use_tool=can_use_tool, hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]}, ), ): if isinstance(message, ResultMessage) and message.subtype == "success": print(message.result)
asyncio.run(main())import { query } from "@anthropic-ai/claude-agent-sdk";import * as readline from "readline";
// Helper để hỏi input người dùng trong terminalfunction prompt(question: string): Promise<string> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => rl.question(question, (answer) => { rl.close(); resolve(answer); }) );}
for await (const message of query({ prompt: "Create a test file in /tmp and then delete it", options: { canUseTool: async (toolName, input) => { // Hiển thị yêu cầu tool console.log(`\nTool: ${toolName}`); if (toolName === "Bash") { console.log(`Command: ${input.command}`); if (input.description) console.log(`Description: ${input.description}`); } else { console.log(`Input: ${JSON.stringify(input, null, 2)}`); }
// Lấy phê duyệt từ người dùng const response = await prompt("Allow this action? (y/n): ");
// Trả về allow hoặc deny dựa trên phản hồi của người dùng if (response.toLowerCase() === "y") { // Allow: tool chạy với input gốc (hoặc đã sửa) return { behavior: "allow", updatedInput: input }; } else { // Deny: tool không chạy, Claude thấy message return { behavior: "deny", message: "User denied this action" }; } } }})) { if ("result" in message) console.log(message.result);}Ví dụ này dùng luồng y/n trong đó bất kỳ input nào khác y đều bị coi là từ chối. Trong thực tế, bạn có thể xây UI phong phú hơn cho phép người dùng sửa yêu cầu, cung cấp phản hồi, hoặc chuyển hướng Claude hoàn toàn. Xem Phản hồi yêu cầu tool để biết mọi cách bạn có thể phản hồi.
Phản hồi yêu cầu tool
Phần tiêu đề “Phản hồi yêu cầu tool”Callback của bạn trả về một trong hai kiểu phản hồi:
| Phản hồi | Python | TypeScript |
|---|---|---|
| Allow | PermissionResultAllow(updated_input=...) | { behavior: "allow", updatedInput } |
| Deny | PermissionResultDeny(message=...) | { behavior: "deny", message } |
Khi allow, tool chạy với input Claude đã yêu cầu trừ khi bạn trả về một input đã sửa, updatedInput trong TypeScript hoặc updated_input trong Python. {/* min-version: 2.1.207 */}Trước v2.1.207, Claude Code từ chối kết quả allow thiếu updatedInput và deny lệnh gọi tool với lỗi validation.
Khi deny, hãy cung cấp một message giải thích lý do. Claude thấy message này và có thể điều chỉnh cách tiếp cận.
from claude_agent_sdk.types import PermissionResultAllow, PermissionResultDeny
# Cho phép tool thực thireturn PermissionResultAllow(updated_input=input_data)
# Chặn toolreturn PermissionResultDeny(message="User rejected this action")// Cho phép tool thực thireturn { behavior: "allow", updatedInput: input };
// Chặn toolreturn { behavior: "deny", message: "User rejected this action" };Ngoài allow hay deny, bạn có thể sửa input của tool hoặc cung cấp ngữ cảnh giúp Claude điều chỉnh cách tiếp cận:
- Approve: cho tool thực thi như Claude đã yêu cầu
- Approve with changes: sửa input trước khi thực thi (ví dụ: làm sạch đường dẫn, thêm ràng buộc)
- Approve and remember: gửi lại một quy tắc quyền được đề xuất để các lệnh gọi khớp bỏ qua prompt lần sau
- Reject: chặn tool và nói cho Claude biết lý do
- Suggest alternative: chặn nhưng hướng Claude tới điều người dùng thực sự muốn
- Redirect entirely: dùng streaming input để gửi cho Claude một chỉ dẫn hoàn toàn mới
Các helper ask_user và askUser trong các đoạn code sau đại diện cho UI prompt riêng của ứng dụng bạn.
Approve - Người dùng phê duyệt hành động nguyên trạng. Truyền input từ callback của bạn không đổi và tool thực thi đúng như Claude đã yêu cầu.
async def can_use_tool(tool_name, input_data, context): print(f"Claude wants to use {tool_name}") approved = await ask_user("Allow this action?")
if approved: return PermissionResultAllow(updated_input=input_data) return PermissionResultDeny(message="User declined")canUseTool: async (toolName, input) => { console.log(`Claude wants to use ${toolName}`); const approved = await askUser("Allow this action?");
if (approved) { return { behavior: "allow", updatedInput: input }; } return { behavior: "deny", message: "User declined" };};Approve with changes - Người dùng phê duyệt nhưng muốn sửa yêu cầu trước. Bạn có thể thay đổi input trước khi tool thực thi. Claude thấy kết quả nhưng không được báo là bạn đã thay đổi gì. Hữu ích để làm sạch tham số, thêm ràng buộc, hoặc giới hạn phạm vi truy cập.
async def can_use_tool(tool_name, input_data, context): if tool_name == "Bash": # Người dùng đã duyệt, nhưng giới hạn mọi lệnh vào sandbox sandboxed_input = {**input_data} sandboxed_input["command"] = input_data["command"].replace( "/tmp", "/tmp/sandbox" ) return PermissionResultAllow(updated_input=sandboxed_input) return PermissionResultAllow(updated_input=input_data)canUseTool: async (toolName, input) => { if (toolName === "Bash") { // Người dùng đã duyệt, nhưng giới hạn mọi lệnh vào sandbox const sandboxedInput = { ...input, command: input.command.replace("/tmp", "/tmp/sandbox") }; return { behavior: "allow", updatedInput: sandboxedInput }; } return { behavior: "allow", updatedInput: input };};Approve and remember - Người dùng phê duyệt và không muốn bị hỏi lại cho loại lệnh gọi này. Tham số thứ ba của callback mang suggestions, một mảng các mục PermissionUpdate đã dựng sẵn. Gửi lại một trong số đó trong updatedPermissions để áp dụng nó. Một suggestion với destination localSettings sẽ ghi quy tắc vào .claude/settings.local.json để các session sau bỏ qua prompt cho lệnh gọi khớp.
Ví dụ Python yêu cầu claude-agent-sdk 0.1.80 trở lên.
async def can_use_tool(tool_name, input_data, context): choice = await ask_user(f"Allow {tool_name}?", ["once", "always", "no"])
if choice == "always": persist = [ s for s in context.suggestions if s.destination == "localSettings" ] return PermissionResultAllow( updated_input=input_data, updated_permissions=persist ) if choice == "once": return PermissionResultAllow(updated_input=input_data) return PermissionResultDeny(message="User declined")canUseTool: async (toolName, input, { suggestions = [] }) => { const choice = await askUser(`Allow ${toolName}?`, ["once", "always", "no"]);
if (choice === "always") { const persist = suggestions.filter( (s) => s.destination === "localSettings" ); return { behavior: "allow", updatedInput: input, updatedPermissions: persist }; } if (choice === "once") { return { behavior: "allow", updatedInput: input }; } return { behavior: "deny", message: "User declined" };};Reject - Người dùng không muốn hành động này xảy ra. Chặn tool và cung cấp message giải thích lý do. Claude thấy message này và có thể thử cách tiếp cận khác.
async def can_use_tool(tool_name, input_data, context): approved = await ask_user(f"Allow {tool_name}?")
if not approved: return PermissionResultDeny(message="User rejected this action") return PermissionResultAllow(updated_input=input_data)canUseTool: async (toolName, input) => { const approved = await askUser(`Allow ${toolName}?`);
if (!approved) { return { behavior: "deny", message: "User rejected this action" }; } return { behavior: "allow", updatedInput: input };};Suggest alternative - Người dùng không muốn hành động cụ thể này, nhưng có ý tưởng khác. Chặn tool và đưa hướng dẫn vào message của bạn. Claude sẽ đọc và quyết định cách tiến hành dựa trên phản hồi của bạn.
async def can_use_tool(tool_name, input_data, context): if tool_name == "Bash" and "rm" in input_data.get("command", ""): # Người dùng không muốn xoá, gợi ý nén thành archive thay vào đó return PermissionResultDeny( message="User doesn't want to delete files. They asked if you could compress them into an archive instead." ) return PermissionResultAllow(updated_input=input_data)canUseTool: async (toolName, input) => { if (toolName === "Bash" && input.command.includes("rm")) { // Người dùng không muốn xoá, gợi ý nén thành archive thay vào đó return { behavior: "deny", message: "User doesn't want to delete files. They asked if you could compress them into an archive instead." }; } return { behavior: "allow", updatedInput: input };};Redirect entirely - Cho một sự đổi hướng hoàn toàn (không chỉ một gợi ý), dùng streaming input để gửi trực tiếp cho Claude một chỉ dẫn mới. Cách này bỏ qua yêu cầu tool hiện tại và đưa cho Claude chỉ dẫn hoàn toàn mới để làm theo.
Xử lý câu hỏi làm rõ
Phần tiêu đề “Xử lý câu hỏi làm rõ”Khi Claude cần thêm định hướng cho một tác vụ có nhiều cách tiếp cận hợp lệ, nó gọi tool AskUserQuestion. Điều này kích hoạt callback canUseTool của bạn với toolName là AskUserQuestion. Input chứa các câu hỏi của Claude dưới dạng lựa chọn trắc nghiệm, mà bạn hiển thị cho người dùng và trả về lựa chọn của họ.
Các bước sau chỉ cách xử lý câu hỏi làm rõ:
1. Truyền callback canUseTool. Truyền một callback canUseTool trong tùy chọn query. Mặc định, AskUserQuestion đã sẵn có. Nếu bạn chỉ định một mảng tools để giới hạn năng lực của Claude (ví dụ, một agent chỉ đọc với Read, Glob, và Grep), hãy đưa AskUserQuestion vào mảng đó. Nếu không, Claude sẽ không thể hỏi câu hỏi làm rõ:
async for message in query( prompt="Analyze this codebase", options=ClaudeAgentOptions( # Đưa AskUserQuestion vào danh sách tools của bạn tools=["Read", "Glob", "Grep", "AskUserQuestion"], can_use_tool=can_use_tool, ),): print(message)for await (const message of query({ prompt: "Analyze this codebase", options: { // Đưa AskUserQuestion vào danh sách tools của bạn tools: ["Read", "Glob", "Grep", "AskUserQuestion"], canUseTool: async (toolName, input) => { // Xử lý câu hỏi làm rõ tại đây } }})) { console.log(message);}2. Phát hiện AskUserQuestion. Trong callback, kiểm tra toolName có bằng AskUserQuestion để xử lý khác với các tool khác:
async def can_use_tool(tool_name: str, input_data: dict, context): if tool_name == "AskUserQuestion": # Implementation của bạn để thu thập câu trả lời từ người dùng return await handle_clarifying_questions(input_data) # Xử lý các tool khác bình thường return await prompt_for_approval(tool_name, input_data)canUseTool: async (toolName, input) => { if (toolName === "AskUserQuestion") { // Implementation của bạn để thu thập câu trả lời từ người dùng return handleClarifyingQuestions(input); } // Xử lý các tool khác bình thường return promptForApproval(toolName, input);};3. Phân tích input câu hỏi. Input chứa các câu hỏi của Claude trong một mảng questions. Mỗi câu hỏi có question (text hiển thị), options (các lựa chọn), và multiSelect (có cho phép chọn nhiều hay không):
{ "questions": [ { "question": "How should I format the output?", "header": "Format", "options": [ { "label": "Summary", "description": "Brief overview" }, { "label": "Detailed", "description": "Full explanation" } ], "multiSelect": false }, { "question": "Which sections should I include?", "header": "Sections", "options": [ { "label": "Introduction", "description": "Opening context" }, { "label": "Conclusion", "description": "Final summary" } ], "multiSelect": true } ]}Xem Định dạng câu hỏi để biết mô tả đầy đủ các trường.
4. Thu thập câu trả lời từ người dùng. Hiển thị các câu hỏi cho người dùng và thu thập lựa chọn của họ. Cách bạn làm điều này tùy vào ứng dụng: một prompt terminal, một form web, một dialog mobile, v.v.
5. Trả câu trả lời về cho Claude. Dựng object answers dưới dạng một record trong đó mỗi key là text question và mỗi value là label của lựa chọn đã chọn:
| Từ object câu hỏi | Dùng làm |
|---|---|
Trường question (ví dụ: "How should I format the output?") | Key |
Trường label của lựa chọn đã chọn (ví dụ: "Summary") | Value |
Với câu hỏi nhiều lựa chọn, truyền một mảng label hoặc nối chúng bằng ", ". Nếu bạn hỗ trợ input tự do, dùng text tùy chỉnh của người dùng làm value.
return PermissionResultAllow( updated_input={ "questions": input_data.get("questions", []), "answers": { "How should I format the output?": "Summary", "Which sections should I include?": ["Introduction", "Conclusion"], }, })return { behavior: "allow", updatedInput: { questions: input.questions, answers: { "How should I format the output?": "Summary", "Which sections should I include?": "Introduction, Conclusion" } }};Định dạng câu hỏi
Phần tiêu đề “Định dạng câu hỏi”Input chứa các câu hỏi Claude đã sinh trong một mảng questions. Mỗi câu hỏi có các trường sau:
| Trường | Mô tả |
|---|---|
question | Text câu hỏi đầy đủ để hiển thị |
header | Nhãn ngắn cho câu hỏi (tối đa 12 ký tự) |
options | Mảng 2-4 lựa chọn, mỗi lựa chọn có label và description. TypeScript: tùy chọn thêm preview (xem bên dưới) |
multiSelect | Nếu true, người dùng có thể chọn nhiều lựa chọn |
Cấu trúc callback của bạn nhận được:
{ "questions": [ { "question": "How should I format the output?", "header": "Format", "options": [ { "label": "Summary", "description": "Brief overview of key points" }, { "label": "Detailed", "description": "Full explanation with examples" } ], "multiSelect": false } ]}Option previews (TypeScript)
Phần tiêu đề “Option previews (TypeScript)”toolConfig.askUserQuestion.previewFormat thêm một trường preview vào mỗi lựa chọn để ứng dụng của bạn hiển thị bản mô phỏng trực quan bên cạnh label. Không có cài đặt này, Claude không sinh preview và trường này vắng mặt.
previewFormat | preview chứa |
|---|---|
| chưa đặt (mặc định) | Trường vắng mặt. Claude không sinh preview. |
"markdown" | ASCII art và các khối code có fence |
"html" | Một fragment <div> có style (SDK từ chối <script>, <style>, và <!DOCTYPE> trước khi callback của bạn chạy) |
Định dạng này áp dụng cho mọi câu hỏi trong session. Claude đưa preview vào những lựa chọn mà so sánh trực quan hữu ích (lựa chọn layout, bảng màu) và bỏ qua khi không cần (xác nhận có/không, lựa chọn chỉ có text). Kiểm tra undefined trước khi render.
import { query } from "@anthropic-ai/claude-agent-sdk";
for await (const message of query({ prompt: "Help me choose a card layout", options: { toolConfig: { askUserQuestion: { previewFormat: "html" } }, canUseTool: async (toolName, input) => { // input.questions[].options[].preview là một chuỗi HTML hoặc undefined return { behavior: "allow", updatedInput: input }; } }})) { // ...}Một lựa chọn với preview HTML:
{ "label": "Compact", "description": "Title and metric value only", "preview": "<div style=\"padding:12px;border:1px solid #ddd;border-radius:8px\"><div style=\"font-size:12px;color:#666\">Active users</div><div style=\"font-size:28px;font-weight:600\">1,284</div></div>"}Định dạng phản hồi
Phần tiêu đề “Định dạng phản hồi”Trả về một object answers ánh xạ trường question của mỗi câu hỏi tới label của lựa chọn đã chọn:
| Trường | Mô tả |
|---|---|
questions | Truyền qua mảng questions gốc (bắt buộc để tool xử lý) |
answers | Object trong đó key là text câu hỏi và value là các label đã chọn |
response | Phản hồi tự do tùy chọn người dùng gõ thay vì trả lời các câu hỏi có cấu trúc |
Với câu hỏi nhiều lựa chọn, truyền một mảng label hoặc nối chúng bằng ", ". Với text tự do theo từng câu hỏi như một lựa chọn “Other”, đặt text của người dùng vào answers[question] như minh họa trong Hỗ trợ input tự do. Chỉ đặt response khi UI của bạn cho phép người dùng bỏ qua card câu hỏi và gõ một phản hồi chung không phải câu trả lời cho câu hỏi cụ thể nào. Khi response được đặt, Claude nhận “The user responded: …” thay vì danh sách câu trả lời theo từng câu hỏi.
{ "questions": [ // ... ], "answers": { "How should I format the output?": "Summary", "Which sections should I include?": ["Introduction", "Conclusion"] }}Hỗ trợ input tự do
Phần tiêu đề “Hỗ trợ input tự do”Các lựa chọn Claude định sẵn sẽ không phải lúc nào cũng đáp ứng điều người dùng muốn. Để người dùng gõ câu trả lời riêng:
- Hiển thị thêm một lựa chọn “Other” sau các lựa chọn của Claude, chấp nhận input text
- Dùng text tùy chỉnh của người dùng làm giá trị câu trả lời (không phải chữ “Other”)
Xem ví dụ đầy đủ bên dưới để có implementation hoàn chỉnh.
Ví dụ đầy đủ
Phần tiêu đề “Ví dụ đầy đủ”Claude hỏi câu hỏi làm rõ khi cần input người dùng để tiếp tục. Ví dụ, khi được yêu cầu giúp quyết định tech stack cho một app mobile, Claude có thể hỏi về cross-platform vs native, sở thích về backend, hay các nền tảng mục tiêu. Những câu hỏi này giúp Claude đưa ra quyết định khớp với sở thích của người dùng thay vì đoán mò.
Ví dụ này xử lý các câu hỏi đó trong một ứng dụng terminal. Đây là những gì xảy ra ở mỗi bước:
- Định tuyến yêu cầu: callback
canUseToolkiểm tra tên tool có phải"AskUserQuestion"và định tuyến tới một handler riêng - Hiển thị câu hỏi: handler duyệt qua mảng
questionsvà in mỗi câu hỏi kèm các lựa chọn đánh số - Thu thập input: người dùng có thể nhập một số để chọn lựa chọn, hoặc gõ text tự do trực tiếp (ví dụ: “jquery”, “i don’t know”)
- Ánh xạ câu trả lời: code kiểm tra input có phải số (dùng label của lựa chọn) hay text tự do (dùng text trực tiếp)
- Trả về Claude: phản hồi gồm cả mảng
questionsgốc và ánh xạanswers
Lưu bản TypeScript thành ask.ts và chạy với npx tsx ask.ts, hoặc lưu bản Python thành ask.py và chạy với python ask.py.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, queryfrom claude_agent_sdk.types import HookMatcher, PermissionResultAllow
def parse_response(response: str, options: list) -> str: """Phân tích input người dùng thành số lựa chọn hoặc text tự do.""" try: indices = [int(s.strip()) - 1 for s in response.split(",")] labels = [options[i]["label"] for i in indices if 0 <= i < len(options)] return ", ".join(labels) if labels else response except ValueError: return response
async def handle_ask_user_question(input_data: dict) -> PermissionResultAllow: """Hiển thị các câu hỏi của Claude và thu thập câu trả lời người dùng.""" answers = {}
for q in input_data.get("questions", []): print(f"\n{q['header']}: {q['question']}")
options = q["options"] for i, opt in enumerate(options): print(f" {i + 1}. {opt['label']} - {opt['description']}") if q.get("multiSelect"): print(" (Enter numbers separated by commas, or type your own answer)") else: print(" (Enter a number, or type your own answer)")
response = input("Your choice: ").strip() answers[q["question"]] = parse_response(response, options)
return PermissionResultAllow( updated_input={ "questions": input_data.get("questions", []), "answers": answers, } )
async def can_use_tool( tool_name: str, input_data: dict, context) -> PermissionResultAllow: # Định tuyến AskUserQuestion tới handler câu hỏi của chúng ta if tool_name == "AskUserQuestion": return await handle_ask_user_question(input_data) # Tự động duyệt các tool khác cho ví dụ này return PermissionResultAllow(updated_input=input_data)
async def prompt_stream(): yield { "type": "user", "message": { "role": "user", "content": "Help me decide on the tech stack for a new mobile app", }, }
# Workaround bắt buộc: dummy hook giữ stream mở cho can_use_toolasync def dummy_hook(input_data, tool_use_id, context): return {"continue_": True}
async def main(): async for message in query( prompt=prompt_stream(), options=ClaudeAgentOptions( can_use_tool=can_use_tool, hooks={"PreToolUse": [HookMatcher(matcher=None, hooks=[dummy_hook])]}, ), ): if isinstance(message, ResultMessage) and message.subtype == "success": print(message.result)
asyncio.run(main())import { query } from "@anthropic-ai/claude-agent-sdk";import * as readline from "readline/promises";
// Helper để hỏi input người dùng trong terminalasync function prompt(question: string): Promise<string> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await rl.question(question); rl.close(); return answer;}
// Phân tích input người dùng thành số lựa chọn hoặc text tự dofunction parseResponse(response: string, options: any[]): string { const indices = response.split(",").map((s) => parseInt(s.trim()) - 1); const labels = indices .filter((i) => !isNaN(i) && i >= 0 && i < options.length) .map((i) => options[i].label); return labels.length > 0 ? labels.join(", ") : response;}
// Hiển thị các câu hỏi của Claude và thu thập câu trả lời người dùngasync function handleAskUserQuestion(input: any) { const answers: Record<string, string> = {};
for (const q of input.questions) { console.log(`\n${q.header}: ${q.question}`);
const options = q.options; options.forEach((opt: any, i: number) => { console.log(` ${i + 1}. ${opt.label} - ${opt.description}`); }); if (q.multiSelect) { console.log(" (Enter numbers separated by commas, or type your own answer)"); } else { console.log(" (Enter a number, or type your own answer)"); }
const response = (await prompt("Your choice: ")).trim(); answers[q.question] = parseResponse(response, options); }
// Trả câu trả lời về cho Claude (phải kèm questions gốc) return { behavior: "allow", updatedInput: { questions: input.questions, answers } };}
async function main() { for await (const message of query({ prompt: "Help me decide on the tech stack for a new mobile app", options: { canUseTool: async (toolName, input) => { // Định tuyến AskUserQuestion tới handler câu hỏi của chúng ta if (toolName === "AskUserQuestion") { return handleAskUserQuestion(input); } // Tự động duyệt các tool khác cho ví dụ này return { behavior: "allow", updatedInput: input }; } } })) { if ("result" in message) console.log(message.result); }}
main();Giới hạn
Phần tiêu đề “Giới hạn”- Subagent:
AskUserQuestionhiện chưa khả dụng trong subagent được sinh ra qua Agent tool - Giới hạn số câu hỏi: mỗi lệnh gọi
AskUserQuestionhỗ trợ 1-4 câu hỏi với 2-4 lựa chọn mỗi câu
Các cách khác để lấy input người dùng
Phần tiêu đề “Các cách khác để lấy input người dùng”Callback canUseTool và tool AskUserQuestion bao phủ hầu hết kịch bản phê duyệt và làm rõ, nhưng SDK còn cung cấp các cách khác để lấy input từ người dùng:
Streaming input
Phần tiêu đề “Streaming input”Dùng streaming input khi bạn cần:
- Ngắt agent giữa tác vụ: gửi tín hiệu huỷ hoặc đổi hướng trong khi Claude đang làm việc
- Cung cấp ngữ cảnh bổ sung: thêm thông tin Claude cần mà không cần chờ nó hỏi
- Xây chat interface: cho phép người dùng gửi message tiếp theo trong khi thao tác dài đang chạy
Streaming input lý tưởng cho các UI hội thoại nơi người dùng tương tác với agent xuyên suốt quá trình thực thi, không chỉ tại các điểm phê duyệt.
Custom tools
Phần tiêu đề “Custom tools”Dùng custom tools khi bạn cần:
- Thu thập input có cấu trúc: xây form, wizard, hoặc workflow nhiều bước vượt ra ngoài định dạng trắc nghiệm của
AskUserQuestion - Tích hợp hệ thống phê duyệt bên ngoài: kết nối tới nền tảng ticketing, workflow, hay phê duyệt sẵn có
- Triển khai tương tác riêng cho domain: tạo tool tùy theo nhu cầu ứng dụng của bạn, như giao diện code review hay checklist deployment
Custom tools cho bạn toàn quyền kiểm soát tương tác, nhưng đòi hỏi nhiều công sức implementation hơn so với dùng callback canUseTool có sẵn.
Tài nguyên liên quan
Phần tiêu đề “Tài nguyên liên quan”- Cấu hình quyền: thiết lập permission mode và quy tắc
- Kiểm soát thực thi bằng hooks: chạy code tùy chỉnh tại các điểm quan trọng trong vòng đời agent
- TypeScript SDK reference: tài liệu API canUseTool đầy đủ
lượt xem