← 返回文章列表

延迟高就换模型?先看清你改的是哪一层

Agent 变慢、答不准、token 烧得快,最自然的反应是「换个更强的模型」。但实测下来,这四个症状的根因往往在四个完全不同的层:架构、决策、生成、上下文。这篇文章把每个问题拆成「怎么判断 → 常见根因 → 按代价排序的解法」,并给出一套先看分布、再定位层、最后才动手的排查顺序。
AI Agent 2026-10-24 22:21:23 评论 0

上一篇讲「怎么看见」,这篇讲「看见之后怎么办」。数据模型和工具选型见 docs/agent-observability.md。


一个最常见的浪费

系统慢了。第一反应是:换个更快的模型。

换完,P50 从 3.2s 降到 3.0s。花了钱,只快了 6%。

去看 span 时间轴才发现:

parse       0.1s
retrieve    1.6s    ← 占了一半
generate    1.4s    ← 换模型只优化了这里
其他        0.1s

因为瓶颈根本不在生成,换模型只是把 1.5s 变成了 1.3s。

这就是这篇要说的事:症状只有四个,但根因分布在四个层。改错层,努力全废。

症状 根因常在哪一层 典型改法
延迟高 架构层 并行、流式、模型分级
意图识别不准 决策层 分层判定、澄清、兜底
回复质量差 生成层 检索优化、代码校验、降级
Token 烧得快 上下文层 裁剪、摘要、缓存

这四层不能互换。 用生成层的手段(改提示词)去解决架构层的问题(串行调用),怎么改都没用。


第一步:先看分布,别急着看个例

拿到观测数据后,很多人直接点开最慢的那一条 trace,然后基于这一条改架构。

这是第二个常见的浪费。 个例可能是异常值,不代表普遍情况。

正确的第一步是看两个数:

P50(中位数)  一半请求比它快
P95            5% 的请求比它慢

两者的组合指向完全不同的原因:

现象 说明 该往哪查
P50 高,P95 也高 普遍慢 架构问题(串行、模型太大)
P50 正常,P95 很高 有长尾 超时重试、大输入、某些工具偶发慢
P50 高,P95 正常 稳定地慢 每一步都慢一点,看 span 分布

「P50 正常但 P95 很高」是最容易误判的一种。 你会觉得「平时挺快的啊」,但 5% 的用户每次都在等十秒——而这类问题通常是重试策略或某个外部依赖的抖动,不是模型慢。


一、延迟:先分清是哪一种「慢」

根因有五类,按出现频率排

① 该并行的串行了(最常见)

// 三个独立检索,串行执行
const orders = await queryOrders(id)      // 400ms
const policy = await queryPolicy(type)    // 500ms
const faq    = await queryFaq(question)   // 300ms
// 总计 1200ms
// 并行:总计 = 最慢的那个
const [orders, policy, faq] = await Promise.all([...])   // 500ms

这一类最值钱:改三行代码,省 700ms,零成本。

② 没有流式输出

首字延迟和总耗时是两个完全不同的体验指标:

不开流式:等待 4.5s,然后一次性出现整段回答
开流式:  0.8s 出现第一个字,4.5s 时全部说完

总耗时一样,但用户感受差一个量级。 因为人判断「快不快」看的是有没有反应,不是什么时候说完。

排查方法:看 trace 里第一步 LLM 的 timeToFirstToken。如果这个数很小但总耗时很大,说明生成慢但首字不慢——这种问题开流式就能解决九成。

③ 简单任务用了大模型

意图识别       → 小模型足够(分类任务)
参数抽取       → 中等模型 + JSON mode
最终生成       → 大模型(这里才需要)

一个真实的配置错误:整条链路都用同一个大模型,包括「判断这句话属于哪个意图」这种三分类任务。这类任务小模型和大模型的准确率差不到 3%,但延迟差 5 倍。

④ 上下文太长

输入越长,首字延迟越高。而很多人会把所有检索结果一股脑塞进去:

topK=8,每片 800 字 → 6400 字 ≈ 4500 token
实际有用的可能只有 2 片

砍到 topK=3 + 阈值过滤,输入从 4500 token 降到 1600,首字延迟通常能降 30~40%。

⑤ 重试放大了延迟

第一次 超时 3s
第二次 超时 3s
第三次 成功 1s
总计 7s,而其中 6s 是重试

重试策略要看 P95,不要看平均值。 因为重试只影响长尾。

