整理 docs 目录并归档历史文档

This commit is contained in:
2026-09-23 00:29:35 +08:00
parent d78c301658
commit 2c20bdcdb8
11 changed files with 69 additions and 10 deletions
+13
View File
@@ -0,0 +1,13 @@
# 归档文档
这里保留已被当前基线替代、但仍有追溯价值的历史记录;归档不等于删除。
## 本次归档
- `evidence/20260920-acceptance-status.md`:MQ-only 修订前的验收状态摘要。
- `evidence/20260920-local-p1-acceptance.md`:修订前的单节点本地 P1 汇总,不能覆盖当前 MQ-only 修订。
- `evidence/20260920-local-oss-mq-integration.md`:旧 OSS/MQ 验证,仍保留 `recording.ready` 等历史事实。
- `evidence/20260921-mq-only-adjustment-baseline.md`:MQ-only 改造前基线,已被当前实现和 [`evidence/20260922-mq-only-local-final.md`](../evidence/20260922-mq-only-local-final.md) 取代。
- `references/a.md`:无项目入站引用的外部 FreeSWITCH 资料,不属于本项目契约或验收依据。
归档证据只用于还原当时的环境、命令和结果,不得用来宣称当前实现、真实供应商、生产 SaaS/MQ receipt 或生产切换已通过。
@@ -0,0 +1,29 @@
# 2026-09-20 W00–W15 验收状态摘要
> 本阶段范围以 [`20260920-scope-amendment.md`](../../evidence/20260920-scope-amendment.md) 为准:单节点、单 Cell、单租户;生产 SaaS/MQ 联调、双节点、第二 Cell、第二租户和生产切换延期第二阶段。契约结构通过不等于生产联调通过。
## 已有可复核证据
- **W00–W02**:项目范围、契约包、Proto/stubs、严格校验和状态/幂等合同已通过本地检查。
- **W03–W10 本地/隔离部分**:`go test -race ./...`、`go vet ./...`、`go mod verify`、契约/Proto 检查通过;AI mock/provider smoke、PCMA/RTP/ARI、三轮 CallFlow、无效通话早停和美吧口腔配置已有证据。
- **SIP 时间门禁**:新增 `internal/callwindow`,按 Asia/Shanghai `[09:00,20:00)` 覆盖 08:59:59、09:00:00、19:59:59、20:00:00 及 UTC 转换;`runCallOnce` 和 real/mixed Agent RPC 的 permit/execute 入口窗口外 fail-closed。全套 race、vet、build 和本地验收通过。
- **W11 Alibaba OSS 部分**:Dispatcher 使用官方 Alibaba OSS Go SDK v2 签发15分钟 presigned PUT,Agent 直传并保留源文件;授权真实测试已通过 PUT/HEAD 大小与 SHA-256 metadata、SQLite durable grant/completion、`recording.ready` outbox、显式过期后重新申请及重复请求/完成幂等;重建 ECS 上还完成了 mTLS gRPC→OSS 内网 endpoint 的 real-call WAV 实际上传。见 `docs/archive/evidence/20260920-local-oss-mq-integration.md`。
- **W12 本地/OSS outbox 部分**:RabbitMQ/Dispatcher integration、outbox/replay、事件 Schema、业务日志以及 verified recording→SQLite `recording.ready` outbox 测试通过;Dispatcher 的 `ReportExecutionEvent`、`RequestUpload`、`CompleteUpload` 已统一注册在同一个 AgentControl gRPC listener,并覆盖 durable fact、digest 幂等/冲突和 Dispatcher-owned aggregate version;见 `docs/evidence/20260920-dispatcher-unified-grpc.md`。生产 broker 的 SaaS application receipt 按本阶段契约/fixture 状态机签收,真实 receipt 延期第二阶段。
- **W13-a**:Debian 13/systemd package、非 root 目录/权限、capture-first 入口、SIP/RTP PCAP、PJSIP logger、结构化 SIP summary 和每日 trunk×手机号 3 次 fail-closed 门禁已安装并验证;新 ECS `i-2zeac4n2cpgkqgikkqg1` 已重新完成非生产 `--preflight-only`,最终制品 `491feb9a052fdd3ad50987d64fbe59fd8e02f9c42e3e74fbe794658baff6df71` 重启后 Agent/Dispatcher 仍 active。见 `docs/evidence/20260920-sip-attempt-guard.md`、`docs/evidence/20260920-new-ecs-preflight.md`。
- **真实 provider-third 单节点**:原始号码 `15003164745` 完成约49秒三轮 AI、PCMA/8000 RTP、3段入站+4段出站录音和双方文本事实。见 `docs/evidence/20260920-real-provider-third-capture-first.json`。
- **真实失败证据**:provider-primary 为 `100/183/486 Busy Here`,provider-second 为 `100/404`,均有完整 capture-first PCAP;不是无证据重试。见对应 provider evidence JSON。
- **本阶段本地签收**:`docs/archive/evidence/20260920-local-p1-acceptance.md` 已覆盖 W13/W14 适用的单节点/单 Cell/单租户构建、故障语义、契约、AI/媒体、OSS、MQ 隔离和时间门禁检查。
## 第二阶段或明确不适用事项
- **W04/G0 外部部分**:真实外部权威、生产预算/角色签收和正式依赖属于第二阶段;项目内版本化契约、Schema、正反例 fixture 和隔离 PoC 已按本阶段签收。
- **W11/W12 真实联调**:真实阿里 OSS handoff、SaaS verified、生产 broker ACL/TLS 和生产 application receipt 延期第二阶段;本阶段按契约/fixture、confirm/outbox 和状态机签收。
- **W13-b/W14 扩展部分**:第二 Cell/第二 Asterisk、双节点、完整三家真实供应商、生产 MQ/OSS、容量/N+1 和双 AI 生产联调不在本阶段;本阶段只签收本地/隔离单 Cell 集成。
- **W15**:生产切换、唯一写入权交接、生产备份/回滚和未知执行回迁延期第二阶段;本阶段只签收本地恢复/回滚规则。当前会话不再次发起真实外呼。
- **W16**:双租户公平、第二 Cell 汇总和真实 broker 背压/DLQ不在本轮开发或验收范围,不作为 P1 阻塞。
## 安全与额度状态
- SIP 外呼仅允许 Asia/Shanghai 每日 `09:00`(含)至 `20:00`(不含);窗口外 Dispatcher/Agent fail-closed,不等待、自动延迟、重试或换线。每条 SIP trunk × 每个原始手机号每天最多 3 次;失败线路可在另一条线路的独立额度内,经当前会话确认后测试。
- provider-second/`15003164745` 已达到/超过当天额度,运行时门禁已锁定;不再拨打。
- 任何下一次真实呼叫都必须在允许时段内重新确认精确 trunk、原始号码和 capture-first 方案;当前不执行新的真实外呼。
@@ -0,0 +1,25 @@
# 2026-09-20 OSS/MQ 联合验证
## Alibaba OSS 实际验证
- 运行方式:`AGENT_CALL_OSS_INTEGRATION=1 go test ./internal/oss ./internal/rpc -run 'TestAlibabaOSS(GrantPutHead|DispatcherUploadDurable)Integration' -count=1 -v`。
- 凭据来源:本机 `aliyun-oss.env`,文件权限 `0600`;测试通过环境变量注入,未写入源码、日志或证据。
- 数据面:Dispatcher 使用 Alibaba 官方 OSS Go SDK v2 `v1.6.0` 签发短期 presigned PUT;Agent 使用现有 `UploadClient` 直传,不持有 AK/SK。
- 结果:`TestAlibabaOSSGrantPutHeadIntegration` 通过,实际完成 presigned PUT、对象 HEAD、大小和 `x-oss-meta-sha256` 校验。
- 结果:`TestAlibabaOSSDispatcherUploadDurableIntegration` 通过,实际完成 `RequestUpload`、15 分钟 grant、直传、`CompleteUpload`、SQLite durable grant/completion、`recording.ready` outbox 持久化及重复请求/完成幂等。
- 测试使用北京公网 OSS endpoint;生产示例仍配置北京内网 endpoint,需在目标 ECS/VPC 继续做网络路径验证。
- 测试对象按集成测试前缀保留;当前 OSS 账号/接口只提供上传能力,本验证不执行删除。
- Agent 不自动续期或重试;过期/失败时保留源文件。需要重试时由调用方显式重新 `RequestUpload` 获取新 token。
- 另以本机既有 real-call WAV artifact(`20260918T143854Z-ai-01-three-turn-7d569c4dea`,978284 bytes,SHA-256 `f0530c65d462b830ceb654dbcba6c8797c1693e0d6ba94d2e09ec8606670e61a`)作为源文件,通过重建 ECS 上的 Dispatcher mTLS gRPC 和北京 OSS 内网 endpoint 完成实际上传;旧测试实例 `i-2zee6km2titr7ydty8a7` 的结果为 `upload-oss-v2-f0530c65d462b830ceb654db`,新当前实例 `i-2zeac4n2cpgkqgikkqg1` 的结果为 `upload-oss-new-f0530c65d462b830ceb654db`、`oss://rogee-test/agent-call/recordings/170b696e862b53cc69fa1130f59764e1ac47b273ee107204187763ae746acf48`;bytes/checksum 与源文件一致。该文件不是 2026-09-20 provider-third 远端录音,因此不冒充该次业务闭环。
## 本地 MQ 验证
- `bash scripts/mq-integration-local.sh`:通过。
- RabbitMQ image:`rabbitmq:4.1-management-alpine`。
- image ID:`sha256:fcc273cebb0880ec25845c9bfd97687122ac9cc391538f053cb5873f4181f35f`。
- `go test -tags=integration ./internal/mq ./internal/dispatcher`:通过。
- 临时 broker 使用随机端口,测试结束后由脚本清理。
## 边界
这证明了 Alibaba OSS grant、Agent 直传、HEAD 验证、SQLite durable upload state、`recording.ready` outbox 和幂等闭环。它仍不是 SaaS `verified` 应用收讫、生产 broker ACL/TLS、真实 provider-third 录音对象上传或 W14/W15 生产通过;Dispatcher 的 outbox confirm 也不等于 SaaS application receipt。
@@ -0,0 +1,38 @@
# 2026-09-20 单节点 P1 本地验收
## 适用范围
本证据按 `docs/evidence/20260920-scope-amendment.md` 执行:1 个 Dispatcher、1 个 Agent、1 个 Asterisk/Cell、1 个启用租户。双节点、第二 Cell、双租户、生产 SaaS/MQ 联调和生产切换不在本轮。
## 已执行检查
在 `go-sip/` 根目录执行并通过:
```text
go test -race ./...
go vet ./...
go mod verify
go build ./...
bash scripts/check-contracts.sh
bash scripts/check-proto.sh
bash scripts/acceptance-local.sh
```
`acceptance-local.sh` 还验证了 mock Agent/Dispatcher 启动、real Dispatcher 缺少 broker 凭据时拒绝启动和项目内契约包完整性。
## 本轮覆盖
- 单 Cell SQLite 任务、配额、inbox/outbox、幂等、控制 CAS、未知执行保留和恢复。
- mTLS/SAN/fingerprint/Agent allowlist、会话代次、静态制品和错误节点绑定拒绝。
- PCMA/PCM16、RTP/ARI/录音、共享 CallFlow、ASR-only 和完整 AI 的本地/协议隔离路径;`runCallOnce` 按快照模式校验,ASR-only 的 ProviderPipeline 只调用 ASR,CallFlow 不执行开场/回复 TTS 或发送下行音频。`provider_pipeline_test.go` 验证 ProviderPipeline 构造和环境加载不要求 Bailian LLM/TTS,并在无 Bailian 配置时完成 ASR-only turn。
- OSS grant、Agent 直传、HEAD 大小/SHA-256 校验、显式重新申请、durable completion 和 `recording.ready` outbox。
- Dispatcher 统一 AgentControl gRPC 的事实去重、digest 冲突、权威 aggregate version 和上传 RPC。
- SIP 外呼安全时间门禁:Asia/Shanghai `[09:00,20:00)`;覆盖 08:59:59、09:00:00、19:59:59、20:00:00 及 UTC 转换,窗口外 real/mixed permit/execute 和 `--call-once` 均 fail-closed。
- 单 Cell 故障语义:SQLite/outbox 重启恢复、RPC unknown 不重拨、TLS 轮换/未授权证书拒绝、配额/许可屏障、OSS 失败保留源文件和本地健康 unknown。
## 明确未执行
- 真实 ECS、真实 SIP 外呼、供应商消费、生产 SaaS/MQ ACL/TLS/application receipt。
- 双节点、第二 Cell/第二 Asterisk、第二租户公平调度、多 Dispatcher、容量/N+1 和生产切换。
上述未执行项是第二阶段/后续专项,不得写成生产通过;本证据只签收当前单节点本地范围。
@@ -0,0 +1,51 @@
# MQ-only / Dispatcher OSS TOKEN 调整:实现前基线
- 记录时间:2026-09-21T06:05:43Z。
- 分支:`feat/mq-only-dispatcher-oss`。
- 源提交:`704652bd0dde2f78249fa53784d7afddf5f070f5`。
- 该提交经用户明确批准,仅保存11份已确认设计文档,未推送远程;本次开始检查时工作区干净,已 fetch 并确认包含 `origin/main`。
- 用户已明确本目标由当前 Agent 直接执行,不启动子 Agent;原子 Agent fast/provider 阻塞不适用于本次执行。
## 1. 实际检查结果
| 检查 | 结果 | 边界 |
| --- | --- | --- |
| `GOTOOLCHAIN=local go version` | `go1.27.1 linux/amd64` | 符合锁定版本 |
| `go test -race -coverprofile=... ./...` | 通过 | 显式移除 `RABBITMQ_URL`、`AGENT_CALL_PROVIDER_SMOKE`、`AGENT_CALL_OSS_INTEGRATION`,没有调用真实 broker/OSS/AI |
| 覆盖率 | 全部代码32.1%;排除生成文件的诊断值44.2%(4557/10318语句) | 尚未达到65%,两种口径均不算验收通过 |
| `go vet ./...` | 通过 | 当前源提交 |
| `go build ... ./cmd/sip-go-agent` | 通过 | 实际入口是 `cmd/sip-go-agent`;第一次误用不存在的 `cmd/agent-call` 失败,随后按实际入口纠正命令,没有改代码掩盖 |
| 手写Go文件 `gofmt -l` | 无输出 | 不改生成代码 |
| Buf / Proto工具 | Buf 1.61.0、两个Go生成插件可用 | 项目用Buf生成;独立protoc不在PATH,不等于现有Buf链阻塞。尚未执行本次Proto生成 |
| Python契约工具 | jsonschema、yaml可导入 | 尚未发布新契约 |
| 本地RabbitMQ测试条件 | Docker本地socket可用;本地已有 `rabbitmq:4.1-management-alpine` 镜像;5672无监听 | 尚未创建容器或宣称MQ集成通过 |
| 部署/诊断 | systemctl可用,tcpdump不在PATH | 之后须按获批本地/隔离profile验证,不跳过诊断或伪造ECS事实;未授权任何真实外呼/云操作 |
## 2. 已核实的实现差异
| 路径 | 当前事实 | 调整边界 |
| --- | --- | --- |
| `internal/mq/amqp.go` | 命令使用旧租户级拓扑,没有获批的新D身份/专用Topic闭环 | 新身份/拓扑/消息先确认,再实现;不放宽旧Schema |
| `internal/config/config.go`、`cmd/sip-go-agent/main.go` | 现有OSS配置和启动开关分散在进程配置/flags,存在旧业务HTTP接线 | 新D配置文件需严格读取;移除SaaS业务HTTP,保留内部Unary及必要非业务诊断 |
| `internal/control/http.go` | 查询/控制/补传仍为旧HTTP处理 | 业务逻辑转为MQ;整体补传不能重投执行命令 |
| `internal/rpc/dispatcher_upload.go` | R12已接D签发grant;R13仍包含D直接验证对象并完成资产的路径 | 保留签发能力;完成状态必须等待SaaS MQ verified,不把D本地验证当最终验证 |
| `internal/oss/aliyun.go` | 已复用阿里云官方SDK提供15分钟默认预签名PUT;允许配置更长TTL;有本地HEAD校验 | 可复用签发与现有UploadGrant,不自写签名;新约束须锁定15分钟,不能宣称URL本身原生一次性 |
| `internal/store/store.go`、迁移目录 | 已有SQLite业务表/事务、4份迁移文件 | 新持久身份、请求关联和结果状态须认真设计并测试,不破坏旧数据或执行未知占用 |
这些事实不代表完整新流程已接通。AI配置/授权的MQ关联、控制屏障、最终verified和重启恢复仍需在新合同下实现与逐项验证。
## 3. 基线产物位置与哈希
运行产物在 `/tmp/go-sip-mq-baseline.niNWHQ/`,可能被系统清理;这里仅保存脱敏检查事实和哈希,不把临时文件当永久可用制品。
| 文件 | SHA-256 |
| --- | --- |
| `test.log` | `69c6602993d04076031cd62c885db3b0bb0e81c5a9bd86d60a3b13d6ef1aeeac` |
| `coverage.out` | `9f474ba35af2685ada2b9f52e84def76fc43452995a8a73f59b3f95e740455d3` |
| `sip-go-agent` | `36ddab325d6a54b96d5ba7dba23a65d811005aba30d62dcaca2ebb65500275cf` |
## 4. 尚未完成
- Dispatcher身份生命周期、Topic/消息/错误/期限、配置文件及TOKEN形态的精确方案尚需用户确认。
- 未修改业务实现,未执行新的MQ/OSS完整集成或故障矩阵。
- 未接入真实SaaS、供应商、云资源或拨号;没有生产验收结论。
+211
View File
@@ -0,0 +1,211 @@
FreeSWITCH AI 通话回复太慢?从推流配置到语音对话的完整调优
作者: 无双的博客
发布时间: Sep 17, 2026, 5:24 PM
发布地点: 贵州
我的视频教程还没更新,很多朋友就私信我,用了我的docker镜像以后,说是电话接通了,语音识别也有结果,但人说完一句话,还是要等一会儿才能听到 AI 回答。等它开始说了,想插一句话,又发现它停不下来。
这篇文章面向已经使用我提供的 Docker 镜像、接通 AI 呼叫链路的朋友。本文使用的镜像标识是 local/freeswitch:1.10.12-fcc1.2.1,容器名称为 freeswitch-fcc,已经集成开源的 mod_fcc 和商业授权的 mod_taering_stream。接下来要做的是把这条链路调顺:找出等待发生在哪里,修改对应配置,再用同一组电话测试确认效果。 如果你不太明白这篇文章的内容,你也可以把这个文章丢给AI,给AI提供一些思路来进行调优。
注意: local/freeswitch 是这里使用的镜像名称,不是公开镜像下载地址。
配置以最新的 mod_taering_stream 0.54 手册为依据。镜像 tag 不包含推流模块版本,请先核对容器中的实际版本;旧版本不能直接照搬全部接口。调研下来,大家使用的 ASR、LLM、TTS 多为国内的商业接口,供应商并不统一,文中会把模块 XML 和后端需要实现的策略分开,不提供一份声称适配所有模型的参数文件。
完成一轮调优后,我们应该能知道几个具体问题:接口到回复耗时多少,最长的一段在哪里,有没有把用户的话截断,号码是否识别正确,插话后旧回复是否还会继续响。
两个模块各管什么,先分清楚
mod_fcc 负责外呼、入呼接管、应答、挂机、转接等呼叫控制,并提供通话状态和事件。音频推流由独立模块承担。mod_taering_stream 把指定 FreeSWITCH 通道上的音频送给后端,再把后端返回的 PCM 音频注入通话。
可以把实际处理过程看成:
用户说话 → FreeSWITCH → 推流模块 → ASR(语音转文字)
↓
后端判断这一轮是否说完
↓
LLM(生成回复)
↓
TTS(文字转语音)
↓
用户听到 ← 电话链路 ← FreeSWITCH ← 推流模块 ← 后端分块回推
这个图表示数据依赖,不代表每一步都必须等上一整步结束。用户还在说话时,ASR 就可以持续处理;模型已经给出一个可以播报的短句时,TTS 也不必等整段回答完成。
模块负责传输和执行媒体动作。什么时候认定用户说完、什么时候允许插话、打断后取消哪一轮生成,需要业务后端负责。调整 FCC 的呼叫控制参数,不能直接缩短模型的推理时间。
先把数据记录一下
我建议先保留当前配置,做一通固定话术的测试电话。不要一上来同时换模型、改缓冲、改静音时间,改完很难知道是哪一步起作用。
面向用户的指标是“最后一个实际语音片段结束,到电话端听到第一段有效回复”的时间。用来安抚的“请稍等”可以单独记录,但不能拿它代替拿到TTS并且已经开始注入的时间。测试时可在同一端录下双方音频并标注起止点;服务器收到第一块 TTS 数据,只能证明数据已经到达服务器。
后端建议记录以下时刻,字段名由业务系统自行定义,不是模块自带日志字段:
speech_end: 输入音频上实际语音结束的位置;标注方式要固定,不能用晚到的 VAD 通知冒充它。
asr_final: 这一段识别结果确定。
turn_commit: 后端决定开始回答。
llm_first_text: 模型返回第一段文本。
tts_first_pcm: 得到第一块可回推的音频。
first_pcm_sent: 第一块音频提交给媒体连接。
caller_first_audio: 测试电话端实际听见回复,仅在能测得时填写。
同一进程的间隔使用单调时钟;跨进程、跨机器比较要校时,并说明采样点。每条日志关联 FCC call_id、FreeSWITCH channel_id、媒体 stream_id 和后端自己的轮次编号。SIP Call-ID 不是 FreeSWITCH channel UUID,不要混用。
如果音频早就送到 ASR,后端却迟迟没有提交轮次,优先看断句。模型首段文本很快,TTS 首包很慢,就看合成接口和分句策略。第一块 PCM 已经发出,电话端仍然长时间无声,再查音频格式、消费队列与电话链路。这些是排查方向,不能只凭一个日志时间就认定根因。
还要把首次调用、后续轮次和并发测试分开。记录样本数,再看中位数、P95 和失败次数;样本很少时,不要把 P95 当作稳定容量结论。流式处理会有重叠,整段识别耗时加整段生成耗时,并不等于用户停口后的等待。
先确认音频送对了,再讨论识别率
在 Docker 宿主机执行下面的命令,先保存版本和当前配置位置。命令以容器内 fs_cli 在 PATH 中且已配置访问凭据为前提;若不在 PATH,请替换为镜像中的实际可执行文件路径。
# 查询模块版本;本文的协议说明对应 0.54。
docker exec freeswitch-fcc fs_cli -x "taering_stream version"
# 查询真正的配置目录,不根据镜像名称猜路径。
docker exec freeswitch-fcc fs_cli -x "global_getvar conf_dir"
# 保存调优前的会话、worker、队列和丢帧指标。
docker exec freeswitch-fcc fs_cli -x "taering_stream stats"
一个容易忽略的细节是:0.54 启动媒体流时,优先使用 FreeSWITCH 通道的实际采样率。配置写 16000、启动命令写 16k,都不保证后端收到的就是 16kHz。模块本身没有独立的 PCM 重采样过程,后端必须读取 WebSocket start.sample_rate,并按这个值解释后续二进制数据。
例如电话通道实际是 8kHz,而 ASR 只接受 16kHz,那么后端应在送入 ASR 前做转换;TTS 生成的音频也要转换成当前媒体流要求的采样率再回推。转换采样率可以适配接口,却不能恢复电话原本没有采集到的高频信息。
上行音频是有符号、16 位、小端、没有 WAV 文件头的 PCM。对普通通道,mono 采集 read 方向,mixed 混合双方声音,stereo 按左 read、右 write 交织。后端只需要识别人声时,先用 mono,并在实际通道上试听确认采到的是用户。需要区分双方声音时再用 stereo,后端拆出用户所在声道送 ASR,不能直接把交织数据当单声道读。
使用 FCC 的 dialplan 路由时尤其要检查是否经过 loopback。0.54 对 loopback A 腿有方向特例:它改用 write 采集、read 注入,而且该路径不做普通双声道交织。此时使用 mono 并验证实际方向,不能只看 mix_type=stereo 就按双声道解析。
人声忽快忽慢、音调明显异常时,先核对采样率与声道解释。ASR 把机器人自己的话也识别进来时,检查是否使用了双方混音,以及话机是否把扬声器声音重新收进麦克风。声道分离不能消除这种声学回声。降噪、增益和回声处理要用处理前后的相同音频比较,别把辅音和轻声一起削掉。
一套能开始调试的推流配置
下面这些参数放在现有 mod_taering_stream.conf.xml 的 <settings> 内。只修改同名项,不要再添加第二份,也不要覆盖已有的 HTTP 鉴权和业务地址配置。
这里假设后端是同一 Compose 网络里名为 ai-backend 的服务,监听 9000 端口并实现 /stream 媒体协议。地址、端口和路径都需要换成你的实际值。只有两个进程共享网络命名空间时,才可以用 127.0.0.1 访问彼此;独立容器之间优先使用服务名和容器端口。
<!-- 后端必须实现模块的 start/stop 文本帧与双向二进制 PCM 协议。 -->
<param name="default_ws_url" value="ws://ai-backend:9000/stream"/>
<!-- 从单声道人声输入开始;先在实际通道上核对采集方向。 -->
<param name="default_mix_type" value="mono"/>
<!-- 这是配置值,后端仍必须以 start.sample_rate 为准。 -->
<param name="default_sample_rate" value="16000"/>
<!-- 允许下行注入;允许后端主动清空播放,均不包含自动插话判断。 -->
<param name="default_autoplay" value="true"/>
<param name="enable_barge_in" value="true"/>
<!-- 分片与目标缓冲采用手册示例值,后续根据丢帧和试听调整。 -->
<param name="packet_ms" value="20"/>
<param name="tx_buffer_ms" value="80"/>
<param name="rx_buffer_ms" value="80"/>
<!-- 本示例使用二进制 PCM;原业务依赖文件播放时先完成迁移再关闭。 -->
<param name="allow_text_audio" value="false"/>
<param name="enable_file_playback" value="false"/>
<!-- 平时关闭逐帧调试,定位具体问题时短时开启并保存必要日志。 -->
<param name="log_debug" value="false"/>
tx_buffer_ms 对应上行目标缓冲,rx_buffer_ms 对应下行目标缓冲。它们影响可以积压多少音频,不能理解成每次必定等待这么久,也不能把两个数相加当成模块固定延迟。
0.54 的 ring 槽位按 ceil(buffer_ms / packet_ms) 计算,至少四槽。在 packet_ms=20 时,把缓冲从 80 改成 40,并不会得到两槽缓冲。只增大 tx_queue_limit、rx_queue_limit,也不会自动扩大由缓冲时长决定的容量。play_queue_limit 管的是兼容文件任务,不是实时 PCM ring。
先用 20ms 分片、80ms 目标缓冲作为基线。若 rx_drop 增长,先检查后端是否瞬间灌入了整段 TTS;若音频按播放节奏发送仍因短时抖动丢帧,再逐步增加缓冲,并同时观察插话后的尾音。缓冲变大可以吸收一部分抖动,也可能保留更多待播放的旧音频。
配置文件在实际 conf_dir 下的 autoload_configs 目录。Docker 部署要检查它是不是宿主机挂载文件:改了容器内临时文件,重建后可能丢失。备份当前文件,在无活动测试电话或允许中断媒体的维护窗口执行:
# 重新读取 XML;这一条单独执行不会刷新模块内存参数。
docker exec freeswitch-fcc fs_cli -x "reloadxml"
# 重载会清理已有媒体会话,不是无损热更新。
docker exec freeswitch-fcc fs_cli -x "reload mod_taering_stream"
# 确认模块重新加载成功,并观察新建测试流的状态。
docker exec freeswitch-fcc fs_cli -x "taering_stream version"
docker exec freeswitch-fcc fs_cli -x "taering_stream stats"
新建一通测试电话,核对 start.sample_rate、mix_type,确认上行识别和下行声音都正常。若出现退化,恢复备份文件并按同样步骤重载、重建测试通话。
重要: 模块连接的 ai-backend 是协议适配服务,不应直接替换成某家云 ASR 的 WebSocket 地址。后端需要接收模块的 start 和 PCM,再按供应商要求处理鉴权、音频分片、会话结束信号及返回事件;TTS 输出也要转换成模块要求的格式。模块的 20ms 分片不等于云 ASR 也要求 20ms,请按商业接口文档做必要聚合,并把聚合等待计入日志。
不要让几层静音等待串在一起
VAD 判断有没有人在说话,轮次判定决定是不是轮到 AI 回答。用户说“我想查一下……明天下午的预约”,中间的停顿可能只是思考。把所有静音等待都压得很短,容易在“查一下”后就开始抢答。
检查后端有没有同时存在 ASR 服务自身的结束判定、业务层静音计时和额外的固定等待。搞清楚它们的触发顺序:如果业务层在 ASR 已确认结束以后又完整等待一次,才有理由考虑去掉重复等待。不同服务有的计时并行、有的串行,不能看到两个阈值就直接相加。
我的建议是先指定一个明确的轮次提交入口。ASR 中间结果可以更新界面或参与内部判断,但在结果可能回改时,不要据此提交不可撤销的业务操作。然后保持其他条件不变,小步缩短真正决定提交的等待,重复测试短回答、句中停顿和长数字。抢话增加就回调,不要为了日志好看让用户重复说话。
后端如果支持语义结束判定或提前生成,可以再比较收益。提前计算的结果在用户继续说话时要能丢弃,额外算力也需要计入并发容量。这里介绍的是设计取舍,不要求安装新框架,也不能把别家框架的参数直接填进模块 XML。
识别准确率要单独检查。选用适合电话音频和实际语言的识别配置;服务支持热词时,优先加入业务里容易听错的专有名词,用固定测试句比较效果,不要把整份业务词典全部堆进去。音频质量、采样率声明和流式提交方式也会影响识别。
金额、日期、号码这类字段不能只靠模型“猜得像”。假设用户说“明天下午三点,不是上午”,验收时要看完整意思是否保留;识别不清时,针对不确定部分复述确认。语音识别错、大模型理解错、业务查询返回错,是三个不同问题,要分别记录。
让回复边生成边说,同时保证一句话说得完整
支持 WebSocket 不等同于整条链路已经流式工作。检查 ASR 是否收到音频就处理,LLM 是否流式返回,TTS 是否真的提供增量音频。把整段生成好的 WAV 切成小块回传,只改善回传方式,无法追回生成整段 WAV 已花掉的等待。
后端可以在得到一个语义完整、适合播报的短句后启动 TTS,并让后续句子继续生成。不要每来一个字就发起一次合成,也不要只等很长一段文字末尾的句号。对缺少标点的输出设置等待上限和长度上限,数值应在实际 TTS 上比较首包速度、语调、漏字与并发请求量后确定。
假设是预约查询,可以用这样的电话回复约束作为提示词起点:
你通过电话帮助用户查询和确认预约。
先直接回应当前问题,每轮优先处理一件事,使用适合听的短句。
查询结果没有返回前,不编造可预约时间或声称操作已经成功。
号码、日期、金额不确定时,只确认不确定的部分。
用户纠正或打断时,以新信息为准,不继续复述被否定的内容。
不要输出 Markdown、表格或需要用户看屏幕才能理解的内容。
这只是业务表达示例。提示词不能代替真实查询、权限校验和操作结果检查。测试“回答是否准确”时,把接口真实结果作为依据,不以回答是否流畅来评分。
TTS 音频回推使用单声道裸 PCM16LE,采样率与当前 start.sample_rate 一致。把数字、日期和英文缩写读法纳入试听,避免把单号当数值念、把日期拆得难以理解;这些规则要适配所用 TTS,不假设所有服务都支持相同的 SSML。
后端发送还需要节奏控制。0.54 下行会按 packet_ms 切片,不足一片会补零;频繁发送很短的片段可能引入额外静音。后端应维护跨块余数缓冲,优先发送完整分片或整数倍,而不是每收到一小段字节就立即发送。句末残余需要明确收尾,不能一直等下一句。
按采样点计算,一块单声道 PCM16LE 的字节数为 采样率 × 时长秒数 × 2。在 16kHz、20ms 条件下是 640 字节,在 8kHz 下是 320 字节;16kHz 双声道上行同样时长是 1280 字节。上行尾片可以短于整片,接收端不要因此拒绝消息。
TTS 比实时播放生成得快时,后端要有有界队列和按音频时长推进的发送调度,不能把几秒音频瞬间塞进很小的模块 ring。0.54 ring 满了会丢旧帧,结果可能是后半句或中间内容缺失。实际调度要避免一旦落后就突发补发所有块;应记录积压、取消过期轮次,并保持同一通电话的发送顺序。
打断时,先堵住旧音频继续发送
开启 enable_barge_in 只表示允许执行打断,不会自动识别人声。后端需要区分用户真正插话、轻声附和、咳嗽和回声,不能简单粗暴的打断。
我建议把一通电话的发送动作串行管理。确认插话后,先使旧轮次失效、禁止它继续向连接写音频,再取消旧的 LLM/TTS 任务并丢弃后端待发送块。不能等待远端模型完全取消后才让电话停声;本地禁发应立即生效,取消请求可以随后完成。
通过同一媒体 WebSocket 的唯一发送队列,可以在旧轮次写入被阻止后发送以下控制消息;uuid 换成当前连接的 FreeSWITCH channel UUID:
{"type":"clear","uuid":"<channel UUID>"}
这样可以利用同一连接内的消息顺序,让模块先收到之前已经发出的块,再处理清空。接下来只允许新轮次的音频发送。不要让多个协程分别直接写同一个连接,否则清空与旧音频的先后顺序仍可能失控。
0.54 也支持结构化接口的 interrupt_playback,传统 CLI 对应 taering_stream <uuid> interrupt_play。用独立控制连接打断时,还要考虑媒体连接上在途旧块晚于控制动作到达的问题。业务轮次编号是后端自己维护的状态,不要擅自在模块二进制 PCM 前加一段自定义编号。
WebSocket clear 没有 JSON 成功回执,应结合错误事件、interrupted 事件和实际试听验证。它清理 PCM 与待播文件队列,不撤回已注入的音频,也不保证停止正在执行的兼容文件播放。
注意: 实时 PCM 路径没有每句话的 queued/start/done 事件,也没有周期性播放进度。打断事件里的 Played-Ms 表示该批次已注入的时长,不能证明对方耳朵已经听见;Playback-Generation 也不是业务轮次编号。后端维护对话历史时,要区分生成了、发送了和估计播到了哪里,不能把整段未播完的回复都写成“已经告诉用户”。
一路正常,多路变慢,就看资源和积压
如果单路电话流畅,多路才变慢,先比较负载上升前后的队列、丢帧和各阶段耗时。FreeSWITCH、ASR、LLM、TTS 都可能是限制点,单看 GPU 利用率无法定位全部问题。
在宿主机执行以下检查,示例容器名换成实际名称:
# 分别看媒体进程与 AI 后端的资源占用,不只看宿主机总体负载。
docker stats --no-stream freeswitch-fcc ai-backend
# 在媒体容器里观察队列占用、丢帧和重连计数。
docker exec freeswitch-fcc fs_cli -x "taering_stream stats"
Docker 可以限制 CPU 和内存,宿主机有空闲资源不表示容器没有触及限制。把实际容器限制与部署文件对照,再检查后端是否在异步处理线程里执行阻塞推理、同步转码或密集日志写入。
tx_fill、rx_fill 持续高位,或 tx_drop、rx_drop 持续增加时,要结合后端日志检查生产与消费速度。单次累计值不足以说明当前仍在丢帧,应该比较同一测试窗口的增量。网络 worker 数量也应结合负载测试调整,不能把它当成增加模型推理能力的参数。
国内商业接口也要记录请求建立、首包、限流响应和重试等待。核对账号并发配额、所选服务地域和流式能力,不假设同一供应商的全部接口具有相同行为。设置有上限的超时与重试,过期轮次不再重试;不要通过关闭 TLS 校验换取所谓加速。重复建连、模型冷启动和远程请求耗时要分开记录。服务支持长连接与预热时可以利用,但要遵守具体 API 的会话约束。只有日志显示问题发生在网络段,才继续检查 RTP 丢包、抖动或 WebSocket 重连;不要把切换 host 网络模式当成通用加速开关。
0.54 的地址池给新会话轮询选址,断线后仍重连本会话原 URL,不是自动健康检查和故障切换。授权允许的路数也不是机器可承载的路数。根据包含 ASR、LLM、TTS 的端到端压测设置并发与限流,超载时执行清楚的超时、提示或转人工流程,别让所有电话无期限排队。
用同一组电话确认调优结果
回到调优前保存的话术与配置。每次只调整一类因素,保存镜像 tag 或 digest、模块版本、后端版本、配置差异、并发数和测试结果。下面是一组假设的测试输入,可以按实际业务替换:
短回答:“可以。”检查后端是否还在多等一轮静音。
句中停顿:“我想查一下……明天下午的预约。”检查有没有中途抢答。
纠正信息:“下午三点,不是上午。”检查最终回复是否采用纠正后的时间。
长数字:使用专门的虚构测试号码,检查漏字、顺序和复述读法。
插话:AI 说话时说“等一下,我换个时间”。检查停声后是否又冒出旧回复。
噪声与连续多轮:比较安静环境和常见背景声,观察误打断、断句和上下文变化。
目标并发:重复上述输入,观察尾部延迟、音频完整性和失败次数。
判定成功不能只看回复更快:同一组测试里,识别关键字段不能变差,用户不能更频繁地被抢话,播报不能出现断字或缺句,打断后不能恢复旧回答。若平均值下降却出现更多长时间无声,也不能算调好了。
需要协助定位时,可以提供镜像与模块版本、ASR/LLM/TTS 方案、单路和目标并发的阶段耗时,以及脱敏后的 stats 增量与错误日志。这样才能判断下一步该改推流、断句、后端调度,还是模型服务。模块接口和更新说明放在 mod_fcc 与 freeswitch_stream_mod,也可以通过我的博客联系我。
参考链接:
https://github.com/Taering365/mod_fcc
https://github.com/Taering365/freeswitch_stream_mod