上一篇我们解决了"输出怎么约束"的问题。但还有一个更上游的问题:Agent 在动手之前,真的理解需求了吗? 大多数 Agent 的默认行为是"接到指令就干",结果干到一半发现需求理解错了,返工成本极高。今天聊 Grill Me 模式------让 Agent 学会"先问清楚再动手",从执行者升级为协作者。
一、问题:太听话的 Agent 反而不靠谱
先看一个真实场景:
erlang
用户:帮我写一个商机分析报告
Agent:(立即开始执行)
→ 调用商机查询工具
→ 调用数据统计工具
→ 生成报告模板
→ 输出一份 2000 字的通用报告
用户:不是这种,我要的是风险评估报告,不是概况报告
Agent:好的,我重新生成...
→ 重新调用工具
→ 换一个模板
→ 输出风险评估报告
用户:也不是这种,我要的是针对特定客户的...
Agent:...
问题出在哪?Agent 假设太多了。
arduino
用户说了"商机分析报告",Agent 假设:
├── 假设 1:分析对象是所有商机 → 实际是某个特定客户
├── 假设 2:报告类型是概况 → 实际是风险评估
├── 假设 3:报告深度是概要 → 实际需要详细分析
├── 假设 4:输出格式是文本 → 实际需要表格 + 图表
└── 假设 5:时间范围是本月 → 实际是本季度
每一个假设都可能错,每个错误都意味着返工。传统 Agent 的模式是"先干再说",Grill Me 的模式是"先问清楚再干"。
二、什么是 Grill Me 模式
Grill 直译为"烤/盘问",在 AI Agent 语境下,指的是 Agent 在执行任务之前,主动向用户提出一系列结构化问题,澄清需求边界和隐含约束。
css
传统模式 vs Grill Me 模式
传统模式:
用户 → [指令] → Agent → [立即执行] → 输出
↓
用户发现不对
↓
重新描述需求
↓
Agent 重新执行
(可能反复多轮)
Grill Me 模式:
用户 → [指令] → Agent → [反问澄清] → 用户回答
↓
Agent 确认理解
↓
[执行] → 输出
(一轮搞定)
Grill Me 的三个核心环节
markdown
Grill Me 核心流程
│
├── 1. 需求分析(Agent 内部)
│ ├── 解析用户指令
│ ├── 识别模糊点和缺失信息
│ └── 生成待澄清问题列表
│
├── 2. 反问澄清(Agent → 用户)
│ ├── 按优先级排序问题
│ ├── 提供默认选项降低回答成本
│ └── 控制问题数量(通常 3-5 个)
│
└── 3. 确认执行(Agent 内部 + 输出)
├── 综合用户回答,更新任务理解
├── 生成执行计划
├── 向用户确认计划(可选)
└── 执行任务
三、什么时候该 Grill,什么时候该直接干
不是所有任务都需要 Grill。简单的、明确的任务反问是浪费时间。
3.1 决策矩阵
markdown
需求明确度
低 ←─────────────→ 高
┌──────────────────────────────┐
高 │ Grill Me │
↑ │ (必须问清楚) │ 直接执行
任务复杂度│ │ (但需监控)
↓ │ 谨慎执行 │
低 │ (快速确认后执行) │ 直接执行
└──────────────────────────────┘
| 场景 | 复杂度 | 需求明确度 | 策略 |
|---|---|---|---|
| "帮我查天气" | 低 | 高 | 直接执行 |
| "帮我查苏州天气" | 低 | 高 | 直接执行 |
| "帮我写一份商机分析报告" | 高 | 低 | Grill Me |
| "把这段代码重构一下" | 中 | 中 | 谨慎执行(快速确认) |
| "帮我回复这封邮件" | 低 | 中 | 直接执行 |
| "帮我设计一个微服务架构方案" | 高 | 低 | Grill Me |
3.2 判断规则
vbnet
是否需要 Grill Me?
判断条件(满足 2 条以上就该 Grill):
├── 任务的输出有多种可能形态
├── 关键参数缺失(对象、范围、格式、深度等)
├── 错误执行的返工成本高
├── 涉及不可逆操作(删除、发送、发布等)
└── 用户指令中存在模糊词("大概"、"差不多"、"看着办")
四、如何让 Agent 学会反问
4.1 策略一:System Prompt 指令(最基础)
最简单的方式是在 System Prompt 中告诉 Agent 要反问:
python
SYSTEM_PROMPT = """你是一个任务规划助手。在执行任何任务之前,你必须:
1. 分析用户指令,识别可能模糊或缺失的信息
2. 提出最多 5 个澄清问题
3. 等待用户回答后再执行
4. 如果用户指令足够明确,可以跳过反问直接执行
反问规则:
- 问题必须是选择题或填空题,不要开放式提问
- 每个问题提供推荐选项(标注"推荐")
- 按重要性排序,最关键的放第一个
- 如果只需要问 1 个问题,不要问 5 个
"""
效果:能用,但不稳定。 LLM 有时候会忘记反问直接执行,有时候问太多无关问题。
4.2 策略二:结构化反问 Schema
用上一篇文章的 Schema 约束来保证反问质量:
java
// 定义反问的输出结构
public record GrillResult(
@Description("是否需要反问")
boolean needGrill,
@Description("需求分析:识别到的模糊点")
List<String> ambiguities,
@Description("澄清问题列表")
List<ClarifyQuestion> questions,
@Description("初步执行计划(用户回答后执行)")
String plan
) {}
public record ClarifyQuestion(
@Description("问题内容")
String question,
@Description("问题类型:单选/多选/填空")
String type,
@Description("选项列表(如果是选择题)")
List<String> options,
@Description("推荐选项索引")
int recommendedIndex,
@Description("问题优先级 1-5,1 最重要")
int priority
) {}
调用流程:
java
// 第一步:Grill 分析
var grill = chatClient.prompt()
.user(userMessage)
.call()
.entity(GrillResult.class);
// 第二步:判断是否需要反问
if (grill.needGrill()) {
// 展示问题给用户,等待回答
return renderQuestions(grill.questions());
} else {
// 直接执行
return executeTask(userMessage, grill.plan());
}
Schema 约束带来的好处:
| 对比项 | 纯 Prompt | Schema 约束 |
|---|---|---|
| 反问触发率 | 不稳定,偶尔忘记 | 稳定,needGrill 字段强制决策 |
| 问题质量 | 时好时坏 | 结构化保证每个问题有选项 |
| 推荐选项 | 经常忘记提供 | recommendedIndex 字段强制 |
| 优先级排序 | 随机 | priority 字段排序 |
| 可测试性 | 无法单元测试 | 可反序列化校验 |
4.3 策略三:对话状态机管理
更工程化的方式是引入状态机,管理"反问 → 回答 → 再分析 → 执行"的完整流程:
sql
Agent 状态机
│
├── IDLE:等待用户指令
│ └── 收到指令 → ANALYZE
│
├── ANALYZE:分析需求,决定是否反问
│ ├── 需求明确 → EXECUTE
│ ├── 需求模糊 → ASK
│ └── 需求矛盾 → CLARIFY
│
├── ASK:向用户提问
│ └── 收到回答 → ANALYZE(重新分析)
│
├── CLARIFY:指出矛盾,请求用户确认
│ └── 用户确认 → EXECUTE
│
└── EXECUTE:执行任务
├── 成功 → DONE
└── 失败 → ANALYZE(带着错误信息重新分析)
java
public enum AgentState {
IDLE, // 等待输入
ANALYZE, // 分析需求
ASK, // 反问澄清
CLARIFY, // 指出矛盾
EXECUTE, // 执行任务
DONE // 完成
}
public class GrillMeAgent {
private AgentState state = AgentState.IDLE;
private List<ClarifyQuestion> pendingQuestions;
private Map<String, String> answers;
private final ChatClient chatClient;
public Response handle(String userInput) {
return switch (state) {
case IDLE, ANALYZE -> startAnalyze(userInput);
case ASK -> processAnswer(userInput);
case EXECUTE -> execute(userInput);
default -> Response.error("Unexpected state");
};
}
private Response startAnalyze(String input) {
this.state = AgentState.ANALYZE;
var grill = chatClient.prompt()
.user(input)
.call()
.entity(GrillResult.class);
if (grill.needGrill()) {
this.state = AgentState.ASK;
this.pendingQuestions = grill.questions();
return Response.questions(grill.questions());
} else {
this.state = AgentState.EXECUTE;
return execute(grill.plan());
}
}
private Response processAnswer(String answer) {
// 解析用户回答,更新答案
for (int i = 0; i < pendingQuestions.size(); i++) {
answers.put(pendingQuestions.get(i).question(),
parseAnswer(answer, i));
}
// 重新分析是否还需要追问
this.state = AgentState.ANALYZE;
return startAnalyze(buildFullRequest());
}
}
五、反问提问的艺术
5.1 好问题 vs 坏问题
arduino
❌ 坏问题(开放式、模糊)
├── "你想怎么做?"
├── "有什么特殊要求吗?"
├── "请详细描述你的需求"
└── 问题本身就需要用户大量思考
✅ 好问题(选择题、有推荐)
├── "报告输出格式:A) Markdown(推荐) B) PDF C) Excel"
├── "分析对象:A) 全部商机 B) 指定客户(推荐:XX科技)"
├── "时间范围:A) 本周 B) 本月(推荐) C) 本季度"
└── 用户只需要做选择,认知负担低
核心原则:问题的认知负担从用户转移到 Agent。 Agent 负责思考有哪些可能性,用户只需要选。
5.2 提问优先级框架
arduino
提问优先级(从高到低)
│
├── P0:不可逆后果(必须先问)
│ └── "此操作将删除选定范围内的数据,确认范围是?"
│
├── P1:核心参数缺失(不问没法做)
│ ├── 分析对象缺失
│ ├── 输出格式未指定
│ └── 关键约束未明确
│
├── P2:影响结果的模糊点(问了对结果影响大)
│ ├── 时间范围
│ ├── 深度/详细程度
│ └── 目标受众
│
└── P3:锦上添花的参数(不问也行)
├── 语言偏好
└── 风格偏好
5.3 批量提问 vs 逐个提问
css
批量提问(推荐)
├── 好处:一轮搞定,减少交互轮次
├── 适合:问题之间相互独立
└── 示例:
"在开始之前,请确认以下信息:
1. 分析对象:A) 全部 B) 指定客户(推荐:XX科技)
2. 报告类型:A) 概况 B) 风险评估(推荐)C) 机会推荐
3. 时间范围:A) 本周 B) 本月(推荐)C) 本季度"
逐个提问
├── 好处:每个问题可以基于上一个回答动态调整
├── 适合:问题之间有依赖关系
└── 示例:
Q1: "分析对象是?"
A1: "指定客户"
Q2: "哪个客户?"(基于 Q1 的答案动态生成)
A2: "XX科技"
Q3: "XX科技的哪个项目?"(基于 Q2 的答案动态生成)
实践建议:默认用批量提问,只在问题间有强依赖时用逐个提问。
六、实战:一个完整的 Grill Me Agent
6.1 场景:技术文档写作 Agent
用户说"帮我写一份技术方案文档",Agent 需要澄清多个关键信息。
java
public class DocWritingAgent {
private final ChatClient chatClient;
// 步骤 1:Grill 分析
public GrillResult analyzeRequest(String userRequest) {
var prompt = """
用户请求:{request}
分析这个请求,识别需要澄清的信息。
对于技术文档写作,通常需要确认:
- 文档类型(方案/规范/总结/评审)
- 目标读者(技术团队/管理层/客户)
- 核心主题
- 输出格式
- 篇幅要求
""";
return chatClient.prompt()
.user(u -> u.text(prompt).param("request", userRequest))
.call()
.entity(GrillResult.class);
}
// 步骤 2:渲染问题,收集回答
public String renderQuestions(List<ClarifyQuestion> questions) {
var sb = new StringBuilder();
sb.append("在开始之前,请确认以下信息:\n\n");
for (int i = 0; i < questions.size(); i++) {
var q = questions.get(i);
sb.append(i + 1).append(". ").append(q.question()).append("\n");
if (q.options() != null && !q.options().isEmpty()) {
for (int j = 0; j < q.options().size(); j++) {
var prefix = (j == q.recommendedIndex()) ? " ★ " : " ";
sb.append(prefix).append((char)('A' + j)).append(") ")
.append(q.options().get(j));
if (j == q.recommendedIndex()) sb.append("(推荐)");
sb.append("\n");
}
}
sb.append("\n");
}
sb.append("直接回复选项字母即可,如:B A C");
return sb.toString();
}
// 步骤 3:综合回答,执行任务
public String executeWithAnswers(String originalRequest,
List<ClarifyQuestion> questions,
List<String> answers) {
// 拼接完整需求
var fullRequest = buildFullRequest(originalRequest, questions, answers);
// 执行写作
return chatClient.prompt()
.user(fullRequest)
.call()
.content();
}
private String buildFullRequest(String original,
List<ClarifyQuestion> questions,
List<String> answers) {
var sb = new StringBuilder();
sb.append(original).append("\n\n已确认的需求:\n");
for (int i = 0; i < questions.size(); i++) {
sb.append("- ").append(questions.get(i).question())
.append(":").append(answers.get(i)).append("\n");
}
return sb.toString();
}
}
6.2 实际交互效果
css
用户:帮我写一份技术方案文档
Agent:
在开始之前,请确认以下信息:
1. 文档类型是什么?
★ A) 系统设计方案(推荐)
B) 接口规范文档
C) 技术评审文档
D) 项目总结
2. 目标读者是谁?
A) 技术团队内部
★ B) 技术管理层(推荐)
C) 业务方
D) 外部客户
3. 核心主题方向?
A) 微服务架构设计
★ B) 数据权限方案(推荐)
C) 性能优化方案
D) 其他(请说明)
4. 预期篇幅?
A) 精简版(3-5 页)
★ B) 标准版(8-12 页)(推荐)
C) 详细版(15+ 页)
直接回复选项字母即可,如:A B B B
用户:A B B B
Agent:
收到,确认需求:
- 类型:系统设计方案
- 读者:技术管理层
- 主题:数据权限方案
- 篇幅:标准版
正在生成文档...
七、Grill Me 的进阶:自适应反问
固定规则的反问不够智能,更好的方式是让 Agent 根据上下文自适应决定反问策略。
7.1 三层反问策略
ini
自适应反问策略
│
├── 第一层:快速判断(无需 LLM)
│ ├── 基于关键词匹配判断任务类型
│ ├── 简单任务(查询、计算)→ 跳过反问
│ └── 复杂任务(生成、设计、分析)→ 进入第二层
│
├── 第二层:LLM 轻量分析(低成本调用)
│ ├── 用快速模型分析需求完整度
│ ├── 输出:needGrill (true/false) + 模糊点列表
│ ├── needGrill = false → 直接执行
│ └── needGrill = true → 进入第三层
│
└── 第三层:结构化反问(完整 Grill Me)
├── 用主模型生成结构化问题
├── 问题和选项基于上下文动态生成
└── 控制问题数量 ≤ 5
分层的好处:简单任务不浪费 LLM 调用,只在真正需要时才启动 Grill。
7.2 上下文积累:不要重复问已经知道的
java
public class ContextAwareGrillAgent {
// 积累的用户偏好和上下文
private UserContext context;
public GrillResult analyzeWithContext(String userRequest) {
var prompt = """
用户请求:{request}
已知上下文:
- 用户角色:{role}
- 常用输出格式:{format}
- 上次分析对象:{lastTarget}
- 用户偏好语言:{language}
基于已知上下文,分析是否还有需要澄清的信息。
已知的信息不要重复问。
""";
return chatClient.prompt()
.user(u -> u.text(prompt)
.param("request", userRequest)
.param("role", context.getRole())
.param("format", context.getPreferredFormat())
.param("lastTarget", context.getLastTarget())
.param("language", context.getLanguage()))
.call()
.entity(GrillResult.class);
}
}
效果对比:
arduino
无上下文(每次都问全套)
├── 第 1 次:"帮写技术文档"
│ └── 问:类型?读者?格式?篇幅?主题?(5 个问题)
├── 第 2 次:"再帮我写一份"
│ └── 问:类型?读者?格式?篇幅?主题?(又 5 个问题)← 重复!
有上下文(只问缺失的)
├── 第 1 次:"帮写技术文档"
│ └── 问:类型?读者?格式?篇幅?主题?(5 个问题)
├── 第 2 次:"再帮我写一份,换成微服务架构"
│ └── 问:篇幅和上次一样吗?(1 个问题)← 只问新增的
八、Grill Me 的代价和缓解
8.1 交互成本
vbnet
无 Grill Me:
├── 用户输入 1 次
├── Agent 执行 → 输出
├── 可能不对 → 用户纠正
├── Agent 重新执行
└── 总交互:2-4 轮
有 Grill Me:
├── 用户输入 1 次
├── Agent 反问 → 用户回答
├── Agent 执行 → 输出
└── 总交互:2 轮
前提是 Grill Me 的反问足够精准。 如果问了无关问题,反而增加交互成本。
8.2 延迟和成本
| 对比项 | 无 Grill Me | 有 Grill Me |
|---|---|---|
| LLM 调用次数 | 1 次 | 2 次(分析 + 执行) |
| 用户交互轮次 | 2-4 轮(含返工) | 2 轮 |
| 总执行时间 | 2-5s | 多 0.5-1s(分析阶段) |
| Token 消耗 | 基准 | +30-50%(分析阶段) |
| 返工概率 | 30-50% | 5-10% |
8.3 缓解策略
arduino
缓解策略
│
├── 1. 提供推荐选项,降低用户思考成本
│ └── "A) 全部(推荐)B) 指定客户" 比 "你想分析哪些?" 好
│
├── 2. 提供默认值,用户可跳过
│ └── "格式默认 Markdown,如需更改请说明"
│
├── 3. 批量提问,减少交互轮次
│ └── 一次问 3-5 个,而不是一个一个问
│
├── 4. 上下文积累,避免重复提问
│ └── 已知偏好不再问
│
└── 5. 延迟反问,先做初步分析
└── 先出个初稿,同时标注"以下假设如有出入请指出"
8.4 延迟反问:一种折中方案
延迟反问(Lazy Grill)
│
├── Agent 先基于现有信息执行
├── 在输出中标注所有假设
├── 用户确认假设是否正确
│ ├── 正确 → 完成
│ └── 不正确 → 基于反馈修正
│
└── 适合:执行成本低、可快速重试的任务
java
public Response lazyGrill(String userRequest) {
// 先直接执行,列出假设
var result = chatClient.prompt()
.user(u -> u.text("""
请求:{request}
基于现有信息完成任务。在输出中:
1. 明确列出你做出的所有假设
2. 标注每个假设的置信度
3. 如果有需要用户确认的关键假设,放在最前面
""").param("request", userRequest))
.call()
.content();
return Response.withAssumptions(result);
}
延迟反问 vs 前置反问的选择:
| 对比项 | 前置反问(Grill Me) | 延迟反问(Lazy Grill) |
|---|---|---|
| 执行前确认 | 是 | 否 |
| 返工风险 | 低 | 中 |
| 交互轮次 | 固定 2 轮 | 1-2 轮 |
| 适用场景 | 高成本任务 | 低成本任务 |
| 用户体验 | 更可控(但多一轮) | 更流畅(但可能要修正) |
九、Grill Me 与其他规划模式的关系
vbscript
Agent 规划模式谱系
│
├── 盲目执行(No Planning)
│ └── 接到指令就干,不分析不规划
│
├── Grill Me(反问式规划)
│ └── 先问清楚需求,再规划执行
│
├── ReAct(推理-行动交替)
│ └── 边执行边推理,遇到问题再调整
│
├── Plan-then-Execute(先规划后执行)
│ └── 先生成完整计划,再逐步执行
│
└── Reflection(反思后改进)
└── 执行后自我评估,迭代改进
| 模式 | 执行前 | 执行中 | 执行后 | 适用场景 |
|---|---|---|---|---|
| 盲目执行 | 无 | 无 | 无 | 简单确定性任务 |
| Grill Me | 反问澄清 | 无 | 无 | 模糊的高成本任务 |
| ReAct | 无 | 推理调整 | 无 | 探索性任务 |
| Plan-Execute | 规划 | 按计划执行 | 无 | 步骤明确的多步任务 |
| Reflection | 无 | 无 | 反思改进 | 质量敏感任务 |
Grill Me 解决的是"执行前"的问题------确保在动手之前,需求的模糊性和隐含假设被显式化。 它和其他模式不是互斥的:一个成熟的 Agent 可以先用 Grill Me 澄清需求,再用 Plan-Execute 规划步骤,执行中用 ReAct 应对变化,执行后用 Reflection 质量把关。
十、最佳实践 Checklist
| # | 实践 | 说明 |
|---|---|---|
| 1 | 复杂任务默认开启 Grill Me | 高复杂度 + 低明确度 = 必须反问 |
| 2 | 问题以选择题为主 | 降低用户认知负担 |
| 3 | 每个问题提供推荐选项 | 用户可以不动脑直接选 |
| 4 | 控制问题数量 ≤ 5 | 问太多用户会烦 |
| 5 | 积累上下文避免重复提问 | 第二次交互只问新增的 |
| 6 | 不可逆操作必须先确认 | 删除、发送、发布类操作 |
| 7 | 用 Schema 约束反问质量 | 结构化输出保证问题质量 |
| 8 | 简单任务跳过反问 | 别为了 Grill 而 Grill |
| 9 | 考虑延迟反问的折中方案 | 先出初稿 + 标注假设,适合低成本任务 |
| 10 | 监控反问后的用户满意度 | 如果用户经常不回答直接说"就按你说的来",说明问题问得不好 |
下一篇我们聊 Agent 设计模式:ReAct、Plan-Execute、Reflection------这三种经典 Agent 模式分别解决什么问题,怎么用代码实现,以及在实际项目中如何组合使用。
本文是 AI Agent 开发实战系列第 9 篇,系列目录: