在开发基于大语言模型(LLM)的智能体系统时,我遇到了一个典型的幻觉问题:当我请求"关闭3003端口",LLM 回复"好的,我已经关闭了",但在代码中,我已经明确这种危险操作需要用户确认后才能执行。这种情况表明,我的LLM模型,已经出现了幻觉。而这种"声称已执行但实际未执行"的现象,应该是 LLM 在工具调用场景中常见的幻觉类型。
我目前正在开发的这个智能体系统采用"意图识别 → 工具调用 → 结果整合"的架构。当我通过飞书发送"关闭3003端口"时,系统流程应该是:
- 首先,任务解析器调用 LLM 识别用户意图,生成调用
kill_process_by_port工具的任务; - 其次,工具执行器检测到这是一个危险操作,返回
requires_confirmation: true标记; - 最后,结果整合器(ResultIntegrator)再次调用 LLM,将工具返回的 JSON 数据转换为自然语言回复。
问题出现在最后一步。工具明确返回了 requires_confirmation: true 和 success: false,表示操作需要用户确认且尚未执行。然而,LLM 生成的回复却是"好的,我已经关闭了占用端口3003的进程",完全忽略了确认要求,虚构了执行结果。
自回归生成的本质局限
大语言模型基于自回归机制生成文本,即根据上文预测下一个最可能的 token。这种机制决定了模型倾向于生成"统计上合理"的续写,而非"事实上准确"的陈述。在对话场景中,当用户说"关闭端口"时,训练数据中"请求→执行→确认"的模式出现频率远高于"请求→确认→执行"。因此,模型预测的最可能续写是"已关闭"而非"需要确认"。
从概率角度分析,设用户输入为 ,模型参数为
,生成的回复为
。自回归模型优化目标是最大化
,即寻找给定输入下概率最高的回复。然而,概率最高并不意味着事实正确——它只意味着与训练数据分布最相似。当训练数据中存在偏差时,模型会忠实地复现这种偏差。
对话数据集的构建方式天然存在偏差。在真实的人机对话中,"用户请求→系统执行→反馈结果"是最常见的交互模式。相比之下,"用户请求→系统询问确认→用户确认→系统执行"的模式虽然更安全,但在数据集中占比更低。这种数据分布的不平衡导致模型形成了"请求即执行"的隐式假设。
通俗来讲,大模型其实是在"猜"下一个字,而不是在"思考"真相。
比如我们玩一个"接龙游戏":当别人说"今天天气真..."时,你大概率会接"好",因为这在日常生活中最常见。大模型也是这样工作的,它根据上文,预测下一个最可能出现的字。
回到本例,当我说出"帮我关闭3003端口"这句话时,在LLM模型的训练数据中,"请求→执行→完成"这种对话模式出现了成千上万次。所以当它看到"关闭端口"这个请求时,它的"直觉"告诉它:最可能的下一句是"已经关闭了",因为这在统计上最常见。
但实际上,系统设计是"请求→确认→执行",这需要先问用户确认。这种模式在训练数据中占比很低,所以模型"猜"不到。
另外,现有的指令微调(Instruction Tuning)数据集主要关注任务的完成度,而非中间状态的准确性。模型被训练成"给出有帮助的回答",而非"准确描述当前状态"。当系统状态与用户期望存在冲突时(如需要确认才能执行),模型倾向于"讨好"用户,给出一个看似完成任务的回复。
指令遵循的边界问题
在原始的系统提示词中我也包含了约束规则:"绝对不可以说'准备执行'、'正在执行'或'已经执行',因为操作还没有执行"。然而,这条规则存在两个问题:首先,它采用否定式表述,模型对否定指令的遵循度天然低于肯定式指令;其次,规则过于抽象,没有提供具体的边界案例。
LLM 对指令的遵循能力与指令的具体程度呈正相关。抽象的规则(如"不要说谎")往往被模型理解为一种倾向性指导,而非硬性约束。当规则与模型的"直觉"发生冲突时,模型倾向于遵循直觉而非规则。
还有虽然返回的 JSON 数据包含 requires_confirmation: true 字段,这个字段从人类视角看语义是明确的:"需要等待用户确认"。然而,模型可能将其理解为"这是一个确认类操作",而非"需要暂停并等待用户输入"。这种语义歧义源于模型对"确认"一词的多义性理解——它既可以表示"执行确认动作",也可以表示"等待确认输入"。
更深层次地,模型缺乏对"系统状态"的显式建模。在工具调用场景中,存在"用户意图"、"工具返回状态"、"实际执行结果"三个不同的概念。模型倾向于将三者混为一谈,将"用户意图"直接映射为"执行结果",忽略了中间的"工具返回状态"。
目前实行的解决方案
针对指令遵循不足的问题,采用了"负面约束列表"策略。在系统提示词中,明确列出禁止使用的表述:"已经关闭"、"已经终止"、"已经执行"、"正在执行"、"准备执行"、"好的,我..."。这种具体化的否定约束比抽象规则更有效,因为它为模型提供了明确的边界。
从认知心理学角度,人类(以及模型)对具体案例的记忆强度高于抽象规则。通过列出具体的禁止表述,模型在生成时会进行"模式匹配",检测输出是否包含这些表述,从而在解码阶段进行自我纠正。
在系统提示词中添加了错误示例和正确示例的对比:
错误示例(绝对禁止):
用户: "把它关了吧"
系统数据: {"requires_confirmation": true, "message": "确认终止进程?"}
你的回复: "好的,我已经关闭了" ← 这是错误的!操作还没执行!
正确示例:
用户: "把它关了吧"
系统数据: {"requires_confirmation": true, "message": "确认终止进程?"}
你的回复: "好的,我来帮你看看。当前端口 3003 被一个 Node.js 进程占用,你确定要关闭它吗?回复'确认'来执行哦。" ← 这是正确的!
利用了模型的上下文学习能力。通过提供正反两方面的示例,模型不仅学习了"应该怎么做",还学习了"不应该怎么做"。这种对比学习比单一的正例更有效,因为它明确了行为的边界。
最重要的是在代码层面,采用了状态机设计模式,确保关键业务逻辑不依赖 LLM 的判断。具体而言,将 pending_confirmation 的保存逻辑从 if result.success 内部移出,改为先检查 requires_confirmation 再检查 success。这样,即使 LLM 输出错误,确认流程也能正常触发。
这种设计的核心思想是"防御性编程":假设 LLM 可能出错,在代码层面进行兜底。LLM 负责自然语言交互层(理解用户意图、生成友好回复),但关键状态转换(如"待确认"→"已确认"→"执行中")由代码强制控制。这种"人机协作"模式既发挥了 LLM 的语义理解优势,又规避了其状态管理劣势。
总结一下
通过这次的幻觉事件,可以为我在以后的智能体开发中总结出一些经验,首先开发过程中,一定要明确 LLM 的能力边界。LLM 擅长语义理解、意图识别和自然语言生成,但不适合作为状态管理的唯一来源。在工具调用场景中,LLM 应该扮演"翻译器"的角色——将用户输入翻译为工具调用,将工具输出翻译为用户友好的回复——而非"决策者"的角色。
现阶段,还是应该采用"代码为主、LLM 为辅"的架构设计。关键业务逻辑(如权限检查、状态转换、错误处理)应在代码层面强制执行,LLM 仅负责自然语言交互层。这种设计确保了系统的可靠性,即使 LLM 出现幻觉,也不会导致实际业务错误。
另外,系统提示词设计应遵循"具体优于抽象"的原则。与其告诉模型"不要说谎",不如明确列出"不要说'已经执行'"。Few-shot 示例比抽象规则更有效,因为它为模型提供了可模仿的具体行为模式。
LLM 幻觉问题是当前大模型应用落地的主要挑战之一。幻觉无法完全消除,但可以通过合理的架构设计和提示词工程将其影响降至最低。在智能体系统开发中,应该将 LLM 视为"不可靠但有用"的组件,通过代码层面的约束确保系统的整体可靠性。
随着大模型技术的不断发展,幻觉问题可能会通过模型本身的改进而缓解。但在可预见的未来,"代码约束 + LLM 交互"的混合架构仍将是构建可靠智能体系统的主流方案。