面试知识库

03 · 延迟优化#

简历原话:延迟优化:通过日志定位瓶颈在串行模型调用;将高频组合步骤交由框架自动执行以减少模型调用次数,商品 ID 引用替代完整信息来压缩上下文。单任务平均模型调用 9→4 次,累计输入 token 79k→22.5k,端到端耗时 28.3s→12.5s。

30 秒口述版#

一个普通的购物任务原来端到端要 28 秒。我按调用链拆时间,发现工具合计只占 2 秒多,其余全是串行的模型调用;其中「比价」「精挑」两轮模型没做任何决策,只是把上一步结果搬进下一步参数。于是我做了两件事:一是检索返回后由框架自动跑比价和精挑,结果以提示注入,模型下一步直接收尾;二是下游工具只收商品 ID,完整信息放在会话级登记表里按 ID 取,模型不用读长 JSON,也不用在参数里重写商品。单任务平均模型调用 9→4 次,累计输入 token 79k→22.5k,端到端 28.3 秒→12.5 秒。

背景与问题#

系统形态:ShoppingX 是单模型单环的 Agent,一个主模型持有全部业务工具,按 Think → Act → Observe → Reflect 循环,直到调用收尾工具结束。开局前框架先跑一次意图解析(planner),把预算、品类、排除项等结构化约束写进会话状态。

原来一条普通单品任务的调用链(以「通勤背包,能装 16 寸笔记本,防泼水,预算 400 以内」为代表):

  1. 意图解析:1 次模型调用
  2. 主循环第 1 轮:模型决定查品类知识(爆款、典型属性)
  3. 主循环第 2 轮:模型发起跨平台检索(同轮多发,框架并发)
  4. 主循环第 3 轮:模型调比价工具,把候选折算成到手价
  5. 主循环第 4 轮:模型调精挑工具,按偏好选出 3 件
  6. 主循环第 5 轮:模型调收尾工具,工具内部再调一次模型写文案和逐件理由
  7. 另有每轮之间的轨迹漂移检查(小模型),平均 2 次

合计平均 9 次模型调用,全部串行。

用户感知到的现象:

  • 发出问题后,前端事件流里能看到一段段「思考中」,每段 2~3 秒,中间夹着几十毫秒就结束的工具事件。
  • 同样的需求,我对照过一个结构更简单的参考实现,一轮只有 3 次模型调用、12~15 秒出结果,我们要 28 秒。
  • 长会话里越往后越慢,因为每一轮都把前面所有工具返回原样再喂一遍。

日志里看到的三个具体问题:

  • 时间几乎全在模型上。一次 28.3 秒的任务里,工具逻辑(检索、精排、到手价计算)合计约 2.5 秒;主循环 5 轮解码共 13 秒左右,收尾工具内部那次模型调用 5.5 秒,意图解析 3.7 秒。瓶颈是「串行的模型调用次数 × 每次的解码量」,不是某个慢工具。
  • 有两轮是纯搬运。比价和精挑两轮,模型的输出只是「调某工具,参数 = 上一步结果里的候选」。精挑需要的预算、排除词、偏好,意图解析阶段早就确定性地写进了会话状态,模型没有新信息要加。
  • 上下文被商品全量 JSON 撑大。检索工具一条返回约 12k 字符,将近一半是商品链接、图片链接和空字段,模型推理根本用不上;更糟的是下游工具的参数要求传「候选列表」,模型要在输出里把几十个商品的标题、价格、链接逐字重写一遍,输出解码是最慢的部分。

做法与取舍#

