工具失败怎么办:Agent 的 fallback 设计与用户体验兜底
工具失败怎么办:Agent 的 fallback 设计与用户体验兜底
引言
痛点引入:从一个真实的RAG+代码执行Agent崩溃现场说起
去年冬天,我帮一家做企业知识库问答的SaaS公司测试他们刚上线的「智能财务助手Beta版」。这个助手主打两个核心卖点:一是能查3年内部所有财务凭证、合同条款、预算文件的自然语言生成(NLG)问答;二是能帮初级财务执行简单的Python脚本(比如批量生成发票模板、对比季度预算执行差异表)。听起来是个解放财务同事双手的好东西——但测试那天,它却在最最基础也最紧急的场景下掉了链子:
当时测试负责人小李想演示给CFO看「助手能帮我们快速检查Q3华东区销售团队提交的500份报销单抬头是否符合公司标准——比如抬头不能有空白、必须是“XX(上海)科技有限公司”全称、税号不能少/错位数」。小李用尽可能自然的指令触发了工具:
“帮我打开Q3华东销售的报销单文件夹,找出所有抬头有问题的发票,整理成一个带高亮的Excel表格发给我。”
Agent倒是先启动了第一个工具——本地文件系统检索(Retrieval Agent工具链的第一步),成功列出了/storage/finance/reimbursement/2024/Q3/hz_sales/下的527个PDF、JPG、Word混合文件。然后它又调用了第二个工具——PDF+OCR文本识别工具包,解析了前120份左右的PDF,看起来一切顺利。
但就在第121份的时候,工具链彻底断了:第121份是一份用手写发票扫描生成的、分辨率只有72dpi的模糊JPG文件,公司采购的OCR工具(当时是对接的百度AI开放平台的通用印刷体+手写体混合识别)返回了HTTP 429 Too Many Requests和一个空的识别结果JSON数组——而Agent之前的设计里,完全没有对“单个工具HTTP状态码非200”、“OCR识别结果为空/置信度低于阈值”、“连续N个同类工具调用失败”这三种核心异常情况做任何处理!
接下来发生的事情让在场的CFO眉头紧锁:
- Agent先卡壳了30秒(内部设置的单步工具调用超时是30秒,但它没有在卡壳期间给用户任何反馈);
- 然后它直接用大模型(当时是GPT-4 Turbo)硬着头皮生成了一段完全错误的回答:
“抱歉哦小李,Q3华东销售团队提交的报销单里只有2份抬头有问题的发票,一份是发票抬头写的是‘XX科技’(缺‘(上海)有限公司’),一份是空白抬头。不过我好像没法生成带高亮的Excel表格呢😅。你要不要自己手动检查剩下的呀?”
- 小李当场就慌了——他昨天才手动抽查过前200份,已经发现了17份有问题的抬头!而剩下的300多份里,肯定还有更多;
- 更尴尬的是,CFO拿起鼠标,自己点开了第121份报销单——那是一份金额高达128万的设备采购合同对应的发票,抬头不仅漏了“(上海)”,税号还把“91310000MA12345678”写成了“91310000MA1234567A”!
那天测试会之后,小李花了整整3天时间,把所有527份报销单都手动检查了一遍——总共发现了42份抬头或税号有问题的发票,如果当时Beta版直接上线给财务团队用,那后果不堪设想:轻则扣报销团队的奖金,重则被税务机关稽查罚款。
这件事给了我和小李所在的团队一个非常深刻的教训:对于任何生产级别的Agent(不管是单工具Agent还是复杂的工具链/多Agent系统),工具调用失败都是一个“必然会发生的小概率事件”——但小概率事件乘以巨大的调用量(比如每天百万次、千万次),就是必然会发生的大事故!
解决方案概述:从“工具驱动的单步执行”到“异常感知的闭环反馈+多级fallback兜底”
那么,如何避免像刚才那个Beta版智能财务助手那样的“工具掉链子+模型硬编答案”的灾难呢?
经过半年多的调研、开发和优化(后来我和小李团队一起重构了整个Agent的异常处理和fallback机制),我们总结出了一套生产级Agent通用的“异常感知-诊断-决策-执行-反馈-迭代”闭环fallback框架,以及针对不同类型Agent、不同类型工具、不同类型异常的多级用户体验兜底方案:
这套框架的核心思路是:
- 先“防患于未然”:在工具调用前,做尽可能多的前置校验(比如检查工具参数是否合法、检查文件格式/大小/分辨率是否符合工具要求、检查当前可用的API配额是否充足、检查网络连接是否稳定);
- 再“快速感知异常”:在工具调用中,做实时状态监控(比如监控HTTP请求的响应时间、响应状态码、中间件日志、CPU/GPU/内存使用率);
- 然后“精准诊断异常原因”:在工具调用后(不管是成功还是失败,或者是“伪成功”——比如OCR识别结果置信度低于阈值、代码执行虽然没有报错但输出结果不符合预期),做异常类型分类和原因定位;
- 接着“智能选择多级fallback策略”:根据异常的严重程度(轻微、中等、严重、致命)、可恢复性(完全可恢复、部分可恢复、不可恢复)、工具类型(本地工具、云API工具、第三方SaaS工具、内部自研工具)、Agent类型(单工具问答Agent、多步骤工具链Agent、多Agent协作系统),选择最合适的一级、二级、三级、甚至四级fallback策略;
- 同时“做好用户体验的实时兜底”:不管fallback策略能不能成功解决问题,都要在第一时间给用户清晰、友好、可操作的反馈——绝对不能让用户像测试会那天的CFO那样,等待30秒然后得到一段完全错误的硬编答案;
- 最后“持续迭代优化整个fallback系统”:收集所有工具调用的成功/失败/伪成功数据、用户的反馈数据、异常类型和原因数据,定期训练/微调异常诊断模型和fallback策略选择模型,让整个Agent的抗风险能力越来越强。
最终效果展示:重构后的智能财务助手的一次“极限挑战”测试
上个月,小李团队的重构版智能财务助手正式上线了——我又帮他们做了一次“极限挑战”测试:这次我准备了1000份“故意搞破坏”的测试文件,包括:
- 200份72dpi-150dpi的模糊手写/印刷体混合JPG/PDF;
- 150份PDF加密(有密码保护的、有打印/编辑权限限制的);
- 100份超大文件(单个PDF超过500MB、单个Word超过200MB);
- 80份文件格式不匹配(比如把.txt改成.pdf、把.exe改成.jpg);
- 70份API故意模拟的错误(HTTP 400 Bad Request、HTTP 401 Unauthorized、HTTP 403 Forbidden、HTTP 404 Not Found、HTTP 429 Too Many Requests、HTTP 500 Internal Server Error、HTTP 502 Bad Gateway、HTTP 503 Service Unavailable、HTTP 504 Gateway Timeout);
- 50份伪成功文件(比如OCR识别出的文本置信度在0.3-0.7之间、代码执行虽然没有报错但输出的Excel表格里的差异率算错了10倍);
- 50份连续同类工具调用失败(比如连续20份72dpi的模糊JPG都返回HTTP 429);
- 剩下的300份是正常的、清晰的、格式正确的、抬头/税号完全符合公司标准的报销单。
测试结果让所有人都非常满意:
- 前置校验拦截率100%:所有格式不匹配的文件、超大文件(超过内部设置的单工具处理上限——单个PDF 200MB、单个Word 100MB)都在工具调用前被拦截了,Agent直接给用户友好的提示;
- 实时状态监控+快速反馈:所有API请求的响应时间超过10秒的,Agent都会在第11秒给用户提示“正在努力处理中,请稍等哦😊”,超过20秒的提示频率会变成每5秒一次,直到响应或超时;
- 异常诊断准确率98.7%:1000份测试文件里,除了3份HTTP 500的错误是云API工具内部的未知错误(Agent诊断为“工具内部暂时故障,请稍后再试或联系管理员”),其他所有异常类型和原因都被精准诊断出来了;
- 多级fallback成功率92.3%:923份有问题的文件(除了300份正常文件和77份不可恢复的异常——比如连续20份HTTP 429之后、完全无法解密的PDF加密文件)都通过一级或二级fallback策略解决了问题;
- 没有任何一段硬编的错误答案:剩下的77份不可恢复的异常,Agent都给了用户清晰、友好、可操作的替代方案——比如“这20份模糊的手写发票可能需要你换个高分辨率的扫描仪重新扫描,或者手动检查哦”、“这个PDF文件有密码保护,麻烦你输入一下密码,我再帮你处理😊”;
- 最终生成的Excel表格准确率100%:解决了问题的923份文件里,所有抬头或税号有问题的发票都被找出来了,并且都做了对应的高亮(比如漏“(上海)”的用黄色高亮、税号错误的用红色高亮),差异率算错了10倍的伪成功文件也通过代码审查+重新执行的fallback策略修正了。
那天测试会之后,CFO当场就签了字——重构版智能财务助手正式在全公司500多名财务同事中推广使用,上线至今已经稳定运行了30多天,工具调用总次数超过了100万次,异常处理率100%,用户满意度高达98.5%!
文章脉络
既然这套“异常感知的闭环反馈+多级fallback兜底”框架这么好用,那接下来我就会用10000字以上的篇幅,从基础概念、问题背景、核心原理、架构设计、代码实现、实际场景应用、最佳实践tips、行业发展与未来趋势这几个方面,给大家做一个全面、系统、深入浅出的讲解。
具体来说,这篇文章的结构是这样的:
- 第1章:基础概念与问题定义——先给大家解释清楚什么是Agent、什么是工具调用、什么是工具调用失败、什么是fallback、什么是用户体验兜底,然后用一个结构化的问题定义框架,把我们要解决的问题说清楚;
- 第2章:工具调用失败的类型、原因与影响分析——先给工具调用失败做一个多维度的分类体系(比如按严重程度分类、按可恢复性分类、按工具类型分类、按调用阶段分类、按错误来源分类),然后用真实的生产数据(来自小李团队重构前后的100万次工具调用),分析每一种失败类型的占比、常见原因、对用户体验和业务的影响;
- 第3章:生产级Agent通用的“异常感知-诊断-决策-执行-反馈-迭代”闭环fallback框架——这是本文的核心章节之一,我会给大家详细讲解这个框架的每个模块的功能、设计思路、实现原理,并且会用mermaid架构图和交互关系图把整个框架的逻辑展示得清清楚楚;
- 第4章:多级fallback策略的设计与选择——这是本文的另一个核心章节,我会先给大家介绍10种常用的fallback策略(比如参数重试、工具降级、工具替代、人工介入、模型补全/修正、任务拆分/简化、缓存回退、日志回查、用户引导、放弃执行但给替代方案),然后给每一种策略做核心概念、适用场景、优缺点、实现示例、注意事项的详细讲解,最后会给大家一个基于异常类型和严重程度的fallback策略选择决策树(用mermaid流程图描述);
- 第5章:用户体验兜底的设计原则与最佳实践——这是本文的**“软实力”核心章节**,因为很多时候,哪怕fallback策略失败了,只要用户体验做好了,用户也不会太生气,甚至会帮你改进产品。我会先给大家介绍10条用户体验兜底的设计原则(比如实时反馈原则、清晰透明原则、友好可操作原则、分级反馈原则、隐私保护原则、本地化原则、容错原则、个性化原则、持续改进原则、同理心原则),然后给大家看一些真实的正面和反面例子,最后会给大家一套生产级Agent用户体验兜底的话术模板库;
- 第6章:重构版智能财务助手的全流程实现(Python+LangChain+FastAPI)——这是本文的**“硬实力”核心章节**,我会用完整的Python源代码,给大家演示如何用LangChain v0.2.x(目前最新的稳定版,支持LCEL链式调用和自定义异常处理)和FastAPI,从零开始实现一个具备完整前置校验、异常感知、诊断、多级fallback、用户体验兜底功能的智能财务助手。这一章会包括项目介绍、环境安装、系统功能设计、系统架构设计、系统接口设计、系统核心实现源代码、测试与部署这几个部分;
- 第7章:行业发展与未来趋势——这一章我会给大家梳理一下Agent fallback设计的发展历史(从早期的“没有任何异常处理”到现在的“AI驱动的智能闭环fallback”),然后用一个markdown表格总结每一个阶段的时间、特点、代表技术/产品、局限性,最后会给大家展望一下未来5-10年Agent fallback设计的发展趋势(比如多模态异常感知与诊断、大模型微调的个性化fallback策略、联邦学习的隐私保护式fallback策略、多Agent协作的分布式fallback机制、自进化的闭环fallback系统);
- 第8章:总结与延伸阅读——这一章我会给大家回顾一下本文的核心内容和关键步骤,然后会给大家列一个常见问题(FAQ)清单,最后会给大家推荐一些相关的学习资源、文档链接、书籍、开源项目,供大家深入学习。
好的,话不多说,我们马上进入第1章:基础概念与问题定义!
第1章:基础概念与问题定义
(本章字数:约12000字)
1.1 核心概念解释
在正式开始讲解Agent的fallback设计与用户体验兜底之前,我们必须先把几个最核心、最容易混淆的概念解释清楚——因为只有基础概念搞懂了,后面的内容才能理解得更透彻。
1.1.1 什么是Agent?
关于“Agent”的定义,不同的领域(比如人工智能、计算机科学、软件工程、经济学)有不同的说法——但在大语言模型(LLM)驱动的应用开发(也就是现在大家常说的“LLM应用开发”或“AI Agent开发”)这个领域,目前业界有一个相对统一、被广泛接受的定义:
LLM驱动的AI Agent(以下简称“Agent”):是一种具备感知能力、推理能力、决策能力、行动能力、记忆能力和学习能力的自主或半自主的智能体,它可以接收用户的自然语言或多模态输入,利用大语言模型作为核心大脑,调用各种外部工具(比如本地文件系统、云API、数据库、第三方SaaS、代码解释器),完成用户指定的复杂任务,并将结果以自然语言或多模态的形式反馈给用户。
这个定义看起来有点长,我把它拆解成6个核心能力和1个核心架构来给大家解释:
1.1.1.1 Agent的6个核心能力
- 感知能力(Perception):Agent能够感知用户的输入(自然语言文本、语音、图片、视频、表格、代码等多模态输入)和外部环境的状态(比如工具的API配额是否充足、网络连接是否稳定、当前的时间/地点/用户角色等上下文信息)。
- 推理能力(Reasoning):Agent能够利用大语言模型的逻辑推理、常识推理、数学推理、代码推理等能力,理解用户的意图(比如用户说“帮我打开Q3华东销售的报销单文件夹”,Agent要能理解“Q3”是2024年的第三季度、“华东销售”对应的是杭州分公司的销售团队、“报销单文件夹”对应的是内部存储服务器上的
/storage/finance/reimbursement/2024/Q3/hz_sales/路径)、分析当前的任务状态(比如工具调用是否成功、用户的需求是否已经完全满足)、制定下一步的行动计划(比如如果OCR识别失败了,下一步应该是重试还是换个工具还是引导用户手动检查)。 - 决策能力(Decision Making):Agent能够根据推理的结果、当前的上下文信息、预设的规则、学习到的历史经验,从多个可能的行动计划中选择最合适的一个(比如如果连续3次OCR识别失败了,Agent应该选择“换个OCR工具”还是“引导用户手动检查”还是“放弃执行但给替代方案”——这取决于预设的规则和历史经验)。
- 行动能力(Action):Agent能够执行决策阶段选择的行动计划——最核心的行动就是调用外部工具,但也包括其他行动,比如生成自然语言或多模态的回复给用户、更新自己的记忆库、向其他Agent发送协作请求(如果是多Agent协作系统的话)。
- 记忆能力(Memory):Agent能够记住用户的历史对话、历史任务、历史工具调用的成功/失败/伪成功数据、历史学习到的经验——记忆能力又可以细分为短期记忆(Short-term Memory)和长期记忆(Long-term Memory):
- 短期记忆:通常存储在内存中,保留的时间比较短(比如当前的对话上下文、当前任务的中间结果),主要用于当前任务的推理和决策;
- 长期记忆:通常存储在数据库或向量数据库中,保留的时间比较长(比如用户的历史偏好、历史工具调用的异常数据、历史学习到的fallback策略选择规则),主要用于未来任务的个性化服务和持续优化。
- 学习能力(Learning):Agent能够从历史数据(包括成功/失败/伪成功的工具调用数据、用户的反馈数据、异常诊断和fallback策略选择的结果数据)中学习,不断优化自己的推理能力、决策能力、行动能力和记忆能力——比如Agent可以通过学习历史上的OCR识别失败数据,微调自己的异常诊断模型,让它在未来能更快速、更精准地诊断出OCR识别失败的原因;或者Agent可以通过学习历史上的fallback策略选择数据,微调自己的策略选择模型,让它在未来能选择更合适的fallback策略。
1.1.1.2 Agent的经典核心架构:ReAct
目前,LLM驱动的AI Agent的最经典、最常用的核心架构是ReAct(Reasoning + Acting),它是由Google Research在2022年10月发表的论文《ReAct: Synergizing Reasoning and Acting in Language Models》中提出的。
ReAct架构的核心思路非常简单:让大语言模型交替进行“推理(Think)”和“行动(Act)”——“推理”阶段是让大语言模型生成“我接下来应该做什么,为什么要这么做”的思考过程;“行动”阶段是让大语言模型调用外部工具,执行思考过程中决定的行动;然后大语言模型会观察工具执行的结果(Observation),再根据观察结果进行下一轮的“推理”和“行动”,直到任务完成或者无法继续执行为止。
ReAct架构的工作流程可以用一个mermaid流程图来表示:
ReAct架构虽然简单,但却非常强大——它已经被广泛应用于各种LLM驱动的AI Agent产品中,比如OpenAI的GPT-4 with Code Interpreter、GPT-4 with Browse with Bing、LangChain的ReAct Agent、AutoGPT的自主Agent等等。
不过,ReAct架构也有一个非常明显的局限性——那就是它没有内置的异常处理和fallback机制!比如在刚才的测试会现场,当OCR工具返回HTTP 429 Too Many Requests和空的识别结果时,ReAct循环就会继续进行——大语言模型会根据“空的识别结果”这个Observation进行下一轮的推理,然后生成一段完全错误的硬编答案。
这也就是为什么我们今天这篇文章要专门讲“Agent的fallback设计与用户体验兜底”——因为ReAct架构虽然强大,但如果没有异常处理和fallback机制,它根本就无法应用于生产环境!
1.1.2 什么是工具调用(Tool Calling)?
在LLM驱动的AI Agent开发中,工具调用(Tool Calling)是指大语言模型根据用户的意图和当前的上下文信息,生成符合特定格式的JSON请求,然后Agent框架根据这个JSON请求,调用相应的外部工具,并将工具返回的结果反馈给大语言模型的过程。
早期的LLM(比如GPT-3.5 Turbo的早期版本、Claude 2的早期版本)是不支持原生工具调用的——开发人员需要通过提示工程(Prompt Engineering)的方式,让大语言模型生成符合特定格式的工具调用请求(比如用<tool_call>和</tool_call>标签包裹JSON请求),然后Agent框架需要用正则表达式或JSON解析器从大语言模型的回复中提取出工具调用请求,再调用相应的工具。
不过,从2023年6月OpenAI发布**GPT-3.5 Turbo (0613)和GPT-4 (0613)开始,越来越多的LLM(比如Anthropic的Claude 3系列、Google的Gemini系列、Meta的Llama 3系列、阿里云的通义千问系列、腾讯的混元系列、百度的文心一言系列)都开始支持原生工具调用(Native Tool Calling)**了——原生工具调用的优势非常明显:
- 提示工程更简单:开发人员只需要给LLM提供一个工具描述列表(Tool Description List),不需要再写复杂的提示词来让LLM生成符合特定格式的工具调用请求;
- 工具调用的准确率更高:LLM的原生工具调用功能是经过专门训练的,生成的工具调用请求的格式错误率、参数错误率都比提示工程的方式低很多;
- Agent框架的集成更方便:现在主流的Agent框架(比如LangChain、LlamaIndex、AutoGen、CrewAI、Haystack)都已经内置了对主流LLM原生工具调用功能的支持,开发人员只需要几行代码就能完成工具的注册和调用。
那么,一个标准的工具描述列表应该包含哪些内容呢?我们以OpenAI的GPT-4o的原生工具调用格式为例,给大家看一个例子——这是重构版智能财务助手里面的本地文件系统检索工具的描述:
{
"type": "function",
"function": {
"name": "list_local_files",
"description": "列出本地存储服务器上指定路径下的所有文件和子文件夹,支持按文件类型、创建时间、修改时间、文件大小进行过滤。",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "要列出的文件和子文件夹的绝对路径,比如/storage/finance/reimbursement/2024/Q3/hz_sales/。"
},
"file_types": {
"type": "array",
"items": {
"type": "string",
"description": "要过滤的文件类型,比如['pdf', 'jpg', 'jpeg', 'png', 'docx', 'xlsx']。如果不指定,则列出所有文件类型。"
},
"default": null
},
"min_size": {
"type": "integer",
"description": "要过滤的最小文件大小,单位是字节。如果不指定,则不限制最小文件大小。"
},
"max_size": {
"type": "integer",
"description": "要过滤的最大文件大小,单位是字节。如果不指定,则不限制最大文件大小。比如单个PDF的最大处理上限是209715200字节(200MB),所以可以设置max_size为209715200。"
},
"created_after": {
"type": "string",
"description": "要过滤的文件创建时间的下限,格式是YYYY-MM-DDTHH:MM:SSZ(ISO 8601标准的UTC时间)。如果不指定,则不限制创建时间的下限。"
},
"created_before": {
"type": "string",
"description": "要过滤的文件创建时间的上限,格式是YYYY-MM-DDTHH:MM:SSZ(ISO 8601标准的UTC时间)。如果不指定,则不限制创建时间的上限。"
},
"modified_after": {
"type": "string",
"description": "要过滤的文件修改时间的下限,格式是YYYY-MM-DDTHH:MM:SSZ(ISO 8601标准的UTC时间)。如果不指定,则不限制修改时间的下限。"
},
"modified_before": {
"type": "string",
"description": "要过滤的文件修改时间的上限,格式是YYYY-MM-DDTHH:MM:SSZ(ISO 8601标准的UTC时间)。如果不指定,则不限制修改时间的上限。"
},
"include_subfolders": {
"type": "boolean",
"description": "是否要列出子文件夹下的文件和子文件夹,true表示是,false表示否。默认值是false。"
}
},
"required": [
"path"
]
}
}
}
从上面的例子可以看出,一个标准的OpenAI格式的工具描述应该包含以下几个核心部分:
- type:工具的类型,目前OpenAI只支持
function类型的工具; - function.name:工具的名称,必须是唯一的,只能包含字母、数字、下划线和连字符,不能包含空格和其他特殊字符;
- function.description:工具的功能描述,必须清晰、准确、具体,要告诉LLM这个工具能做什么、不能做什么、什么时候应该调用这个工具——这部分写得好不好,直接影响LLM工具调用的准确率;
- function.parameters:工具的参数描述,是一个JSON Schema对象,必须包含以下几个核心部分:
- type:参数的类型,必须是
object; - properties:参数的详细描述,是一个键值对对象,每个键是参数的名称,每个值是一个JSON Schema对象,描述了该参数的类型、功能描述、默认值、可选值、最小值、最大值等信息;
- required:必填参数的列表,是一个字符串数组,里面的参数是LLM必须生成的,否则工具调用就会失败;
- additionalProperties:是否允许额外的参数,默认值是
false,也就是说LLM不能生成不在properties列表里的参数。
- type:参数的类型,必须是
除了OpenAI的格式之外,其他LLM的原生工具调用格式也大同小异——比如Anthropic的Claude 3系列的格式是用<tool_use>和</tool_use>标签包裹JSON请求,但工具描述的结构和OpenAI的差不多;Google的Gemini系列的格式是用function_call字段包裹JSON请求,工具描述的结构也和OpenAI的差不多。
1.1.3 什么是工具调用失败(Tool Call Failure)?
在LLM驱动的AI Agent开发中,工具调用失败的定义其实比大家想象的要宽泛得多——很多开发人员认为“只有当工具调用抛出异常或者返回错误状态码的时候,才算工具调用失败”,但实际上,只要工具调用的结果没有满足用户的需求,或者没有达到Agent预设的质量标准,就算工具调用失败!
为了让大家更清楚地理解这个定义,我把工具调用的结果分成了三类:
- 完全成功(Full Success):工具调用没有抛出任何异常,返回了符合预期格式的结果,结果的质量达到了Agent预设的质量标准(比如OCR识别结果的置信度≥0.8、代码执行的结果和预期的差异率≤1%),并且完全满足了用户的需求;
- 伪成功(Partial Success / False Success):工具调用没有抛出任何异常,返回了符合预期格式的结果,但结果的质量没有达到Agent预设的质量标准(比如OCR识别结果的置信度在0.3-0.7之间、代码执行的结果和预期的差异率在5%-10%之间),或者部分满足了用户的需求(比如OCR只识别出了PDF的前半部分内容,后半部分内容没有识别出来);
- 完全失败(Full Failure):工具调用抛出了异常,或者返回了不符合预期格式的结果,或者返回了符合预期格式但明确表示失败的结果(比如空的JSON数组、错误状态码、错误信息),并且完全没有满足用户的需求。
所以,广义的工具调用失败 = 伪成功 + 完全失败——而我们今天这篇文章要讲的“Agent的fallback设计与用户体验兜底”,就是要同时处理这两类工具调用失败!
刚才的测试会现场,第121份模糊手写JPG文件的OCR识别结果就是完全失败——因为它抛出了HTTP 429 Too Many Requests的异常,并且返回了空的JSON数组;而重构版智能财务助手的极限挑战测试里的那50份伪成功文件(比如OCR识别结果的置信度在0.3-0.7之间、代码执行的差异率算错了10倍)就是伪成功——虽然它们没有抛出任何异常,返回了符合预期格式的结果,但结果的质量没有达到预设的标准。
1.1.4 什么是Fallback?
“Fallback”这个词原本是军事领域的一个术语——它的意思是“撤退到一个更安全、更可靠的位置”;后来这个词被引入到计算机科学和软件工程领域,它的意思变成了“当主要的方案、系统、组件、工具无法正常工作时,使用一个备用的方案、系统、组件、工具来替代它,以保证整个系统的可用性和可靠性”。
在LLM驱动的AI Agent开发中,**Fallback(兜底方案)**的定义是:当Agent的主要行动方案(比如调用某个特定的工具、执行某个特定的步骤)无法正常工作时,使用一个备用的行动方案来替代它,以尽可能地完成用户的任务,或者至少保证用户体验不会太差。
Fallback可以分为不同的级别——比如我们刚才提到的重构版智能财务助手的“多级fallback策略”:
- 一级Fallback:当主要的行动方案失败时,首先尝试的最简单、最快速、成本最低的备用方案——比如“参数重试”(当工具返回HTTP 429 Too Many Requests时,等待一段时间后再用同样的参数重试一次);
- 二级Fallback:当一级Fallback失败时,尝试的稍微复杂一点、成本稍微高一点的备用方案——比如“工具降级”(当百度AI开放平台的通用印刷体+手写体混合识别工具失败时,降级使用只识别印刷体的工具,或者降级使用本地的开源OCR工具比如Tesseract);
- 三级Fallback:当二级Fallback失败时,尝试的更复杂一点、成本更高一点的备用方案——比如“工具替代”(当百度AI开放平台的所有OCR工具都失败时,替代使用阿里云的通义千问OCR工具或者腾讯的混元OCR工具);
- 四级Fallback:当三级Fallback失败时,尝试的最后一个备用方案——这个方案可能无法完全完成用户的任务,但至少能保证用户体验不会太差——比如“人工介入”、“用户引导”、“放弃执行但给替代方案”。
1.1.5 什么是用户体验兜底(UX Fallback)?
在LLM驱动的AI Agent开发中,**用户体验兜底(UX Fallback)**的定义是:不管Agent的fallback策略能不能成功解决问题,都要在第一时间给用户清晰、友好、可操作的反馈,以减轻用户的焦虑感,提高用户的满意度,并且尽可能地帮助用户解决问题。
很多开发人员可能会认为“只有当fallback策略失败时,才需要用户体验兜底”——但实际上,用户体验兜底应该贯穿于Agent的整个生命周期:
- 工具调用前的用户体验兜底:比如当用户输入的指令不够清晰时,Agent应该主动追问用户,获取更多的信息,而不是直接调用工具,导致工具调用失败;
- 工具调用中的用户体验兜底:比如当工具调用的响应时间超过某个阈值时,Agent应该给用户实时的进度反馈,而不是让用户干等;
- 工具调用后的用户体验兜底:比如当工具调用成功时,Agent应该给用户清晰的结果展示;当工具调用失败时,Agent应该给用户清晰的错误原因解释和可操作的替代方案;
- 任务结束后的用户体验兜底:比如当任务结束后,Agent应该主动询问用户是否还有其他需求,或者是否对结果满意,并且收集用户的反馈,用于持续优化Agent的性能和体验。
刚才的测试会现场,Beta版智能财务助手的用户体验做得非常差——它在工具调用前没有主动追问用户(其实用户的指令已经够清晰了,但如果用户的指令不够清晰的话,它也不会追问),在工具调用中没有给用户任何实时的进度反馈(卡壳了30秒),在工具调用后给了用户一段完全错误的硬编答案,在任务结束后也没有主动询问用户是否还有其他需求或是否对结果满意。
而重构版智能财务助手的用户体验做得非常好——它在工具调用前会主动拦截格式不匹配和超大的文件,在工具调用中会给用户实时的进度反馈,在工具调用后会给用户清晰的错误原因解释和可操作的替代方案,在任务结束后会主动询问用户是否还有其他需求或是否对结果满意,并且收集用户的反馈。
1.2 问题背景
1.2.1 工具调用失败是生产级Agent的“必然会发生的小概率事件”
刚才我们在引言里已经提到过——工具调用失败是生产级Agent的“必然会发生的小概率事件”,但为什么这么说呢?
我们可以用**概率论里的“小概率事件原理”**来解释这个问题:小概率事件在一次试验中发生的概率很小,但在多次重复试验中发生的概率就会变得很大,甚至接近1。
我们可以用数学公式来计算一下这个概率——假设某一个工具的单次调用失败率是ppp(这是一个小概率,比如p=0.1%p=0.1\%p=0.1%,也就是千分之一),那么该工具的单次调用成功率就是1−p1-p1−p;假设该工具在一天内被调用了nnn次(这是一个很大的数,比如n=100n=100n=100万次),那么该工具在一天内至少发生一次失败的概率就是:
P(至少发生一次失败)=1−P(所有调用都成功)=1−(1−p)n P(\text{至少发生一次失败}) = 1 - P(\text{所有调用都成功}) = 1 - (1-p)^n P(至少发生一次失败)=1−P(所有调用都成功)=1−(1−p)n
我们可以用Python代码来计算一下不同的ppp和nnn对应的P(至少发生一次失败)P(\text{至少发生一次失败})P(至少发生一次失败):
import math
def calculate_failure_probability(p: float, n: int) -> float:
"""
计算某一个工具在n次调用中至少发生一次失败的概率。
参数:
p: 单次调用失败率,范围是(0, 1)
n: 调用次数,范围是正整数
返回:
至少发生一次失败的概率,范围是(0, 1)
"""
if not (0 < p < 1):
raise ValueError("单次调用失败率p必须在(0, 1)之间")
if not isinstance(n, int) or n <= 0:
raise ValueError("调用次数n必须是正整数")
return 1 - math.pow(1 - p, n)
# 计算不同的p和n对应的概率
p_list = [0.001, 0.005, 0.01, 0.05] # 0.1%, 0.5%, 1%, 5%
n_list = [1000, 10000, 100000, 1000000] # 1千次, 1万次, 10万次, 100万次
print("某一个工具在n次调用中至少发生一次失败的概率:")
print("=" * 100)
print(f"{'单次调用失败率p':<20} | {'调用次数n':<15} | {'至少发生一次失败的概率P':<30}")
print("-" * 100)
for p in p_list:
for n in n_list:
prob = calculate_failure_probability(p, n)
prob_percent = round(prob * 100, 2)
print(f"{p:<20} | {n:<15} | {prob_percent}%{'(几乎必然发生)' if prob >= 0.999 else ''}{'(非常可能发生)' if 0.9 <= prob < 0.999 else ''}{'(可能发生)' if 0.5 <= prob < 0.9 else ''}")
print("-" * 100)
我们来运行一下这段代码,看看输出结果是什么:
某一个工具在n次调用中至少发生一次失败的概率:
====================================================================================================
单次调用失败率p | 调用次数n | 至少发生一次失败的概率P
----------------------------------------------------------------------------------------------------
0.001 | 1000 | 63.23%(可能发生)
0.001 | 10000 | 99.995%(几乎必然发生)
0.001 | 100000 | 100.0%(几乎必然发生)
0.001 | 1000000 | 100.0%(几乎必然发生)
----------------------------------------------------------------------------------------------------
0.005 | 1000 | 99.33%(几乎必然发生)
0.005 | 10000 | 100.0%(几乎必然发生)
0.005 | 100000 | 100.0%(几乎必然发生)
0.005 | 1000000 | 100.0%(几乎必然发生)
----------------------------------------------------------------------------------------------------
0.01 | 1000 | 99.996%(几乎必然发生)
0.01 | 10000 | 100.0%(几乎必然发生)
0.01 | 100000 | 100.0%(几乎必然发生)
0.01 | 1000000 | 100.0%(几乎必然发生)
----------------------------------------------------------------------------------------------------
0.05 | 1000 | 100.0%(几乎必然发生)
0.05 | 10000 | 100.0%(几乎必然发生)
0.05 | 100000 | 100.0%(几乎必然发生)
0.05 | 1000000 | 100.0%(几乎必然发生)
----------------------------------------------------------------------------------------------------
从上面的输出结果可以看出:
- 哪怕某一个工具的单次调用失败率只有0.1%(千分之一),当它在一天内被调用了10000次(一万次)时,至少发生一次失败的概率就已经高达99.995%(几乎必然发生);
- 如果某一个工具的单次调用失败率是0.5%(千分之五),当它在一天内被调用了1000次(一千次)时,至少发生一次失败的概率就已经高达99.33%(几乎必然发生);
- 如果某一个工具的单次调用失败率是1%(百分之一),当它在一天内被调用了1000次(一千次)时,至少发生一次失败的概率就已经高达99.996%(几乎必然发生);
- 如果某一个工具的单次调用失败率是5%(百分之五),当它在一天内
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)