# AssemblyAI 的 FDE 紧盯用量冲高后的沉默

> 客户上手岗负责从首次 API 调用到首月生产，轮询原型最容易卡住客户。

原文: https://fdetimes.net/zh/guides/assemblyai-fde-onboarding-api-to-production/

周一，一位开发者创建了 API key，发了几段测试录音。到周三，他的请求量翻了好几倍。到周五，几乎没有请求了。仪表盘上的用量曲线先是攀升，随后直线下跌，而没有人提交工单说明发生了什么。

对负责客户上手的前线部署工程师（FDE），也就是 AssemblyAI 的 FDE Onboarding 岗位来说，这条曲线就是一个要处理的案例。AssemblyAI 对该岗位的描述是：推动新客户在其各条产品线上完成激活。

这个人负责从首次 API 调用到稳定生产使用的整个过程，更准确地说，是从首次调用到生产流量第一个月之间的这段时间。

这里考验的不是对自家产品的熟悉程度，而是在客户不开口时判断他们卡在哪里，深入他们的代码把问题解决，并在系统运行平稳后及时抽身。

最容易想象的场景是一个靠轮询获取转写结果的原型，因为 AssemblyAI 自己的文档就提醒过，这种做法会撞上速率限制。

## 为什么要在用量还在上升时主动联系？

AssemblyAI 的招聘信息写得很明确：FDE 管理一批处于上手阶段的客户，一旦某位开发者的用量上升，就要主动联系，在对方沉默之前帮他扫清障碍。这个触发点很合理。用量上升意味着原型第一次面对真实负载，也正是它最容易出问题的时候。

**要点:** 用量上升后突然沉默的客户，通常是卡住了，而不是一切顺利。

这个岗位也和业务结果挂钩。招聘信息要求候选人有“carrying a number”的经历，即曾经背负并完成过指标。所以，别以为支持电话答完就算结束。只有客户在生产环境稳定运行，这项工作才算完成。

## 具体案例：轮询原型在负载下崩溃

设想一家做会议纪要工具的初创公司。他们的开发者用最自然的方式写了原型：上传音频文件，拿到转写 ID，然后每隔几秒循环查询状态，直到结果返回。十个文件时一切正常。等他们把全部历史录音推上去，问题就来了。

第一个故障来自速率限制。AssemblyAI 的文档指出，转写轮询按 key 和转写 ID 分别限速。超出限制，API 会返回 HTTP 429 和 Retry-After 请求头。如果客户代码忽略这个请求头并立即重试，就会不断收到 429，日志里堆满报错。

第二个故障是并发。默认情况下，免费账户同时只能运行 5 个预录音频转写任务，付费账户则是 200+。自定义额度需要与销售或支持团队协商。一个用免费 key 推送数千个文件的团队会发现队列几乎不动，很容易认定 API 太慢。

结果就是开发者沉默了。他分不清问题出在自己的设计还是服务商，于是干脆不再尝试。如果 FDE 在周三、用量曲线还在上升时就联系他，这一切本可以在一开始就被拦下。

## 解决办法：用 Webhook 替代轮询

AssemblyAI 自己的文档推荐用 Webhook 来避免 429，并把它描述为替代轮询的生产方案。客户注册一个 URL，转写完成时由 AssemblyAI 调用它。有一个重要条件：如果端点没有在 10 秒内返回 2xx，AssemblyAI 会重发，总共最多 10 次。

这个条件引出两条设计原则。第一，处理程序必须快速响应，因此不能在请求内拉取转写、生成摘要或写入多张表。第二，处理程序必须幂等，即同一事件收到两次，最终也只处理一次，因为重试可能在你已经处理完之后才到达。

安全方面，Webhook 可以用自定义 HTTP 请求头鉴权，请求头由你自己在请求中设置。Webhook 还从固定 IP 发出，可以加入白名单：美国区域为 44.238.19.20，欧盟区域为 54.220.25.36。

如果客户的安全团队问 Webhook 从哪里来，这两个 IP 地址加上鉴权请求头，就是需要提前备好的答案。

和客户开发者一起写的修复代码大致如下。请求头名称和 payload 字段名仅作示意，请以最新文档为准。