1. 先量再改:按调用链拆时间#

  • 怎么做:每次模型调用在 Langfuse 里有一条 generation 记录(输入/输出 token、缓存命中、耗时),前端事件流里每个工具有开始/结束时间戳,两者按会话 ID 对齐,拼出一张「谁在什么时候占了多少秒」的时间线。另外写了一个固定口径的测量脚本:固定 query、每遍新会话、3 遍取中位,输出墙钟、模型调用次数、输入/输出 token、工具序列。
  • 为什么:最初的直觉是「检索和精排慢」,想加缓存。时间线一拉出来,品类知识查询 0.6 秒、比价 0.04 秒、精排 0.3 秒,加缓存几乎没收益。先量避免了在错误的地方下功夫。
  • 否掉的:只看总耗时做 A/B。总耗时受模型随机性影响大(同一 query 模型有时多搜一次),必须同时看调用次数和工具序列是否一致,才能判断改动是信号还是噪声。

2. 高频组合步骤交给框架自动执行#

  • 怎么做:检索工具成功返回后,框架给本轮会话打一个「已武装」标记;下一次调用模型之前(此时同轮并发的几路检索都已合流),框架自己依次跑比价和精挑,精挑按会话里已确定的约束执行,结果拼成一段「系统已自动完成比价与精挑,勿再调用,精选结果如下」的提示注入给模型。模型读到后下一步直接调收尾工具。
  • 自动执行走的是和模型亲手调用同一条后处理管线:结果截断、schema 校验、进展信号、候选计数都一样,所以后面的防死循环、收尾校验等规则看到的状态与模型亲手调一致,不需要为自动步骤写特例。
  • 边界:
    • 套装轮(一次买多个槽位,比如「旅行三件套」)不自动,因为它要先经用户确认组成,再按槽位组合优选;
    • 模型显式调了精挑工具就解除武装,不重复执行;
    • 自动步骤任何异常都吞掉并解除武装,退回「模型自己调」的老路径,失效方向只会回到改前,不会更差;
    • 比价、精挑两个工具本身保留,模型仍可在追问轮显式调(比如「只看亚马逊的」)。
  • 为什么选这个:这两步是「输入确定、零决策」的步骤,让模型来调只是在花一整轮解码时间把数据搬过去。机制能做对的事,不交给模型。
  • 否掉的方案:
    • 把比价 + 精挑合并进检索工具:检索是同轮多发、按平台并发的,合并后每一路各自精挑,拿不到跨平台合流后的全局视角;而且检索工具职责会变得很重。放在「下一次模型调用前」这个时点,天然就是合流之后。
    • 写成固定 Workflow:普通单品轮确实是固定序列,但追问轮、换品类、召回为空时需要模型判断,硬编码流程会丢掉 Agent 的灵活性。现在是「常见路径机制跑,异常路径模型接」。
    • 靠提示词让模型一次发完三个工具:比价和精挑依赖检索结果,同一轮发不了;而且提示词约束不稳定。

3. 收尾文案由主模型在参数里写,工具只排版#

  • 怎么做:收尾工具原来内部再调一次模型写开场白和逐件理由,这次调用要把整段上下文重新送一遍。现在把「总述、逐件理由、偏离意图的说明」改成收尾工具的入参,由主模型在调用时直接写;工具只按商品 ID 从登记表取回标题、到手价、图片、链接,确定性地排成商品卡。前端的逐字流式从模型写参数的增量里取,体验不变。
  • 为什么:主模型读完精选结果后本来就「知道」为什么选这几件,让它顺手写出来,比再起一个模型、重新喂一遍上下文便宜得多。
  • 否掉的:给收尾工具内部换更快的小模型。实测只省不到 1 秒,而且小模型写的理由和主模型的判断脱节;直接删掉这次调用更彻底。

