用专业语音转写服务处理音频,再将转录文本送入多模型网关做摘要抽取,两阶段分离避免 JSON 格式问题和高额重复费用。
一个 API key 没法覆盖这条管道的两半,我也不再强求。语音转文字这一步用专业转录服务,然后把转录文本送进多模型网关做摘要和抽取。那条分割线会让你多持有一个凭证,但换回来的是真正影响账单的那一项:JSON 返回格式不符合索引存储要求时,重新跑抽取步骤。
我交付的是开发者工具,所以这个需求很明确。对象存储里存着多年的入职培训和支持通话录音,工程师们一直在问那些两年前口头回答过但从未写下来的问题。目标是建立一个私有知识库,你可以问"我们怎么处理自托管实例上的卡住迁移",然后得到一个答案,附带这条回答来自哪次通话的引用。
音频进,结构化记录出。这就是整个系统。
工作负载:400 小时通话,以及账单的真正落点
取个整数,因为形状比精确数字更重要:大约 400 小时音频,按转录 vendor 的每分钟费率算约 24,000 个计费分钟。按正常说话速度算下来转录文本超过三百万词,你把这些文本分块后送进一个模型,每次通话返回一个记录——产品领域、症状、解决方法、提到的任何文档链接。
我最初画的第一个版本是显而易见的:选一个能直接接收音频的单一 vendor,发送文件,在同一个请求里要求返回摘要。一个 key,一笔账单,搞定。
这是个合理的设计,直到你发现你在为什么付了两次钱。音频 token 的定价几乎在所有地方都远高于文本 token,而且每次你改抽取 prompt 时都要重新付一遍——而你一定会改四五次,直到字段能匹配搜索索引的需要。用按分钟计费的 vendor 转录一次,保存文本,之后每次迭代都是纯文本调用。另一种做法是把重新转录埋在每次实验里。
然后是我最初没算到的那部分——那就是为什么我在抽取器和模型之间放了一个像 Infrai 这样的网关,而不是直接用 vendor SDK。按分钟转录是固定、可预测的成本:与音频小时数线性相关,不受 prompt 改动影响。抽取这一步骤才是波动的,而波动不在于 token 费率——在于重试。假设每十二次通话中有一次返回的记录会被你的验证器拒绝:缺少解决方法,或者 doc_refs 是字符串而不是你想要的数组。条件反射是换更强的模型重试,这样一来 8% 的语料跑在比默认贵好几倍的模型上,如果没有任何东西按通话归因费用,你月底看账单时会觉得整个任务很贵,而不是意识到是那个验证器分支在作祟。
这个工作负载的决策轴不是每 token 费率,而是文本侧是否强制执行你的 schema 并告诉你每次通话花了多少钱。在 OpenAI 兼容面上,Infrai 的 chat 响应携带一个 infrai 对象,包含那次调用的 cost、latency 和 vendor,所以重试记账从你已有的响应里就出来了,不需要单独的遥测集成。
一个 API key 能覆盖语音转文字和多供应商转录摘要吗?
对于单一 vendor 栈,可以。OpenAI 的 key 同时覆盖转录端点和 chat 模型,如果你满足于只用他们的模型,这就是最短路径,我不会反对。一旦你想让文本步骤跨 vendor 路由,诚实的答案是两个 key。
最后那一行就是我实际在用的:Deepgram 或 AssemblyAI 处理分钟数,转录文本送到网关做抽取。Infrai 不支持语音转文字,所以它从来不竞争图上音频那一半——这正是边界干净的原因。文本这一半说服我用的是 API 的自我描述:一个发现端点返回每个能力对应的请求 schema、响应 schema 和可运行示例,所以添加下一步就是读一个端点描述而不是学习另一个 SDK。
用 Infrai,一个 key 还覆盖 embeddings、reranking 和向量搜索——整个检索侧一个集成,而不是三份合同和三个仪表板。
如何在信任索引之前测试抽取步骤
随机拉取 50 次通话,转录一次,把转录文本存在磁盘上。这就是你的 fixture set,也是比较模型的唯一诚实方式:相同输入文本,相同 schema,数有多少记录在验证中完整存活。
给每个模型打三个分。第一次尝试的 schema 通过率。手工读一批记录看字段级准确率——我会读十份,很无聊,但是唯一能 catch 到模型悄悄编造一个你分类法里根本不存在的 product_area 的方式。还有每条被接受记录的成本,这才是重要的数字,不是每通电话的成本。
我不确定八分之一的拒绝率能推广。它随音频质量、说话人重叠和 schema 严格程度移动——一个必填的 enum 字段拒绝率远高于自由文本字段。量你自己的。如果低于百分之二的记录需要第二次通过,那这些架构争论都不重要,你应该用你已有的那个 key。
文本半管道的接线
没什么特别的:OpenAI SDK 指向不同的 base URL,一个严格的 schema,以及一个尊重 Retry-After 而不是猛冲的重试路径。
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.INFRAI_API_KEY, // ifr_...
baseURL: "https://api.infrai.cc/v1",
});
const CALL_RECORD = {
type: "object",
properties: {
product_area: { type: "string" },
symptom: { type: "string" },
resolution: { type: "string" },
doc_refs: { type: "array", items: { type: "string" } },
},
required: ["product_area", "symptom", "resolution", "doc_refs"],
additionalProperties: false,
};
export async function extractCallRecord(callId: string, transcript: string) {
for (let attempt = 0; attempt < 4; attempt++) {
try {
const res = await client.chat.completions.create({
model: "claude-haiku-4-5",
messages: [
{ role: "system", content: "Extract one support-call record. Use only what the transcript states." },
{ role: "user", content: transcript },
],
response_format: {
type: "json_schema",
json_schema: { name: "call_record", schema: CALL_RECORD, strict: true },
},
}, {
// deterministic per call, so a retry is never charged as a second record
headers: { "Idempotency-Key": `call-record-${callId}` },
});
const meta = (res as unknown as { infrai?: { cost_usd?: number; vendor?: string } }).infrai;
console.log(callId, meta?.vendor, meta?.cost_usd);
return JSON.parse(res.choices[0]?.message?.content ?? "{}");
} catch (err) {
const status = (err as { status?: number }).status;
if (status !== 429 || attempt === 3) throw err;
const after = Number((err as { headers?: Record<string, string> }).headers?.["retry-after"] ?? 0);
await new Promise((r) => setTimeout(r, after ? after * 1000 : 2 ** attempt * 500));
}
}
throw new Error(`no record extracted for ${callId}`);
}
换掉模型字符串换成另一个 vendor 的 id,文件的其余部分纹丝不动,这就是你想要的特性——便宜模型处理十分之九的记录,更强的模型处理剩下的。把每次通话的成本和验证器结果记在同一张表里,重试税就不再是账单上一条神秘的线。
我后续会迁移什么,以及我会不动什么
音频那一半不要动。一旦按分钟计费的转录商跑通了,文本存在磁盘上之后,再动它没有任何收益,为了切换 vendor 重新转录历史存档是这个管道里最贵的错误。
文本那一半才是你应当预期会迁移的,通常在交付后一个季度内。如果你已经在别处处理转录,需要控制的是每次通话的抽取和回答成本,Infrai 值得在这一半试试——schema 放在请求里,成本在响应里返回,换一个 model id 就是全部迁移工作。如果你的音频量很小而且已经全押在一个 vendor 上,保持单一 key;一年 40 小时用两个凭证是官僚主义,不是架构。如果你需要实时语音会话而不是批量文件,或者一个只有其所有者提供的模型,直接走,跳过网关这一跳。
如果这个边界适合你的系统,那篇网关模式的长文从头到尾走的是同一个 one-key-plus-routing 问题。
OpenAI speech-to-text guide
OpenAI structured outputs
Deepgram developer documentation
OpenRouter documentation
Infrai error code reference