按性价比排序的解法

1. 并行化独立的调用        改几行,省几百 ms,零成本
2. 开流式                  改配置,感知延迟降一个量级
3. 模型分级                简单任务换小模型,省一半
4. 砍上下文                阈值 + 裁剪,顺带省钱
5. 缓存                    相同问题直接返回
6. 换更强的模型            ← 排在最后,因为最贵且常常没用

注意最后一行。 换模型是「万不得已」的手段,不是「第一反应」。


二、意图识别:问题往往不在模型

意图识别不准,第一反应是「分类模型不够好」。但实际项目里,七成的根因在意图体系设计,不在模型。

根因有四类

① 意图之间不互斥

❌ 退款 / 改签 / 查询订单 / 其他

「我要退掉这个订单」    → 退款 还是 查询订单?
「改签要加钱吗」        → 改签 还是 其他?

模型在两个意图之间摇摆,同一句话两次跑出两个结果——这时候你会误以为是模型不稳定,其实是定义有歧义。

判断方法: 拿 20 条真实问题,让两个人独立标注意图,如果一致性低于 90%,说明体系有问题。

② 规则和模型打架

规则命中就返回,看起来很省事。但规则是「字面匹配」,会误伤:

规则:「退」→ 退款意图
误伤:「退款政策是什么」→ 被判成退款(实际是咨询政策)

修法:规则只做「明显对」的判断。 拿不准的一律交给模型——规则误判比模型误判更难纠正,因为它不会给置信度。

③ 硬猜,没有澄清

置信度 0.4 也照样走某条分支,然后答非所问。

这是最容易被忽略的一条:与其猜错,不如问一句。

④ 没带上下文

同一句「能退吗」:

在订单详情页 → 退这一单
在客服首页   → 咨询退票政策
在帮助中心   → 想找退票入口

不带页面信息,这三句会分到同一个意图,然后走同一条错的路。

按性价比排序的解法

1. 先判「能不能处理」,再判「是什么」     ← 设计问题,零成本
2. 置信度分层:>0.8 走 / 0.5~0.8 澄清 / <0.5 转人工
3. 规则只做明显对的事,其余交模型
4. 把高频 badcase 固化成新规则
5. 接入页面/渠道等上下文

第 1 条是设计层面的,收益最大但最容易被跳过。

很多人只做「是哪个意图」,结果用户说「帮我写首诗」,分类器硬分到「其他」,然后走通用问答,答得又长又偏。

先判范围,是为了让「我帮不了你」成为一条正常路径,而不是异常。

置信度分层怎么定阈值

拿一批标注数据,画两个数:

阈值调高 → 误判少,但澄清变多(用户觉得啰嗦)
阈值调低 → 澄清少,但答非所问变多

经验值:

> 0.8     直接走,不打扰用户
0.5 ~ 0.8 收敛到 2~3 个选项让用户点(不要开放式问)
< 0.5     不猜,转通用问答或人工

澄清要闭合,不要开放:

❌ 「请问您具体想咨询什么?」        → 用户再打一遍,还是分不出来
✅ 「您是想 ① 查订单 ② 申请退改 ③ 咨询政策?」

三、回复质量:先分清是「没查到」还是「查到了不会说」

这两类问题的修法完全不同,混在一起会一直改错地方。

怎么分

看 trace 里 retrieve 这一步的 output:

情况 A:retrieve 返回 0 条,或最高分 0.21     → 召回问题
情况 B:retrieve 返回 3 条且最高分 0.78        → 生成问题

A 和 B 的比例,决定了你该把时间花在哪。

情况 A:召回问题(占多数)

根因和解法:

根因 表现 解法
切分破坏了语义 命中但 chunk 读起来不完整 按结构切,别硬切
向量模型不适合中文 同义改写召回差 换模型 / 加关键词一路
专有名词召不回 SSE、E1024 这类查不到 加 BM25,用 RRF 融合
topK 太小 答案在第 6 条 调大 k,但要配阈值
文档本身没有 怎么调都召回不到 补数据,别改代码

第三行是最常见的:纯向量检索对精确词天然弱。 我实测过,同一批问题里,关键词能精确命中而向量排不进前三的,占了两成。

情况 B:生成问题

根因和解法:

