📌 系列简介:「JS全栈AI Agent学习」系统学习 21 个 Agent 设计模式,篇数随学习进度持续更新。
⏱️ 预计阅读时间:15 分钟
📖 原书地址:adp.xindoo.xyz
前端转 JS 全栈,正在学 AI,理解难免有偏差,欢迎批评指正 ~
🗺️ 系列导航
| 篇 | 主题 | 状态 |
|---|---|---|
| 第一篇 | 提示链 · 路由 · 并行化 | ✅ |
| 第二篇 | 反思 · 工具使用 · 规划 | ✅ |
| 第三篇 | 多智能体 · 记忆管理 · 学习适应 | ✅ |
| 第四篇 | MCP 协议 | ✅ |
| 第五篇 | 目标设定与监控 · 异常处理与恢复 | ✅ |
| 本篇 | Human-in-the-Loop 设计 | ✅ |
前言
上一篇讲目标监控和异常处理,结尾提到了 Human-in-the-loop——什么时候该让人介入。
当时我给了一个简单的判断原则:影响最终结果 + 难以撤回,就介入。
但这只是"要不要介入"的问题。这一章要讲的,是更难的那个问题:
在什么时候,用什么方式,把决策权交还给人?
这个问题在 my-resume 项目里非常具体。很多开源项目也有嘛,就是分析自己简历,然后提出参考意见并优化。
每一条信息都是用户的真实经历——Agent 没有权利自己"脑补",更不能随便改。
怎么在"帮用户做事"和"不越权替用户做决定"之间找到平衡?这就是 HITL 要解决的事。
PS:现在还在跟着学,代码实战的推进到这部分再一起放出来了,目前刚重构完还没还没把AI的功能串起来
后面设计的一个功能就是能帮识别下简历问题,有时手滑年份错了,可能还好,但对HR来说很致命。从实际问题出发,自己当产品,自己即是用户就好,慢慢完善,一边学习一边做。
一、一个让 Agent 卡住的问题
假设你正在用简历优化 Agent,它在扫描你的简历时,发现了这样一个问题:
- A公司任职时间:2018年3月 — 2020年6月
- C公司任职时间:2017年9月 — 2019年4月
两段时间有将近两年的重叠。
这时候 Agent 面临一个选择:
- 自己改? 改哪个?改成什么?它不知道哪个才是真实的。
- 不管它? 这个矛盾如果出现在正式简历里,会让 HR 直接质疑真实性。
- 问用户? 问,但怎么问?问什么?
这个看似简单的问题,背后藏着 AI Agent 设计中最核心的一个命题:
在什么时候,用什么方式,把决策权交还给人?
这就是本章的主题:Human-in-the-Loop(HITL)。
二、HITL 是什么?
Human-in-the-Loop,直译是"把人放在循环里"。
用三句话理解它:
| 模式 | 描述 | 问题 |
|---|---|---|
| 全自动 | AI 自己做所有决定 | 遇到信息不足时,只能瞎猜 |
| 全人工 | 每一步都问用户 | 用户体验极差,跟没有 AI 一样 |
| HITL | AI 做能做的,人做该做的 | ✅ 两者平衡 |
HITL 的核心不是"让 AI 更笨",而是:
承认有些决定本来就该人来做,AI 的职责是识别出这些时刻,并优雅地把决策权交出去。
接下来,拆解实现 HITL 的六大核心机制。
三、机制①:介入时机——Agent 先自己找答案
最容易犯的错误:发现问题就问用户。
这会导致用户被频繁打断,体验极差。正确的做法是:
Agent 先尝试自己解决,真的解决不了,才介入。
判断标准:有没有足够的上下文自行决策?
还是简历场景。Agent 看到用户写了:
"我是一个积极主动、善于沟通的人"
这句话太泛了,Agent 想把它改得更具体。这时候该问用户吗?
不该。 Agent 应该先去项目经历里找支撑证据——这件事它自己能做:
async function enrichSelfDescription(profile) {
const { selfDescription, projects } = profile;
// 先在项目经历里找支撑证据
const evidence = await findSupportingEvidence(projects, selfDescription);
if (evidence.length > 0) {
// 找到了 → 直接补充,不打扰用户
return {
action: 'auto_enrich',
result: buildEnrichedDescription(selfDescription, evidence),
};
} else {
// 找不到 → 才介入
return {
action: 'require_human',
reason: '自我评价缺乏具体项目支撑,需要用户补充',
};
}
}
这个判断逻辑用一句话总结:
能自己解决 → 不介入
不能自己解决 → 才介入
看起来简单,但它是后续所有机制的基础前提。
四、机制②:结构化选项——别问开放问题
当 Agent 决定介入时,怎么问同样重要。
开放问题 vs 结构化选项
❌ 糟糕的问法:
"您的两段工作经历时间有重叠,请问是怎么回事?"
用户看到这个问题,需要自己思考、自己组织语言、自己判断该改哪里——认知负担极高。
✅ 正确的问法:
"发现您的工作经历存在时间重叠,请选择处理方式:
- A:A公司时间有误,应为 2019年3月 — 2020年6月
- B:C公司时间有误,应为 2019年9月 — 2020年4月
- C:两段经历确实重叠(如兼职),我来手动说明"
用户只需要选一个字母,认知成本降到最低。
这个设计思路,和我们做前端交互设计是一个道理——不要让用户面对空白输入框,给他选项,降低决策成本。
A/B/C 选项的设计原则
function buildInterventionOptions(conflict) {
return {
question: conflict.description,
options: [
{
key: 'A',
label: conflict.suggestion_a, // Agent 推断的方案A
action: 'auto_fix_a',
},
{
key: 'B',
label: conflict.suggestion_b, // Agent 推断的方案B
action: 'auto_fix_b',
},
{
key: 'C',
label: '以上都不对,我来手动说明', // 兜底选项,永远存在
action: 'pause_for_human', // 暂停,等用户补充
},
],
};
}
注意 C 选项永远存在。它的作用是:
保留用户的最终控制权,无论 Agent 推断得多准,用户都可以说"都不对,我自己来"。
这不是产品的妥协,而是对用户自主权的尊重——也是用户信任 Agent 的基础。
五、机制③:介入粒度——问题有大有小,介入要分级
并不是所有的介入都一样重。Agent 需要识别当前问题属于哪个粒度级别,再决定如何介入。
三个粒度级别
字段级(Field-level):缺一个具体数据,补上就好。
场景:手机号只有10位,少了一位数字。 处理:直接问"您的手机号是否为 138XXXX?",一句话解决。
段落级(Block-level):某个模块的内部逻辑有问题,需要用户理清一块内容。
场景:项目经历里有三个项目,时间线混乱,无法判断先后顺序。 处理:列出三个项目,请用户确认排序依据。
全局级(Global-level):输入内容与任务目标根本不匹配,需要重新确认方向。
场景:用户投的是前端工程师岗位,但简历通篇没有提到任何技术栈。 处理:这不是逻辑问题,而是内容本身无法支撑任务,需要从全局重新确认。
粒度判断逻辑
function classifyInterventionLevel(issue) {
switch (issue.scope) {
case 'single_field':
// 缺一个字段值,补上即可
return 'field';
case 'block_logic':
// 某模块内部逻辑不完整,缺少判断依据
return 'block';
case 'global_mismatch':
// 整体内容与目标任务不匹配
return 'global';
}
}
粒度越高,用户需要做的事越多,也越容易产生疲劳感——这就引出了下一个机制。
六、机制④:批量介入——别一个一个问,打包说
用户疲劳是真实存在的
想象一下:Agent 问了你第1个问题,你回答了。问了第2个,你回答了。第3个、第4个、第5个……
到第3个问题开始,大多数用户已经开始不耐烦了。更糟糕的是,如果前3个都是小问题(字段级),第4个突然是全局级的大问题,用户早就没耐心认真回答了。
做过用户访谈或者产品测试的同学应该有体会——用户的耐心是有限的,而且消耗得比你想象的快。
解法:先做完能做的,再打包告诉用户
Agent 扫描全文
↓
收集所有问题,分类整理
↓
能自己解决的 → 先默默处理掉
↓
剩下不能解决的 → 打包成一份"阶段总结"
↓
一次性告知用户,用户一次性补充
↓
继续后续流程
阶段总结的模板示例
✅ 已完成优化:
- 自我评价已结合项目经历补充了具体案例
- 技能标签已按岗位要求重新排序
- 教育经历格式已统一
⚠️ 需要您补充以下信息,以便继续优化:
1. [字段级] 手机号疑似缺少一位,请确认
2. [段落级] A公司与C公司任职时间有重叠,请选择处理方式(A/B/C)
3. [全局级] 未发现前端相关技术栈,请确认目标岗位方向
补充完成后,我将继续为您完成剩余优化 ~
async function runBatchedIntervention(profile) {
const issues = [];
// 第一遍扫描:收集所有问题
const scanResult = await scanProfile(profile);
for (const issue of scanResult.issues) {
if (issue.canAutoFix) {
// 能自己解决的,直接处理
await autoFix(profile, issue);
} else {
// 不能解决的,加入待询问列表
issues.push(issue);
}
}
if (issues.length === 0) return { status: 'complete' };
// 打包成一次介入,而不是多次打断
return {
status: 'need_human',
summary: buildSummaryMessage(profile, issues),
issues,
};
}
这个设计的核心思想:
把"打扰用户"这件事的次数压到最低,但每次打扰都要有价值、有上下文、让用户看到进度。
七、机制⑤:前后回溯——用户回答后,不是结束
用户补充完信息,Agent 不能直接继续往下走。它需要做两件事:
往后看:后续内容跟着改
用户确认了"A公司时间有误,应为2019年3月",那么:
- 简历里所有引用了这段时间的地方,都要同步更新
- 基于这段时间计算的"工作年限",也要重新计算
往前看:之前内容有没有新矛盾
用户的补充可能引入新的矛盾。比如:
用户把 A公司时间改成了 2019年3月 — 2020年6月 但之前已经处理好的 B公司时间是 2019年1月 — 2020年3月 现在又重叠了……
这让我想到写代码改 bug 的感受——改了一个地方,另一个地方又冒出来了。Agent 的回溯机制,就是在系统层面把这件事自动化。
async function postInterventionRevalidation(profile, updatedFields) {
// 往后看:同步更新所有受影响的字段
await propagateChanges(profile, updatedFields);
// 往前看:重新扫描,检查是否引入了新矛盾
const newIssues = await scanProfile(profile);
if (newIssues.issues.length > 0) {
// 发现新矛盾 → 进入升级循环
return {
status: 'new_conflict_found',
issues: newIssues.issues,
};
}
return { status: 'clean' };
}
八、机制⑥:升级循环——新矛盾出现,再次介入
前后回溯发现了新矛盾,怎么办?
再次进入介入流程。 这就是"升级循环(Escalation Loop)"。
但循环不能无限进行,需要一个收敛条件——这和上一篇讲反思模式时的"最多3次"是同一个道理:边际收益递减,超过上限就该人工接手,而不是让 Agent 继续转圈。
async function escalationLoop(profile, maxRounds = 3) {
let round = 0;
while (round < maxRounds) {
const result = await runBatchedIntervention(profile);
if (result.status === 'complete') {
// 没有新问题,循环结束
return { status: 'done', rounds: round };
}
// 有问题,等待用户响应
const userResponse = await waitForUserInput(result.summary);
await applyUserResponse(profile, userResponse);
// 前后回溯
const revalidation = await postInterventionRevalidation(
profile,
userResponse.updatedFields
);
if (revalidation.status === 'clean') break;
round++;
}
if (round >= maxRounds) {
// 超过最大轮次,诚实告知用户
return {
status: 'max_rounds_reached',
message: '检测到复杂冲突,建议您手动检查以下内容后重新提交',
};
}
}
超过最大轮次的处理方式,我觉得这里有一个很重要的设计原则:
诚实地告诉用户"这个我处理不了",比假装处理完要好得多。
Agent 承认自己的边界,反而会让用户更信任它。
九、完整流程图
把六大机制串在一起,完整的 HITL 流程如下:
用户提交内容
↓
Agent 扫描全文,收集所有问题
↓
┌─────────────────────────────┐
│ 对每个问题: │
│ 有足够上下文? │
│ ├─ 是 → 自动处理(机制①) │
│ └─ 否 → 加入待询问列表 │
└─────────────────────────────┘
↓
待询问列表为空?
├─ 是 → 输出结果,流程结束
└─ 否 → 按粒度分级(机制③)
↓
打包成阶段总结(机制④)
↓
展示给用户:A/B/C 选项(机制②)
↓
用户响应
↓
前后回溯(机制⑤)
↓
有新矛盾?
├─ 有 → 升级循环,回到扫描(机制⑥)
└─ 没有 → 输出结果,流程结束
上述是和AI讨论出来的结论,实际上,已有功能都是 已有简历 -> 反推回填内容;
这一块设计后面是想做一个Agent功能,能快速高效生成简历模版。慢慢来,边学边完善吧。
十、核心洞察总结
| 机制 | 核心思想 | 一句话记住 |
|---|---|---|
| ①介入时机 | Agent 先自己找答案 | 能自己解决的,不打扰用户 |
| ②结构化选项 | 给选项,不问开放问题 | A/B/C 选项 + 永远有兜底的 C |
| ③介入粒度 | 问题分三级,介入方式不同 | 字段级 · 段落级 · 全局级 |
| ④批量介入 | 打包打扰,不零散打断 | 把打扰次数压到最低 |
| ⑤前后回溯 | 用户回答后,双向检查 | 往后同步,往前验证 |
| ⑥升级循环 | 新矛盾再次介入,有收敛条件 | 超过上限,诚实告知,交给人 |
结语
读完这一章,我最大的感受是:
HITL 不是 AI 能力不足的妥协,而是一种设计哲学。
它承认了一件事:有些决定,本来就该人来做。AI 的职责不是替代人的所有判断,而是:
- 识别出哪些决定超出了自己的能力范围
- 优雅地把这些决定交还给用户
- 降低用户做决定的认知成本
- 保护用户不被无意义的打扰淹没
这六个机制,本质上都在回答同一个问题:
怎么让 AI 和人的协作,比任何一方单独工作都更好?
对于 my-resume 的全栈改造来说,这章给了我一个很清晰的产品设计原则:
Agent 的边界感,和开发者的边界感是一回事。 知道什么该自己做,什么该交出去,什么时候该说"这个我不确定,你来决定"——这是靠谱的标志,不是能力不足的表现。
学到这里,越来越觉得:AI 工程和软件工程,底层真的是同一套思维。 边界感、容错、分层处理——工程师早就在做了,只不过现在的执行者从代码变成了模型。
下一篇预告: 第14章——RAG(检索增强生成)。Agent 有了工具、有了目标、有了人机协同,下一步是让它真正"有记忆"——从外部知识库里检索信息,而不是只靠训练数据回答问题。
💬 系列地址:持续更新中
📖 原书地址:adp.xindoo.xyz
🛠️ 实战项目:my-resume(静态页面 → NestJS + 数据库 + AI + 部署上线,进行中)
如果这篇对你有帮助,欢迎点赞收藏,我们下篇见 👋