大语言模型Function Calling中的不确定性管理:构建可靠AI智能体的关键策略
1. 从“我知道”到“我知道我不知道”:智能体可靠性的新挑战
最近在折腾大语言模型的应用开发,特别是围绕Function Calling(函数调用)这个核心能力构建一些自动化工作流。相信很多同行都踩过类似的坑:你精心设计了一套工具函数,满怀期待地交给模型去调用,结果它信心满满地给你返回了一个完全错误的参数,或者调用了一个风马牛不相及的函数。这种“一本正经地胡说八道”不仅让人哭笑不得,更严重的是,它会直接导致整个自动化流程的崩溃,让所谓的“智能体”变得极不可靠。
问题的根源,恰恰在于当前大多数语言模型在Function Calling时,都表现得像一个“无所不知”的专家。它们被训练成对于任何输入,都要给出一个确定的、结构化的输出。然而,现实世界充满了模糊性、信息缺失和模型知识边界。当用户的问题超出模型的知识范围,或者指令描述存在歧义时,一个可靠的系统不应该强行给出一个可能错误的答案,而应该有能力说:“对不起,这个问题我目前无法处理,需要更多信息或人工介入。”
这就是“The ‘I Don‘t Know’ Filter”(“我不知道”过滤器)这个概念的核心价值。它不是一个具体的算法或库,而是一种设计范式与实现策略的集合。其目标是为基于大语言模型的智能体(Agent)注入一种关键能力:不确定性感知与可靠决策。简单来说,就是教会AI在没把握的时候“闭嘴”或“求助”,而不是硬着头皮乱来。这对于构建真正能在生产环境中稳定运行的、负责任的AI应用至关重要。无论是客服机器人、数据分析助手还是代码生成工具,这种能力都是其从“玩具”迈向“工具”的关键一步。
2. 为什么Function Calling特别需要“不确定性”管理?
要理解“我不知道”过滤器的重要性,我们得先拆解一下标准Function Calling的工作流程及其脆弱性。一个典型的流程包括:用户输入自然语言指令 -> 大语言模型理解意图并选择匹配的工具函数 -> 模型生成符合该函数要求的参数(通常是JSON)-> 系统执行函数并返回结果。
这个链条的每个环节都可能引入不确定性:
2.1 意图理解的模糊地带用户的指令可能不精确。例如,用户对股票分析助手说:“帮我看看苹果的表现。”这里的“苹果”是指Apple Inc. (AAPL)这只股票,还是指水果市场的行情?又或者是对设计助手说:“把这个背景调亮一点。”“亮一点”是多少?是增加10%的亮度还是50%?模型如果选择一个它认为“最可能”的解读,并自信地调用get_stock_price(symbol=“AAPL”)或adjust_brightness(value=30),一旦猜错,结果就是无用的甚至有害的。
2.2 知识边界与实时性缺失模型的知识有截止日期,且不包含非公开信息。当用户问“特斯拉今天下午的股价是多少?”时,如果模型的知识截止到2023年7月,它可能根据记忆中的历史数据编造一个价格,或者调用一个需要实时数据但参数错误的函数。它“不知道”自己不知道今天的具体数据。
2.3 工具函数描述的匹配误差我们为模型提供工具列表时,会包含名称和描述。模型需要判断用户请求是否与某个工具的描述匹配。这种匹配基于语义相似度,本身就有概率性。一个请求可能同时与两个工具的描述部分匹配(例如,“总结这篇文章”可能匹配summarize_text()和extract_key_points()),模型可能会选择置信度略高的那个,但未必是用户真正想要的。
2.4 参数生成的幻觉与格式错误这是最常见的坑。即使模型正确选择了函数,在生成结构化参数(如JSON)时,也可能产生“幻觉”,编造出不存在的参数项,或者提供格式错误、类型不符的值(例如,需要整数却给了字符串,需要特定枚举值却给了无效值)。
传统的、没有“我不知道”过滤器的流程,会像一台没有故障报警灯的机器,无论内部计算多么不确定,都会强制输出一个结果。而引入“我不知道”过滤器,就是在流程中增加了多个“质量检查点”和“熔断机制”,当不确定性超过某个阈值时,流程会暂停,转而向用户请求澄清、提供备选方案,或者安全地降级处理。
3. 构建“我不知道”过滤器的核心组件与策略
实现一个有效的“我不知道”过滤器,不是简单地让模型在输出前加一句“我不确定”,而是一个系统工程。它需要结合提示工程、置信度评估、后处理逻辑以及备选流程设计。下面我们来拆解几个核心的构建策略。
3.1 提示工程:引导模型表达不确定性
最直接的方法是在系统提示词(System Prompt)中明确要求模型识别并表达不确定性。这比单纯禁止它猜测更有效。
基础提示示例:“你是一个谨慎可靠的助手。当用户请求涉及以下情况时,你必须主动承认知识或能力限制,并请求澄清,而不是猜测:
- 信息模糊,存在多种可能解释(例如,‘苹果’可能指公司或水果)。
- 请求的信息超出你的知识截止日期或范围。
- 你无法确定哪个工具函数最符合用户意图。
- 你无法从指令中提取出工具函数所需的全部必要参数。 如果你不确定,请输出一个特定的JSON结构,例如
{“need_clarification”: true, “reason”: “参数X缺失或模糊”, “question_to_user”: “您指的‘苹果’是苹果公司(AAPL)还是水果?”}”
实操心得:仅仅在提示词中要求是不够的。模型,特别是较小规模的模型,可能会“过度谨慎”或“过度自信”。因此,提示词需要与后续的置信度评分结合使用。一个有效的技巧是使用“思维链”(Chain-of-Thought)提示,要求模型在输出最终决定前,先输出其推理过程,我们在后处理阶段可以解析这个推理过程来评估其确定性。
3.2 置信度评分:量化“不知道”的程度
这是过滤器的技术核心。我们需要一个可量化的指标来判断模型输出的可靠性。有几种常见方法:
- 生成概率(Logprobs):许多API(如OpenAI)可以提供模型在生成每个token(词元)时的对数概率。对于函数调用,我们可以计算整个
function_call部分(包括函数名和参数)的平均生成概率或最低概率。一个低概率通常意味着模型在“勉强”生成这些内容,不确定性高。- 计算示例(概念性):假设模型生成了
function_call: {“name”: “get_weather”, “arguments”: “{\”city\“: \”Paris\“}”}。API返回了arguments中每个token的logprob。如果”Paris”的logprob很低,可能意味着模型不太确定城市是不是巴黎。
- 计算示例(概念性):假设模型生成了
- 备选方案分析(N-best Lists):要求模型(或通过API参数如
logit_bias、top_logprobs)不仅给出最佳答案,还给出几个备选答案。如果最佳答案的置信度只比第二、第三名高一点点,说明模型自己也很纠结,这时就应该触发“我不知道”流程。- 场景举例:用户说“订一张去纽约的票”。模型可能同时以相近的概率建议调用
book_flight(destination=”NYC”)和book_train(destination=”New York”)。这时,过滤器应该捕获这种竞争关系,并反问用户:“您是想预订航班还是火车票?”
- 场景举例:用户说“订一张去纽约的票”。模型可能同时以相近的概率建议调用
- 专用不确定性评估模型:可以训练或微调一个小的分类器模型,专门评估主模型输出的不确定性。这个评估模型的输入可以是原始用户查询、主模型的完整输出(包括思维链)、以及生成概率等特征,输出是一个0到1的置信度分数。
3.3 后处理与验证层
在模型输出之后、执行函数之前,插入一个验证层。这个层根据置信度评分和预定义的规则来决定下一步行动。
# 伪代码示例 def uncertainty_filter(model_response, confidence_score, tools_definition): """ 处理模型响应,根据置信度决定行动。 """ if confidence_score < UNCERTAINTY_THRESHOLD: # 置信度过低,启动澄清流程 clarification_needed = True reason = “模型对函数选择或参数生成的置信度过低。” # 可以尝试从模型的思维链或备选方案中提取具体问题 question = construct_clarification_question(model_response) return {“action”: “ask_user”, “question”: question, “reason”: reason} # 置信度达标,继续验证参数 func_name = model_response.function_call.name func_args = model_response.function_call.arguments # 1. 验证函数是否存在 if func_name not in [t[“name”] for t in tools_definition]: return {“action”: “ask_user”, “question”: f“工具‘{func_name}’不存在。请重新描述您的需求。”, “reason”: “未知工具调用”} # 2. 验证参数格式和必填项 validation_result = validate_arguments(func_name, func_args, tools_definition) if not validation_result[“is_valid”]: return {“action”: “ask_user”, “question”: validation_result[“message”], “reason”: “参数验证失败”} # 3. 验证参数值合理性(业务逻辑层) sanity_result = sanity_check_arguments(func_name, func_args) if not sanity_result[“is_sane”]: return {“action”: “ask_user”, “question”: sanity_result[“message”], “reason”: “参数合理性检查失败”} # 所有检查通过,执行函数 return {“action”: “execute”, “function_call”: model_response.function_call} # 使用 filtered_action = uncertainty_filter(llm_output, confidence_score=0.65, tools_definitions) if filtered_action[“action”] == “ask_user”: # 向用户发送 filtered_action[“question”] send_to_user(filtered_action[“question”]) elif filtered_action[“action”] == “execute”: result = call_function(filtered_action[“function_call”])注意事项:阈值UNCERTAINTY_THRESHOLD的设置需要根据实际场景通过测试来调整。设置过高会导致模型频繁“弃权”,用户体验差;设置过低则过滤器形同虚设。一个可行的办法是进行A/B测试,根据任务完成率和用户满意度来动态调整。
3.4 安全降级与备选流程
当过滤器触发后,系统不能只是简单地说“我不知道”然后挂起。必须设计友好的备选流程:
- 主动澄清:如上例所示,基于不确定性的具体原因(模糊指令、缺失参数等),生成一个具体、清晰的问题反问用户。
- 提供选择:如果是不确定哪个工具,可以将2-3个最可能的选项(及其解释)呈现给用户选择。例如:“您是想‘总结全文’(生成一段概述)还是‘提取要点’(列出几个关键句子)?”
- 默认安全操作:在某些高风险场景(如金融交易、设备控制),当不确定性高时,系统应自动执行风险最低的操作或直接转为人工处理。
- 记录与学习:将所有触发“我不知道”的案例记录下来,包括用户原始输入、模型输出、置信度分数和最终的人工处理结果。这些数据极其宝贵,可以用于后续分析模糊模式,优化工具描述,甚至微调模型。
4. 实战:在LangChain中为Agent添加简易置信度检查
理论讲完了,我们来看一个在流行框架LangChain中落地的简化示例。假设我们有一个查询天气的Agent。
首先,我们定义一个自定义的LLM包装器,用于获取模型输出的原始logprobs(这里以OpenAI为例,需要能访问相关API)。
from langchain.llms.base import BaseLLM from langchain.schema import LLMResult from typing import Any, List, Optional, Dict import openai class OpenAIWithLogprobs(BaseLLM): model_name: str = “gpt-3.5-turbo” def _generate(self, prompts: List[str], stop: Optional[List[str]] = None) -> LLMResult: responses = [] for prompt in prompts: # 调用OpenAI API,请求返回logprobs response = openai.ChatCompletion.create( model=self.model_name, messages=[{“role”: “user”, “content”: prompt}], max_tokens=150, logprobs=True, # 关键参数 top_logprobs=5, # 获取top 5的备选token及其概率 stop=stop ) # 简化处理:我们这里只关心最终输出的文本和logprobs message = response.choices[0].message logprobs = response.choices[0].logprobs responses.append(LLMResult(generations=[[{ “text”: message.content, “generation_info”: {“logprobs”: logprobs} }]])) return LLMResult(generations=[[gen] for gen in responses[0].generations[0]] if responses else []) @property def _llm_type(self) -> str: return “openai-with-logprobs”接下来,我们创建一个自定义的Agent执行器,它在调用工具前插入置信度检查。
from langchain.agents import AgentExecutor, BaseSingleActionAgent from langchain.schema import AgentAction, AgentFinish import json import math class UncertaintyAwareAgentExecutor(AgentExecutor): confidence_threshold: float = 0.5 # 置信度阈值 def _take_next_step(self, name_to_tool_map: Dict[str, Any], inputs: Dict[str, str]) -> List[AgentAction]: # 调用父类方法获取模型决定的下一步动作 intermediate_steps = self._plan(inputs, self.intermediate_steps) # 假设 intermediate_steps 返回的是一个 AgentAction 或 AgentFinish # 这里我们需要从LLM结果中提取logprobs,实际情况更复杂,需要自定义Agent类来传递logprobs # 以下为概念性代码 llm_output = self.agent.llm_output # 假设这里存储了带logprobs的输出 if llm_output and “logprobs” in llm_output.get(“generation_info”, {}): logprobs = llm_output[“generation_info”][“logprobs”] # 计算一个简单的平均置信度(实际应用需要更精细的计算,如只计算function_call部分) avg_logprob = self._calculate_avg_logprob(logprobs) confidence = math.exp(avg_logprob) # 将对数概率转换回概率(近似) if confidence < self.confidence_threshold: # 置信度过低,不执行动作,返回一个要求澄清的AgentFinish clarification = “我对您请求的理解置信度较低。能否请您更具体地描述一下您的需求?” return [AgentFinish({“output”: clarification}, log=“”)] # 置信度达标,继续正常流程 return super()._take_next_step(name_to_tool_map, inputs) def _calculate_avg_logprob(self, logprobs): # 简化计算:平均所有token的logprob # 实际中需要过滤掉无关token(如提示词部分) total = 0 count = 0 if logprobs and “content” in logprobs: for item in logprobs[“content”]: total += item[“logprob”] count += 1 return total / count if count > 0 else -float(‘inf’)踩坑实录:在实际集成中,最大的挑战是从LangChain的标准流程中“拦截”并解析模型的原始输出(包括logprobs)。LangChain的高级抽象在方便使用的同时,也隐藏了底层细节。你可能需要自定义BaseSingleActionAgent类,重写plan方法,确保能将LLM的完整响应(包括元数据)传递到执行器。此外,计算函数调用部分的置信度需要精确地定位输出文本中对应function_call的token范围,这需要对tokenization和API响应结构有较深的理解。
5. 超越二值判断:不确定性作为连续信号
一个更高级的思路是,不把“知道”和“不知道”看作非黑即白的判断,而是将“不确定性”作为一个连续的信号,贯穿整个智能体的决策流程。
5.1 基于不确定性的动态工具选择
智能体可以同时评估多个工具的适用性,并给出一个概率分布。例如,对于请求“分析这份销售数据”,工具calculate_summary_statistics的匹配置信度为0.7,generate_trend_chart为0.6,identify_outliers为0.4。系统可以设置一个规则:如果最高置信度工具的概率低于0.8,且第二高置信度工具的概率与之相差小于0.2,则自动进入“多工具建议”模式,向用户展示前2-3个可能相关的工具及其简要说明,让用户选择。
5.2 参数生成中的模糊处理与集合返回
当模型对某个参数不确定时,可以不生成一个确定值,而是生成一个可能值的集合或一个范围。例如,用户说“找一些价格适中的餐厅”。模型可以调用search_restaurants(price_range=[“$$”, “$$$”]),其中price_range是一个包含了“中等”和“中等偏上”的集合,而不是武断地选择其中一个。后端函数需要能处理这种多值参数,返回更广泛的结果让用户筛选。
5.3 不确定性在链式调用中的传播
在复杂的多步工作流中,前一步的不确定性会传播到后续步骤。一个健壮的系统应该能跟踪和累积这种不确定性。例如,第一步数据提取的置信度是0.8,第二步基于此数据分析的置信度是0.9,那么最终结论的综合置信度可能只有0.72。系统可以在输出结果时附带一个“置信度分数”的说明,例如“根据现有信息,该结论的置信度约为70%,建议您核对原始数据X和Y。”
5.4 与外部知识源和验证器的联动
“我不知道”过滤器不应该是一个终点,而应该是一个触发器,触发系统去寻求外部帮助。这可以包括:
- 知识库检索:当模型对某个事实不确定时,自动触发向量数据库检索,用检索到的证据来补充或修正模型的输出。
- 代码执行器验证:对于涉及计算或逻辑判断的请求,让模型生成代码,然后在沙箱中执行代码来验证结果。如果代码执行出错或结果异常,则触发不确定性流程。
- 人类在环(Human-in-the-loop):对于置信度低于某个严格阈值(或涉及高风险操作)的请求,直接放入人工审核队列,等待人工确认后再执行。
6. 评估“我不知道”过滤器的效果:不仅仅是准确率
引入过滤器后,如何评估其效果?传统的准确率(Accuracy)指标可能不再适用,因为系统现在会主动拒绝一些它没把握的任务。
需要建立一套新的评估体系:
- 可靠性(Reliability):系统在承诺给出答案时,答案的正确率。这个指标应该比引入过滤器前有显著提升,因为低置信度的错误答案被过滤掉了。
- 覆盖率(Coverage):系统成功处理(包括直接回答和通过澄清后回答)的查询占总查询的比例。过滤器不能过于保守,导致覆盖率暴跌。
- 澄清效率:系统提出的澄清问题是否精准、有效?用户需要多少轮交互才能得到最终答案?平均澄清轮次越少越好。
- 用户满意度:通过调查或隐式反馈(如任务完成率、后续使用频率)来衡量。用户是更喜欢一个偶尔犯错但高效的助手,还是一个非常谨慎但步骤繁琐的助手?这需要权衡。
- 失败模式分析:仔细分析那些被过滤器错误放行(假阴性)和错误拦截(假阳性)的案例。假阴性导致错误,假阳性导致用户体验下降。通过分析这些案例,可以持续优化置信度阈值和验证规则。
个人体会:在实际项目中,我们引入了一个基于规则和简单概率检查的初级过滤器后,生产环境中的严重错误(如调用错误API、传递荒谬参数)减少了约60%,但平均对话轮次增加了1.2轮。经过几轮迭代优化提示词和澄清话术后,平均对话轮次增加降到了0.8轮,同时错误率继续下降。这个权衡是值得的,尤其是在错误成本高的领域(如金融、医疗咨询)。最终,一个可靠的智能体,其价值不在于它永远正确,而在于它知道自己能力的边界,并能以一种安全、可控的方式与用户协作。
