Claude Code是如何彻底解决大模型工具调用的幻觉问题?
随着Claude code源码被逆向,我们可以更好的借鉴这个产品优秀的设计思路,来解决我们开发agent的各种问题。
不管大语言模型(LLM)有多聪明,它们本质上是“文字接龙”的高手。当它们被赋予了使用工具的操作权限(比如读写文件、运行命令)时,常常会犯一些让人头疼的毛病,也就是所谓的 “幻觉”,比如:
- 捏造工具:调用了一个根本不存在的工具。
- 胡填参数:少传了必填参数,或者把数字填成了字符串。
- 急于作答:还没确认工具是否执行成功,就自信满满地说“我已经搞定了”。
- 健忘症(丢失上下文):前面执行过的工具结果,聊着聊着就忘了。
claude-code 项目作为一个强大的 Agent(智能体),为了解决这些问题,在架构设计上花了很多心思。简单来说,项目并没有指望大模型“不犯错”或者“变聪明”,而是搭建了一套 “发现错误 -> 结构化报错 -> 引导模型自己修正” 的闭环防御系统。
下面我们结合代码,用通俗易懂的方式来拆解这套机制。
一、核心原理:工具调用是怎么跑起来的?
首先,在 claude-code 中,大模型和本地工具库是怎么沟通的呢?请看下面的流程图:
这里面的核心机制在 src/query.ts 中的 queryLoop(查询循环)。
这个循环是一个 while(true) (死循环):
- 把以前的聊天记录、可用的工具列表,打包发给大模型。
- 大模型如果不确定答案,就会回复一个特殊指令:
{"type": "tool_use", "name": "读文件", "input": {...}}(我需要用工具)。 - 本地代码拦截到这个指令,去执行真正的动作。
- 本地代码把执行结果打包成:
{"type": "tool_result", "content": "文件内容是..."},再次发给大模型。 - 大模型看到结果后,再决定是继续用其他工具,还是给出最终回答。
二、怎样防止大模型产生“幻觉”?(十层防弹衣)
为了防止模型“胡作非为”,claude-code 在每个关键环节都安检,相当于给系统穿了“十层防弹衣”。
🧱 防弹衣 1:防“胡言乱语”(系统提示词约束)
在项目启动时(src/constants/prompts.ts),代码会给大模型发一段非常严格的“纪律要求”:
- 要求严谨:“在没看懂代码前,不要乱提修改建议。”
- 要求诚实:“如实报告结果!如果测试失败了就说失败,千万别说谎掩盖。没运行过的事情就直说,别暗示你成功了。”
这是从心理学层面(Prompt 工程) 预防它产生幻觉。
🧱 防弹衣 2:防“凭空捏造”(不存在的工具检测)
有时候模型会自己“发明”一个工具并尝试调用。面对这种情况,代码没有崩溃(见 src/services/tools/toolExecution.ts)。
它会拦截:“等一下,你用的这个工具的名字不存在!”
然后,系统会把这个错误信息当做一次工具执行结果反馈给模型:
[错误:没有找到你调用的这个工具,请看下现有的工具列表重新选择]
模型收到这个错误后,一般会自己“哦,不好意思,应该是用 XXX 吧”,然后自动修正。
🧱 防弹衣 3:防“张冠李戴”(Zod 参数严格校验)
找到了对的工具,但模型乱填参数怎么办?(比如本该填数组的地方它传了字符串)。claude-code 所有的工具定义都使用了 Zod(一个很强大的类型校验库)。
在执行工具前,Zod 会先扫一眼参数:
- 校验失败:立刻终止,并生成一份清晰的错误报告(比如 “参数
file_path是必填的但你没填”),扔给模型让它重发。 - 这也保证了真正的工具逻辑代码,永远拿到的都是安全的、格式绝对正确的数据。
🧱 防弹衣 4:防“纸上谈兵”(业务逻辑深度验证)
参数格式对了就一定安全吗?不一定。
每个工具自己还有一个 validateInput 函数进行深度体检,比如在 FileEditTool(文件编辑工具)中:
- 你要修改的文件,实际上存在吗?
- 文件会不会太大(超过 1GB)把内存撑爆?
- 你是不是试图在没读取过这个文件的情况下,就要盲写修改它?
如果有以上问题,同样会报错打回,让模型重新走正规流程。
🧱 防弹衣 5:防“看太多眼花”(ToolSearch 延迟加载法)
大模型有两点不好:一是读的内容太多会分心,二是内容太多很贵(烧钱)。
项目里有 50 多种内部工具和外部工具。如果一开始就把所有工具的详细说明书塞给大模型,它很容易晕,甚至产生幻觉乱用。
所以通过 ToolSearchTool 实现了**“延迟加载”**:
- 一开始只告诉模型有这些工具的“名字”,但不教它怎么用(隐藏具体参数要求)。
- 当模型想用某个复杂工具时,它发现自己不知道参数,它就得先用
ToolSearch这个工具,去查目标工具的说明书。 - 查完学会了,再回过头去调用真实工具。
这个妙招不仅省钱,还大大降低了模型混淆工具参数引发的幻觉问题。
🧱 防弹衣 6:防“做事有头无尾”(强制 result 协议闭合)
底层 API 规定:模型只要发出了 tool_use(工具调用请求),客户端就必须必须给它反馈 tool_result(工具执行结果)。
如果遇到网络突然断开、或者用户强行按 Ctrl+C 中断了代码,那模型岂不是在苦苦等待结果?
代码里有一个专门收尾的医生(yieldMissingToolResultBlocks):如果遇到意外,医生会挨个给刚才没执行完的工具补一张死亡证明([系统错误:用户中断了]),发送给模型。这样永远保证了对话系统的连续性。
🧱 防弹衣 7:防“滔滔不绝被斩断”(输出截断自动恢复)
大模型每次说话的字数是有上限的(比如 8000 个 Token)。
如果模型写代码写到一半被系统强行掐断(达到上限:max_output_tokens 错误),就会引发严重的上下文错乱。claude-code 很聪明:它并不会把这个错误抛给用户,而是偷偷瞒着用户,向模型发一条紧急消息:
“你刚才的输出达到了字数上限被截断了,不要道歉,不要总结,直接从上一句话断掉的地方接上继续说。”
系统最多会自动抢救 3 次,完美解决模型长回答中断的 Bug。
🧱 防弹衣 8:防“自我感觉良好”(Stop Hooks 后验检查)
解决 “急于作答 (Premature Answer)” 的终极武器叫 Stop Hooks。
大模型有时候改了一行代码,觉得没问题了,就准备对用户说“我完成任务了”。
此时,Stop Hooks 发挥作用。系统在模型报告完成之前,会自动运行一堆检查脚本(比如项目自动编译跑一遍、单元测试跑一遍)。
只有测试通过了,模型才能真正对用户撒花。如果测试报错了(Blocking Errors),报错信息会被硬塞回给大模型:“别急着走,你写的代码跑不通(附上报错),赶紧再改改!”
🧱 防弹衣 9:防“左手打右手”(并发安全分区)
模型聪明的时候,会一次性发好几个工具调用的请求。
但是不同的工具能不能一起跑?
- 读数据(只读):随便并行,大家互不干涉。
- 写数据/删文件:不能并行,万一 A 工具刚写进去的内容被 B 工具删了呢。
系统通过isConcurrencySafe()标记,把工具进行分类。一旦发现冲突,就老老实实排队串行执行,避免因并发导致的系统状态混乱(这种混乱反馈给模型,会让它彻底抓狂)。
🧱 防弹衣 10:防“狗熊掰棒子”(历史自动压缩)
对话越长,前面发生的事情模型越容易忘(遗忘型幻觉)。
系统包含一套超轻量的自动记忆压缩机制 (autocompact)。当历史字数超过安全阈值时,程序会将前面的旧聊天“折叠”成一句话摘要,并清除掉旧的、没必要长久记忆的冗长工具结果内容。
配合提示词:“请随时总结你看到的重要信息,因为原本的冗长日志可能会被清除”,引导模型养成做短笔记的好习惯。
三、这个“死循环”是如何退出的?
虽然核心的 queryLoop 是一个 while(true) 的“死循环”,但它并不是真正的无限执行下去。它有几个精确设计的 “退出通道”,当检测到以下 5 种情况时,循环就会终止并把控制权交还给用户:
1. 自然退出:大模型觉得“办完了”(正常流程)
每次大模型回复后,程序都会检查:模型本次有没有生成调用工具的代码块?
如果没有调用工具,意味着模型分析发现它已经收集到了足够的信息,开始用“纯文本”直接给出最终的答案了。
代码中有一个标志位 needsFollowUp 会变成 false。循环检测到之后,运行结算逻辑(如 Stop Hooks 检查),然后通过 return { reason: 'completed' } 完美谢幕。
2. 人工阻断式退出(用户的 Ctrl+C)
如果大模型在使用工具时“一意孤行”或者调用耗时太久,用户按下了 ESC 或 Ctrl+C。
底层的 AbortController 就会发出中断信号。系统捕获后会停止所有请求,给模型留下一句“被用户强制中断”的记录,并通过 return { reason: 'aborted_streaming' } 紧急抛出退出。
3. 被“督导”拦下式退出(Stop Hooks 的一票否决权)
即使模型自己觉得完成了,触发了后验检查脚本(Stop Hooks)。如果测试脚本报错,且某个特定的高级 Hook 认为“这个任务偏离了方向已经无法挽回”,它可以发出一个特殊信号 preventContinuation: true。
系统看到这个信号后就会认输,通过 return { reason: 'stop_hook_prevented' } 强行退出并告知用户。
4. 彻底崩坏的边缘退出(达到 Token 的物理极限)
即便有自动抢救功能(截断自动接力、聊天历史压缩等),抢救也是有极限的:
- 输出超限:如果模型连续 3 次被截断且没能把话接下去。
- 输入框爆满:如果上下文怎么压缩都还是超过最高承载量(Prompt Too Long),导致彻底发不出去请求。
这个时候,系统只能无奈地通过return { reason: 'prompt_too_long' }或image_error报错退出。
5. 钱烧完了系统强杀退出(预算耗尽)
为了防止进入死胡同导致疯狂调用 API 产生巨额账单:
系统会统计这一轮循环消耗的 Token 预算 (task_budget)。如果消耗达到了预设的安全阈值,循环机制也会被强行掐断,输出提示“Token 预算使用达到上限”后退出。
一句话总结:只要大模型还在要求“我要用工具”、只要任务预算充裕、且测试没有通过,这个死循环就会一直自动转下去帮你打工;直到它真正得出了答案闭嘴、被人为叫停、或是把所有容错方法都试完后,循环才会终止。
四、最终总结
看完以上机制,总结一下 claude-code 防止 LLM 乱来的核心哲学:
“不相信模型,拥抱失败,构建反馈闭环。” (Fail-closed with feedback)
它假设大模型就是一个“虽然聪明但毛手毛脚的员工”。系统不祈祷员工永远不犯错,而是建立了一套极其完善的规章制度(Schema校验),并在员工犯错的每一种可能性(记不住、乱写、瞎猜、做到一半跑路)上,都安装了监控和引导:
只要你犯错,系统就会拦截,并且把错误原因翻译成员工听得懂的话(报错反馈)丢给你,让你自己修。
这套体系保障了 claude-code 在实际应用中无比强大和稳定。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)