4. 商品 ID 引用替代完整信息#

  • 会话级候选登记表:检索工具把完整候选(含链接、图片、原价币种等)按商品 ID 登记到本轮会话的状态表里;比价回写到手价,精挑回写入选理由,登记表是本轮候选的唯一真相。
  • 下游工具只收 ID:比价、运费、收尾、下单工具的模型可见参数只有商品 ID 列表,完整候选在工具内部按 ID 取回;精挑工具更进一步,连 ID 都不收,直接吃登记表全集。候选体参数对模型不可见,模型想传也传不了。
  • 工具返回给模型的是紧凑投影:只留模型做决策要用的字段(ID、标题、美元价、评分、品类,加已填的到手价和理由),去掉链接、召回相似度、恒为 0 的字段和尚未填充的阶段字段。检索返回从约 12k 字符降到约 3k 字符,且这份返回每轮都会被重读。
  • 跨轮引用:登记表只活一轮,用户下一轮说「买第 2 个」时,按 ID 回源向量库取回,顺手再登记。
  • 为什么:输入侧,每次模型调用都要重读全部历史工具返回,12k 字符的返回在 4 次调用里就是 4 倍放大;输出侧,模型逐字重写几十个商品是最贵的解码。ID 同时压了两边。
  • 附带收益:链接和价格不再经过模型,杜绝了模型改写链接、抄错价格的可能。
  • 否掉的:
    • 只删字段、仍按值传:输入能省一些,但下游工具参数仍是候选列表,输出重吐的问题还在。
    • 靠上下文压缩:框架自带的压缩在上下文超过阈值时才触发,并且会生成摘要、破坏前缀缓存;我们的问题是「每一轮都背着用不上的字段」,应该从源头不放进去。

5. 几处顺手的小改#

  • 品类知识预取与意图解析并发:原来主循环第 1 轮固定花一整轮去查品类知识。现在框架在开局跑意图解析的同时并发预取品类知识,结果直接放进上下文,省掉一轮模型调用。
  • 工具说明瘦身:15 个工具的说明文字(模型每次都要读)从 7395 字符压到 1945 字符,只留参数语义和「何时调用」。
  • 精排候选封顶 15 条:精排一批 30 条约 335 毫秒,封顶后只省 0.3 秒左右。这一刀收益小,但成本几乎为零。
  • 没做的:到手价计算和精排并发。实测到手价算 240 件只要 0.04 秒,并发收益不到 0.05 秒,不值得加一层复杂度。

流程图 / 架构图#

改前 vs 改后:一次普通单品任务的时序#

sequenceDiagram
    autonumber
    participant U as 用户
    participant F as 框架
    participant M as 主模型
    participant T as 工具
    rect rgb(245,235,235)
    Note over U,T: 改前:9 次模型调用,全部串行
    U->>F: 购物需求
    F->>M: 意图解析
    F->>M: 第1轮
    M->>T: 查品类知识
    F->>M: 第2轮
    M->>T: 并发检索多平台
    T-->>F: 候选全量 JSON 约 12k 字符
    F->>M: 第3轮
    M->>T: 比价 参数里逐字重写候选
    F->>M: 第4轮
    M->>T: 精挑 参数里逐字重写候选
    F->>M: 第5轮
    M->>T: 收尾
    T->>M: 工具内部再调一次模型写文案
    end
    rect rgb(235,245,235)
    Note over U,T: 改后:4 次模型调用
    U->>F: 购物需求
    par 并发
        F->>M: 意图解析
    and
        F->>T: 预取品类知识
    end
    F->>M: 第1轮
    M->>T: 并发检索多平台
    T-->>F: 紧凑投影约 3k 字符 全量进登记表
    F->>T: 自动比价加精挑
    F->>M: 第2轮 注入精选结果提示
    M->>T: 收尾 参数只有商品ID加文案
    T-->>U: 按ID取回全量 排成商品卡
    end

改后的 4 次 = 意图解析 1 次 + 主循环 2 轮 + 轨迹漂移检查 1 次(收尾轮跳过)。

自动比价精挑的武装与执行#

