结构化提问
Agent不会问你问题。单靠这个就不行了。
你可以构建一个askUser工具。Agent会看到它,阅读描述,然后继续不使用。在开发者聊天中训练的模型吸收了大量“让我帮你弄明白”的能量。他们觉得问很软弱。他们宁愿猜测。
修复分为两部分。工具很小,系统提示词则负责告诉Agent询问是正确的选择。
学习成果
这是一个askUser工具,可以回答一个问题和2到4个选项,还有一个系统提示词里的 # Handling Ambiguity 部分,可以写出什么时候使用。模糊的话提示词触发工具。具体的提示词不支持。
快速路径
- 加一个
askUser工具,带question和options(2到4根弦) - 在
buildSystemPrompt中添加一个# Handling Ambiguity部分,告诉Agent先搜索,然后询问,再行动 - 用两个提示词验证:一个模糊,一个具体
动手练习 8.1
制作工具,添加提示词部分,确认两半都正常工作。
要求:
- 用上面的schema加
askUser到src/tools.ts - 在
execute中,将选项格式化为编号列表,并返回模型将传递给用户的字符串 - 再加上
# Handling Ambiguity``buildSystemPrompt。告诉Agent:先搜寻,第二问,第三行动 - 运行两个提示词:一个是歧义的(“添加认证”)和一个特定的(“在auth.ts的第42行添加一个空检查”)。确认只有第一个触发
askUser
实现提示:
askUser工具的execute其实并不等用户。它返回一个描述问题和选项的字符串。围绕其的Harness(或读取输出的用户)在下一回合提供答案- 系统提示词脚本的工作比这里的工具描述还多。如果没有提示词部分,模型也会把
askUser当作可选,即使提示词很模糊 - 提示词中通常有两个例子就足以固定模式。别让它过载
工具
import { tool } from "ai";
import { z } from "zod";
export function createAskUserTool() {
return tool({
description: `Ask the user a multiple-choice question.
WHEN TO USE: scoping ambiguous tasks, choosing between approaches,
resolving a missing detail before acting.
WHEN NOT TO USE: you already have enough context to proceed.
DO NOT USE FOR: rhetorical questions or progress updates.`,
inputSchema: z.object({
question: z.string().describe("The question to ask the user"),
options: z
.array(z.string())
.min(2)
.max(4)
.describe("Two to four options for the user to pick from"),
}),
execute: async ({ question, options }) => {
const formatted = options.map((o, i) => `${i + 1}. ${o}`).join("\n");
console.log(`\nQuestion: ${question}\n${formatted}\n`);
return `Asked: "${question}"\nOptions:\n${formatted}\n\n(Awaiting user response.)`;
},
});
}工具会打印问题和选项stdout(让用户看到),并返回相同的字符串内容(模型在消息历史中看到)。模型知道问题正在进行中,不会假设问题已经被回答。
系统提示词的增建
sections.push(`
# Handling Ambiguity
When the task is ambiguous or has multiple valid approaches:
1. Search the code or docs to gather context first
2. Use askUser to let the user choose. Do NOT guess.
3. Examples: "add auth" -> ask OAuth or JWT; "set up a db" -> ask Postgres or SQLite
Specific tasks (with file paths, line numbers, or precise instructions) do not
need askUser. Act directly.`);编号协议很重要。“搜索、询问、行动”为模型提供了可遵循的顺序。没有它,Agent要么提得太早(在没有足够上下文让问题有用之前),要么提问太晚(已经开始构建错误的答案)。
**Warning:模型更愿意探索而非ask**
即使有协议,Agent还是会读取三四个文件后才抽出askUser。这是正确的行为,因为第一步是“先搜索”。这点仍然值得知道,因为如果你看着它运行并感到不耐烦,模型并没有忽视你。这需要收集背景,以便提出一个有用的问题。
如果bash被审批阻挡,Agent就无法执行获取上下文所需的命令。可能永远都达不到第二步。审批系统和askUser处于紧张状态,这种紧张是真正的架构摩擦,而非需要修复的漏洞。
接入工具
const tools = {
read: createReadTool(sandbox),
grep: createGrepTool(sandbox),
bash: createBashTool(sandbox, createApproval({ mode: "interactive" })),
task: createTaskTool(sandbox, { read, grep }),
askUser: createAskUserTool(),
};Agent的工具清单现在包括askUser。系统提示词告诉它什么时候使用。用户终端显示问题。模型的消息历史显示该问题正在等待中。
动手试试
运行一个模糊的任务,观察Agent提出的问题:
bun run index.ts . "Add authentication to this project"你应该看到Agent读取几个文件,然后调用 askUser“我应该用哪种认证策略?”以及“OAuth”、“JWT”、“会话cookies”等选项。终端打印出问题。模型坐着等待。
现在运行一个具体任务,确认Agent没有要求:
bun run index.ts . "Add a null check at line 42 of src/auth.ts before the database query"Agent应该直接做出改变。没有askUser 调用。
npx tsc --noEmit提交
git add src/tools.ts src/system.ts index.ts
git commit -m "feat(askUser): add structured question tool with ambiguity protocol"完成标准
- [ ]
askUser工具是有线的,支持2到4种选项 - [ ]
# Handling Ambiguity部分在系统提示词 - [ ] 模糊提示词触发
askUser - [ ] 具体的提示词不行
- [ ]
npx tsc --noEmit
**注意:让问题记住**
现在返回字符串askUser模型继续。在真实的Harness中,Harness会暂停,收集用户的选择,并作为下一条用户消息传回。画出那个停顿是什么样子。Harness在哪里拦截了工具调用的结果?用户的回答会入对话的哪里?模块8.2的事件方式自然是将这一点连接起来的自然地点。
参考实现
export function createAskUserTool() {
return tool({
description: `Ask the user a multiple-choice question.
WHEN TO USE: scoping ambiguous tasks, choosing between approaches,
resolving a missing detail before acting.
WHEN NOT TO USE: you already have enough context to proceed.
DO NOT USE FOR: rhetorical questions or progress updates.`,
inputSchema: z.object({
question: z.string().describe("The question to ask the user"),
options: z
.array(z.string())
.min(2)
.max(4)
.describe("Two to four options for the user to pick from"),
}),
execute: async ({ question, options }) => {
const formatted = options.map((o, i) => `${i + 1}. ${o}`).join("\n");
console.log(`\nQuestion: ${question}\n${formatted}\n`);
return `Asked: "${question}"\nOptions:\n${formatted}\n\n(Awaiting user response.)`;
},
});
}