Spring AI Alibaba实战:基于Hook机制实现Human-in-the-Loop人工审核
1. 项目缘起:当AI决策需要一双“人眼”
最近在折腾一个基于大模型的智能客服项目,用上了Spring AI Alibaba这套框架。框架本身很强大,把模型调用、上下文管理、工具调用这些脏活累活都封装好了,开发效率确实高。但跑着跑着,问题就来了:AI它毕竟不是人,在一些关键业务节点上,它的判断可能会“飘”,或者给出一些看似合理但实则风险很高的建议。比如,用户询问一个非常具体的、涉及内部未公开政策的退款规则,模型基于训练数据“推测”了一个答案,但这个答案可能与公司最新规定有出入。直接把这个答案给用户,轻则造成客诉,重则引发合规风险。
这就是典型的AI应用“最后一公里”问题:模型输出到最终用户动作之间,缺少一个可靠的安全阀。我们需要一种机制,在AI给出关键决策或生成重要内容时,能够暂停一下,让真人(比如业务专家、审核员)看一眼,确认无误后再放行,或者由人给出更准确的指令。这种模式,在学术和工业界被称为“Human-in-the-Loop”(人机回环,简称HITL)。而Spring AI Alibaba框架,通过其强大的Hook(钩子)机制,为我们实现这种“人工介入”提供了极其优雅和灵活的解决方案。这不是简单的“审核后台上加个按钮”,而是将人的判断深度嵌入到AI智能体的推理和工作流执行链路中,让AI的“自主”与人的“掌控”达到平衡。
2. 理解Spring AI Alibaba的Hook机制:AI流程的“检查点”
在深入实战之前,必须把Spring AI Alibaba中Hook的概念吃透。你可以把它理解成高速公路上的“收费站”或“检查站”。AI智能体(Agent)的执行流程就像一辆车在预设的路线上行驶,而Hook就是在这条路线上预先设置好的点位,当流程执行到这些点位时,会主动“钩住”流程,执行我们预设的代码。
Spring AI Alibaba的Hook体系是事件驱动型的,它定义了智能体生命周期中的关键事件。对于我们实现人工介入,最核心的是以下几个Hook:
AgentActionHook: 当智能体决定要调用一个工具(Tool)时触发。这是介入的“黄金点位”。比如,智能体判断需要调用“创建工单”工具,在真正调用前,我们可以通过Hook拦截,将工具调用的参数(用户问题、AI分析结论)展示给人审核,由人决定是否执行、或修改参数后再执行。AgentFinishHook: 当智能体即将结束并返回最终答案给用户时触发。这是最后一道防线。我们可以在这里对AI生成的最终答复进行内容安全、事实准确性或话术规范的复核。AgentPromptHook: 在构建发送给大模型的提示词(Prompt)前后触发。可以用于动态注入上下文信息,或根据人工之前的判断调整提示策略。ObservationHook: 在工具执行完成,返回观察结果(Observation)给智能体后触发。可以用于对工具执行的结果进行校验或加工。
为什么是Hook而不是简单地在业务代码里写if-else?因为Hook是框架层面的、声明式的介入方式。它将控制逻辑与业务逻辑解耦,你不需要修改智能体本身的核心代码,只需要像插件一样注册你的Hook实现。这使得系统更容易维护、扩展,并且能保证介入行为的一致性。例如,你可以为“财务相关工具”和“客服话术生成”配置不同的人工介入策略,而这些策略的管理都在统一的Hook配置中完成。
3. 实战蓝图:设计一个可运营的人工介入工作流
光有Hook还不够,我们需要一套能跑通的、可运营的流程。核心思路是:拦截 -> 挂起 -> 人工处理 -> 恢复。下面我以一个“智能订单处理助手”为例,拆解整个设计。
场景:用户对话:“我想申请订单123456的退款,因为商品破损。” AI智能体经过分析,需要调用create_refund_ticket(创建退款工单)工具。
目标:在调用此工具前,需要客服主管审核退款原因和金额。
系统组件设计:
- 介入决策器(Hook实现):这是一个Spring Bean,实现了
AgentActionHook接口。它的beforeAction方法会判断当前要执行的工具是否需要人工审核。判断依据可以配置化,例如工具名称、工具参数中的金额阈值、用户风险等级等。 - 任务挂起与存储:一旦决定需要介入,当前AI智能体的执行上下文(
AgentContext)必须被完整地保存下来,包括对话历史、当前状态、待执行的动作参数等。同时,生成一个唯一的“人工任务ID”,将流程挂起。这里需要引入一个持久化存储,比如Redis或数据库,用于保存这些挂起的上下文。 - 人工任务管理后台:这是一个独立的Web界面或集成到现有客服后台的模块。它列出所有待处理的人工任务,展示任务详情(如用户问题、AI分析、待执行工具及参数)。审核员可以在此界面进行“通过”、“拒绝”或“修改参数后通过”的操作。
- 流程恢复器:当人工在后台完成操作后,系统需要根据“人工任务ID”找到挂起的上下文,注入人工决策的结果(新的工具参数或直接的结果),然后唤醒对应的智能体,让其从挂起点继续执行。
这个流程的关键在于AgentContext的序列化与反序列化。Spring AI Alibaba的AgentContext包含了智能体的记忆、当前计划等状态,必须确保其被完整保存和恢复,否则智能体“失忆”,流程就无法继续。
踩坑提示:
AgentContext默认可能包含一些不可序列化的对象(如某些复杂的PromptTemplate)。在实现上下文存储时,你可能需要自定义一个AgentContextSerializer,或者只保存其中关键、可序列化的部分(如conversationId,memoryId,currentPlan的文本表示),在恢复时再根据这些信息重建上下文。这是一个需要仔细处理的细节。
4. 核心代码实现:从拦截到恢复的完整链路
接下来,我们进入代码环节。我会省略掉项目搭建、依赖引入(主要是spring-ai-alibaba相关starter)等基础步骤,聚焦于人工介入的核心代码。
4.1 实现人工介入决策Hook
首先,我们实现一个AgentActionHook,它负责在工具执行前进行拦截和决策。
@Component @Slf4j public class HumanReviewActionHook implements AgentActionHook { @Autowired private HumanTaskService humanTaskService; // 人工任务管理服务 @Autowired private ApplicationEventPublisher eventPublisher; @Override public AgentAction beforeAction(AgentAction agentAction, AgentContext context) { String toolName = agentAction.toolName(); Map<String, Object> toolInput = agentAction.toolInput(); // 1. 判断是否需要人工介入:这里可以根据工具名、参数等规则判断 if (needHumanReview(toolName, toolInput)) { log.info("工具 [{}] 执行需人工审核,挂起流程。", toolName); // 2. 创建人工审核任务 HumanReviewTask task = new HumanReviewTask(); task.setTaskId(UUID.randomUUID().toString()); task.setAgentContext(context); // 注意:这里需要处理上下文序列化 task.setActionName(toolName); task.setActionInput(toolInput); task.setStatus(TaskStatus.PENDING); task.setCreateTime(LocalDateTime.now()); // 3. 保存任务,挂起流程 humanTaskService.saveTask(task); // 4. 发布事件,通知前端有新的待审核任务(可选,可通过WebSocket) eventPublisher.publishEvent(new NewHumanTaskEvent(this, task.getTaskId())); // 5. 抛出特定异常,中断智能体当前执行链 throw new HumanInterventionRequiredException("等待人工审核", task.getTaskId()); } // 不需要介入,直接返回原action return agentAction; } private boolean needHumanReview(String toolName, Map<String, Object> input) { // 示例规则:所有涉及“退款”、“赔偿”且金额大于500的工具调用都需要审核 if (toolName.contains("refund") || toolName.contains("compensate")) { Object amountObj = input.get("amount"); if (amountObj instanceof Number) { return ((Number) amountObj).doubleValue() > 500.0; } } // 可以配置更复杂的规则,或从数据库/配置中心读取 return false; } @Override public AgentAction afterAction(AgentAction agentAction, AgentContext context, Observation observation) { // 工具执行后的Hook,这里我们不需要在after做介入,直接返回 return agentAction; } }关键点解析:
beforeAction方法在工具执行前被调用。我们在这里做判断。HumanInterventionRequiredException是一个自定义的运行时异常。它的作用是优雅地中断本次智能体的调用。智能体的执行框架会捕获这个异常,并将其作为本次调用的最终结果返回(通常是错误信息),而不会继续执行工具。这比让工具执行失败更可控。AgentContext的保存:直接保存context对象可能有问题。更稳健的做法是,在HumanTaskService.saveTask方法内部,调用一个ContextSerializer将context转换为可存储的字符串(如JSON),恢复时再反序列化。
4.2 构建人工任务处理后台与恢复接口
人工任务后台是一个相对独立的系统,这里我们只关注其与AI流程恢复相关的核心接口。
首先,是HumanTaskService的服务层,负责任务持久化。我们使用一个简单的Map模拟,生产环境请替换为数据库。
@Service public class HumanTaskService { private final Map<String, HumanReviewTask> taskStore = new ConcurrentHashMap<>(); public void saveTask(HumanReviewTask task) { // 序列化AgentContext String serializedContext = AgentContextSerializer.serialize(task.getAgentContext()); task.setSerializedContext(serializedContext); task.setAgentContext(null); // 清空原始对象,避免内存泄漏和序列化问题 taskStore.put(task.getTaskId(), task); } public HumanReviewTask getTask(String taskId) { return taskStore.get(taskId); } public void updateTask(String taskId, HumanReviewTask task) { taskStore.put(taskId, task); } }然后,提供一个REST API供人工审核后台调用,用于提交审核结果。
@RestController @RequestMapping("/api/human-review") public class HumanReviewController { @Autowired private HumanTaskService taskService; @Autowired private AgentExecutionResumer executionResumer; // 流程恢复器 @PostMapping("/{taskId}/resolve") public ApiResponse resolveTask(@PathVariable String taskId, @RequestBody TaskResolution resolution) { HumanReviewTask task = taskService.getTask(taskId); if (task == null || !TaskStatus.PENDING.equals(task.getStatus())) { return ApiResponse.error("任务不存在或已处理"); } // 更新任务状态 task.setStatus(TaskStatus.valueOf(resolution.getStatus())); task.setReviewComment(resolution.getComment()); task.setReviewer(resolution.getReviewer()); task.setReviewTime(LocalDateTime.now()); if ("APPROVED".equals(resolution.getStatus())) { // 审核通过,恢复AI流程 try { // 1. 反序列化上下文 AgentContext restoredContext = AgentContextSerializer.deserialize(task.getSerializedContext()); // 2. 获取人工决策后的新参数(可能被审核员修改过) Map<String, Object> newActionInput = resolution.getApprovedInput() != null ? resolution.getApprovedInput() : task.getActionInput(); // 3. 调用恢复器 Object result = executionResumer.resumeWithNewAction(restoredContext, task.getActionName(), newActionInput); task.setResumeResult(result); task.setStatus(TaskStatus.COMPLETED); return ApiResponse.success("任务已处理,AI流程已恢复", result); } catch (Exception e) { task.setStatus(TaskStatus.FAILED); log.error("恢复AI流程失败", e); return ApiResponse.error("恢复流程失败: " + e.getMessage()); } } else if ("REJECTED".equals(resolution.getStatus())) { // 审核拒绝,流程终止,可以记录原因并通知用户 task.setStatus(TaskStatus.REJECTED); // 可以触发一个通知,或者让AI重新生成回复 return ApiResponse.success("任务已拒绝,流程终止"); } taskService.updateTask(taskId, task); return ApiResponse.success("任务状态已更新"); } }4.3 实现流程恢复器(AgentExecutionResumer)
这是最核心也最棘手的一环。Spring AI Alibaba的智能体执行通常是请求-响应式的,如何让一个“死去”的上下文重新“活”过来,并继续执行?
我们的策略是:重新创建一个智能体调用,但为其注入之前保存的上下文和人工决策的结果。这里需要深入框架内部,理解其执行机制。
@Component public class AgentExecutionResumer { @Autowired private ApplicationContext applicationContext; @Autowired private AgentMemoryRepository memoryRepository; // 假设有记忆存储仓库 public Object resumeWithNewAction(AgentContext previousContext, String actionName, Map<String, Object> actionInput) { // 1. 恢复记忆(关键步骤) // AgentContext中通常包含memoryId,我们需要确保之前的记忆被加载到新的会话中 String memoryId = previousContext.memoryId(); AgentMemory memory = memoryRepository.findById(memoryId).orElse(null); if (memory == null) { // 如果记忆丢失,可能需要根据serializedContext中的对话历史重建 memory = reconstructMemoryFromContext(previousContext); } // 2. 获取或创建新的Agent实例 // 这里假设你的Agent是通过@Bean定义的,并且可以通过ConversationId来关联上下文 Agent agent = applicationContext.getBean(Agent.class); // 获取你的主Agent Bean // 3. 构建一个“伪”的AgentAction,模拟Hook之前的那个动作,但使用人工审核后的参数 AgentAction newAction = new AgentAction(actionName, actionInput); // 4. 关键:手动执行工具调用,并处理结果 // 这里需要获取到Tool的实例。Spring AI Alibaba通常将Tool注册为Bean。 Tool tool = applicationContext.getBeansOfType(Tool.class) .values() .stream() .filter(t -> t.getName().equals(actionName)) .findFirst() .orElseThrow(() -> new RuntimeException("Tool not found: " + actionName)); // 5. 执行工具 Observation observation = tool.call(newAction.toolInput()); // 6. 将工具执行的结果作为新的“用户输入”或“观察”,再次调用Agent,让它基于这个结果继续推理 // 我们需要构造一个新的“用户消息”,其内容是工具执行的结果摘要。 String observationSummary = observation.toString(); // 这里需要根据你的Observation结构来提取关键信息 UserMessage userMessage = new UserMessage("工具执行结果: " + observationSummary); // 7. 将之前的对话历史和新的用户消息(工具结果)一起,作为新的输入传递给Agent // 这里简化处理,实际你需要将previousContext中的消息历史取出,加上新的userMessage,构造新的Prompt Prompt prompt = constructPromptWithHistoryAndNewObservation(previousContext, userMessage); // 8. 调用Agent,获取最终回复 AgentResponse response = agent.call(prompt, memory); // 注意传入恢复的memory return response.output(); } // 以下为辅助方法,需要根据你的具体项目结构实现 private AgentMemory reconstructMemoryFromContext(AgentContext context) { ... } private Prompt constructPromptWithHistoryAndNewObservation(AgentContext context, UserMessage newMessage) { ... } }深度解析与踩坑点:
- 记忆的连续性:这是恢复流程最大的挑战。
AgentMemory存储了对话的历史。你必须确保恢复后的智能体“记得”之前发生的事情。AgentContextSerializer在序列化时,必须把memoryId或完整的记忆内容保存下来。 - 工具执行与Agent调用的衔接:我们的流程是:AI决定调用工具 -> 被人拦截 -> 人审核通过 -> 执行工具 -> 将工具结果反馈给AI继续思考。在恢复时,我们手动执行了工具(第5步),但之后需要巧妙地“骗过”AI,让它认为这个工具结果是正常流程中产生的。方法就是构造一个包含工具结果的新
UserMessage,并连同历史一起喂给AI(第6、7步)。 - 状态丢失:有些智能体(如使用
ReAct模式的)内部会有Plan、Step等状态。简单的AgentContext序列化可能无法完全保存这些内部状态。对于复杂的智能体,你可能需要自定义一个更强大的Agent子类,重写其执行逻辑,使其支持从某个检查点(Checkpoint)恢复。这属于进阶用法,需要对框架源码有较深理解。
实操心得:在项目初期,如果智能体逻辑不复杂,可以采取一种“简化恢复”策略:不追求精确恢复到挂起时的内部状态,而是在人工审核后,重新开启一个全新的智能体会话,但将之前用户的问题、AI的分析结论、人工审核后的决策,作为系统提示词(
System Prompt)的一部分注入。然后直接让这个新的智能体去执行审核后的动作。这样避免了复杂的上下文序列化与恢复,牺牲了一点连贯性,但大大降低了实现复杂度。你可以根据业务对连贯性的要求来选择方案。
5. 前端交互与用户体验设计
人工介入不能只靠后端,必须有一个高效、清晰的前端界面让审核员操作。这部分虽然偏前端,但作为全栈开发者,必须考虑整体体验。
人工任务列表页:
- 以表格形式展示待处理任务,列包括:任务ID、创建时间、用户问题(摘要)、待执行工具、状态、紧急程度(可根据规则自动标注)。
- 提供筛选和排序功能(按时间、按工具类型、按金额等)。
任务详情与处理页:
- 信息面板:
- 原始用户输入:完整展示用户的提问。
- AI分析与决策:以清晰的方式展示AI的思考链(如果框架支持并暴露了
Chain of Thought)。例如:“用户意图:申请退款 -> 原因:商品破损 -> 需要调用的工具:create_refund_ticket -> 工具参数:{orderId: ‘123456’, amount: 650.00, reason: ‘商品破损’}”。 - 上下文对话历史:展示本次会话中之前的所有对话轮次,帮助审核员理解来龙去脉。
- 操作面板:
- 通过:点击后,工具参数表单可编辑,审核员可以微调金额、原因描述等字段,然后提交。
- 拒绝:点击后,需填写拒绝理由(下拉选择或文本框)。提交后,该任务关闭,并可选择是否让AI重新生成回复(例如,告知用户“退款申请需补充材料”)。
- 转交/加签:复杂任务可以转给其他专家或需要多人会签。
- 设计原则:
- 信息降噪:高亮关键信息(如金额、订单号、工具名)。
- 操作便捷:常用操作(通过、拒绝)按钮突出,减少操作步骤。
- 实时性:利用WebSocket或长轮询,当有新任务或任务状态变更时,列表页能实时刷新,避免审核员手动刷新。
6. 进阶:动态介入策略与效果度量
基本的“审核后执行”模式跑通后,我们可以思考更智能的介入策略和如何评估其效果。
动态介入策略:
- 基于置信度:如果大模型在调用工具时,能输出一个置信度分数(某些模型或框架支持),我们可以设置阈值。低于阈值的操作自动进入人工审核队列。
- 基于业务规则引擎:将
needHumanReview的判断逻辑抽离出来,接入一个轻量级的规则引擎(如Drools, Easy Rules)。这样,业务人员可以在不重启服务的情况下,动态调整审核规则(如“促销期间,所有退款审核阈值上调至1000元”)。 - 基于用户画像:对于高风险用户(新用户、有投诉历史的用户),其所有敏感操作都默认走人工流程。
- 分级审核:不同等级的操作由不同权限的人员审核。小额退款由普通客服审核,大额或争议退款由主管审核。
效果度量与闭环优化: 人工介入不是终点,而是优化AI的起点。必须建立度量体系。
- 介入率:人工审核任务数 / AI工具调用总数。监控这个指标的变化,如果介入率突然升高,可能意味着模型效果下降或业务规则变更。
- 人工推翻率:审核员修改参数或拒绝执行的比例。这是衡量AI决策准确性的黄金指标。高推翻率说明AI在该类任务上不可靠,需要针对性优化(如补充示例、调整提示词、甚至微调模型)。
- 平均处理时间(AHT):从任务创建到被人工处理完成的时间。这直接影响用户体验。需要优化任务分配机制和审核界面效率。
- 问题归类:为审核员的“拒绝”操作打上标签(如“事实错误”、“逻辑不符”、“风险过高”)。定期分析这些标签,可以发现AI能力的短板,为后续的提示工程、数据清洗或模型迭代提供明确方向。
通过收集这些数据,我们就能形成一个“AI执行 -> 人工介入 -> 数据收集 -> 模型/策略优化 -> AI执行”的增强闭环。Human-in-the-Loop 最终目的不是永远依赖人,而是通过人的反馈,让AI变得越来越可靠,逐步减少不必要的介入,在保证安全的前提下提升自动化水平。
7. 避坑指南与最佳实践
在项目落地过程中,我总结了一些容易踩的坑和值得推荐的做法:
上下文序列化的陷阱:正如前面反复提到的,
AgentContext的完整序列化是个难题。最佳实践是,不要在Hook中保存完整的上下文对象。而是保存最小必要信息:conversationId,memoryId,userId, 以及当前AI的“计划”或“下一步”的文本化表示。在恢复时,利用conversationId和memoryId去记忆存储中重新加载对话历史,并基于保存的“计划”文本,通过Prompt工程的方式,引导新的智能体会话继续执行。这比强行序列化Java对象更可靠。Hook的注册顺序与作用范围:Spring AI Alibaba允许注册多个Hook,它们会按顺序执行。确保你的
HumanReviewActionHook在调用链中的位置是正确的。通常,它应该在执行工具的最外层。同时,可以通过条件注解(如@ConditionalOnProperty)或Profile来控制某些Hook只在特定环境生效。超时与熔断:人工审核流程可能很长。前端调用AI的接口需要有合理的超时设置,并在抛出
HumanInterventionRequiredException时返回一个友好的中间状态(如“您的问题已提交审核,请稍候”)。同时,后台的AI调用在被恢复时,也要设置超时,防止因人工任务过期或上下文错误导致线程长时间挂起。幂等性与重复执行:要防止人工重复点击“通过”导致工具被重复调用(如重复创建工单)。在
AgentExecutionResumer中,对于每个taskId,在执行恢复操作前检查状态,确保只执行一次。或者,在工具层面实现幂等性。安全与权限:人工审核后台是高风险操作界面。必须做好严格的权限控制(RBAC),确保只有授权人员才能处理对应类型的任务。所有审核操作必须记录详尽的审计日志(谁、在何时、处理了哪个任务、做了什么操作、理由是什么)。
用户体验的兜底:不是所有场景都适合让用户“等待审核”。对于实时性要求高的对话,可以设计这样的流程:AI告知用户“您的请求需要进一步核实,我们将在X分钟内通过电话/消息回复您”。然后异步走人工审核流程,审核完成后,通过其他渠道(如客服系统、短信)通知用户结果。这样既保证了安全,又不阻塞对话。
实现Human-in-the-Loop不是一个一蹴而就的特性,而是一个需要精心设计、持续迭代的子系统。它考验的不仅是你对Spring AI Alibaba框架的掌握程度,更是你对业务风险、用户体验和系统架构的综合权衡能力。当你看到AI在人的“护航”下,越来越稳定、可靠地处理核心业务时,这一切的复杂设计都是值得的。
