AI Agent开发实战:实现人机协同的await human()编程范式
在实际 AI 代理(AI Agent)的开发中,一个常见的瓶颈是代理无法处理超出其预设能力或权限范围的任务。例如,一个自动处理工单的代理可能遇到需要人工审批的报销申请,或者一个客服代理需要将复杂的投诉转交给人类专员。传统的做法往往需要中断整个自动化流程,通过邮件、消息通知等外部渠道手动交接,这破坏了流程的连贯性,也增加了状态跟踪的复杂度。Handoff这个概念,或者说await human()这种编程范式,正是为了解决 AI 代理与人类之间的无缝协作问题而提出的。它允许开发者在代理的代码逻辑中,像等待一个异步函数调用完成一样,优雅地“等待”人类介入、决策或提供输入,待人类操作完成后,代理能自动获取结果并继续执行后续流程。
本文将深入探讨如何在实际项目中实现await human()模式。我们将从理解其核心机制开始,然后构建一个模拟的工单处理 AI 代理,在其中集成人工审批节点。你会看到如何设计一个简单的“任务队列”服务来挂起代理任务、如何通过 Web 界面向人类展示待办事项、以及代理如何轮询或通过回调恢复执行。整个过程将使用 Node.js 环境进行演示,但设计思想是跨语言和框架通用的。无论你是正在构建自动化工作流、智能客服系统,还是复杂的业务处理代理,掌握这种“人机协同”模式都能显著提升系统的灵活性和实用性。
1. 理解await human()的核心机制与设计挑战
在异步编程中,await关键字用于等待一个 Promise 完成,期间当前执行上下文会被挂起,直到异步操作返回结果。await human()是一种类比,它将“人类执行某个操作”抽象为一个异步任务。其核心思想是:AI 代理的执行流在遇到需要人类干预的节点时暂停,将控制权和必要上下文交给一个“人工任务接口”;人类通过该接口完成操作后,代理能接收到操作结果,并从暂停点继续执行。
1.1 为什么不是简单的消息通知?
很多初级实现会采用“触发通知 -> 代理结束或进入等待循环 -> 人工处理 -> 人工重新触发代理”的模式。这种模式存在几个问题:
- 状态丢失:代理进程可能结束,其内存中的执行上下文(如变量、中间结果)会丢失。
- 流程断裂:人工操作和自动化流程是分离的,需要额外的系统来记录“某个工单正等待人工处理第 X 步”。
- 恢复复杂:人工完成后,需要重新实例化代理并手动告诉它从哪一步继续,逻辑容易出错。
await human()模式的目标是让“等待人类”变得像调用一个数据库查询 API 一样自然,对代理的主逻辑流程侵入最小。
1.2 关键组件与数据流
要实现这一模式,通常需要设计以下几个核心组件:
- 代理执行引擎:能够执行代理逻辑(可能是代码、工作流 DSL 或 LLM 驱动的推理),并支持在特定点“暂停”和“恢复”。
- 任务挂起与持久化存储:当代理执行到
await human()时,需要将当前代理的“状态”(包括堆栈、变量、程序计数器等快照)安全地保存起来(例如存入数据库或 Redis)。同时,生成一个唯一的taskId用于后续关联。 - 人工任务接口:一个面向人类的界面(Web 页面、移动端应用、邮件链接等),用于展示待处理的任务详情,并提供操作表单(如“批准”、“驳回”、“输入处理意见”)。
- 任务恢复机制:人类在界面上提交操作后,系统需要能根据
taskId找到被挂起的代理状态,将其恢复,并将人类操作的结果作为await human()表达式的返回值注入,使代理继续执行。
其简化数据流如下:
AI代理运行 -> 遇到`handoffTask` -> 保存状态,生成`taskId` -> 通知人类接口(创建待办) -> 代理实例暂停/结束 人类通过接口查看任务 -> 做出决策并提交 -> 系统根据`taskId`加载代理状态 -> 注入决策结果 -> 恢复代理执行 -> 代理继续后续逻辑1.3 技术选型考量
在具体实现时,有几个关键决策点:
- 状态持久化:对于简单的、无状态的代理,可能只需要保存任务描述和参数。对于复杂的、有状态的代理(如使用 LangChain 的 AgentExecutor),需要序列化整个执行器状态。这通常需要框架支持(如 LangChain 的
save_context)。 - 恢复方式:是让原代理进程长驻等待(通过轮询数据库),还是先结束进程,待人工操作后再重新启动一个进程并加载状态?后者更适用于 Serverless 或短时任务场景,但对状态序列化要求更高。
- 人类接口:是集成到现有后台管理系统,还是独立开发一个简单的任务处理面板?这取决于业务复杂度。
在接下来的部分,我们将采用一种折中且易于理解的方案:使用内存/数据库存储任务对象,代理以轮询方式等待。这虽然不够高效,但能清晰地展示整个原理。
2. 环境准备与项目结构
我们将使用 Node.js 环境,因为它天然支持异步操作,且易于构建轻量级 Web 服务。这个示例将模拟一个“员工报销审批AI代理”。
2.1 环境与依赖
确保你的系统已安装 Node.js(版本 16 或以上)和 npm。然后初始化项目并安装必要依赖。
mkdir ai-agent-handoff-demo cd ai-agent-handoff-demo npm init -y安装依赖:
npm install express lowdb uuidexpress: 用于快速搭建提供人工任务接口的 Web 服务器。lowdb: 一个简单的本地 JSON 文件数据库,用于持久化存储“人工任务”和“代理状态”。在生产环境中应替换为 PostgreSQL、MongoDB 或 Redis。uuid: 用于生成唯一任务 ID。
此外,我们还需要axios用于代理轮询任务状态(模拟代理主动查询),但为了简化,我们将代理逻辑和 Web 服务器放在同一个进程内演示,因此暂不安装。
项目结构如下:
ai-agent-handoff-demo/ ├── package.json ├── db.json # lowdb 自动生成的数据库文件 ├── server.js # Express Web 服务器(含人工任务接口) ├── agent.js # AI 代理模拟逻辑 └── handoff.js # 核心的 handoff (await human) 实现模块2.2 初始化数据库模型
在项目根目录创建server.js,首先初始化 Express 和 Lowdb。
// server.js const express = require('express'); const low = require('lowdb'); const FileSync = require('lowdb/adapters/FileSync'); const { v4: uuidv4 } = require('uuid'); const adapter = new FileSync('db.json'); const db = low(adapter); // 初始化数据库结构 db.defaults({ handoffTasks: [], agentSessions: [] }).write(); const app = express(); app.use(express.json()); // 用于解析 JSON 请求体 const PORT = 3000;我们定义了两个数据集合:
handoffTasks: 存储等待人工处理的任务。每个任务包含id,type,status,parameters,result等字段。agentSessions: 存储被挂起的代理会话状态。每个会话包含taskId(关联 handoffTasks),agentState(代理序列化状态)等。这是一个简化模型,真实场景的agentState可能很复杂。
3. 实现核心的handoff(await human) 函数
这是连接 AI 代理和人类接口的桥梁。它需要完成:创建任务、保存代理上下文、等待人类处理、返回处理结果。
创建handoff.js:
// handoff.js const { v4: uuidv4 } = require('uuid'); // 假设 db 对象可以通过某种方式共享,这里我们通过函数参数传入 // 在实际项目中,你可能使用一个全局的数据库连接或依赖注入。 /** * 模拟 await human() 的核心函数 * @param {Object} taskDescription - 任务描述,用于人类界面展示 * @param {Object} db - 数据库实例 * @returns {Promise<Object>} - 返回人类处理的结果 */ async function handoff(taskDescription, db) { const taskId = uuidv4(); console.log(`[Agent] 遇到需人工处理节点,创建任务: ${taskId}`); // 1. 创建任务记录,状态为 pending const task = { id: taskId, type: taskDescription.type || 'approval', status: 'pending', title: taskDescription.title, description: taskDescription.description, parameters: taskDescription.parameters || {}, // 任务所需参数,如报销金额、申请人 createdAt: new Date().toISOString(), result: null // 人类处理结果 }; db.get('handoffTasks').push(task).write(); // 2. 在实际场景中,这里会保存当前代理的完整状态 (agentState) 到 db.agentSessions // 并与 taskId 关联。本例中我们简化,不保存复杂状态,仅用轮询模拟等待。 // const agentState = { /* 序列化的代理变量、堆栈等信息 */ }; // db.get('agentSessions').push({ taskId, agentState }).write(); // 3. 模拟代理挂起,轮询等待任务状态变为 completed 或 rejected let result = await waitForHumanDecision(taskId, db); console.log(`[Agent] 任务 ${taskId} 处理完成,结果:`, result); return result; } /** * 轮询函数,等待人类决策 */ function waitForHumanDecision(taskId, db, interval = 2000) { return new Promise((resolve) => { const checkInterval = setInterval(() => { const task = db.get('handoffTasks').find({ id: taskId }).value(); if (task && task.status !== 'pending') { clearInterval(checkInterval); resolve(task.result); // 返回人类操作的结果 } }, interval); }); } module.exports = { handoff };这个handoff函数模拟了await human()的行为:
- 接收任务描述,创建唯一 ID 和数据库记录。
- 将任务状态设为
pending(等待中)。 - 进入一个轮询循环,定期检查数据库中该任务的状态。
- 一旦发现状态不再是
pending(即人类已处理),就返回处理结果 (task.result),从而让外层的await操作完成,代理继续执行。
注意:轮询(Polling)在简单 demo 中可行,但在生产环境会浪费资源。更好的方式是使用 Webhook 或消息队列(如 Redis Pub/Sub、RabbitMQ)。当人类提交处理结果时,系统主动通知正在等待的代理进程或触发一个新的进程加载状态并恢复执行。
4. 构建模拟的 AI 代理流程
现在,我们创建一个模拟的 AI 代理,它处理报销申请,并在金额超过一定阈值时“移交”给人类审批。
创建agent.js:
// agent.js const { handoff } = require('./handoff.js'); // 注意:这里需要获取 db 实例。为了演示,我们假设通过某种方式共享。 // 更优雅的方式是将 db 作为参数传递给 agent 的主函数。 const low = require('lowdb'); const FileSync = require('lowdb/adapters/FileSync'); const adapter = new FileSync('db.json'); const db = low(adapter); /** * 模拟AI代理处理报销单的主函数 * @param {Object} expenseReport - 报销单对象 */ async function processExpenseReport(expenseReport) { console.log(`\n=== AI代理开始处理报销单 [ID: ${expenseReport.id}] ===`); console.log(`申请人: ${expenseReport.employeeName}, 金额: ${expenseReport.amount}, 类别: ${expenseReport.category}`); // 步骤1: 基础规则校验 (AI自动处理) if (!expenseReport.employeeName || expenseReport.amount <= 0) { console.log('[Agent] 基础校验失败,流程终止。'); return { success: false, reason: 'Invalid report data' }; } // 步骤2: 检查金额阈值,决定是否需要人工审批 const AUTO_APPROVAL_LIMIT = 5000; // 自动审批上限 5000 元 if (expenseReport.amount > AUTO_APPROVAL_LIMIT) { console.log(`[Agent] 报销金额 ${expenseReport.amount} 超过自动审批限额 ${AUTO_APPROVAL_LIMIT},需要人工审批。`); // 关键点:这里执行 handoff,相当于 await human() const humanDecision = await handoff( { type: 'expense_approval', title: `报销审批 - ${expenseReport.employeeName}`, description: `报销金额 ${expenseReport.amount} 元,类别:${expenseReport.category},事由:${expenseReport.description}`, parameters: { reportId: expenseReport.id, amount: expenseReport.amount, employee: expenseReport.employeeName } }, db // 传入数据库实例 ); // 程序执行流在此挂起,直到 handoff 函数返回(即人类处理完毕) console.log(`[Agent] 收到人工审批结果:`, humanDecision); if (humanDecision.approved !== true) { console.log(`[Agent] 报销单被拒绝。理由: ${humanDecision.comment || '无'}`); return { success: false, reason: 'Rejected by manager', comment: humanDecision.comment }; } console.log(`[Agent] 报销单已获批准。`); } else { console.log(`[Agent] 报销金额在限额内,自动批准。`); } // 步骤3: 后续自动化处理 (例如,生成凭证、通知财务等) console.log(`[Agent] 执行后续财务系统集成...`); // 模拟一些处理时间 await new Promise(resolve => setTimeout(resolve, 1000)); console.log(`[Agent] 报销流程全部完成。`); return { success: true, reportId: expenseReport.id }; } // 模拟启动代理处理 (async () => { // 模拟两份报销单 const reports = [ { id: 'EXP-001', employeeName: '张三', amount: 1200, category: '差旅', description: '上海出差交通费' }, { id: 'EXP-002', employeeName: '李四', amount: 8500, category: '设备采购', description: '购买开发笔记本电脑' } ]; for (const report of reports) { const result = await processExpenseReport(report); console.log(`处理结果:`, result); console.log('---\n'); } // 代理“主循环”可能在此等待或结束。在实际系统中,代理可能是一个常驻服务。 })();这个代理模拟了完整的业务流程:
- 接收报销单。
- 进行基础校验(AI自动完成)。
- 判断金额是否超过阈值(5000元)。
- 如果超过,则调用
await handoff(...),创建人工审批任务,并等待。 - 收到人工审批结果后,根据结果决定是继续后续流程还是终止。
- 如果未超过阈值,则自动完成后续流程。
运行node agent.js,你会看到对于 8500 元的报销单,代理会打印出需要人工审批并创建任务,然后卡住,因为它在轮询等待任务状态变化。此时,就需要我们的人类接口来改变任务状态。
5. 创建人工任务 Web 接口
我们需要一个简单的 Web 界面,让“经理”能看到待审批的任务列表,并可以审批或拒绝。我们在server.js中增加 API 路由。
继续编辑server.js,在初始化代码后添加:
// server.js (续) // API: 获取所有待处理 (pending) 的任务 app.get('/api/tasks/pending', (req, res) => { const tasks = db.get('handoffTasks').filter({ status: 'pending' }).value(); res.json(tasks); }); // API: 获取单个任务详情 app.get('/api/tasks/:id', (req, res) => { const task = db.get('handoffTasks').find({ id: req.params.id }).value(); if (task) { res.json(task); } else { res.status(404).json({ error: 'Task not found' }); } }); // API: 处理任务(批准或拒绝) app.post('/api/tasks/:id/process', (req, res) => { const { action, comment } = req.body; // action: 'approve' or 'reject' const taskId = req.params.id; let task = db.get('handoffTasks').find({ id: taskId }).value(); if (!task) { return res.status(404).json({ error: 'Task not found' }); } if (task.status !== 'pending') { return res.status(400).json({ error: 'Task already processed' }); } const updateData = { status: action === 'approve' ? 'completed' : 'rejected', processedAt: new Date().toISOString(), result: { approved: action === 'approve', comment: comment || '', processedBy: 'Manager' // 实际应从登录会话获取 } }; db.get('handoffTasks').find({ id: taskId }).assign(updateData).write(); // 关键步骤:在实际系统中,这里需要触发等待中的代理恢复执行。 // 例如,向一个消息队列发送事件,或者调用一个预定义的 Webhook。 console.log(`[Server] 任务 ${taskId} 已被处理,状态更新为: ${updateData.status}`); // 本例中,代理在轮询,状态更新后轮询函数会检测到。 res.json({ success: true, taskId, ...updateData }); }); // 启动服务器 app.listen(PORT, () => { console.log(`人工任务接口服务器运行在 http://localhost:${PORT}`); console.log(`待办任务列表: http://localhost:${PORT}/api/tasks/pending`); });同时,我们还需要一个极简的 HTML 页面来展示任务和进行操作。在server.js同一目录下创建public文件夹,并在其中创建index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>AI Agent 人工任务处理中心</title> <style> body { font-family: sans-serif; margin: 2rem; } .task { border: 1px solid #ccc; padding: 1rem; margin-bottom: 1rem; border-radius: 5px; } .pending { background-color: #fff3cd; } .completed { background-color: #d4edda; } .rejected { background-color: #f8d7da; } button { margin-right: 0.5rem; } textarea { width: 100%; margin-top: 0.5rem; } </style> </head> <body> <h1>待处理的人工任务</h1> <div id="taskList">加载中...</div> <script> async function loadTasks() { const resp = await fetch('/api/tasks/pending'); const tasks = await resp.json(); const container = document.getElementById('taskList'); if (tasks.length === 0) { container.innerHTML = '<p>暂无待处理任务。</p>'; return; } container.innerHTML = tasks.map(task => ` <div class="task pending" id="task-${task.id}"> <h3>${task.title}</h3> <p><strong>描述:</strong> ${task.description}</p> <p><strong>参数:</strong> ${JSON.stringify(task.parameters)}</p> <p><strong>创建时间:</strong> ${new Date(task.createdAt).toLocaleString()}</p> <div> <button onclick="processTask('${task.id}', 'approve')">批准</button> <button onclick="processTask('${task.id}', 'reject')">拒绝</button> <textarea id="comment-${task.id}" placeholder="处理意见(可选)"></textarea> </div> </div> `).join(''); } async function processTask(taskId, action) { const comment = document.getElementById(`comment-${taskId}`).value; const resp = await fetch(`/api/tasks/${taskId}/process`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, comment }) }); const result = await resp.json(); if (result.success) { alert(`任务 ${taskId} 处理成功!`); loadTasks(); // 重新加载列表 } else { alert(`处理失败: ${result.error}`); } } // 每10秒自动刷新一次列表 loadTasks(); setInterval(loadTasks, 10000); </script> </body> </html>为了让 Express 能提供这个 HTML 页面,需要在server.js开头添加静态文件中间件:
// server.js 开头部分添加 const express = require('express'); const app = express(); app.use(express.json()); app.use(express.static('public')); // 新增这一行,提供静态文件服务 const PORT = 3000;现在,完整的系统组件都已就绪。
6. 运行验证与结果分析
让我们启动系统并观察整个await human()流程是如何工作的。
6.1 启动服务
打开一个终端,启动 Web 服务器(人工任务接口):
node server.js你会看到输出:人工任务接口服务器运行在 http://localhost:3000。
6.2 启动 AI 代理
打开另一个终端,运行我们的模拟 AI 代理:
node agent.js输出将类似于:
=== AI代理开始处理报销单 [ID: EXP-001] === 申请人: 张三, 金额: 1200, 类别: 差旅 [Agent] 报销金额在限额内,自动批准。 [Agent] 执行后续财务系统集成... [Agent] 报销流程全部完成。 处理结果: { success: true, reportId: 'EXP-001' } --- === AI代理开始处理报销单 [ID: EXP-002] === 申请人: 李四, 金额: 8500, 类别: 设备采购 [Agent] 报销金额 8500 超过自动审批限额 5000,需要人工审批。 [Agent] 遇到需人工处理节点,创建任务: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx代理会在处理第二张报销单(8500元)时停住,因为它执行到了await handoff(...),进入了轮询等待状态。同时,数据库 (db.json) 中会新增一条状态为pending的任务记录。
6.3 人类介入处理
- 在浏览器中打开
http://localhost:3000(因为我们把index.html放在了public目录,Express 会自动提供它)。 - 页面会通过调用
/api/tasks/pending接口,列出所有待处理任务。你应该能看到李四的报销审批任务。 - 点击“批准”或“拒绝”按钮,并可以填写处理意见。
- 点击按钮后,浏览器会调用
/api/tasks/:id/processAPI 来更新任务状态。
6.4 观察代理恢复执行
当你点击“批准”后,观察运行agent.js的终端。大约 2 秒内(轮询间隔),你会看到类似输出:
[Agent] 任务 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 处理完成,结果: { approved: true, comment: '', processedBy: 'Manager' } [Agent] 收到人工审批结果: { approved: true, comment: '', processedBy: 'Manager' } [Agent] 报销单已获批准。 [Agent] 执行后续财务系统集成... [Agent] 报销流程全部完成。 处理结果: { success: true, reportId: 'EXP-002' } ---代理成功接收到了人类审批的结果(approved: true),并从await handoff(...)之后继续执行,完成了整个报销流程。
如果你点击的是“拒绝”,代理则会收到approved: false,并据此终止流程,返回拒绝结果。
至此,一个完整的await human()模式演示完成。AI 代理的流程在需要时被优雅地中断,人类通过专用接口介入,操作完成后代理自动恢复,仿佛只是调用了一个耗时较长的 API。
7. 常见问题排查与优化方向
在实际项目中使用这种模式,会遇到比演示更复杂的情况。以下是常见问题及解决思路。
7.1 常见问题排查表
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
代理在handoff后无限等待,人类操作后无反应。 | 1. 轮询间隔太长或轮询逻辑出错。 2. 任务状态更新后,未正确保存到数据库或保存的字段名不一致。 3. 代理进程崩溃,状态丢失。 | 1. 检查waitForHumanDecision函数的轮询逻辑和数据库查询条件。2. 查看 db.json文件,确认对应任务记录的status和result字段是否已更新。3. 查看代理进程日志是否有异常退出。 | 1. 缩短轮询间隔用于调试,或改用 Webhook/消息通知机制。 2. 确保更新数据库的代码路径正确,并使用数据库事务保证一致性。 3. 实现代理状态的持久化快照,并在进程重启后能恢复。 |
| 人类处理页面看不到待办任务。 | 1. API 路由错误或服务器未运行。 2. 数据库查询条件错误(如过滤了 status)。3. 前端页面 JS 报错,无法调用 API。 | 1. 直接访问http://localhost:3000/api/tasks/pending看是否返回 JSON 数据。2. 检查服务器控制台有无请求日志和错误。 3. 打开浏览器开发者工具,查看网络请求和 Console 标签页。 | 1. 确保服务器端口正确,且 API 路径与前端代码匹配。 2. 核对数据库集合名称和字段名。 3. 修复前端 JS 错误,或提供更友好的错误提示。 |
| 代理恢复后,上下文变量丢失或错误。 | 代理状态序列化/反序列化不完整或错误。 | 检查保存agentState的逻辑,确保包含了所有必要的执行上下文(如局部变量、循环索引、LLM 的对话历史等)。 | 使用框架提供的状态管理工具(如 LangChain 的save_context)。对于自定义代理,设计一个轻量级的、包含必要信息的状态对象,避免序列化整个运行时。 |
| 多个人同时处理同一任务,导致状态混乱。 | 缺乏并发控制。 | 观察数据库,同一任务是否被多次更新。 | 在任务处理 API 中加入乐观锁或悲观锁。例如,在更新前检查status是否为pending,并使用数据库的原子操作(如findOneAndUpdate)来确保只有第一个请求能成功。 |
7.2 生产环境优化方向
演示中的轮询和内存状态简化模型不适合生产。以下是升级建议:
状态持久化与恢复:
- 使用专门的持久化存储(如 PostgreSQL, MongoDB)。
- 设计健壮的状态序列化方案。对于 LangChain Agent,可以利用其
save_context和load_context方法。 - 考虑使用工作流引擎(如 Temporal、Camunda)来管理有状态的长周期流程,它们内置了暂停、恢复和持久化能力。
通信机制:
- 替代轮询:使用 Webhook。当代理挂起时,向一个回调 URL 注册自己。人类处理完成后,系统调用该 URL 通知代理恢复。
- 使用消息队列:代理将任务发布到队列后,订阅一个以
taskId命名的主题。人类处理完成后,系统向该主题发布结果消息。 - Serverless 场景:代理函数在
handoff处直接结束,并将状态存入数据库。人类处理完成后,触发一个新的函数执行,加载状态并继续。
人工接口增强:
- 集成到现有的 OA、CRM 或工单系统。
- 支持更丰富的操作类型:填写表单、上传附件、选择选项等。
- 增加任务优先级、截止时间、分配规则和通知(邮件、钉钉、企微)。
安全与权限:
- 任务接口需要身份认证和授权,确保只有有权限的人能处理。
- 对传入
handoff的任务参数和返回的结果进行验证和清理。 - 记录所有人工操作的操作日志,便于审计。
超时与异常处理:
- 为
handoff设置超时时间(例如24小时)。超时后,任务状态可自动更新为“超时”,代理根据超时结果执行预设的备选逻辑(如升级、默认拒绝等)。 - 处理人类接口服务不可用的情况,需要有重试或降级策略。
- 为
8. 最佳实践与扩展场景
8.1 实现await human()的最佳实践
- 明确任务边界:
handoff点应该对应一个清晰、原子化的人类操作。避免将一个复杂的、多步骤的人类流程塞进一个handoff调用。 - 提供充足上下文:传递给人类接口的
taskDescription应包含决策所需的所有信息,避免人类再去其他系统查询。 - 设计幂等的恢复操作:代理恢复执行后,其后续操作应该是幂等的,防止因重复恢复导致重复执行(如重复打款)。
- 版本化状态结构:随着代理逻辑迭代,其状态结构可能变化。在序列化状态时加入版本号,以便在恢复时进行兼容性处理。
- 日志与可观测性:在
handoff创建、人类处理、代理恢复等关键节点记录详细日志,并集成到监控系统,便于追踪流程卡点。
8.2 扩展场景举例
- 复杂审批流:不止一次
handoff。例如,代理可以根据规则决定需要“直属领导审批”还是“财务审批”,创建不同类型的任务。甚至可以实现多级审批,上一级批准后自动创建下一级任务。 - 人机协作标注:AI 代理初步处理数据(如分类、摘要),将低置信度的结果通过
handoff交给人类复核和修正,修正后的数据可以反馈给 AI 模型进行学习。 - 客服升级:聊天机器人遇到无法解决的问题时,
handoff给人工客服,并将会话历史、用户信息、问题分析摘要一并传递。 - 异常处理兜底:代理在执行自动化脚本或调用外部 API 失败时,将错误信息和上下文
handoff给运维人员,由人工决定重试、跳过还是修复。
await human()模式的核心价值在于将人类智能无缝地编织到自动化流程中,作为 AI 代理能力的一种自然延伸。通过本文的讲解和实现,你应该已经掌握了其基本原理和实现方法。在实际项目中,关键在于根据你的技术栈和业务需求,选择合适的持久化、通信和状态管理方案,从而构建出稳定、高效且易于维护的人机协同系统。