flowchart TD
    A["检索工具成功返回"] --> B{"本轮是套装轮?"}
    B -- 是 --> Z["不武装 由模型走套装流程"]
    B -- 否 --> C["会话打上已武装标记"]
    C --> D["同轮其余检索并发返回 合流"]
    D --> E["下一次调用模型之前"]
    E --> F{"已武装 且登记表有候选?"}
    F -- 否 --> M["照常调用模型"]
    F -- 是 --> G["解除武装"]
    G --> H["比价 按ID折算到手价 回写登记表"]
    H --> I["精挑 按会话约束选3件 回写理由"]
    I --> J["结果过同一条后处理管线"]
    J --> K["注入提示: 已自动完成 勿再调用"]
    K --> M
    H -. 任何异常 .-> X["吞掉异常 退回模型自己调"]
    I -. 任何异常 .-> X
    X --> M
    N["模型显式调了精挑工具"] --> G2["解除武装 不重复执行"]

商品 ID 引用的数据流#

flowchart LR
    S["检索工具"] -- 全量候选 --> R[("本轮候选登记表<br/>按商品ID索引")]
    S -- 紧凑投影 ID 标题 价格 评分 --> CTX["模型上下文"]
    CTX -- 只写商品ID加理由 --> SUM["收尾工具"]
    R -- 按ID取回 链接 图片 到手价 --> SUM
    PC["比价"] -- 回写到手价 --> R
    PK["精挑"] -- 回写入选理由 --> R
    SUM --> CARD["商品卡 发给前端"]
    Q[("向量库")] -. 跨轮按ID回源 .-> R

数字怎么来的#

测量口径#

  • 测试集:普通单品轮的固定 query,每条跑 3 遍,每遍用全新会话 ID(复用会变成追问轮,数字不真实),墙钟取中位后再对 query 求平均。「通勤背包,16 寸笔记本,防泼水,预算 400 以内」是序列最稳定的一条,也是逐步对比时的主参照;套装轮不在这组数字里。
  • 模型调用次数:从每个任务的 token 记账树里数,包括意图解析、主循环每一轮、工具内部的模型调用、漂移检查小模型,不只数主循环。
  • 累计输入 token:一次任务内所有模型调用的输入 token 之和(含命中缓存的部分),来自模型返回的用量字段。它反映的是「整个任务一共让模型读了多少」,每次调用重读历史上下文都会累加进去。
  • 端到端耗时:从服务端收到任务到发出最终结果事件的墙钟时间,不含回合结束后在后台跑的长期记忆抽取(用户感知不到)。
  • 工具序列一致性:3 遍的工具名序列完全相同才算稳定;不一致的批次数字只作参考,不当作信号。

逐步对照(主参照 query,3 遍中位)#

阶段端到端耗时模型调用累计输入 token说明
改前28.3s979k意图解析 + 主循环 5 轮 + 收尾内部 1 次 + 漂移检查 2 次
+ 自动比价精挑21.3s643.7k主循环少两轮,连带少一次漂移检查
+ 收尾文案入参化19.9s542.7k收尾工具内部那次调用消失
+ 品类知识预取并发、精排封顶17.1s432.6k主循环第 1 轮不再专门查品类知识
+ ID 引用与紧凑投影、工具说明瘦身12.5s422.5k调用次数不变,每次读得少、写得少
  • 9→4 次:前三步各去掉一类调用;第四步不减次数。
  • 79k→22.5k:一半来自调用次数减少(每少一次就少重读一遍全部上下文),一半来自每次读的东西变少(检索返回 12k→3k 字符、工具说明 7395→1945 字符)。
  • 最后一步为什么能省 4.6 秒:输入少了 1 万 token 只贡献 1 秒左右的首 token 延迟;主要收益在输出侧,收尾参数从「整包商品」变成「ID + 每件一句理由」,输出 token 从约 1.2k 降到约 0.7k,这部分解码大约 3 秒。
  • 改后 12.5 秒的构成:意图解析约 3.7 秒(品类知识预取藏在它里面)、检索 + 精排 + 自动比价精挑约 2.5 秒、主循环两轮解码约 5 秒(收尾轮写文案占大头)、其余是网络与事件推送。

质量有没有掉#

