ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

LLM Agent非确定性治理:评估、稳定化与线上可靠实践

LLM Agent非确定性治理:评估、稳定化与线上可靠实践

你有没有遇到过这种场景:用户向 Agent 问了同一个问题,第一次得到一条完整准确的回答,第二次却得到完全不同的结果,甚至直接跑偏。用户不会关注底层模型有多聪明,只会留下一句"这个机器人不稳定"。这其实是所有生产环境 LLM Agent 系统都必须面对的核心矛盾:模型本身具备非确定性,而生产体验要求稳定可靠。这篇文章就来聊聊 Agent 工程里最关键的"非确定性治理":问题从哪来、如何用评估和迭代的方式把随机输出一步步约束住,最终让系统在线上交出可靠体验。

1. 非确定性不是 bug,而是 LLM 系统的底层属性

1.1 同样的问题,为什么第二次回答就不一样了

LLM 本质上是一个基于概率的序列生成模型。它在生成每一个 token 的时候,都在预测"下一个词最可能是什么",再加上采样策略(temperature、top-p),导致同一个 prompt 被多次调用时,输出天然存在波动。

很多团队会下意识认为"只要把 temperature 调成 0 就没问题了"。实际上并非如此:部分模型在批量推理、投机采样、并行解码等场景下,即便 temperature 为 0,也会因为计算顺序和浮点精度差异产生不一致的输出。所以非确定性是消除不了的,只能通过外围工程手段去控制,这是 Agent 工程和传统软件开发在认知层面最本质的区别。

1.2 Prompt 远比你以为的更敏感

Agent 系统的每个决策环节都以 prompt 为驱动:意图识别、工具选择、参数填充、结果解析……每个环节都暴露在 prompt 敏感性之下。修改一个措辞、改变示例的排列顺序,都可能引发行为偏移,而且这种偏移往往不是局部性的。

举个例子。某团队在系统提示词里把"不要编造订单号"改成"请严格使用用户提供的订单号",本意是更严谨,结果模型开始对没有附带订单号的正常提问也拒绝回答。这就是典型的 prompt 敏感性:你以为是在做收紧约束,模型理解成"所有查询必须有订单号"。这类行为漂移只有通过回归测试才能发现,靠肉眼 review 文本很难觉察。

1.3 上下文越长,决策越容易漂移

Agent 是多步交互系统,每执行一步,对话历史、工具调用记录就会继续往上下文里追加。上下文一大,模型对早期约束的记忆会逐渐衰减,尤其当工具返回的内容里存在重复、冗余或矛盾信息时,模型很容易被带偏。

比如一开始告诉模型"只回答与订单相关的问题",但在第五轮对话时,模型可能已经忘了这条约束,开始回答无关内容。这种"约束遗忘"不是模型变笨了,而是注意力分配被不断更新的内容挤占。处理办法不是反复在对话里提醒,而是把关键约束放在用户输入之前,并且每轮都强制模型先输出一段结构化思考摘要,把约束检测显式化。

1.4 单步小误差,链路大事故

一个任务可能要拆成"理解意图→选择工具→填充参数→解析返回→组织回答"好几步。每一步的正确率就算有 95%,五步下来整体成功率也只有 77% 左右。链路越长,不确定性越是乘法叠加的关系,而 Agent 恰恰是天然的长链路系统。

0.95^5 ≈ 0.774

这也是为什么 Agent 类应用比单轮问答的出错率高一个数量级。单轮问答错了,重新问一次即可;Agent 链路里任何一个节点出错,最终交给用户的结果就是错的,而且排查成本高得多。理解这个放大效应,才会明白为什么要在每一个节点上都做防御,而不是只在最终输出处加一道检查。

1.5 环境漂移:线上系统永远在流动

除了模型本身的不确定,生产环境里还有一层"外部漂移"。模型服务商升级了底层版本、工具接口返回结构的字段名变了、Prompt 模板被同事顺手改了一句……这些变化都不会立刻暴露在功能测试里,但会在某个时间点突然改变 Agent 的行为分布。

