跳到正文

结构化提问

Agent不会问你问题。单靠这个就不行了。

你可以构建一个askUser工具。Agent会看到它,阅读描述,然后继续不使用。在开发者聊天中训练的模型吸收了大量“让我帮你弄明白”的能量。他们觉得问很软弱。他们宁愿猜测。

修复分为两部分。工具很小,系统提示词则负责告诉Agent询问是正确的选择。

学习成果

这是一个askUser工具,可以回答一个问题和2到4个选项,还有一个系统提示词里的 # Handling Ambiguity 部分,可以写出什么时候使用。模糊的话提示词触发工具。具体的提示词不支持。

快速路径

  1. 加一个askUser工具,带questionoptions(2到4根弦)
  2. buildSystemPrompt中添加一个# Handling Ambiguity部分,告诉Agent先搜索,然后询问,再行动
  3. 用两个提示词验证:一个模糊,一个具体

动手练习 8.1

制作工具,添加提示词部分,确认两半都正常工作。

要求:

  1. 用上面的schema加askUsersrc/tools.ts
  2. execute中,将选项格式化为编号列表,并返回模型将传递给用户的字符串
  3. 再加上# Handling Ambiguity``buildSystemPrompt。告诉Agent:先搜寻,第二问,第三行动
  4. 运行两个提示词:一个是歧义的(“添加认证”)和一个特定的(“在auth.ts的第42行添加一个空检查”)。确认只有第一个触发askUser

实现提示:

  • askUser工具的execute其实并不等用户。它返回一个描述问题和选项的字符串。围绕其的Harness(或读取输出的用户)在下一回合提供答案
  • 系统提示词脚本的工作比这里的工具描述还多。如果没有提示词部分,模型也会把askUser当作可选,即使提示词很模糊
  • 提示词中通常有两个例子就足以固定模式。别让它过载

工具

ts
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(让用户看到),并返回相同的字符串内容(模型在消息历史中看到)。模型知道问题正在进行中,不会假设问题已经被回答。

系统提示词的增建

ts
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处于紧张状态,这种紧张是真正的架构摩擦,而非需要修复的漏洞。

接入工具

ts
const tools = {
  read: createReadTool(sandbox),
  grep: createGrepTool(sandbox),
  bash: createBashTool(sandbox, createApproval({ mode: "interactive" })),
  task: createTaskTool(sandbox, { read, grep }),
  askUser: createAskUserTool(),
};

Agent的工具清单现在包括askUser。系统提示词告诉它什么时候使用。用户终端显示问题。模型的消息历史显示该问题正在等待中。

动手试试

运行一个模糊的任务,观察Agent提出的问题:

bash
bun run index.ts . "Add authentication to this project"

你应该看到Agent读取几个文件,然后调用 askUser“我应该用哪种认证策略?”以及“OAuth”、“JWT”、“会话cookies”等选项。终端打印出问题。模型坐着等待。

现在运行一个具体任务,确认Agent没有要求:

bash
bun run index.ts . "Add a null check at line 42 of src/auth.ts before the database query"

Agent应该直接做出改变。没有askUser 调用。

bash
npx tsc --noEmit

提交

bash
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的事件方式自然是将这一点连接起来的自然地点。

参考实现

ts
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.)`;
    },
  });
}

非官方简体中文翻译 · 原课程来自 Vercel Academy