为什么模型这一轮的输出,不该让它「既要念又要解析」

一段输出该说人话还是吐 JSON,唯一判据是「给谁消费」;工具该 eager 还是 deferred,看延迟红线乘以 tool 数量。

Module E · 第 2 讲

立靶:同一轮输出,被塞了两个互相打架的职责

你在做一个语音 Agent。产品提了个需求:通话过程中,把候选人的画像实时存进数据库——顺手的事,让对话那一轮的 LLM 输出时,顺便吐一段 JSON 不就行了?

于是你改了 prompt,要求模型「先正常回答用户,然后在末尾附上一段结构化画像」。上线一试,灾难来了:语音念到一半开始念「左花括号、引号、name 冒号」,口语节奏全乱,用户听着像机器人卡壳。

问题不在模型不行,在于你让一轮输出同时背了两个职责:一段要立刻喂给 TTS 念给人听,另一段要给程序解析存库。这两件事对「输出长什么样」的要求是反的——人要流畅口语,程序要稳定字段。强行合并,两头都不讨好。

这就是上下文七层里 L4(结构化 IO)和 L5(工具集成)这两层最常踩的坑。它们看起来都在管「模型的输出该长什么样、能触发什么动作」,但真正要回答的,是两个独立又互相纠缠的问题:这段输出该 schema 驱动还是自然语言直出?这些工具该一开始全给,还是要用时再加载? 答错任意一个,要么口语崩、要么 token 爆、要么用户听到一段死寂的静默。

框架:两条判据 + 一句分层口诀

先把两层分清楚。最容易混的地方在于:一个 tool 的参数也是一段 schema,那它到底算 L4 还是 L5?

答案是两层都落,不矛盾。看的角度不同:

L4 结构化 IOL5 工具集成
管什么「输出/调用那段结构长什么样、谁来解析」有没有这个动作、模型会不会发起调用」
方向主要管出(response 格式契约)管出(模型发起调用)+ 入(tool 结果回灌)
谁消费下游程序解析 / 存库 / 渲染外部系统执行副作用(挂断、查库、发消息)
静态/动态静态(schema 每轮固定)静态(tool 列表每轮固定)

分层口诀:L5 管「有没有这个动作」,L4 管「那段结构长什么样」。一个 tool 同时落两层——它既是「L5 的一个动作」,又自带「L4 的一段参数结构」。判一个 tool 主要落哪层,先问它本质是动作还是格式约束:能发起调用、触发副作用的,L5 是主体;参数 schema 只是它附带的 L4 面。别看它「返回结构化数据」就往 L4 跳。

L4 的唯一判据:这段输出要给谁消费。

输出消费方选择理由
给人 / TTS 念出来自然语言直出要的是流畅口语,JSON 没法念,强塞 schema 反而憋坏语气
给下游程序解析 / 存库 / 打分 / 触发动作schema 驱动(JSON / tool_call)程序要稳定字段,自然语言解析不可靠
既要念又要程序拿拆两次调用:对话流直出人话 + 旁路一次 schema 提取别让一次输出同时背两个职责

L5 的判据:延迟红线 × tool 数量。 eager 是把所有 tool schema 一开始就写进上下文,模型每轮都看得到签名;deferred 是 schema 不预载,模型要用时先「搜/取」再调(典型实现是某种 ToolSearch 或 MCP 懒加载)。

eager(预先全给)deferred(按需加载)
机制tool schema 写进 tools 字段,每轮都在schema 不预载,用时先搜/取再调
延迟低(签名已在,直接调)首用 +若干百毫秒到数秒(要先加载 schema)
token每轮重发所有 tool schema(占静态前缀)省:没用到的 tool 不进上下文
规模tool 少时最优;多了干扰选择 + 撑爆 token几十上百个 tool 才划算

deferred 不是免费的省 token 神器。每个 deferred tool 首次用都有一次 round-trip 成本——模型得先发一轮去「取」schema,再发一轮真调用。在低延迟场景,这一下就是用户听到的一段静默。所以判据不是「tool 多就 deferred」,而是「延迟能不能容忍这次首用 round-trip」。

数据 / case:两套 IO、一次工具升级、一次黑盒塌缩

案例 A 对话交互 vs 案例 B 离线分析(同一套业务,两条通路)。 假设一个语音招聘 Agent:

案例 A 对话交互(实时通路)案例 B 离线分析(旁路/事后)
LLM 输出自然语言直出,还会限制最大输出 token 逼短口语结构化 JSON:多维评分 / 多项画像
为什么输出立刻喂某语音端点念给用户 → 必须人话输出给程序存库 / 打分 / 渲染卡片 → 必须稳字段
L4 选择几乎没有结构化(直出自然语言)schema 驱动,走 LLM-as-judge
通路在通话关键路径上(每轮要 TTS)不在通话关键路径(事后异步跑)