延迟改动最怕「快了但答错了」。每一步合入前跑全量单测,另外看三件事:工具序列是否稳定、最终清单件数和到手价是否与改前一致、收尾理由是否仍逐件贴合意图。收尾文案改由主模型写、工具说明大幅删减这两步,额外用 Rubric 评测的 15 条子集复核了业务红线项(到手价口径、不编造商品),没有新增红线失败。

追问 Q&A#

Q1:你说「通过日志定位瓶颈」,具体看的什么日志? 两类。一是 Langfuse 里每次模型调用的 generation 记录,有输入/输出 token、缓存命中和耗时;二是我们自己的事件流,每个工具有开始和结束时间戳。按会话 ID 把两者对齐,就能画出一次任务的时间线。一拉出来就很清楚:28 秒里工具合计 2.5 秒,其余是模型调用,而且一次接一次串行。每轮主循环 2~3 秒,收尾工具内部那次 5.5 秒。

Q2:为什么不直接换个更快的模型? 换过意图解析的模型,那是链路外的一次性调用,换起来风险小。但主循环模型换掉等于换全场主力,工具选择、收尾文案、异常处理全要重新评测。而且时间线显示问题是「调用次数 × 每次解码量」,换快模型只是把每次变快一点,9 次串行还是 9 次。先减次数、减解码量,这是和模型无关的收益,以后换模型也能叠加。

Q3:自动执行比价和精挑,不就退化成 Workflow 了吗?还算 Agent 吗? 只有「检索成功、非套装轮」这条最常见的路径是机制跑的,而且模型仍然看得到结果、决定收尾还是重搜。召回跑题时模型会换词再搜,追问轮会显式调精挑,套装轮走确认流程——这些需要判断的地方还是模型在决策。我的原则是:输入确定、零决策的步骤交给机制,需要判断的交给模型。

Q4:自动精挑要用的偏好和约束从哪来?会不会和模型的理解不一致? 从会话状态里来。开局意图解析会把预算、品类、排除词、软偏好写成结构化字段,精挑工具无参调用时就按这份约束执行。改前模型调精挑时,传的其实也是这份约束,并没有加新信息,所以两者一致。如果模型认为约束需要调整,它可以显式调精挑工具覆盖,框架看到后就解除自动执行。

Q5:质疑一下:同一个 query 模型有时多搜一次,你怎么知道 28.3→12.5 不是随机波动? 三个办法。第一,每条 query 跑 3 遍取中位,不看单次。第二,同时看工具序列是否一致,主参照 query 在每一步都是 3 遍序列完全相同,所以对比有效;序列不稳定的 query(比如会被拆成套装的礼物类需求)我只当参考,不拿它的墙钟说事。第三,每一步都能在调用次数上解释:少了哪一轮、少了哪次内部调用,墙钟下降和调用次数下降是对得上的。

Q6:如果自动比价精挑执行失败了怎么办? 所有异常在自动步骤内部吞掉并记日志,同时解除武装。模型下一次调用时就看不到注入的精选结果,会按原来的方式自己调比价和精挑。失效方向就是退回改前的行为,慢一点但结果正确,不会卡住主循环。

Q7:ID 引用有什么风险?模型把 ID 写错了怎么办? 收尾和下单工具拿到 ID 后按登记表取回候选,取不到的直接跳过,所以写错的 ID 不会变成一件编出来的商品;下单工具还要求 ID 必须在本轮登记表或历史候选里,这是写操作边界的一部分。另一个风险是 ID 会被模型写进给用户看的文案里,我们在推给前端前把文案里的 ID 替换成商品短名。反过来说,ID 引用让链接和价格完全不经过模型,模型改写链接、抄错价格的问题反而没了。

Q8:用户隔了几轮说「就买第二个」,登记表只活一轮,怎么找回来? 登记表只保留本轮,是为了防止状态无限增长。跨轮引用时按 ID 回源向量库,把商品 payload 取回来再登记一次,本轮后续的工具就不用再回源。到手价这类派生字段会重新算,因为汇率和收货国可能变了。

