将HTTP 200、JSON解码、Schema验证、转录非空分为四个独立检查节点,返回显式结果而非单一成功标志,用fixture验证拒绝输入不产生任何调用。
Short answer: 一个语音转文本 API 客户端应该把 HTTP 成功、JSON 解码、Schema 验证和非空转录文本视为四个独立的检查项;只有经过验证的文本才能进入索引或 Prompt。
运营约束改变了设计:HTTP 200 且响应为 {"text": null} 在传输层已完成,但没有产生可用结果。Node.js 或 TypeScript 类型无法在运行时对该响应进行验证。在提供者边界放置一个防御性解析器,返回显式结果,并用 Fixture 证明被拒绝的输入不会产生任何 Chunk、Vector 或模型调用。
把这些检查做得无聊一些。这是好事。
从四个结果开始,而不是一个成功标志:接受的文本、空转录文本、格式错误的 JSON 和无效的 Schema。它们描述了不同的证据。格式错误的 JSON 意味着解码失败。无效的 Schema 意味着解码成功但值的形状或类型错误。空意味着预期字段为 null、"" 或空白字符。接受意味着它是字符串,且在应用程序选择的规范化后至少有一个非空白字符。
这个区别在检索之前很重要。如果 null 被强制转换为 String(value),字面词 "null" 可能变成一个 Chunk;如果对象被强制转换,"[object Object]" 可能紧随其后。这两个字符串对于简单的守卫来说看起来都是非空的。一旦被嵌入,它们就更难与真实内容区分开来,而且转录任务可能看起来已完成,即使用户无法搜索他们上传的内容。OWASP 十大 LLM 应用风险是相关的背景知识:下游模型系统需要明确的信任边界。本地解析器就是这样一种边界,而不是完整的安全方案。
不要猜测备用字段名。适配器可能有意识地支持多个文档化的信封,但一个通用解析器如果遍历 text、transcript、content 和 result,可能绑定的只是恰好是字符串的元数据。每个信封都应该有自己的适配器和 Fixture。共享的应用契约可以保持小巧。
问题是严格的拒绝会牺牲可挽救的内容。当一个不可替代的录音必须产生每个可恢复的片段时,或者当实时字幕合法地发出没有文本的临时帧时,这就不适用了。在这些情况下,在适用的保留策略下保留源,并添加单独的审查或 pending_segment 状态。不要悄悄地削弱批量摄取契约。
解析器应该接受原始响应体,而不是已经解码的字典。这样可以保留语法失败和形状失败之间的区别。它也应该独立于 HTTP 重试、存储和特定于提供者的身份验证。那些决策以不同的速度变化,混合它们会把一个小的验证规则变成困难的集成测试。
示例是 Python,因为相同的代码可以从笔记本 Fixture 移动到生产 Worker 而不改变契约。TypeScript 实现应该暴露相同的可辨识状态;关键点是对未知的运行时检查,因为编译时注解不会检查响应字节。
from dataclasses import dataclass
from enum import Enum
import json
from typing import Any
class TranscriptStatus(str, Enum):
ACCEPTED = "accepted"
EMPTY = "empty_transcript"
MALFORMED = "malformed_json"
INVALID_SCHEMA = "invalid_schema"
@dataclass(frozen=True)
class TranscriptResult:
status: TranscriptStatus
text: str | None = None
def parse_transcript(body: str) -> TranscriptResult:
try:
payload: Any = json.loads(body)
except json.JSONDecodeError:
return TranscriptResult(TranscriptStatus.MALFORMED)
if not isinstance(payload, dict):
return TranscriptResult(TranscriptStatus.INVALID_SCHEMA)
value = payload.get("text")
if value is None:
return TranscriptResult(TranscriptStatus.EMPTY)
if not isinstance(value, str):
return TranscriptResult(TranscriptStatus.INVALID_SCHEMA)
normalized = value.strip()
if not normalized:
return TranscriptResult(TranscriptStatus.EMPTY)
return TranscriptResult(TranscriptStatus.ACCEPTED, normalized)
注意这个函数没有做什么。它不重试、不记录 body、不搜索另一个键、不持久化状态、也不调用嵌入模型。它只分类一个边界。这种狭窄性是有用的——提供者适配器可以改变,而索引 Worker 继续消费 TranscriptResult。
我用 HTTP 200 Fixture 作为第一个回归测试用例,因为它立即捕获了诱人的 response.ok 快捷方式。不需要编造网络行为:向解析器输入精确的字符串,然后断言其结果以及没有下游工作。一个专注的语料库可以覆盖有效对象、null、空字符串和仅空白字符的字符串、数字、数组、截断的对象和纯非 JSON 文本。
import pytest
@pytest.mark.parametrize(
("body", "expected"),
[
('{"text":"hello"}', TranscriptStatus.ACCEPTED),
('{"text":null}', TranscriptStatus.EMPTY),
('{"text":""}', TranscriptStatus.EMPTY),
('{"text":" "}', TranscriptStatus.EMPTY),
('{"text":42}', TranscriptStatus.INVALID_SCHEMA),
('[]', TranscriptStatus.INVALID_SCHEMA),
('{"text":', TranscriptStatus.MALFORMED),
('not json', TranscriptStatus.MALFORMED),
],
)
def test_parse_transcript(body: str, expected: TranscriptStatus) -> None:
result = parse_transcript(body)
assert result.status is expected
if expected is not TranscriptStatus.ACCEPTED:
assert result.text is None
这是我关心的笔记本到生产的检查点。笔记本可以打印返回的任何内容;生产边界必须对其进行了确定性分类。快速 Fixture 也将提供者调用和 Prompt 成本排除在内部测试循环之外。
解析器测试是必要的但太局部化了。用户要求的是可搜索的文本,所以端到端断言应该描述那个状态。对于接受的 Fixture,确认一条转录记录被提交、一个索引操作被发出,并且可搜索文档仍然与上传的相关性 ID 关联。对于每个被拒绝的 Fixture,确认零个 Chunk、零个 Vector 和零个 Prompt 调用。如果向量搜索使用带 pgvector 的 Postgres,事务边界和数据库约束可以保护源和可搜索记录之间的关系;解析器不应该依赖那种存储选择。
没有转录文本,就没有索引。
传输计数器回答请求是否完成。解析器计数器显示接受、空、格式错误和无效 Schema 结果的分布。产品检查协调接受的上传与索引的文档。准确性也属于工具,使用同意的语料库和适合应用程序的度量;RAG 工作流应该关心姓名、标识符和领域术语的检索,而不仅仅是是否存在一些文本。
每当客户端适配器、规范化策略、队列消费者或索引路径改变时,运行固定的语料库。然后在受控环境中重放有代表性的音频。跟踪延迟和每分钟音频的接受字符数,但也要在成本核算中包含被拒绝的工作——重试会消耗转录和编排预算,即使它们从未产生 Prompt 上下文。低的每次成功调用成本可能隐藏了重复的失败尝试。
我不确定是否存在通用重试计数,因为正确的决策取决于 API 文档化的错误语义和录音的价值。用提供者契约和产品策略解决那个不确定性,而不是宽泛的 except 块。空内容通常需要不同的输入或审查;格式错误的内容可能遵循文档化的重试规则;无效的 Schema 应该发出集成信号。对于流媒体,你的里程可能有所不同,因为最终化和排序增加了批量解析器不需要的状态。
可观测性应该解释分类,而不要将敏感内容复制到日志中。记录相关性 ID、适配器版本、内容类型、body 长度、音频时长、延迟和结果。默认避免原始音频、凭证和完整转录文本。如果需要诊断保留,给它一个明确的用途、访问策略和删除窗口,而不是继承通用的应用程序日志设置。
状态转换使静默丢失可见。上传可以从 received 移动到 processing,然后到 accepted、empty、malformed 或 invalid schema;只有 accepted 可以推进到 indexed。将 indexed 定义为可搜索副作用的确认,而不仅仅是成功的队列发布。用幂等性密钥存储转换,这样允许的重试不会创建重复的转录行或嵌入。
最终选择是一种风险权衡。批量 RAG 摄取受益于在污染检索之前拒绝不确定的文本。实时字幕优先考虑连续性,可能容忍临时空段。合规存档可能需要人工审查而不是另一个自动化尝试。在将这个策略复制到另一个工作负载之前,衡量误接受成本、误拒绝成本、接受到索引的协调以及重试支出。
可靠的设计是有意识地保守的:解析一次、在运行时验证、显式表示失败状态,并断言用户的下游结果。HTTP 状态是关于传输的证据。它不是转录文本。
https://owasp.org/www-project-top-10-for-large-language-model-applications/