ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI Agent开发实战:实现人机协同的await human()编程范式

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 {PromiseObject} - 返回人类处理的结果 */ 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 langen head meta charsetUTF-8 titleAI 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 idtaskList加载中.../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 classtask pending idtask-${task.id} h3${task.title}/h3 pstrong描述:/strong ${task.description}/p pstrong参数:/strong ${JSON.stringify(task.parameters)}/p pstrong创建时间:/strong ${new Date(task.createdAt).toLocaleString()}/p div button onclickprocessTask(${task.id}, approve)批准/button button onclickprocessTask(${task.id}, reject)拒绝/button textarea idcomment-${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 代理能力的一种自然延伸。通过本文的讲解和实现你应该已经掌握了其基本原理和实现方法。在实际项目中关键在于根据你的技术栈和业务需求选择合适的持久化、通信和状态管理方案从而构建出稳定、高效且易于维护的人机协同系统。
返回列表