很多团队遇到过一种诡异现象:某天线上成功率突然掉了几个点,但代码和配置都没有改动。查到最后,往往是底层模型服务悄悄更新了版本,或者某个数据源的结构发生了变化。这种漂移无法预测,所以必须靠监控基线和版本管理来接住。这也是为什么"迭代优化"不是一个阶段性的动作,而是 Agent 系统的常规状态。

2. 没有评估基线,迭代就是原地打转

2.1 为什么"感觉变好了"不可信

对非确定性系统做优化,最难的是判断"这次改动到底有没有用"。今天感觉效果好,可能只是随机波动;明天感觉变差了,也可能只是某个样本恰好碰上了。没有稳定的评估基线,所有迭代都建立在感觉之上,改来改去也说不清哪一版更好。

我在实际项目里见过不少团队,优化 prompt 完全靠"多试几次看结果顺不顺眼"。这种方式的致命问题在于:非确定性系统里,单次结果的好坏不能代表整体质量。一次调用成功可能是运气,一次失败也可能是运气。只有在一个固定、可复现的样本集上统计多次运行的结果,才能得出有统计意义的结论。

2.2 评估集从哪里来

评估集是迭代优化的地基。它不是随便找几十条问题凑合,而是从真实业务场景里沉淀出来的。通常建议从这么几个渠道收集:

  • 线上日志中抽样的真实用户问题;
  • 历史 Bad Case:用户反馈不满意、规则引擎告警、人工抽检查出来的错误;
  • 边界场景:空输入、超长输入、多轮对话、指令模糊的请求;
  • 工具异常场景:工具返回空结果、超时、参数非法等。

这里有个容易被忽略的点:评估集里既要有"常见问题",也要刻意保留"稀有但重要"的问题。如果评估集全是高频场景,优化的结果很容易牺牲掉长尾场景。

评估集不是一次建完就完事。我见过做到比较成熟的项目,每星期都会往评估集里补充新的失败样本,保持它对线上真实分布的覆盖度。一套半年不更新的评估集,就像一套半年不更新的测试用例,参考价值会越来越低。

2.3 评估维度怎么设计

不同业务对"可靠"的定义不同。以带工具调用的 Agent 为例,通常会拆出这样几个维度:

  • 意图识别正确率:用户到底要什么,模型有没有理解对;
  • 工具选择正确率:选没选对工具;
  • 参数填充正确率:参数是否完整、格式是否正确;
  • 回答与工具返回的一致性:模型有没有忠实于检索/查询结果;
  • 约束与安全:该拒绝的请求是否被正确拒绝;
  • 效率:平均调用次数、耗时、Token 消耗。

实际项目中不一定每个维度都建独立指标,但至少要把这些维度都覆盖到。因为优化某一项很容易牺牲另一项:比如为了让回答更"友好",模型可能开始绕开工具直接编造结果;为了让"成功率"数字更好看,模型可能总是拒绝复杂任务。评估维度就是给系统立规矩,告诉它哪些红线不能碰。

2.4 评估怎么执行,怎么形成基线

离线评估流程一般是:把评估集批量回放给 Agent 系统,让系统带着真实链路(该调工具就调工具、该读数据就读数据)跑一遍,然后打分。打分可以分三层:

  1. 规则匹配:输出格式、枚举值、必填字段是否合规;
  2. 大模型裁判:让一个独立的 LLM 按评分标准对回答质量打分;
  3. 人工抽检:对置信度低的样本由人来复核。

三层机制各有用处:规则匹配快且稳定,但只能检查表面;大模型裁判覆盖度高,但本身也是非确定性模型,需要把评分标准写得很细;人工抽检准,但成本高,只适合用来校准裁判的尺度。