精妙处全在「职责分离」:通话中绝不让 LLM 吐 JSON(会卡 TTS、破坏口语节奏);想要结构化画像,就放到通话后单独一次 LLM 调用用 schema 提。回到开头那个「通话中实时存画像」的需求,正解不是改对话流去吐 JSON,而是另起一条旁路:把语音转写流异步喂给另一个 pipeline LLM 做提取写库,主对话链路一个字都不动。对话归对话、提取归提取。

一次工具升级。 某语音 Agent 起步只有 1 个工具(一个「挂断通话」的动作),用 eager——只 1 个 tool、又是延迟红线场景,必须 eager。后来产品想加到 20 个工具(查询、预约、发送通知……)。继续全 eager 吗?全 eager 会每轮重发 20 份 schema(token 爆)+ 模型在 20 个里容易选错。但纯 deferred 也不行——语音场景里 deferred 首用的那段静默是体验红线。务实解是分层:高频核心 tool(如挂断)保 eager 低延迟,长尾低频 tool 走 deferred。换成一个普通文字 chatbot,没有「静默听感」这条约束,就可以更激进地全 deferred。

一次黑盒塌缩。 当你把语音 Agent 从「pipeline 通路」(每轮一次 chat completion)换成「端到端 realtime 通路」(speech-to-speech 黑盒 session),会发现 L4/L5 直接塌缩消失:realtime session 级只注入一次 instructions,没有「轮」的边界,供应商不暴露 tool 调用钩子,压缩也黑盒自管。结构化输出和工具调用这套能力,没有立足点了。

realtime 塌缩的取舍含义很硬:想给语音 Agent 加结构化输出 / 工具调用能力,就得留在 pipeline 通路(每轮一次 chat completion,tool/schema 才挂得上)。一旦上端到端 realtime 黑盒,L4/L5 这套能力基本让渡给供应商了,换来的是端到端低延迟。鱼和熊掌想兼得,唯一的折中是旁路一条转写流异步喂 pipeline 提结构化,主链路保 realtime 不动。

还有一块容易漏的暗物质:tool 的 description 是每轮重发的静态 token。一个 tool 的 schema 加上一段几百字的描述,会随 tools 字段每轮全量重发,但它从不出现在你的 system prompt 文件里——只读 prompt 文件做审计会把这几百字漏掉(这和「写了不等于生效、要追代码」同源)。而 tool schema 属于静态前缀,本该和 L1 一起进 prompt cache 只发一次。没做 cache 的话,eager 模式下 tool 越多,这部分隐形 token 越是大头。

可操作做法:PM 与工程两条线

PM 视角——用它做判断和沟通:

  1. 接到「让模型顺便输出个结构化数据」的需求,先问一句「这段输出最终给谁消费」。给人念 → 自然语言;给程序 → 结构化;既要念又要解析 → 明确拆成两次调用,别让一轮背两个职责。
  2. 谈工具体验时,用「延迟红线 × tool 数量」对齐:低延迟场景(语音、实时)默认 eager,别被「deferred 省 token」带跑;工具堆到几十个再谈 deferred,且要算上首用 round-trip 的体验代价。
  3. 警惕「结构化输出更高级」的直觉——在对话流里强塞 JSON 是体验灾难。结构化是手段不是目的。
  4. 选实时架构前先问清楚:这条产品线未来要不要工具调用 / 精细结构化?要,就别轻易上端到端 realtime 黑盒,否则等于把这层能力让渡出去。

工程视角——用它做架构决策:

  1. 用「输出给谁消费」拆通路:实时对话归对话(自然语言直出,限输出长度逼短口语),结构化提取归旁路(事后/异步一次 schema 调用),两条物理分开。
  2. tool 选型按「延迟红线 × 数量」分层:高频核心 tool 保 eager,长尾低频走 deferred;语音/低延迟场景一律 eager 兜底。
  3. 把 tool schema 当静态前缀对待——和 L1 一起进 prompt cache,别让几百字的 description 每轮全量重发。
  4. 审计认「实跑通路」而非设计稿:设计文档里画了完整 JSON schema、stage 输出契约,不等于线上真启用了;审 L4/L5 要看代码实际走哪条路。
  5. 上 realtime 前留好 pipeline 通路的退路:要么主链路保 pipeline,要么旁路一条转写流喂 pipeline 做结构化提取,别等塌缩了才发现工具能力没地方挂。

收口

L4 管输出长什么样,L5 管能调什么动作,一个 tool 同时落两层不矛盾。schema 还是自然语言,只看输出给谁消费;eager 还是 deferred,只算延迟红线乘以 tool 数量。一次输出别既要念又要解析——拆两次调用,对话归对话、提取归提取。