Q9:累计输入 token 降了 72%,但前缀缓存命中本来就高,真的能省时间吗? 缓存命中能省计算和费用,但不是免费:命中部分仍然占首 token 延迟的一部分,没命中的新增内容更要完整算一遍。而且我们的问题一半在于「每轮都有新的 12k 字符工具返回进来」,这部分每次都是新内容、必然不命中。实测输入降 1 万 token 贡献约 1 秒;最后那步省下的 4.6 秒里,大头其实是输出侧:模型不用在参数里逐字重写商品了。

Q10:为什么不把检索、比价、精挑合成一个大工具,让模型一次调完? 检索是按平台同轮多发、框架并发的,每一路只看到自己平台的结果。比价和精挑需要在所有平台合流之后做,才有全局视角。把它们放在「下一次模型调用前」这个时点,天然就是合流之后,不用在工具之间自己做同步。另外大工具的参数会很复杂,模型更容易填错。

Q11:如果以后要继续降,下一步做什么? 改后 12.5 秒里意图解析占约 3.7 秒,主循环两轮解码约 5 秒。意图解析可以换更小的专用模型——这正是我做的 Query 解析模型后训练的方向。收尾轮的文案解码可以通过流式把「首屏」提前,用户看到第一个字的时间比完整耗时更重要。检索和精排那 2.5 秒已经不是主要矛盾。

Q12:这些改动对套装类需求有帮助吗? 自动比价精挑按设计不介入套装轮,因为套装要先让用户确认组成,再按槽位做组合优选,所以那条路径主要受益于 ID 引用和工具说明瘦身,输入 token 同样降了一半以上,但墙钟主要取决于模型拆几个槽、每个槽搜几次,波动大,我没有把它放进简历的数字里。

相关八股#

1. LLM 推理的 Prefill 和 Decode 两个阶段有什么区别?各自的瓶颈是什么?

  • Prefill:把整段输入一次性并行算完,生成全部位置的 KV,产出第一个 token;计算密集,耗时随输入长度大致线性增长,决定首 token 延迟(TTFT)。
  • Decode:之后每一步只生成一个 token,每步都要读一遍模型权重和已有 KV cache;访存密集,GPU 算力用不满,耗时约等于「输出 token 数 × 每 token 间隔」。
  • 所以同样 1000 个 token,放在输出里比放在输入里贵一个数量级以上。
  • 关联:这就是 ID 引用的主要收益在输出侧的原因——模型不再逐字重写商品,比少读 1 万输入 token 省得多。

2. KV Cache 和前缀缓存(Prompt Cache)是什么关系?

  • KV Cache:单次请求内部,decode 每步复用前面位置的 Key/Value,避免重复计算。
  • 前缀缓存:跨请求复用,服务端把某段前缀算好的 KV 存下来,下一个请求前缀逐 token 完全一致时直接取用,只算新增部分。
  • 命中条件是「前缀精确匹配」,前面任何一个字符变了,后面全部失效;所以稳定内容放前面、变化内容放后面。
  • 命中能省计算和费用,但命中部分仍占用上下文窗口,新增内容仍要完整 prefill。
  • 关联:Agent 每轮重读全部历史,累计输入 token 随调用次数近似平方增长;减少调用次数和缩小工具返回是从源头少读。

3. 串行调用链怎么估算优化上限?(Amdahl 定律)

  • 加速比 = 1 / ((1 − p) + p / s),p 是可优化部分占比,s 是该部分的加速倍数。
  • 如果工具只占 10%,就算把工具优化到 0,整体也只快约 11%。
  • 优化前先量占比,把力气花在占比最大的部分。
  • 关联:时间线显示工具只占 2.5/28.3 秒,所以放弃了给工具加缓存,转而减少模型调用次数。