这样得到的一组分数就是当前版本的基线(baseline)。以后每次改动 prompt、升级模型、调整链路,都在同一组评估集上重跑,对比基线看是变好还是变差。这才是"迭代优化"真正的起点——没有基线之前的一切修改,都只能叫试错。

3. 把随机输出约束进工程框架:四个稳定化手段

评估解决了"怎么知道好不好"的问题,接下来要解决"怎么让它好"的问题。这里分享四个在生产里被反复验证有效的手段。

3.1 结构化输出:别让模型自由发挥

Agent 工程里最怕的是模型输出一段自然语言,让下游去猜意思。正确做法是给每一步输出定义严格结构,用 JSON Schema 约束字段、类型和枚举值。工具调用最好走 function calling 机制(即对话模型原生的函数调用接口),让模型输出结构化的调用指令,而不是自由文本。

给一个简单的 schema 示例:

{ "type": "object", "properties": { "intent": { "type": "string", "enum": ["query_order", "cancel_order", "other"] }, "order_id": { "type": "string" }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 } }, "required": ["intent", "confidence"] }

有了这个约束,下游解析就不需要费劲理解自然语言,格式错误率会大幅下降。但要注意:JSON Schema 只能约束"形状",不能约束"语义"。模型完全可能输出一个格式合法但内容错误的 JSON,所以结构化输出只是第一步,不能替代校验层。

3.2 校验-重试回路:给模型第二次机会

再严格的结构化约束也不可能 100% 生效。模型可能漏掉必填字段,也可能把枚举值写错。所以生产级 Agent 必须在模型输出之后加一道校验层:校验不通过,就把错误信息回传给模型,让它带着错误信息重新生成。

伪代码示意:

MAX_ATTEMPTS = 3 for attempt in range(MAX_ATTEMPTS): output = call_llm(prompt, schema) errors = validate(output, schema) if not errors: return output prompt = prompt + "\n上次输出未通过校验,错误:" + str(errors) + "\n请修正后重新输出。" return fallback_response()

这个循环看起来简单,但实际效果非常好。因为它利用的是模型"知错能改"的能力——只要告诉它哪里错了,它大概率会按正确格式再生成一次。要注意的是:

  • 设置最大尝试次数,防止无限调用浪费 Token;
  • 每次重试把错误信息明确拼接进 prompt,而不是让模型盲猜;
  • 超出次数后走降级逻辑,给固定兜底回答。

很多人会担心重试增加延迟和成本,实际跑下来这个开销远小于问题重新排队带来的损失。多数情况下,一次重试就能纠正格式问题,额外代价在可接受范围内。

3.3 降低链路复杂度:拆分任务、锚定示例

非确定性在单步上的风险无法根除,但可以通过设计把风险收敛到最小。比如把一个大而复杂的任务拆成多个小步骤(单纯意图识别 + 单独参数抽取),每一步只负责一件事,模型犯错的概率会小很多。一个小任务意味着更短的 prompt、更聚焦的指令、更少的上下文干扰。

同时尽量把 Few-shot 示例与控制指令放在显眼位置。示例的作用就是锚定行为,一个贴合的示例比十句抽象的"不要犯错"有效得多。但示例也不是越多越好,过多的示例会拉长上下文,反而稀释注意力。实践下来,同类型示例三到五条足够,关键是覆盖到高频变化形态。

3.4 护栏与降级:确定失败时的行为

可靠的系统不是永不失败,而是在任何失败路径上都有确定的行为。生产部署时必须设计好这样几层护栏:

  • 内容安全过滤:模型输出先过一层敏感词/内容策略检查;
  • 调用失败兜底:工具异常、网络异常时返回固定话术;
  • 无结果降级:确认不了的信息绝不伪造,明确说"无法确认";
  • 上限保护:单次任务限制最大工具调用次数,防止死循环。

