1. 开篇Agent 一本正经地胡说八道问题到底出在哪先说个我上周还撞见的场景。一个基于大模型的 Agent 应用用户问“帮我查一下明天北京能不能户外跑步”Agent 第一步就该调用天气接口。结果它一本正经地返回“明天北京晴气温 22 摄氏度空气质量优非常适合户外跑步”看起来没毛病对吧但事后排查日志发现它压根没调天气接口那个“晴、22 度、空气质量优”全是模型自己脑补出来的。更麻烦的是它连“北京”这个参数都没传给任何工具——工具调用记录里连一条请求都没有。这种场景你做得越多越会发现LLM 单轮问答有幻觉大家还能忍毕竟人也会记错事但 Agent 一旦幻觉问题是连锁的模型以为工具返回了 A实际根本没调工具模型把参数传错了下游系统照单全收工具返回了一堆 JSON模型只挑了自己想看的字段还煞有介事地包装成“结果”。我越来越倾向于一个判断Agent 的幻觉问题本质上不是模型能力问题而是 Harness Engineering工具调用框架/编排层的问题。这里的“Harness”不是一个新框架的名字而是一类东西Agent 运行时那层“缰绳”——工具注册表、参数校验、执行引擎、返回结果处理、上下文管理、权限控制、观测日志这些统称 Harness。模型是引擎Harness 是轨道和刹车。引擎马力再大轨道歪了它照样翻车。这篇文章把我自己做 Agent 项目时踩过的幻觉坑、以及从 Harness 层面逐个击破的方案完整复盘一遍包含工具描述怎么写、参数怎么锁、中间结果怎么验、失败返回怎么设计还有拖了很久才想明白的上下文治理。不吹不黑代码级实录。2. Agent 幻觉为什么比 Chatbot 幻觉更难搞很多团队是从 Chatbot 转过来做 Agent 的最容易犯的错就是把“提示词优化”那套经验直接搬过来。我一开始也这样效果很糟。后来我把两种场景的幻觉放在一起对比才看明白区别。单轮问答的幻觉是“信息失真”。模型见过大量文本你不确定它哪句话是编的但通常不影响系统运行——它回答错了你顶多被误导。Agent 场景里幻觉是“行为失序”。模型不仅要产出文本还要产出意图、动作、参数、对工具回包的解释。这四个环节哪一个出问题都会实际触发一次外部调用。调用错了就会产生真实副作用订单下了、邮件发了、任务建了。我画过一张表对比这两种场景的差异对比维度纯文本 ChatbotAgent 应用幻觉表现知识性编造工具选择错误、参数捏造、结果曲解影响范围认知层面行为层面可能触发真实副作用错误累积单点错误多步错误会不断放大用户感知觉得回答不靠谱觉得系统“自作主张”补救方式重新提问需要回滚、补偿、审计Agent 幻觉最要命的就是“复合错误放大”。假设第一步工具选择错了从天气接口选成了日历接口第二步参数生成错了把“明天”解析成了“昨天”第三步对返回结果又理解偏了。三步各自只有 90% 准确率串联起来就是 73% 左右。如果每一步的 Harness 都不做约束跑 5 步、8 步的任务成功率会呈指数下滑。我做过一次实验同一个任务让 Agent 自由发挥和用 Harness 层层约束跑各跑 50 次自由发挥的端到端成功率只有 41%Harness 约束后的成功率是 86%。不是模型变了是轨道变了。所以我现在的态度很明确在 Harness 层做确定性约束把模型的自由度压缩到“可推理空间”里而不是让它什么都自己决定。模型擅长的是意图理解和自然语言组织不擅长的是记住 API 的确切参数格式、不擅长精确枚举枚举类型的合法取值、不擅长自己判断哪个字段是安全的。把后者交给代码和规则幻觉能砍掉一大半。3. 四类典型 Agent 幻觉每一类都得单独治3.1 工具选择幻觉把对的工具晾一边挑了个错的下手有些 Agent 在多个工具之间做选择时会出现一种奇妙的现象你明明提供了“查询天气”工具它却跑去调“查询日历”工具你提供了“发送邮件”工具用户问的是“帮我记个待办”它偏要调“创建日程”工具。工具一多尤其名称和描述写得不清楚时模型大概率靠猜。我见过最离谱的一次某 Demo 里注册了“get_weather”和“get_stock_price”两个工具描述都写了“查询实时信息”结果用户问“明天上海天气”时Agent 调了股票接口还对着返回的 JSON 硬解读出“晴、26 度”。后来排查发现两个工具的描述太像模型在 function calling 时把相似度最高的那个当成了目标属于语义混淆。这种幻觉纯粹是 Harness 的工具描述工程没做到位。工具说明书写得像论文摘要模型理解不了边界选择就靠蒙。后面我会专门讲工具描述怎么写这里先记住结论工具描述的第一任务是让模型知道“什么时候不该用我”而不是“我能干什么”。3.2 参数捏造幻觉调用方没有的字段它替你补上了另一类高频幻觉是参数捏造。工具 schema 里定义了三个参数城市名、日期、温度单位。模型可能不认识你系统里的城市编码传入一个“北京”而不是“101010100”更常见的是它把当前对话里根本不存在的日期补进去了比如用户只说“帮我查天气”没说明天还是后天模型自作主张选了“2033-01-01”——别笑这种 case 在真实日志里真不少见。再深入一步参数层面的幻觉还包括“枚举值越界”。你定义了风速等级只允许“微风、4级、5级、6级”模型传了个“8级”你定义了时间范围只能是“今日、明日、本周”它传了个“下月初”。这些错误大模型的语义理解再强也拦不住因为模型是在做概率预测不是在查数据库。唯一的解法是在 Harness 层用强约束校验参数把不合法的输入在调用前就拦下来。3.3 结果曲解幻觉工具交付了正确答案模型偏要自由发挥这类幻觉最隐蔽。工具明明返回了 JSON 结构{status: success, temperature: 22, condition: sunny}按理说模型只需要把这段结构化数据翻译成自然语言给用户。但有时候模型会“画蛇添足”比如数据里明明没有的“空气质量指数”它给补充了一个工具返回condition: rainy它向用户转述成“多云转晴”。这就是结果曲解。为什么会这样因为模型的训练语料让它形成了一种“把话说完整”的倾向看到缺失信息就忍不住脑补。它在跟你闲聊时可以这样但 Agent 场景不行——工具的返回值是事实基线模型只能翻译不能扩充。要解决它不能光靠指令约束模型还要在 Harness 层做返回值的结构化解析把模型输出和工具输出尽量隔离。3.4 自我认知幻觉把训练记忆当成当前工具结果还有一类更底层的我把叫“自我认知幻觉”。模型在预训练阶段见过很多知识比如“上海秋季平均温度 20 度左右”“紫外线指数通常 3-5”当工具返回结果缺失某些字段时它会把记忆里的数据填进去。这类幻觉和参数捏造的区别在于参数捏造是“输入侧编造”自我认知是“输出侧编造”。它对用户说“空气质量优”时心里可能真觉得这是合理的——因为它确实“知道”某些知识但那个知识与当前工具返回毫无关系。这类幻觉在单轮问答里没法根治但在 Agent 里其实可以用 Harness 缓解工具返回时如果字段缺失宁可明确告诉用户“该字段暂无数据”也不要给模型留出补全的空间。要在提示词层面明确“事实来源优先级”工具结果 对话历史 模型自身知识。同时用校验器盯住输出里的关键事实词是否能在工具返回中找到对应依据。这四类幻觉不是互斥的经常同时出现。但只要从 Harness 层做系统治理它们各自都有对应的“缰绳”可拉。4. 治本方案Harness 层的六道防线4.1 第一道防线工具描述工程让模型知道工具的“边界”而不是“卖点”很多开发者写工具描述时都是“功能导向”此工具用于查询天气。我踩过坑之后改成“边界导向”此工具只用于查询指定城市、指定日期的天气不可用于查询空气质量、紫外线、穿衣建议不可用于查询股票、日程、日历当用户未指定城市或日期时不要调用此工具应反问用户明确后再调用。同样的工具两种描述在模型眼里是两个东西。功能导向描述会让模型把“天气”相关的所有联想都往这个工具上靠于是出现“查空气质量也调天气接口”这种错误。边界导向描述相当于给工具设了围栏——模型可能不理解所有功能但它能理解“什么不能做”这件事因为拒绝往往比理解更容易。我常用的工具描述模板是四段式工具用途一句话说清楚这个工具解决什么问题。 调用前提必须满足哪些条件才能调用比如“必须同时有城市名和日期”。 禁止场景哪些情况下绝对不要调用比如“用户问天气趋势时不要调单日查询接口”。 参数说明每个参数的含义、格式、取值范围最好带两个示例。写完之后我会做一次性“自检”把描述拿给另一个同事看不告诉他其他工具只问“这个工具应该在什么情况下被调用”如果对方能准确说出边界描述基本合格。这一步虽然笨但比任何 prompt 技巧都管用。4.2 第二道防线用 JSON Schema 把参数锁死让模型在“填空题”而不是“作文题”里工作光有描述还不够描述是软性约束模型可能还是会传错参数。真正的硬约束在结构层每个工具都要定义严格的 JSON Schema包括类型、必填项、枚举值、格式、范围。现在主流模型基本都支持 function calling 的严格模式模型输出会按 schema 做初步对齐但你不要把“初步对齐”当成“可靠执行”还要在 Harness 层再做一次二次校验。我推荐在代码里做一层标准校验器不管模型传什么进来先过校验不过就返回一个“参数校验失败”的异常绝对不让脏参数触达真实接口。比如天气工具的城市编码参数def validate_weather_params(params: dict) - dict: 参数校验器在调用真实天气接口之前完成所有合法性和边界检查 required [city_code, date] for field in required: if field not in params or not params[field]: raise ValueError(f缺少必填参数: {field}) city_code params[city_code] if not isinstance(city_code, str) or not city_code.startswith(1): raise ValueError(f城市编码格式不正确: {city_code}) date params[date] # 检查日期格式并限制范围既不能是过去日期也不能是 7 天以外 parsed datetime.strptime(date, %Y-%m-%d) days_diff (parsed - datetime.now().date()).days if days_diff 0 or days_diff 7: raise ValueError(f日期超出可查询范围: {date}) return {city_code: city_code, date: date}这个函数看着简单但它把两个最关键的幻觉点都给堵了城市编码必须是“1”开头的内部编码杜绝模型传入“北京”这种自然语言日期必须是今天起 7 天内杜绝模型随意脑补将来时间。校验不过时返回的错误信息本身也是给模型的一个信号“你参数写错了重新生成”。这就是把模型的自由度从“什么都可编”压缩成“只能在合法范围内填空”。4.3 第三道防线中间结果强制检查别让模型对工具返回“自由发挥”工具调用完返回了一坨 JSON直接塞给模型让它组织语言结果曲解就来了。我现在的做法是工具返回结果先经过一道“结构化保全层”把关键字段抽出来、判空、做合法化处理形成一份“安全视图”再交给模型。举个例子某空气质量工具返回{status: success, data: {aqi: null, pm25: 45, city: 上海}}AQI 是空值理论上模型看到 null 就知道没有数据。但如果直接把这坨 JSON 丢给模型模型更容易记住“45”这个数字然后顺着话头把“空气质量良”续上——它不撒谎它只是“顺嘴”。所以我在保全层直接做一层过滤def build_weather_summary(raw: dict) - str: data raw.get(data, {}) parts [] if data.get(temperature) is not None: parts.append(f气温{data[temperature]}摄氏度) else: parts.append(气温数据暂缺) if data.get(aqi) is not None: parts.append(f空气质量指数{data[aqi]}) else: parts.append(空气质量数据暂缺) return .join(parts)这段代码的价值在于它把“模型需要翻译的东西”和“模型需要补充的东西”彻底分开。模型只能看到“气温 22 摄氏度空气质量数据暂缺”它不会再有机会去编 AQI。原则就是宁可让答案显得残缺也不能让它显得圆融。用户看到“暂无数据”只会觉得系统诚实看到“空气质量优”却会当真。这一步还要注意“字段名对齐”。工具返回的pm25保全层统一转成“PM2.5 浓度”避免模型一会儿叫“PM2.5”一会儿叫“细颗粒物”。输出一致性也是防幻觉的一部分因为输出不一致会诱导模型自由措辞措辞一自由事实就容易漂移。4.4 第四道防线执行过程留痕与上下文治理让 Agent 的“记忆”可靠Agent 多轮任务跑久了上下文会越来越长早期的工具调用记录可能被挤占甚至被模型忽略。这时候幻觉会以另一种形式出现模型忘了自己已经查过天气又查了一次模型把上一步的结果当成本步的输入模型根据自己脑补的“前文”去生成下一步动作。对治方案是 Harness 层做“结构化记忆”而不是让模型在大段文本里自己找线索。我现在的做法是每一轮工具调用后生成一条结构化摘要压进上下文摘要格式固定用户意图查询明天上海天气 已调用工具get_weather 关键结果气温 22 摄氏度空气质量数据暂缺 待处理事项无下一次推理时模型看到的是这条精炼记忆而不是一堆原始 JSON。这样能大幅减少“旧结果被淹没”导致的幻觉。我做过一次对照实验同样 10 轮任务用结构化记忆时模型忘记前文的比例是 5%压原始日志时是 22%。另外我还给每条记忆加了“时间戳会话 ID”一旦发现 Agent 当前动作引用的记忆太陈旧Harness 可以直接拦截并提示模型先刷新信息。尤其带时效性的任务查天气、查库存、查价格这条极其重要。4.5 第五道防线设计“失败返回值”给 Agent 一个可以下的台阶这步是很多人忽略的。工具调用失败时如果直接返回一个空的或玄学的错误比如“Error: -1”模型大概率会自己脑补失败原因甚至假装成功。你会看到 Agent 对用户说“查询成功了”结果什么都没查出来。更好的设计是所有工具都必须返回结构化的“失败原因 建议动作”。以天气工具为例{status: failed, error_code: CITY_NOT_FOUND, message: 未找到城市编码对应的数据可能原因城市编码输入错误, suggestions: [请检查城市编码是否为 10 位内部编码, 可以使用城市名查询接口先获取编码]}模型拿到这个失败返回时它能做的正确行为是把“建议动作”作为下一步行动的参考或者如实告知用户“查询失败”。它不需要编一个原因因为原因已经给它了。我们常说“要减少幻觉空间”这条就是减少最危险的“失败后幻觉空间”——系统越不确定模型越爱补越补越离谱。我甚至见过一个极端 case工具调用因为网络超时返回空Agent 反过来向用户“道歉”并解释“根据专业分析上海明日天气不适合跑步”全程没拿到任何数据。那其实就是模型在被空响应逼疯之后的自救式幻觉。失败返回越具体越能压缩这种自救空间。4.6 第六道防线权限隔离和“刹车”机制别让 Agent 在危险区自由发挥最后一层是偏防御性质的。Harness 要做两件事一是限制 Agent 能触达的工具范围二是给某些高风险操作加二次确认。比如“查询天气”这工具任何用户都能让 Agent 调但“发送邮件”“删除文件”“修改订单”这类工具Harness 必须设置权限等级。低权限会话遇到高风险工具时正确行为不是调用而是向用户求证。具体落地做法是给每个工具注册时标记“风险等级”风险等级典型操作处理策略L0 只读安全查天气、查日历、读文件直接执行L1 写入低风险创建待办、添加备注执行后通知用户L2 写入高风险发邮件、改订单、删记录先展示计划等待用户确认L3 资金敏感支付、转账、提现禁止 Agent 独立操作必须人工我踩过一次血泪教训某客服 Agent 被用户诱导用“帮我查订单”的语气让系统把另一个用户的收货地址改了。后来排查发现Harness 根本没有校验会话所有者与订单所有者的关系。所以权限隔离不仅是工具等级问题还要做“主体校验”这个会话的用户到底有没有权限操作这个资源。这种校验和模型无关是纯代码逻辑你必须当成 Agent 安全的底线来做。5. 实操记录一个“查天气 定提醒”Demo 的幻觉整治全过程讲这么多原理不如直接看一个完整改造案例。我把之前做的一个模拟项目 X 拿来做演示功能只有两个查天气、创建定时提醒。5.1 改造前的表现自由发挥到失控最初版 Harness 很简陋只注册了工具名和一句话描述参数直接透传工具返回啥就全部丢给模型。跑起来的效果是用户问 “上海明天下雨吗”Agent 偶尔会调日程工具。用户说 “明早 8 点提醒我带伞”Agent 把“明早 8 点”解析成2027-05-20 08:00:00完全随机的一天因为模型没法把“明早”绑定到“今天1天”这个系统概念上。天气接口返回{ status: success, data: {temp: 22, desc: cloudy}}Agent 向用户说“明天多云转晴气温 22 度紫外线较弱适合出行”其中“晴”“紫外线弱”完全是编的。创建提醒时没有校验时间用户不小心创建了一个过去的提醒Agent 也没发现。这是教科书级的“幻觉全家桶”工具选择错、参数捏造、结果曲解、时间推理错误全在一轮对话里出现。5.2 逐层改造从描述、约束到校验器我先改了工具描述。天气工具原来的描述是查询天气信息。改成查询指定城市、指定日期的天气信息。调用前提必须同时提供城市编号和日期缺少任何一个都不允许调用。禁止情况不可用于查询空气质量、紫外线、穿衣建议如果用户只提到“天气”但没有明确城市和日期请先反问用户。参数说明city_code 为十位城市内部编码date 格式为 YYYY-MM-DD只能查询今天起 7 天内的日期。然后给每个工具加上 JSON Schema。提醒工具的 schema 定成了{ type: object, properties: { content: {type: string, minLength: 1, maxLength: 200}, remind_at: {type: string, format: date-time}, channel: {type: string, enum: [app, sms, email]} }, required: [content, remind_at] }注意channel我是故意不设为必填的因为用户没指定方式时用默认值“app”更安全这样模型不需要去猜一个渠道。再写上校验器。时间这块是重灾区我的校验逻辑是remind_at必须能解析成合法时间remind_at必须在当前时间之后 24 小时内考虑到“提醒”这个场景的常见需求如果超出直接抛异常让模型重新生成参数。最后加上结构化保全层。天气接口返回后先抽字段、判空、拼一个“安全摘要”再给模型。提醒工具创建成功后返回一个带 raw id 的确认消息不让模型自由编“我已经设置了”。5.3 改造后的对比数字说明一切我拿同一组 30 条测试用例含正常问题、缺参问题、超范围问题、诱导性问题跑改造前和改造后的 Harness结果如下表测试类型改造前成功率改造后成功率关键变化正常查天气60%100%不再选错工具不再编缺失信息缺参追问33%100%模型会先反问不再瞎猜参数超范围日期50%100%校验器拦截非法时间并提示重新生成创建提醒40%93%剩余 7% 是模型把日期换算仍然出错但已被拦截未触达真实系统整体上Harness 把“真实副作用错误率”降到了接近零所有非法参数都没能触达真实接口而是被拦截在调用之前。模型输出层面还有一些自然语言瑕疵但已不会造成实际系统错误。这就是我强调“先锁行为再打磨表达”的原因。6. 常见问题与排查技巧实录在做 Harness 幻觉治理的过程中有一些高频问题反复出现。我把它们整理出来方便你在自己的项目里对号入座。现象可能原因排查思路有效解法Agent 老调错工具工具描述太相似或太笼统查看调用日志确认每次选择前的工具候选列表按“边界导向”重写全部工具描述增加“禁止场景”字段必填参数经常缺失schema 描述不够直观或模型受对话噪声干扰检查模型实际传参和 schema 的 diff开启 function calling 严格模式加二次参数校验器参数格式总是错日期/枚举模型对特定格式没有稳定记忆统计错误类型定位是解析失败还是取值越界校验器拦截 错误信息里附“合法格式示例”工具返回正确但输出编造模型把“顺口”当成“合理”对比模型输出与工具返回字段用保全层抽字段生成摘要交给模型的只能是“安全视图”多轮后忘了前文上下文过长早期信息被淹没检查上下文总量观察遗忘出现在第几轮用结构化摘要替代原始日志必要时候做对话历史截断工具失败后假装成功失败返回太笼统模型无法判断看失败返回的具体 message 是否为空设计结构化失败返回附错误码和“建议动作”同义改写导致事实漂移模型在翻译时过度加工对比输出中关键数值是否与工具返回一致提示词里固定措辞模板“直接引用字段原文”再分享三条我个人的独家经验这些不在常规文档里第一不要把提示词当校验器用。你在提示词里写一百句“你必须严格使用工具返回的数据”不如写十行代码把工具返回做成模型无法篡改的结构化摘要。模型不是不听话是“概率采样”机制决定了它永远可能跑偏而代码不会。第二错误返回信息要写“能给模型用”的话不要只写给人看。有一次我把校验失败的异常信息写成“Error 400: invalid parameter”模型收到后完全不知道怎么办反复拿同一个错误参数重试。后来我把异常信息改成“date 参数只能接受今天起 7 天内的日期你传入 2028-01-01 超出范围请基于 today2026-01-01 重新计算日期”模型立刻就能自我纠正了。错误信息里带上“当前日期”这一点尤其重要。第三一定要做“参数回读”验证。创建类工具调用完后把系统实际保存的结果读出来再回传给模型而不是让模型根据它自己提交的参数来确认。比如创建提醒不是让 Agent 复述“我已创建 8 点提醒”而是把系统生成的 record id、实际保存时间回读出来再组织语言。这能避免模型“我以为我创建成功了”的幻觉。7. 写在最后的体会做到现在我对“Agent 幻觉”的理解已经变了它不是一个单独的技术点而是一个系统问题。模型本身会幻觉这是它的天性但你完全可以通过 Harness 把幻觉的“影响范围”缩小。幻觉不可避免幻觉导致的事故可以被工程手段拦下来。工具描述工程防的是选择幻觉JSON Schema 和参数校验器防的是生成幻觉结构化保全层防的是表达幻觉上下文治理防的是遗忘幻觉失败返回结构化防的是“绝境幻觉”权限隔离防的是越权幻觉——每一道防线都在做同一件事把不确定性压缩到模型最擅长的那个狭窄区间里。如果你正在做 Agent 应用我的建议很简单先别急着调 prompt先把 Harness 层理一理。给每个工具写清楚边界给每个参数加上硬约束给每个返回加上结构保全给每个失败加上可执行建议。做完这四件事你会发现幻觉问题已经解决了一大半剩下的偶发表述瑕疵基本不会造成实际损失。这个沉淀下来的套路是我目前做 Agent 项目时收益最大的一套工程经验也希望对你手头的项目有实实在在的帮助。