```python
from flask import Flask, request, abort
import os

app = Flask(__name__)
SECRET = os.environ["WEBHOOK_SECRET"]

@app.post("/hooks/transcript")
def transcript_done():
    # 1. 用创建任务时配置的自定义请求头鉴权
    if request.headers.get("X-Webhook-Secret") != SECRET:
        abort(401)
    event = request.get_json(force=True)
    tid = event.get("transcript_id")
    # 2. 幂等：该 ID 已入队或已处理则跳过
    if not seen_before(tid):      # 例如 INSERT ... ON CONFLICT DO NOTHING
        enqueue_fetch(tid)        # 由 worker 稍后拉取并处理转写
    # 3. 在 10 秒期限之前立即返回 2xx
    return "", 200
```

对于迁移期间必须继续轮询的团队，最低限度的修复是读取 Retry-After 并按它给出的时长等待，而不是用固定延迟。同时确认他们用的是免费 key 还是付费 key。如果需要更高的并发，就请销售介入：这是一个跨职能岗位，要与销售、支持、产品和工程团队协作。

## 自己处理一个上手案例的五个步骤

第一步，盯住用量信号，而不是等工单。调用量骤增、连续出现 429 错误、免费 key 触及并发上限，都值得发一条简短消息，问问客户在做什么。

第二步，要求看代码。AssemblyAI 希望自己的 FDE 能快速读懂陌生的客户代码库，并与客户工程师一起交付可用的修复，通常是 Python 或 JavaScript/Node 服务。花 30 分钟读调用 API 的那部分代码，往往比开一小时会了解得更多。

第三步，修复真正出问题的那一层。有时是结果获取方式（轮询还是 Webhook），有时是账户额度，有时是网络或白名单。每一层要找的人不同，所以别甩过去一份十项待办清单。

第四步，在预发布或测试环境中验证，不要碰客户的生产数据管道。发送重复 payload，故意让处理程序耗时超过 10 秒，再读日志看重试如何发生。第五步，把经验反馈给产品和支持团队，让下一个客户不再踩同样的坑。

## 常见错误

最常见的错误是把所有繁重工作都放进 Webhook 处理程序，超过 10 秒，然后同一事件收到好几次，有时还会生成重复的纪要。第二个错误是不读 Retry-After，就把 429 归咎于服务商。

第三个错误出在 FDE 自己身上：以为安静的客户就是满意的客户。第四个错误是发文档而不是给修复。卡住的客户需要的是一个 pull request，而不是又一个链接。

## 如何把这项技能写进简历

阅读岗位描述时，留意 activation、first API call to production、carry a number 这类措辞。它们说明这个岗位的考核标准是客户能否走到生产环境。AssemblyAI 的招聘信息还要求了解实时与流式协议，例如 WebSockets 和音频管道，如果你没接触过，可以写一个小型流式客户端来练手。

在简历上，“将客户数据管道从轮询迁移到幂等 Webhook，消除 429 错误，两周内上线生产”这样一行，远比罗列一串框架有分量。如果还没有真实客户，可以在个人项目上做同样的练习，并记录前后对比数据。

对于文章开头那位开发者，机会就在周三、用量曲线还在上升的时候。到了周五，他已经沉默，再想把他拉回来就难得多了。

**本周动手试试:**

- 写一个只做三件事的小型 Webhook 处理程序：校验密钥请求头、把 ID 写入队列、返回 200。然后把同一个 payload 发送两次，确认任务只执行一次。
- 打开一个从未读过的 Python 或 Node 开源仓库，定时 30 分钟，找出它调用外部 API 的位置，然后写下你会提出的修复方案。
- 把简历中的一行改写成结果导向：哪个客户或团队、从什么状态到上线生产、用了多长时间。

## 信源

- [Forward Deployed Engineer, Onboarding (AssemblyAI, via Accel Jobs)](https://jobs.accel.com/companies/assemblyai/jobs/78049975-forward-deployed-engineer-onboarding)

- [Forward Deployed Engineer, Onboarding (AssemblyAI, Built In)](https://builtin.com/job/forward-deployed-engineer-onboarding/9301251)

- [AssemblyAI Docs: Webhooks (pre-recorded audio)](https://www.assemblyai.com/docs/pre-recorded-audio/webhooks.md)

- [AssemblyAI Docs: Concurrency (pre-recorded audio)](https://www.assemblyai.com/docs/pre-recorded-audio/concurrency)