这些兜底行为本身要写死在代码里,而不是交给模型自己决定。很多 Agent 系统翻车,不是因为模型能力不行,而是失败路径上没有任何确定性的兜底,最终行为和用户的期望随机匹配。把"失败时会怎样"提前设计清楚,系统从"可能失控"变成"可控的波动"。

4. 线上失败模式与排查:从一次错误回答定位到根因

4.1 高频失败模式

生产环境里最常见的失败,往往是几类固定模式:

失败模式典型表现常见根因
参数幻觉工具调用里的参数来自模型编造用户输入缺失信息,模型自行补全
解析错误工具返回正确,但模型没正确提取结果结构复杂,prompt 没交代清楚
行为漂移改动整体 prompt 后旧场景变差prompt 敏感性,缺少回归测试
循环调用反复调用同一工具,状态不前进没有设置调用上限,模型陷入死局
忽略约束用户诱导下绕过系统约束护栏不严密,对抗性提问没有拦截

这些失败模式最麻烦的地方在于:它们不一定每次出现,可能出现一次消失一次,所以很难通过"多试几次"来定位。必须有完整的 trace 记录,才能在失败发生的那一次里去还原现场。

4.2 排查链路:不要靠猜,要靠轨迹

对非确定性系统,靠"重新跑一次看看能不能复现"来排查是最低效的方式。因为随机性决定了复现不了不等于问题不存在。正确的排查链路是:

  1. 从监控面板或用户反馈拿到出问题的请求 ID;
  2. 打开该请求的完整轨迹(trace),逐条查看每一步的输入输出;
  3. 找到第一个与预期不一致的节点,而不是盯住最终回答;
  4. 针对该节点单独做小范围复测,确认根因;
  5. 把这次请求保存为回归用例,修复后在同一请求上验证。

这里我建议每个 Agent 项目从一开始就接入可观测性平台,记录完整调用链、Token 消耗、模型输出。没有轨迹数据,任何排查都只能看到冰山一角。很多团队第一批产出评估集和 trace 系统的时间,往往比他们预期得更晚,而恰恰是这个环节决定了后面优化效率的高低。

4.3 一个完整的定位修复实例

某团队做了一个物流查询 Agent(模拟项目X),上线后约 12% 的查询会把"运输中"的包裹说成"已签收"。最终回答五花八门,很让人头大。

排查过程:先拿到一条失败请求的 trace。发现 Agent 成功调用了物流查询接口,工具返回了一个包含多条轨迹记录的 JSON 数组。但模型在组织回答时,直接读取了数组的最后一个元素,没有对"状态字段"做任何枚举判断,而最后一条记录恰好是"已签收"的历史状态,于是把历史节点当成了当前状态,回答自然出错。

根因确认后做了两件事:

第一,在 prompt 里明确交代:"必须使用状态字段最新记录,且只把状态值映射为运输中、已签收、已揽收等枚举结果"。 第二,在回答生成前加一道规则校验:状态值必须存在于预设枚举集合中,否则触发一次重试。

改动后同一评估集上的失败率从 12% 降到 1% 以下,且连续一周没有再出现同类错误。

这个案例比较小,但是很有代表性:问题不在模型不聪明,而在于缺少对输出语义的校验层。很多稳定化手段都是在这个层级上做文章。排查的意义就在这里——把一次线上故障变成一条新的评估用例,让同类问题之后再也跑不出视线。

5. 让可靠性成为常态:监控、回放与灰度迭代

5.1 线上监控什么,怎么设置告警

生产环境永远会出现离线评估覆盖不到的意外。所以上线之后,必须建立一套持续监控体系。核心指标可以参考这样一个清单:

指标统计口径关注原因
任务成功率完整跑完且最终输出合法的请求占比整体可靠性最直接的度量
平均调用次数每任务调用大模型的次数次数异常升高说明可能循环
重试率触发校验重试的比例过高说明结构化输出没约束好
工具错误率工具侧抛错占比是模型填错参数还是外部服务异常
负面反馈率用户点踩/投诉比例最终靠真实用户体验来兜底
单请求成本Token 消耗重试和循环会烧钱,需要设阈值

