1. U-ESS v1.0 是怎么来的:LLM 应用交付中的稳定之痛
1.1 一次线上事故:同样的输入,两种结局
先讲一个我亲身经历过的事。去年我在负责一个面向终端用户的智能客服项目(下文就称模拟项目X),当时模型能力已经调得相当不错,业务方也验收通过了。结果上线第三天,线上就炸了:同一个用户问题,上午返回的是标准的 JSON 格式,下午突然变成了一长段带 Markdown 标记的自然语言。下游解析模块拿到之后直接抛异常,整个问答链路 30% 的请求都在报错。
我当时的第一反应是“模型抽风了”,但后来做了大量复现实验后意识到,这根本不是某个单点故障,而是整个LLM 执行态缺乏一个统一的稳定性基线。模型本身的输出天然带有随机性,加上上下文里的历史对话在变、并发请求在堆积、外部工具返回的数据在波动,任何一个环节不稳定,最终都会放大成用户可感知的“时好时坏”。
也正是从那次事故开始,我不再把“稳定性”当成一个运维话题,而是当成一个需要在设计阶段就明确下来的工程指标。最近我看到这份通用 LLM 执行态稳定性标准(Draft v1.0),也就是 U-ESS v1.0,算是把过去两年里零散踩坑的经验做了一个系统化的定义。这篇文章我想结合自己在一线落地 LLM 应用的经历,把这个标准拆开揉碎,讲讲它到底想解决什么问题、怎么把它用起来,以及它有哪些还不够完善的地方。
1.2 稳定性的问题为什么不是“模型问题”
很多团队跟我一开始一样,遇到 LLM 应用表现不稳定,第一反应就是换更强的模型、调 prompt、加 few-shot 示例。这些手段确实有用,但有一个前提:你得先确认问题到底出在模型的能力层,还是出在系统的执行层。
我举个具体的例子。模拟项目X 里有一个功能是让模型从用户留言里抽取“商品名称、期望价格、联系时间”三个字段。刚上线时偶尔会抽错,这属于模型能力问题,我通过优化 prompt 和增加 few-shot 就解决了。但后来出现的字段缺失、JSON 里多了注释、返回了纯文本这类问题,无论我怎么调 prompt 都压不住。原因很简单:模型推理时的温度参数是 0.8,采样本身就带随机性;我又把多轮对话历史全部塞进上下文,导致早期的干扰信息影响了当前判断。这些都不在 prompt 能控制的范围内,它们属于执行态的参数、状态和流程问题。
所以执行态稳定性标准要管的,不是“模型答得对不对”,而是“模型在整个运行过程中,输出格式是否可预期、内容波动是否在可接受区间、会话状态是否可恢复、失败之后系统能不能自愈”。这类问题只有用工程手段才能解决,靠“打磨 prompt”是治标不治本。
1.3 U-ESS 标准想框定什么:执行态的定义
U-ESS v1.0 给我最大的启发是它把“执行态”明确成了一个可讨论、可度量、可验收的工程对象。在这份标准里,执行态描述的是:一次 LLM 推理任务从请求进入系统,到最终结果返回给调用方,整个过程中所有与模型交互相关的状态集合。
如果把它拆开看,我理解至少包含五类:推理参数状态(温度、top_p、最大输出长度等)、上下文状态(当前轮输入、历史对话、系统提示词、被截断或压缩后的上下文)、输出状态(原始模型输出、结构化解析后的结果、解析失败时的兜底内容)、会话状态(多轮对话里的临时变量、用户身份信息、业务上下文)、以及外部依赖状态(工具调用的结果、数据库查询结果、上游接口返回的数据)。
这个定义的价值在于:它让“稳定性”不再是一句口号,而是变成了一份可以逐项对照的检查清单。更重要的是,它指出了稳定性问题的本质——不是某一个状态单独出错,而是多个状态在时间维度上耦合,最终导致系统行为偏离预期。我会在下一章把这五个维度展开来讲,每个维度都会结合我真实的踩坑经历,告诉大家标准里的条目落到代码层面具体是什么意思。
2. U-ESS 五个稳定维度逐个拆解
2.1 输出格式稳定:不止是 JSON
U-ESS 标准里关于输出格式稳定性的描述,第一眼看上去会觉得很基础:要求模型始终返回符合预定义格式的数据结构。但如果你真的在线上跑过 LLM 应用,就会知道这个目标远没有想象中那么简单。
我在模拟项目X 里验证过一组数据:在温度 0.7、使用同一个 prompt、连续调用 200 次的前提下,直接让模型“以 JSON 格式返回”的结果里,约有 6% 的输出会带上 ```json 代码块标记,约 2% 会在 JSON 之外多出“好的,这是您要的信息:”这类前缀,还有大概 1% 会直接把 JSON 里的布尔值写成“是/否”。单看每一个问题好像都不致命,但汇总到一个日请求量百万级的系统里,这就是上万次失败。
针对这类问题,标准里提供的思路是“三层防护”。首先在 prompt 层用明确的格式约束,比如给出完整的 JSON Schema 示例而不是一句“请返回 JSON”;其次在模型层,能启用 JSON Mode 之类的结构化输出能力就尽量启用,这个模式下模型会内部修改解码逻辑,格式稳定性会大幅提升;最后在应用层必须有一个容错解析器,能剥离代码块标记、纠正常见格式偏差、在字段缺失时填默认值。三层都做了,格式稳定性才能到 99.9% 以上。
我还想多提一句,真的不要迷信“这个模型 JSON 能力很强,不用做容错”。事实上,即使是最好的模型,在长上下文、多轮对话、输出长度接近上限的时候,格式崩塌的概率也会显著上升。容错解析不是脏活累活,它是一个成熟 LLM 应用的标配。
2.2 内容一致稳定:温度与采样的确定性边界
内容一致性,说的是同样的问题在相同条件下多次调用,模型给出的语义结论是否保持一致。这里要区分两种需求:一种是需要完全确定性的场景,比如审核、判题、合同条款抽取;另一种是允许一定创造性波动的场景,比如文案生成、头脑风暴。
U-ESS v1.0 的聪明之处,是没有一刀切地要求“所有场景都必须结果一致”,而是定义了一个“确定性边界”:你需要先明确自己的应用允许多大的内容波动,然后再去选择对应的采样参数组合。我在实践中习惯用一个简化模型来理解这件事:温度接近 0 的时候,模型基本会选概率最高的 token,输出接近贪婪解码;温度调高后,低概率 token 被采样的机会变大,输出多样性增加,但胡说八道的风险也同步上升。
如果你的业务属于高确定性场景,我建议直接把温度设为 0,同时把 top_p 设为 1。不要以为温度 0 就是绝对稳定,我在实测中遇到过温度 0 下,因为批次不同、显存状态不同,输出在个别 token 上仍然出现极小概率波动的情况。所以更可靠的做法是给关键字段设计一个“语义等价校验器”:比如抽取出来的日期字段,用程序判断它是不是一个合法日期;分类字段,判断它是否在枚举列表里。模型的不确定性永远存在,我们能做的就是在执行态加一道确定性围栏。
2.3 时序稳定:延迟、超时与重试的幂等要求
时序稳定这个词听起来有点抽象,翻译成大白话就是:每次请求的响应时间是否在预期范围内,以及当超时或失败发生时,系统的重试机制会不会引发更严重的问题。这一块 U-ESS 里着墨不少,我觉得切中了很多团队的真实痛点。
先说响应时间的波动。LLM 推理的延迟跟输入 token 数、输出 token 数、当前服务器负载都有关系。最夸张的一次,模拟项目X 在流量高峰时,同样的请求延迟从 800ms 一路飙到 8s。如果你把超时时间固定设成 3s,那么高峰期就是成片的超时报错。我现在的做法是分层设置超时:首 token 延迟设一个值,总响应时间设一个值,再根据历史 P95 延迟动态调整。单独的固定超时不能应对这种高度波动的场景。
比延迟更隐蔽的是重试引发的问题。假设一个调用超时了,你在应用层做了重试,第二次调用成功了,但第一次调用其实只是响应慢了,最终也写入了数据库。于是同一条数据被写入两次。做 LLM 应用的人往往会忽略:LLM 的调用天然不是幂等的,重试一次就可能产生一份全新的输出。U-ESS 标准里强调给每个请求生成 request_id,并在业务写入链路中做去重,这恰恰是我在实际项目中花了好大代价才学到的教训。凡是涉及到写操作、扣费、发送消息的场景,重试必须带上业务幂等键,否则迟早要出大事故。
2.4 会话状态稳定:多轮上下文的可恢复性
会话状态稳定性是标准里最贴近业务感知的部分,因为它直接决定了用户在多轮对话里的体验。我理解这份标准对会话状态的要求可以归纳为三件事:完整保存、稳定恢复、平滑降级。
完整保存的意思是,每一轮对话结束后,你要把当前完整的会话状态持久化下来,包括历史消息、模型返回的临时结果、工具调用的中间数据。很多团队图省事,只保存了用户消息和模型回复,结果一旦系统重启或者模型上下文被截断,就再也无法恢复当时的业务状态。
稳定恢复则要处理一个更大的难题:模型的上下文窗口是有限的,当历史对话超过窗口长度时,你必须决定哪些历史该保留、哪些该压缩。模拟项目X 早期出现过一次典型事故:系统采用“滑动窗口截断”策略,直接丢弃最早的消息。表面上看上下文长度符合限制了,但模型突然“失忆”,用户两轮前提供的收货地址被系统追问了一遍又一遍。原因就是被截断的消息里有关键业务信息。后来我把截断策略改成了“关键信息抽取 + 摘要压缩”两层:先用一个模型把全量历史压缩成结构化摘要,再把最近几轮原始消息拼在后面。这样既控制长度,又最大限度保留关键信息。
平滑降级指的是当会话状态确实损坏或无法恢复时,系统要能体面地引导用户重新开始。这听起来很基本,但很多系统在这个场景下会直接报错,这比模型答错更伤用户信任。
2.5 工具调用稳定:函数契约与执行约束
工具调用(function calling)是现在 LLM 应用里最常见的复杂功能。模型输出一个结构化的调用意图,系统去执行真实的函数,再把结果回填给模型。U-ESS 把工具调用的稳定性提出来,我认为是因为这个链路的失败点比单纯的文本生成要多得多。
第一个坑在于模型输出的函数参数不一定符合要求。模型可能把“date=2024-12-01”写成“date=十二月一号”,或者把数字参数直接传成字符串。我在项目里主要靠两层来解决:一是申明工具时给每个参数写清楚 type 和 description,能写 enum 约束就写;二是在执行函数之前加一层参数校验,不合法就直接返回“参数错误”给模型,让它自己修正。
第二个坑是并发和竞态。如果你的多个工具调用是并行执行的,而它们又共享了某个可变状态,那么结果就可能相互覆盖。我部署过一个票据查询系统,模型在一次回复里同时调用了“查询余票”和“锁定座位”两个函数,因为共享了座位状态变量,后返回的结果把先返回的覆盖了,导致用户明明查到了票,锁定座位却失败。后来我引入了 per-session 的独立状态空间,每个会话的操作都在自己的副本上进行,问题才真正解决。
3. 把标准落地成工程实践:聊聊我建议的配套刀法
3.1 执行态快照与指标采集:没有数据就没有“稳定性”
标准给你定义了方向和维度,但真正到了自己的项目里,如果没有一套数据采集机制,你根本不知道自己当前的系统稳不稳定。所以在落地 U-ESS v1.0 的时候,我做的第一件事不是改代码,而是搭“执行态快照”的采集管道。
所谓执行态快照,就是在一次 LLM 调用完整走完后,把当时的输入参数、模型配置、上下文长度、原始输出、解析后结果、耗时、 tokens 消耗、重试次数、最终状态全部记录成一条结构化日志。我把这些日志统一汇入存储,按天、按用户、按接口维度做聚合分析。有了这些快照,你就可以在一个中心化看板上看到比如“请求总数 2,000,JSON 解析失败 12 次,失败率 0.6%”“温度 0.8 的请求内容漂移率远高于温度 0.2”这样的结果。没有这些指标,你想判断系统是否稳定就只能是凭感觉。
一个很容易被忽略的细节是:快照必须记录“失败现场”的完整上下文。很多日志系统只记录报错信息,丢失了当时的 prompt、历史对话和工具返回,导致事后无法复现。所以我的建议是,快照的保留级别要按“可完整重放请求”的标准来设计。代价是存储成本上升,但换来的是定位问题的时间从小时级降到分钟级,这笔账非常划算。
3.2 稳定性回归测试集:把边界用例固化下来
有了数据采集,下一步就是建立一套稳定性回归测试集。这套测试集的目的不是测模型能力,而是测执行态在各类边界条件下是否稳定。我在项目里维护了一个约 90 条用例的集合,覆盖了几个常见类型:格式边界、上下文边界、工具边界和并发边界。
这里我列几类比较典型的边界用例,方便你做参考:
| 用例类型 | 具体场景 | 预期行为 |
|---|---|---|
| 格式边界 | 请求要求 JSON,但温度很高 | 解析器容错后仍能提取关键字段 |
| 上下文边界 | 历史消息超过窗口长度 2 倍 | 摘要压缩生效,关键信息不丢失 |
| 工具边界 | 工具返回参数缺失 | 函数校验拦截,向模型返回明确报错 |
| 并发边界 | 10 个会话并行调用同一工具 | 各会话状态互不覆盖 |
| 重试边界 | 同一请求重试 3 次并重复写库 | 业务幂等键命中,只写入 1 条 |
这套用例集跑一遍只需要二十分钟,但每次模型版本升级、prompt 调整、框架升级之前必须全量回归。我有一次升级了一个底层链路追踪组件,功能测试全过了,回归测试里“JSON 输出稳定性”这一项挂了,排查之后发现是新组件改了流式传输的拼接逻辑,导致在切块边界上多了一个多余字符。这种问题靠人工评审根本发现不了,靠回归集就能稳稳兜住。
3.3 用确定性参数做基线,用采样参数做波动分析
读 U-ESS 的时候,我最有共鸣的一个观点是:稳定性不是一个“全有或全无”的开关,而是一个你在不同参数区间内定义的达标线。所以标准落地时,我给自己的系统设了两套运行基线。
第一套叫“确定性基线”:温度设为 0,固定所有可固定参数,用于所有需要高重现性的场景,比如审核、抽取、分类。第二套叫“波动分析基线”:温度设为业务需要的值,连续采样多次,统计输出的语义相似度、格式失败率和内容漂移率,得到一个“允许波动范围”。例如某文案生成场景,我实测温度 0.9 时,同一 prompt 采样 5 次,语义相似度在 0.75 到 0.92 之间,这个范围就是这条业务的稳定性标准。
有了这两套基线,再去定义线上告警就非常清爽了。比如“确定性基线场景下,格式失败率连续 5 分钟超过 0.1%”是一个告警;“波动分析基线场景下,语义相似度低于 0.7”是另一个告警。这比我以前简单粗暴地设 99% 成功率阈值要科学得多,因为不同类型的应用,“稳定”的定义本来就是不一样的。
4. 真实项目里的稳定性事故复盘
4.1 重试风暴:一次超时引发的重复扣款
U-ESS 里讲幂等设计的那几段,让我想起一个印象极深的线上事故。当时我在做一个基于 LLM 的单据自动审核系统,审核通过后就触发财务侧的支付接口。某天下午支付接口出现了短暂抖动,响应时间从正常的 1s 飙升到 12s,触发了应用层的超时重试机制。第一次请求其实已经被第三方接收了,只是响应慢,第二次重试又成功触发了一笔支付。
最后的结果是同一张单据被扣了两次款。这个事故让我反思了很久,问题不只在重试逻辑,更在于整条链路缺少一个全局视角的业务幂等键。如果我在第一次请求发出时就生成一个 payment_request_id,并写入本地存储,那么重试时只需要先查这个 id 是否已经处理过,就能拦截重复操作。LLM 应用跟传统接口有一个巨大的区别:传统接口的响应是确定的,重试可以放心执行;而 LLM 每次调用都可能产出不同的工具调用序列,重试的风险被显著放大。所以,凡是要执行写操作的工具调用,必须把幂等键设计提进标准。
4.2 上下文截断导致的状态错乱
这是前面提过的“失忆”事故的完整复盘。模拟项目X 当时的上下文管理策略是:当总长度超过 12,000 token 时,保留最近 8,000 token,直接丢弃更早的内容。在一次长对话中,用户提到“请把发票寄到我在第二句话里留下的地址”,而那句包含地址的信息恰好因为长度限制被截断了。模型在后续轮次中开始编造一个根本不存在的地址,业务人员追查了半天才发现是上下文丢失。
这个问题给我一个非常深的体会:上下文管理策略应该是“业务感知”的,而不是纯按 token 数量切割。如果一个字段被标记为业务关键信息,那么无论上下文怎么压缩,它都必须被保留。我后来在系统里加了一个状态变量表,把业务关键字段独立于对话历史存储,每一轮都重新注入。这样即使对话历史被截成碎片,业务状态依然是完整的。U-ESS 里“关键状态与敏感历史分离维护”的建议,本质上就是这个思路。
4.3 并发下的工具调用竞态:函数状态覆盖
第三个事故最隐蔽,也最能体现“执行态”跟传统后端状态管理的差异。我有一个系统,允许用户在一次请求里让模型完成多个操作:查询余额、试算利息、提交调整申请。最开始这三个操作是串行执行的,耗时很长。为了让体验更流畅,我改造为并发执行,但当时没有仔细考虑这些函数共享了一些模块级变量。
上线后发现一种诡异的现象:用户发起一个“查询余额并提交调整申请”的请求,得到的调整回执里显示的余额却是另一个并发请求的数据。原因是两个请求的查询结果写到了同一个变量里。这个问题的根因是“会话隔离不彻底”。后来我按 U-ESS 的思想,把每个请求的执行态封装成独立的上下文对象,所有工具调用只从自己的上下文读写数据,彻底从架构上规避了跨请求污染。这个改造之后,同类事故就再也没有出现过。
5. 我对这份草案的态度:适合谁、怎么用、还缺什么
5.1 适合与不适合的主流场景
U-ESS v1.0 这份草案,我最想强调的是它并不是为所有人准备的。如果你做的是纯“娱乐型”LLM 应用,比如一次性对话、随机文案生成,用户对波动不敏感,那么这套标准的完整落地对你来说可能是过度设计。你只需要从中截取“输出格式容错”和“基础指标采集”两块,就能获得不错的收益。
但如果你是做企业级应用,比如客服、审核、数据分析、自动化操作工具,用户对结果稳定性和资金安全有明确要求,那这份标准几乎每一节都跟你的生死直接相关。我当时把标准里的检查项对照了一遍现有系统,发现竟然有接近 40% 的检查点是缺失的,很多我以为是“偶发小概率问题”的故障,其实都是标准里已经点名过的典型风险。对于一个团队来说,这份草案更大的价值不一定是照搬条文,而是它提供了一份“稳定性体检清单”。
5.2 直接照搬会踩哪些坑
草案毕竟是草案,直接照搬也会有一些摩擦。最大的一个坑是:标准里描述的目标状态是理想化的,它假设你有一个全链路可控的工程团队,但对于只用托管 API 的团队来说,部分底层参数(比如解码策略、重复惩罚)根本拿不到,没法完全照做。我建议的做法是把标准当成“目标语言”,用它来定义自己系统的稳定性目标,然后逐项做映射,做到不了的地方就加注释说明原因和替代方案。
另外一个坑是“追求指标收敛而牺牲业务效果”。我刚落地时对内容一致性定了很严格的标准,把温度一律压到 0,结果某些创意型场景的内容质量明显下降。后来我才意识到,稳定性标准应该分场景差异化设定,不能一把尺子量所有业务。这份草案其实没有特别强调这一点,需要你自己在落地时去细化。
5.3 我还希望后续版本补上的内容
最后说一下我的个人期待。目前草案对“评估与验收”的描述相对薄弱,它给了维度、给了要求,但没有给一个可量化的打分卡。比如“输出格式稳定性达到什么量级算合格?内容波动在什么水平是可接受的?多长的上下文覆盖率算达标?”我的想法是,后续版本如果能配套一个参考性的稳定性评级卡,比如 0 到 5 级,每级给出对应的指标区间和适用范围,那这份标准的可操作性会再上一个台阶。
另外一个我希望补充的方向是“流式输出的稳定性”。现在多数应用都采用流式输出,用户看到的是逐字生成的效果。流式场景下的稳定性问题跟非流式完全不同:断句是否合理、切块边界是否破坏 JSON 结构、中断后续传是否流畅,这些都还没有被标准充分覆盖。我在实际项目里为了处理“流式输出到一半请求超时”的问题,不得不专门设计了一套缓存拼接和断点续传机制。
不过话说回来,一份规范类文档最怕的不是“不够全”,而是“开放即定型”。U-ESS 目前是 Draft v1.0,意味着它还有大量迭代空间。就我个人的角度,它已经把我这几年踩过的坑系统性地串联起来了。下次跟团队一起复盘线上稳定性问题,我打算直接把这份标准作为讨论的底稿,逐条对照,比从零开始画架构图高效得多。