为什么模型这一轮的输出,不该让它「既要念又要解析」
一段输出该说人话还是吐 JSON,唯一判据是「给谁消费」;工具该 eager 还是 deferred,看延迟红线乘以 tool 数量。
立靶:同一轮输出,被塞了两个互相打架的职责
你在做一个语音 Agent。产品提了个需求:通话过程中,把候选人的画像实时存进数据库——顺手的事,让对话那一轮的 LLM 输出时,顺便吐一段 JSON 不就行了?
于是你改了 prompt,要求模型「先正常回答用户,然后在末尾附上一段结构化画像」。上线一试,灾难来了:语音念到一半开始念「左花括号、引号、name 冒号」,口语节奏全乱,用户听着像机器人卡壳。
问题不在模型不行,在于你让一轮输出同时背了两个职责:一段要立刻喂给 TTS 念给人听,另一段要给程序解析存库。这两件事对「输出长什么样」的要求是反的——人要流畅口语,程序要稳定字段。强行合并,两头都不讨好。
这就是上下文七层里 L4(结构化 IO)和 L5(工具集成)这两层最常踩的坑。它们看起来都在管「模型的输出该长什么样、能触发什么动作」,但真正要回答的,是两个独立又互相纠缠的问题:这段输出该 schema 驱动还是自然语言直出?这些工具该一开始全给,还是要用时再加载? 答错任意一个,要么口语崩、要么 token 爆、要么用户听到一段死寂的静默。
框架:两条判据 + 一句分层口诀
先把两层分清楚。最容易混的地方在于:一个 tool 的参数也是一段 schema,那它到底算 L4 还是 L5?
答案是两层都落,不矛盾。看的角度不同:
| L4 结构化 IO | L5 工具集成 | |
|---|---|---|
| 管什么 | 「输出/调用那段结构长什么样、谁来解析」 | 「有没有这个动作、模型会不会发起调用」 |
| 方向 | 主要管出(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 视角——用它做判断和沟通:
- 接到「让模型顺便输出个结构化数据」的需求,先问一句「这段输出最终给谁消费」。给人念 → 自然语言;给程序 → 结构化;既要念又要解析 → 明确拆成两次调用,别让一轮背两个职责。
- 谈工具体验时,用「延迟红线 × tool 数量」对齐:低延迟场景(语音、实时)默认 eager,别被「deferred 省 token」带跑;工具堆到几十个再谈 deferred,且要算上首用 round-trip 的体验代价。
- 警惕「结构化输出更高级」的直觉——在对话流里强塞 JSON 是体验灾难。结构化是手段不是目的。
- 选实时架构前先问清楚:这条产品线未来要不要工具调用 / 精细结构化?要,就别轻易上端到端 realtime 黑盒,否则等于把这层能力让渡出去。
工程视角——用它做架构决策:
- 用「输出给谁消费」拆通路:实时对话归对话(自然语言直出,限输出长度逼短口语),结构化提取归旁路(事后/异步一次 schema 调用),两条物理分开。
- tool 选型按「延迟红线 × 数量」分层:高频核心 tool 保 eager,长尾低频走 deferred;语音/低延迟场景一律 eager 兜底。
- 把 tool schema 当静态前缀对待——和 L1 一起进 prompt cache,别让几百字的 description 每轮全量重发。
- 审计认「实跑通路」而非设计稿:设计文档里画了完整 JSON schema、stage 输出契约,不等于线上真启用了;审 L4/L5 要看代码实际走哪条路。
- 上 realtime 前留好 pipeline 通路的退路:要么主链路保 pipeline,要么旁路一条转写流喂 pipeline 做结构化提取,别等塌缩了才发现工具能力没地方挂。
收口
L4 管输出长什么样,L5 管能调什么动作,一个 tool 同时落两层不矛盾。schema 还是自然语言,只看输出给谁消费;eager 还是 deferred,只算延迟红线乘以 tool 数量。一次输出别既要念又要解析——拆两次调用,对话归对话、提取归提取。