所有这些指标都需要按版本维度做对比。改了一版 prompt,成本涨了,成功率涨了,这没问题;成功率涨了但负面反馈率也涨了,就要警惕是不是评估集覆盖不全,系统用某种隐蔽的方式在牺牲用户体验。

告警阈值不要直接拍脑袋。比较实用的做法是:先观察 2 到 4 周的基线数据,取均值加减若干倍标准差作为告警线。比如平均调用次数超过 8 次,或重试率超过 15%,就要触发调查。宁可先定得宽松一点,也不要让告警噪音把团队训练成不看告警的习惯。

5.2 Bad Case 自动回流:构建数据闭环

监控不只是看数字,更重要的是把异常样本自动收回来。我比较推荐的做法是三层收集机制:

  • 规则触发:输出格式不合法、工具调用数值超范围等,自动打标;
  • 用户反馈:用户点踩或有投诉入口的,自动携带 trace 入库;
  • 人工抽检:每天抽一部分成功样本复查,因为有时候模型答错用户也根本发现不了。

收集回来的 Bad Case 经过清洗和归类,每周补充进评估集。这样迭代的素材就源源不断,越迭代越贴近真实线上分布。这套机制跑起来后,团队优化 prompt 就不再依赖灵感,而是手里有一批真实的、会反复出错的样本,改一版测一版,效果肉眼可见。

5.3 灰度发布与版本管理:改 prompt 也要像改代码一样谨慎

很多团队改模型 prompt 像聊天一样随手改。这在大模型应用里是大忌,因为一次措辞调整可能同时影响十个场景的性能。推荐的做法是:

  • prompt 和配置全部纳入版本管理,每次改动都能看到差异;
  • 每次改动先在评估集上跑离线回归,和上一版本基线对比;
  • 线上下发采用灰度策略:先放 5% 流量观察一两天,确认成功率、重试率、成本指标都没有恶化,再逐步放大到全量。

模型版本升级同理,底层模型一变,所有 prompt 的效果都可能重构,必须完整回归一遍,不能凭感觉直接切。这一点我有过教训:某次底层模型升级后,之前调好的 prompt 在部分场景出现了严重的格式问题,由于直接全量切换,问题在线上爆了两天才被发现。灰度流程不完善,交的学费都是实打实的。

5.4 迭代节奏怎么定

可靠性不是一次性搞出来的,而是靠持续的小步快跑磨出来的。结合我接触过的项目,比较健康的节奏是:

  • 每周固定补充和清洗一次 Bad Case;
  • 每周跑一次全量离线评估,产生最新基线;
  • 每两到四周做一次 prompt/链路优化版本的上线;
  • 每次上线前必须有离线评估报告和灰度计划支撑。

节奏可以根据团队资源调整,但核心是保证"评估-优化-验证-上线"这个循环一直转下去。很多 Agent 项目一开始效果不错,后来问题越来越多,正是因为循环停掉了:线上样本不回流,评估集不更新,prompt 越改越散,最后系统变成一团谁都动不得的乱麻。

这篇内容写到最后,我想说一个自己在实际项目里的感受:做 LLM Agent 工程,最大的心态转变是从"追求模型一次性输出完美答案"变成"接受模型会犯错,然后把犯错变成可控事件"。

非确定性堵不住,但可以用评估、约束、校验、降级、监控这一整套工程手段,把不确定的范围越缩越小。如果你现在手头的 Agent 也在上线后不断冒出新毛病,建议先从两件事入手:搭一套可回放的评估集,然后把校验-重试回路加到所有模型输出的下游。这两件事做完,系统体验通常会有肉眼可见的提升。

之后再把失败样本回流、灰度发布这些机制逐步补上。等这套循环转起来,你手头的 LLM 系统,离"可靠生产体验"就不远了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进