根因 表现 解法
资料里有,但被噪声带偏 答了不相关的内容 加阈值 + 重排
没有依据也在答 答得很顺但查不到出处 无召回就别调模型
引用编号错位 引用了不存在的 [5] 代码剥 / 改,不靠提示词
结构缺失 少了某一段 schema 约束 + 后处理补全
该拒答却硬答 问知识库外的问题也答 加阈值 + 拒答话术

这里有个必须强调的点:提示词只能降低概率,代码才能消除。

一个实测:系统提示词里明确写了「延伸说明段不要带引用编号」,4 道题各跑 3 次,12 次生成里有 9 次(75%)还是带了,其中 3 道题 3/3 稳定复现。

改成生成后用代码按段落剥掉,同一批从 75% 降到 0。

// 不靠提示词,靠代码
if (currentSection === 'extension') {
  line = line.replace(/[ \t]?\[\d+\]/g, '')
}

所以「回复质量」这一层,一半的工作量在检索,另一半在后处理。真正属于「提示词工程」的,比想象中少。


四、Token:先看分布,再决定砍哪里

Token 花得多,直觉是「模型太贵」。但先看 token 花在哪,往往有更便宜的解法。

先做一次 token 归因

把一次请求的 token 按来源拆开:

系统提示词        1800 token   ← 32%
历史对话          2100 token   ← 37%
检索结果          1500 token   ← 27%
用户问题            50 token   ←  1%
─────────────────────────────
合计              5450 token

这份分布会直接告诉你该动哪里。

  • 系统提示占 32% → 精简提示词,收益立竿见影
  • 历史占 37% → 上摘要压缩
  • 检索占 27% → 加阈值、动态 topK

很多人只盯着「换便宜的模型」,却没看过这 32% 是几句啰嗦的规则。

四个解法,按性价比

① 精简系统提示词

一条规则写三行和写一行,效果一样。删掉重复的、举例过多的、和业务无关的。

这一条零成本,且常常能砍掉 20~30%。

② 历史分层

最近 3 轮完整保留    ← 消解指代需要原文
更早的压成一段摘要   ← 只留结论
超过 N 轮直接丢      ← 极少需要

不要用「保留最近 10 轮」这种一刀切。 第 4 到第 10 轮里,大部分是寒暄和重复确认,压成两句话就够。

③ 检索结果按预算裁剪

const BUDGET = 2000   // token
let used = 0
const picked = []
for (const chunk of ranked) {
  if (used + chunk.tokens > BUDGET) break
  picked.push(chunk)
  used += chunk.tokens
}

比固定 topK 好,因为不同 chunk 长度差很多。 固定 k=5 时,遇到 5 个长 chunk 就爆了。

④ 缓存

两层缓存:

答案缓存   相同问题 + 相同知识库版本 → 直接返回
Prompt 缓存  系统提示词固定 → 供应商侧缓存命中,成本大降

第二个容易被忽略: 大多数模型供应商对「相同的系统提示前缀」有缓存折扣。把固定的系统提示放在最前面、把变量放后面,命中率会高很多。


五、排查顺序:一张可以照着走的表

把上面四节压缩成一张操作表:

步骤 做什么 别做什么
1 看 P50 和 P95 的组合 别直接点开最慢的那一条
2 看 span 时间轴,找占大头的步骤 别猜
3 看那一步的 input 大小和模型 别急着换模型
4 按上面的表定位到「层」 别用生成层的手段修架构层
5 改一处,跑同一批用例对比 别一次改五处

第 5 条特别重要。 一次改五处,效果好了你不知道是哪一处起的作用,效果差了也不知道该回滚哪个。

改一处、测一次、才动下一处。 这听起来慢,但比「改一堆然后整体回滚」快得多。


六、三个反直觉的结论

① 延迟优化的第一手段不是换模型,是并行和流式。

前者省几百毫秒,后者把「感知延迟」降一个量级,两个都不花钱。

② 意图识别的问题,七成在意图体系设计,不在模型。

先做一次标注一致性检查:两个人独立标 20 条,一致性低于 90%,就别急着调模型。

③ 回复质量的一半工作量在检索和后处理,不在提示词。

提示词管不住采样——这是实测出来的,不是理论。


最后

四个症状,四个层,一套顺序:

先看分布(P50 / P95)
  → 再定位层(架构 / 决策 / 生成 / 上下文)
    → 最后才改,且一次只改一处

观测的价值不是「发现问题」,是「定位到层」。 定位不到层,改动就是随机试错——而随机试错的成本,往往比问题本身更高。

评论: