这次我们聊一个 AI Agent 实战里绕不开的问题:Agent 一遇到不懂的、不确定的、报错的情况,就直接把会话转给人工,这样到底行不行?
先说结论:在很多团队里,这个动作不是“兜底”,而是“甩锅”。用户感知是“这个 AI 像个摆设”,运营感知是“人工工单量根本没降下来”,技术感知是“Agent 好像做了,又好像没做”。真正合格的 Agent 实战,不是避免所有失败,而是建立一套“先自救、再降级、最后才转人工”的分层决策机制。
这篇文章会用一个具体的客服订单查询场景展开,讲清楚 AI Agent 为什么不能一遇到问题就转人工、在哪几个节点最容易误转、如何设计分级兜底策略、如何用代码实现自校验和降级链路,以及转人工时怎样做到“带着上下文转”而不是“干转”。
如果你正在做 AI Agent 落地,尤其是客服、售后、内部工单这类项目,这篇文章可以直接收藏。
1. 核心问题:转人工为什么成了 Agent 的“默认出口”
从实战看,Agent 转人工本身不是问题,问题是转得太早、太频繁、太无脑。常见的触发方式有三种:
- 意图识别置信度低于阈值,直接转人工。
- Tool 调用抛异常,直接转人工。
- 大模型回答内容为空或校验不通过,直接转人工。
这三种方式看起来都有道理,但实际效果往往很差。原因在于:大多数“失败”并不是真的无法处理,而是 Agent 只尝试了一条路径,没有尝试第二条路径。
举一个真实的处理逻辑对比。一个订单查询 Agent,用户问“我前天买的那个东西怎么还没到”。如果走“意图识别失败转人工”的路线,结果就是用户被转走。但如果 Agent 具备以下能力,情况完全不同:
- 识别出这是“物流查询”意图,但置信度只有 0.6。
- 先用模糊查询把用户最近 30 天订单拉出来。
- 再筛选出状态为“已发货”且“物流未更新”的订单。
- 如果只有一条匹配,直接展示物流信息。
- 如果有多条匹配,反问用户确认订单编号。
这个过程看起来很简单,但覆盖了意图识别、工具调用、结果筛选、多轮澄清四个环节。任何一个环节设计不到位,Agent 都会倾向于“转人工”。
所以,这篇文章的核心观点是:转人工应该是一个 Agent 经过多级自救后的最终出口,而不是异常分支的默认出口。
2. 适用场景与使用边界
这个思路适用于以下场景:
- 客服问答 Agent,包括电商、金融、企业服务等垂直领域。
- 内部工单系统,比如 IT 支持、HR 咨询、财务报销。
- 知识库问答场景,尤其是文档检索结果不理想时。
- 需要调用多个外部工具的 Agent,比如查订单、查库存、查物流。
同样,也存在不适合硬扛的场景:
- 涉及账号安全、资金操作、法律条款确认等高风险操作,人工介入必须前置。
- 用户情绪激烈、多次表达不满时,继续让 Agent 尝试反而会放大负面体验。
- 监管明确要求必须人工处理的流程,Agent 不能越权。
这里需要特别强调的是,安全边界比技术能力更重要。在任何线上系统中,人工永远不能被彻底移除,Agent 的“不转人工”不是把人工通道关掉,而是让转人工变得更有价值、更少发生、更精准。
从版权、隐私和合规角度来看,Agent 在处理用户订单、个人信息、聊天记录时,必须遵守最小化原则,只取解决当前问题所需的数据,不能把用户数据用于训练,也不能在无授权的情况下跨系统调用敏感接口。
3. 实战前置:一套可复用的分支决策结构
在写代码之前,我们先约定一套通用的 Agent 分支决策结构。后面所有示例都会围绕这套结构展开。
普通 Agent 的处理流程通常是一条直线:
接收用户消息 -> 意图识别 -> 调用工具 -> 返回结果这套流程缺少三个关键节点:识别校验、结果校验、兜底策略。
改造后的决策链路应该是这样:
接收用户消息 -> 意图识别 -> 置信度判断 -> 工具调用 -> 结果校验 -> 生成回答 -> 质量校验 -> 返回结果 | | | | v v v v 澄清会话 失败重试 结果修正 降级回答也就是说,Agent 在每一个关键节点都要回答一个问题:“当前结果能不能直接交给用户?”如果不能,是重新尝试、换一种方式,还是确认信息后继续。
下面用订单查询这个场景,逐步实现这套结构。
4. 环境准备与工程目录设计
这个实战案例不需要特定的一键部署工具,你可以基于现有的 Agent 框架来改,比如 LangChain、Spring AI、Dify 自定义工具流程,或者直接用 OpenAI/国产大模型的 Function Calling 接口。
建议的最小环境:
- Python 3.10 或以上。
- 一个可用的 LLM API,支持 Function Calling 或 Tool Use。
- 一个测试用的模拟订单数据库,可以用 SQLite 或内存 JSON。
- 日志系统,至少使用标准 logging。
目录结构可以参考:
agent_project/ ├── agent/ │ ├── __init__.py │ ├── core.py # Agent 主流程 │ ├── intent.py # 意图识别与置信度判断 │ ├── tools.py # 订单查询、物流查询工具 │ ├── validators.py # 结果校验器 │ └── fallback.py # 降级与转人工策略 ├── data/ │ └── orders.json # 模拟订单数据 ├── logs/ │ └── agent.log └── main.py # 入口脚本这里有一个容易被忽略的点:目录分层决定排查效率。如果所有逻辑都写在一个文件里,出现问题后你很难判断到底是意图识别错了、工具返回错了,还是最终回答生成错了。
5. 核心逻辑一:意图识别不能只看置信度
很多团队做意图识别时,只保留一个最终标签和一个置信度分数,然后设定一个固定阈值,比如低于 0.7 就转人工。
这个做法的问题很明显:置信度的含义取决于模型训练数据分布。如果训练集里“查订单”的样本特别多,那么任何和订单沾边的输入都可能被分到这一类;如果训练数据很少,真正的订单查询也可能只有 0.5 的置信度。
更务实的做法是给意图识别加两个辅助信号:
- 可澄清性:如果用户意图模糊,Agent 能不能通过反问来缩小范围。
- 可替代工具:当前意图不能用,有没有其他工具可以完成相近目标。
代码示例:
# agent/intent.py def parse_intent(user_message: str) -> dict: # 实际项目中这里调用 LLM 或分类模型 # 返回结果包含 intent、confidence、status return { "intent": "query_logistics", "confidence": 0.62, "status": "ambiguous" } def should_clarify(intent_result: dict) -> bool: # 只有当结果不可靠,且意图可澄清时,才进入澄清流程 if intent_result["confidence"] < 0.7 and intent_result["status"] == "ambiguous": return True return False def clarify_message(original_message: str) -> str: # 返回澄清问题,由 Agent 发送给用户 return "您是想查询订单物流状态,还是想申请售后处理?"这里的关键是:不是一低于阈值就转人工,而是先判断“这个模糊能不能通过对话消除”。绝大多数意图模糊,用一句反问就能解决。
6. 核心逻辑二:工具调用失败要有重试和替代路径
工具调用是 Agent 实战中出现问题最多的环节。失败原因通常包括:
- 参数缺失,比如用户没有提供订单号。
- 外部接口超时或暂时不可用。
- 数据格式变化,解析失败。
- 权限不足,Agent 没有访问某个系统的资格。
- 查询结果为空。
针对这些失败,降级顺序应该是:
- 检查参数是否完整,不完整则反问用户补齐。
- 检查同一工具能否用其他参数重试。
- 尝试替代工具,比如物流查询失败时,用订单详情的物流字段代替。
- 如果前三步都失败了,才进入转人工流程。
代码示例:
# agent/fallback.py def call_with_fallback(user_info: dict, order_id: str = None): # 第一步:参数缺失时澄清 if not order_id: return {"action": "ask_user", "message": "请提供订单号或收件人手机号后四位"} # 第二步:主工具调用 result = query_order(user_id=user_info["id"], order_id=order_id) if result["status"] == "success": return {"action": "reply", "data": result} # 第三步:日志记录失败原因,并尝试替代工具 logger.warning(f"query_order failed: {result['error']}") alt_result = query_order_by_phone(user_id=user_info["id"], phone_tail=user_info.get("phone_tail")) if alt_result["status"] == "success": return {"action": "reply", "data": alt_result} # 第四步:保留结构化上下文,转人工 return { "action": "transfer_human", "reason": "主工具与替代工具均查询失败", "debug_info": { "order_id": order_id, "primary_error": result["error"], "alt_error": alt_result.get("error") } }注意第四步:转人工时,一定要携带上下文。用户不需要对人工客服重复一遍自己的问题,人工客服也不需要从零开始排查。这就是“结构化转人工”和“干转”的核心区别。
7. 核心逻辑三:回答生成后必须经过质量校验
还有一个转人工高发节点是:大模型返回内容为空、内容与工具结果不一致、或者生成了模型自己编造的订单信息。
要解决这个问题,可以在回答前加一个校验器:
# agent/validators.py def validate_response(response_text: str, tool_data: dict | None) -> dict: # 情况一:内容为空 if not response_text or len(response_text.strip()) < 5: return {"valid": False, "reason": "empty_response"} # 情况二:工具数据存在,但回答中缺少关键字段 if tool_data: expected_fields = tool_data.get("required_fields", []) missing = [f for f in expected_fields if f not in response_text] if missing: return {"valid": False, "reason": "missing_fields", "missing": missing} # 情况三:回答中包含明确的错误断言,比如“订单不存在”但实际查询到了订单 if tool_data and tool_data.get("order_exists") and "订单不存在" in response_text: return {"valid": False, "reason": "contradiction"} return {"valid": True}有了这个校验器,Agent 在把回答发送给用户之前,可以多一步判断。如果回答不合法,宁可重新调用一次生成模型,也不要直接把错误内容发出去。
这一点对于客服场景尤其重要。用户可能无法分辨 AI 回答错在哪里,但一定会因为错误信息产生投诉。转人工之前先自检,是最便宜的纠错手段。
8. 完整实战:一个订单查询 Agent 的程序主流程
下面把上面的模块串起来,形成一个完整的、带分级兜底策略的 Agent 主流程。
# agent/core.py class OrderAgent: def __init__(self, user_info: dict): self.user_info = user_info self.max_retries = 2 self.conversation_history = [] def handle(self, user_message: str) -> dict: self.conversation_history.append({"role": "user", "content": user_message}) # 1. 意图识别 intent = parse_intent(user_message) # 2. 意图可澄清时优先澄清 if should_clarify(intent): return self._ask_user(parse_intent.none) # 实际会调用 LLM 生成澄清问题 # 3. 调用工具,带失败重试 tool_result = None for attempt in range(self.max_retries): tool_result = call_with_fallback(self.user_info, extract_order_id(user_message)) if tool_result["action"] != "ask_user": break logger.info(f"tool call attempt {attempt + 1} finished") if tool_result["action"] == "transfer_human": return self._transfer_human(tool_result) # 4. 生成回答 response_text = self._generate_response(user_message, tool_result) # 5. 回答质量校验 validation = validate_response(response_text, tool_result.get("data")) if not validation["valid"]: logger.warning(f"validation failed: {validation['reason']}") response_text = self._generate_response(user_message, tool_result, retry=True) self.conversation_history.append({"role": "assistant", "content": response_text}) return {"action": "reply", "message": response_text} def _transfer_human(self, reason_info: dict) -> dict: # 结构化转人工 return { "action": "transfer_human", "message": "我来为您转接人工客服", "context": { "user_id": self.user_info["id"], "reason": reason_info["reason"], "debug_info": reason_info.get("debug_info"), "history": self.conversation_history[-6:] } }这个流程的核心变化在于:每一步都有“返回起点重新尝试”的能力,而不是直接进入人工通道。人工转接被放在最后,且自带完整的上下文信息。
9. 功能测试与效果验证
写完了逻辑,下一步就是验证。建议从以下几个维度测试这类 Agent。
9.1 模糊意图测试
输入:“我买的东西呢?”
预期结果:Agent 不是直接转人工,而是反问用户“您是想查物流、申请退款,还是咨询售后政策?”
判断标准:只要能进入澄清流程,就说明意图识别模块的降级策略生效。
9.2 工具参数缺失测试
输入:“帮我查一下订单”
预期结果:Agent 获取不到订单号,应该要求用户补充。
失败情况:如果 Agent 直接说“查询失败转人工”,说明参数缺失的兜底逻辑没有生效。
9.3 工具失败重试测试
模拟主查询工具返回超时。
预期结果:Agent 自动记录日志,并改用替代工具查询。用户无感知。
判断标准:日志中能看到primary_error和alt_result。
9.4 回答质量校验测试
构造一个场景,让第一轮生成回答不包含订单号,或者回答内容与工具返回矛盾。
预期结果:校验器拦截错误回答,触发重新生成。
判断标准:日志中出现validation failed记录,且用户最终收到的是修正后的回答。
9.5 转人工质量测试
当所有兜底策略都失败时,人工客服收到工单里应该包含:
- 用户 ID。
- 用户原始问题。
- Agent 尝试过的动作。
- 具体失败原因。
- 最近的对话历史。
人工客服不需要重新询问用户基础信息,这是判断转人工设计是否合格的最重要指标。
10. 资源占用与性能观察
如果你使用的是云端大模型 API,性能观察的重点不在显存,而在调用次数、时延和 token 消耗。
建议在日志里记录以下指标:
- 每个用户会话的意图识别耗时。
- 工具调用重试次数。
- 回答校验拦截次数。
- 转人工次数与转人工原因分布。
- 平均每次会话消耗 token 数。
如果是本地部署模型,比如用 vLLM 或 Ollama 跑开源模型,则需要额外观察:
- 显存占用是否在请求高峰时溢出。
- 模型推理延迟是否影响用户体验。
- 长上下文输入是否导致首 token 延迟明显增加。
一个实操建议:为不同节点设置独立的日志字段,而不是全部塞进一条 message。结构化的日志字段可以直接接入 Prometheus 或 Elasticsearch,方便后续做监控告警。
11. 常见问题排查与解决方案
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Agent 频繁转人工 | 意图识别阈值设置过高,且缺少澄清策略 | 查看日志中转人工前置信度和状态 | 增加澄清节点,降低阈值前先做对话澄清 |
| 工具调用失败后直接结束会话 | 没有实现重试和替代工具 | 检查工具调用分支逻辑 | 加入参数补齐、重试、替代工具三级降级 |
| 回答内容与工具返回数据不一致 | 生成环节没有把工具结果注入提示词,或注入后被截断 | 对比工具返回数据和最终回答文案 | 在生成前拼接工具结果,并用校验器检查关键字段 |
| 转人工工单信息量少 | 转人工时只传了当前消息 | 检查转人工函数上下文参数 | 统一结构化工单格式,携带历史会话、调试信息 |
| 模型生成内容为空 | 输出 token 限制太小 | 查看 API 返回内容 | 增加 max_tokens,并加入空内容重试 |
| 用户重复表达不满 | Agent 没有识别情绪状态 | 检查是否有情绪识别或敏感词检测节点 | 高风险情绪下,主动转人工而不是继续尝试 |
12. 最佳实践与合规建议
最后给出一套工程化建议,能帮你减少无效转人工,同时保证用户体验。
第一,不要全局只用一个提示词模板。订单查询和售后投诉的处理方式完全不同。建议按意图拆分提示词,并在系统 Prompt 中明确告诉模型:“当工具返回结果为 None 时,不要摘引不存在的字段。”
第二,所有工具调用必须有超时限制。外部接口一旦超时,要立即进入降级逻辑,不能在等待中无限阻塞用户会话。
第三,转人工不等于结束会话。转人工后,原始会话内容必须持久化,并在人工端显示完整上下文。这里可以使用 Redis 缓存会话状态,避免 Agent 进程重启导致上下文丢失。
第四,批量排查 Agent 问题时,不要用真实用户数据。建议准备一套脱敏的模拟数据集,把容易触发失败的输入整理成回归测试用例。
第五,涉及人脸、声音、订单数据、个人隐私时,必须遵循最小化访问原则。Agent 只允许读取完成当前任务所需的数据,不能跨权限调用接口,也不能将读取到的用户数据用于后续非授权场景。
第六,人工客服的角色不是“帮 AI 收拾烂摊子”,而是“处理 AI 确认无法处理的事情”。这个定位差别会在工单流转、绩效评估、系统设计上带来完全不同的方案。
13. 总结与下一步
AI Agent 转人工这个话题,本质上比拼的不是模型聪明不聪明,而是工程链路完不完整。一个遇到问题就转人工的 Agent,只是给人工客服增加负担;一个具备“澄清、重试、校验、降级”能力的 Agent,才能真正把人工工单量降下来,同时保证用户体验。
这篇文章的核心观点,简单概括为三条:
- 意图识别不准时,先澄清再判断,不要直接转人工。
- 工具调用失败时,先补齐参数、重试、换工具,再做最后兜底。
- 转人工时必须携带上下文和调试信息,让人工客服能无缝接手。
你可以先从“工具失败重试”和“回答质量校验”这两个节点入手改造。这两个节点改造成本最低,收益最明显。改完之后再看日志里的转人工分布,会明显看到原因从“unknown”变成具体可排查的错误类型。
后续可以继续扩展的方向包括:多轮对话中的上下文裁剪策略、人工工单的自动分类与优先级判断、以及基于历史会话的 Agent 效果评估体系。方向很多,核心是一件:把转人工从“默认出口”改成“最后出口”。