4. 并发执行多个 IO 任务的常见方式?Java 里怎么写?

  • 思路:多个互不依赖的 IO 调用同时发出,总耗时取最慢那个,而不是求和。
  • Java:CompletableFuture.supplyAsync 各自提交到线程池,allOf(...).join() 等全部完成;要注意自定义线程池,别用默认的 ForkJoinPool 公共池跑阻塞 IO。
  • 有依赖的步骤用 thenCompose 串起来,无依赖的用 thenCombine / allOf 并起来。
  • 异常处理:单个任务失败时用 exceptionally / handle 给默认值,避免一个失败拖垮整批。
  • 关联:跨平台检索同轮多发由框架并发;意图解析和品类知识预取也是并发发出。

5. 为什么性能对比要看中位数和分位数,而不是平均值?

  • 平均值容易被个别长尾拉偏;中位数(P50)反映典型情况,P95/P99 反映长尾体验。
  • 样本少时(比如每组 3 遍),中位数比平均值更抗异常点。
  • 做 A/B 时要控制变量:同一 query、同一模型、新会话、排除缓存预热差异,并确认执行路径一致。
  • 关联:每条 query 3 遍取中位,并要求工具序列一致才把墙钟差异当作信号。

6. 按引用传递和按值传递的区别?分布式系统里的对应模式?

  • 按值传递:把完整数据复制给被调用方;按引用传递:只传一个句柄,被调用方按需取。
  • 消息系统里的 Claim Check 模式:大消息体放存储,消息里只带一个 key,消费方凭 key 取数据,降低消息体积。
  • 代价:多一次查询,要处理引用失效(数据被删、过期)的情况。
  • 关联:候选登记表就是 Claim Check——上下文里只流转商品 ID,全量数据在登记表里,跨轮失效时回源向量库。

7. 流式输出(Streaming)能降低延迟吗?

  • 不降低总耗时,但降低用户感知延迟:首个 token 一出来就推给前端,用户看到的是「开始回答」的时间。
  • 常见实现:服务端 SSE 或 WebSocket 逐块推送;模型侧开 stream 模式,工具调用的参数也是按增量吐出的。
  • 衡量指标要区分 TTFT(首 token 时间)和总耗时。
  • 关联:收尾文案改成主模型在工具参数里写之后,前端逐字流式改为从参数增量里截取,保证体验不退化。

8. 性能优化路径的降级设计原则是什么?

  • 优化路径出错时,应该退回到一条慢但正确的路径,而不是报错或给错结果(fail-safe 而不是 fail-fast)。
  • 常见做法:开关控制、异常吞掉加日志、降级后打标记便于统计降级率。
  • 优化必须可关闭,方便对照实验和线上回退。
  • 关联:自动比价精挑任何异常都退回「模型自己调」,并保留开关一键回到改前行为做对照。

9. Workflow 和 Agent 怎么取舍?

  • Workflow:步骤和顺序由代码写死,可预测、快、便宜,但遇到计划外情况只能报错或走默认分支。
  • Agent:模型在循环里自己决定下一步调什么,灵活,能处理异常路径,但每一步都要一次模型调用,慢且有随机性。
  • 常见折中:高频、确定的路径由代码执行,模型只在需要判断的节点介入;Anthropic 的 Agent 实践文章也建议「能用简单方案就别上 Agent」。
  • 关联:检索后比价 + 精挑是高频确定路径,交给框架;换词重搜、追问、套装确认仍由模型判断。

10. 上下文越长,除了慢和贵还有什么问题?

  • 注意力被稀释:长上下文中间位置的信息更容易被忽略(Lost in the Middle),无关字段越多,模型越容易被干扰。
  • 触发上下文压缩:超过阈值后要做摘要,摘要是二手信息,可能丢细节,还会让前缀缓存失效。
  • 输出也会变长:输入里有大段 JSON 时,模型倾向于在输出里照抄结构。
  • 关联:紧凑投影去掉链接、召回分、空字段,不只是省 token,也让模型只看到做决策要用的字段。