这是一份提交作品,参与的是Hacktoberfest 周末挑战赛:为朋友而构建
我在东京独自生活了四年,但当我的洗衣机开始漏水时,我盯着楼管员的电话号码看了二十分钟才按下拨号键。
我朋友在不得不打电话取消她的网络合同时遇到了同样的情况。服务商的解释对她来说听起来很陌生。日本商家在电话里使用的日语并不是你日常使用的日语,而且在通话中你无法观察对方的表情,也没有时间去查任何东西。
所以我做了 Before I Call,一个让你在打真实电话之前先用 AI 练习通话的应用。我为自己和处于同样情况的朋友们做了它,并把它发给了其中一位朋友,看看它是否真的有用。他们的回复在下面。
下面是它的实际运行效果:
我构建了什么
你描述你需要打的电话,或者从示例中选择一个(房屋维修、诊所、错过的快递、市政厅、饮食要求、失物招领、账单)。然后你和扮演电话另一端的人(比如前台或你的楼管员)的 AI 对话。它说礼貌的日语,一次问一个问题,并等待你的回答。没有评分。
如果你卡住了,你不必挂断。解释问题会在通话旁边打开一个助手,显示这个问题的含义以及你可以给出的回答。通话中的 AI 看不到它,所以角色扮演会继续进行。慢速重放会以 0.7 倍速再次播放问题,而重复问题会让 AI 再说一遍。汉字带有振假名,每一行都有罗马字,已知词汇在你悬停或点击时会显示含义。
当你说再见时,AI 会挂断,你会得到一张通话卡片:整段对话及其读音和含义,以及你通话中出现的有用词汇列表。你可以把它下载为 PDF,在打真实电话时放在身边。
它不会编造日期、价格或可用性,也不能为你预订任何东西。它只用于练习。
设置一次通话。你可以用英语或日语输入或口述,也可以从示例开始。增强提示词会整理粗略的笔记,而不会添加你没有提供的事实。
在通话中途寻求帮助:
通话卡片上的词汇列表:
我朋友说的话
我把链接通过 WhatsApp 发给了朋友,让他们告诉我是否有帮助。这是回复:
我没想到 PDF 会是他们喜欢的部分。我做它是为了真实通话准备的速查表,但对于备考 JLPT 的人来说,它也是一份来自他们实际对话的词汇表。
演示
试用地址before-i-call.onrender.com。无需注册。语音练习需要麦克风。如果没有麦克风,请点击播放引导示例观看日语或英语的录制通话。
代码
Before I Call
排练你一直拖延的那通电话。
一个耐心的 AI 语音伙伴,用于日常日语和英语通话,每个词都有注音,
帮助从不打断对话,还有一张可随身携带的通话卡。
在云端使用,或者免费且离线地在自己的电脑上使用搭配 Gemma 4、Whisper 和 Kokoro。
问题所在
打电话是第二语言中最难的日常对话。没有面孔可读,没有时间查任何东西,而且对方以母语速度说话。住在日本的人会推迟给楼管、诊所或快递公司打电话,不是因为他们应付不了对话,而是因为打电话本身感觉有风险。
短语手册对第一句话有帮助。它们不能……
MIT 许可,在挑战期间构建。它有 87 个测试(80 个 Python,7 个 Node),README 解释了如何在本地运行。
我如何构建它
通话运行在 ElevenLabs Agents 上
实时通话是一个 ElevenLabs 智能体。它处理所有音频:WebRTC 连接、语音识别、判断你何时说完、打断以及语音。我对每个场景使用同一个智能体。当通话开始时,应用将该通话的提示、第一句话、语言和语音作为覆盖发送,因此同一个智能体可以是日语楼管或英语诊所接待员。
我把智能体的语速放慢到 0.85 倍以方便学习者,并将通话限制在两分钟,服务器上有每日限制,这样额度不会在一天内用完。挂断使用内置的end_call工具。它的描述告诉模型,只有在你的问题处理完毕、并且你表示不再需要其他东西之后才使用它,而不是在你只是说谢谢的时候。解释问题开启一个仅文本的第二会话,并附带一个辅助提示,这样解释就永远不会出现在通话记录中。慢速回放使用 ElevenLabs 文本转语音(Flash v2.5,0.7 倍速)。
该智能体目前已经进行了 113 次对话。ElevenLabs 会自动按主题对它们分组,排名前三的主题与应用中的示例一致:预约(27)、洗衣机(19)和错过配送(17)。
我为什么改用自定义 LLM
刚开始时,该智能体使用的是 ElevenLabs 托管的某个 LLM,Qwen3.5-397B-A17B。它的用量从我因 Hacktoberfest 获得的同一批 ElevenLabs 额度中扣除,而且消耗得很快。ElevenLabs 标价约为每分钟 $0.016 和每条消息 $0.003,这还不包括语音。自定义 LLM 在 ElevenLabs 这边不产生费用:
我想把那些额度留给语音,所以我把智能体切换到了自定义 LLM,并指向 DigitalOcean 上的 Gemma 4 31B,它改为从我的 DigitalOcean 账户计费。按照我的追踪代码用于 Gemma 的每 token 价格(每百万输入 token $0.18,每百万输出 token $0.50),下面追踪记录中的那一轮大约花费 $0.0002。ElevenLabs 额度现在只用于支付音频费用。
起初,自定义 LLM 直接指向 DigitalOcean。后来我在中间加上了自己的服务器,这样我就能追踪并检查每一条回复。这是该智能体现在的 LLM 设置:
每一轮,智能体都会把对话发送到我在 Render 上的 FastAPI 服务器,服务器再将其转发到 DigitalOcean 无服务器推理上的 Gemma 4 31B,响应会直接流式返回,因此只要第一批词到达,语音就可以开始。
Render 上的代理
自定义 LLM URL 指向我自己的服务器:一个由 Render 作为单个 Docker Web 服务运行的 FastAPI 应用。同一个服务同时提供 React 应用、API 和代理。整个设置是一个render.yaml仓库中的 Blueprint(此处已删减):
services:
- type: web
name: before-i-call
runtime: docker
plan: starter
healthCheckPath: /api/health
disk:
name: usage-data
mountPath: /var/data
sizeGB: 1
envVars:
- key: LLM_PROXY_KEY
sync: false
- key: GRADIENT_MODEL
value: gemma-4-31B-it
- key: MAX_CONCURRENT_CALLS
value: '2'
- key: MAX_CALL_SECONDS
value: '120'
- key: MAX_CALLS_PER_VISITOR_DAY
value: '3'
密钥是sync: false,所以它们设置在 Render 仪表盘中,从不提交。保护我额度的限制是普通的环境变量:同时两次调用,每次两分钟,每位访客每天三次调用。1 GB 磁盘保存这些限制和分析数据的 SQLite 文件,因此它们在重新部署后依然存在。
代理是一个单一的路由。它只接受智能体的密钥,拒绝除 Gemma 4 31B 之外的每个模型,并在原样传递 DigitalOcean 的流的同时读取每个数据块以进行追踪和规则检查(已精简):
@router.post('/llm/v1/chat/completions')
async def chat_completions(request: Request):
...
if not isinstance(body, dict) or body.get('model') != model:
# 该密钥永远不能用于其他更昂贵的模型。 raise HTTPException(400, f'Only{model}is available.')
...
async def relay():
async for chunk in response.aiter_bytes():
stream.feed(chunk)
yield chunk
return StreamingResponse(relay(), media_type='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
一个常开的 Web 服务适合这种情况。每个回复都是一个流式响应,在 Gemma 说话时保持打开。服务器为 DigitalOcean 复用一个 HTTPS 客户端,因此一轮对话不必为新的 TLS 握手付出代价。当你打断 AI 时,ElevenLabs 会放弃该请求,因此代理关闭流并在 Sentry 中将该轮记录为已取消。
这是代理在 Render 日志中工作的样子,来自智能体的每个 Gemma 请求一行:
该服务由 Blueprint 管理,Blueprint 与仓库保持同步,每次推送到 main 都会自动部署:
修复数字的读法
第一个版本把 7時 读作“nana-ji”。没人这么说。日语数字会根据其后的量词改变读法:7時 是 しちじ,10分 是 じゅっぷん,4月1日 是 しがつついたち,2人 是 ふたり。日期、时间和人数正是你在电话中会说的内容,所以这必须正确。
我用一个发音词典解决了这个问题:118 条是我手写的,另有 5,989 条是为日期、时刻、时长和量词生成的,总共 6,107 条。一个脚本会构建这些规则并作为发音词典上传到 ElevenLabs 智能体,应用在慢速回放和录制的演示中使用同一份列表。屏幕上的文字不变。这是智能体上的那个词典:
为 Gemma 调整提示词
切换到 Gemma 意味着要重写提示词。我用智能体的纯文本运行来测试它。旧提示词从未调用end_call(2 个场景中 0 个),而调整后的提示词在全部 4 个场景中都挂断了。它还会把普通词保留为汉字,把数字写成假名,以便语音能正确读出,并且它会询问练习用的信息而不是你的真实信息。在真实通话中它有时仍会漏掉挂断,Sentry 捕捉到了这一点。调整后提示词的开头:
用 Sentry 检查每一条回复
由于每一轮都经过我的服务器,我可以对照练习通话的规则检查每条回复。如果你说了再见而 AI 没有挂断,如果它“确认”了一个预订,或者它把数字或英文字母放进了日语语音中,那就会变成一个 Sentry 问题,并标记上提示词版本和轮次编号。每个请求也是一条gen_aitrace,包含首 token 时间、token 数量和预估成本。通话结束后你可以点击举报这条回复,举报会显示在该轮次的 trace 旁边。只有你选择附加时,对话文本才会发送到 Sentry。
这里有一个真实的例子。10 月 3 日,练习伙伴在学习者说完再见后没有挂断,Sentry 在四分钟内捕捉到了 8 次。每条规则都有自己的问题:
该问题列出了它每次发生的时间以及来自哪个版本:
作为对比,这里有一轮伙伴确实挂断了的记录。那个execute_tool end_callspan 就是挂断。Gemma 调用耗时 1.27 s(Sentry 对它的平均值是 1.49 s),使用了约 1,100 个输入 token 和 43 个输出 token,成本不到一美分。Trace 不包含对话文本,所以 Input 标签页是空的。
所有这些 Gemma 调用都运行在 DigitalOcean 的无服务器推理上,所以我从来不需要运行 GPU 服务器。在 10 月 3 日和 4 日这两天里,应用向 Gemma 4 31B 发送了约 128 万个输入 token,并收到了约 7.5 万个输出 token。大部分是输入,因为每一轮都会发送指令和到目前为止的对话,而回复都很短,就像上面追踪记录中的 43 个 token。
我一路遇到的 bug
- Gemma 有时会在回复中写出语音指令,比如
[calm]或者[pause]。应用会在屏幕上的文本中把它们去掉。 - 小模型有时会大声说出工具调用,比如
end_call(reason="done"),而不是调用工具。应用会在它被朗读之前移除那段文本,并将其视为挂断。 - 在本地运行 Gemma 4 时,最初的回复返回为空,因为模型把所有 token 都花在了隐藏推理上。设置
reasoning_effort: "none"解决了这个问题,现在回复在我笔记本电脑的 GPU 上大约需要半秒钟。 - Kokoro 的日语语音无法在 Windows 上安装,因为 pyopenjtalk 需要 CMake 来构建。pyopenjtalk-plus 有预构建的 wheel,所以我换成了它。
- 罗马字把 10月4日 显示为 "10tsuki4hi"。罗马字和振假名现在使用与语音相同的读音列表,所以它读作juugatsu yokka。
为什么开放式创新很重要?
托管版本运行在付费 API 上。我还想要一个免费且私密的版本,这样同一份代码可以完全在笔记本电脑上运行。有了LOCAL_VOICE=1,Whisper 负责语音识别,Gemma 4 E2B(4.3 GB,通过 Ollama)扮演另一个人,Kokoro-82M 负责说话。解释、慢速重放、增强提示、单词列表、振假名和 PDF 都仍然可用。不会向提供商发送任何内容,它可以离线工作,而且你可以用一个设置切换模型(LOCAL_LLM_MODEL)。
在我的笔记本电脑上(Ryzen 9 5900HS,RTX 3060 6 GB),AI 会在 2.4 秒内向你打招呼,在大约 6 秒内回答你说的话,而一次解释需要 1.4 秒。
本地版本没那么好。ElevenLabs 的声音听起来更自然,响应更快,还能让你打断它。本地版本需要点击才能说话,而且小模型不像 31B 那样严格遵循角色扮演规则。这一切之所以能实现,是因为 Gemma 的权重是开源的:同一个模型系列既能跑在 DigitalOcean 上,也能跑在我的笔记本电脑上,使用相同的提示词、规则和发音列表。Furigana 和 romaji 不需要模型。开源的 pykakasi 和 Janome 库就能处理它们。
奖项类别
-
ElevenLabs 最佳使用:每个通话都通过 WebRTC 由一个智能体处理,支持按通话覆盖提示词、开场白、语言和声音,内置的
end_call工具、指向 Gemma 的自定义 LLM、用于解释的独立纯文本会话、用于慢速回放的文本转语音,以及智能体上的一个包含 6,107 条词条的发音词典。 -
Sentry 智能体追踪最佳使用:每个 Gemma 请求都是一条
gen_ai追踪记录,包含首 token 时间、token 数和成本。回复会对照角色扮演规则进行检查,用户报告会链接到确切的对话轮次。 - DigitalOcean 最佳使用:无服务器推理为每个通话轮次、解释、提示词增强和单词列表运行 Gemma 4 31B,无需管理 GPU。
- Gemma 最佳使用:Gemma 承担四项工作(通话中的另一个人、解释助手、提示词增强器和选词器),在云端以 31B 运行,在笔记本电脑上以 E2B 运行。
-
Render 最佳使用:一个 Docker Web 服务,来自一个
render.yamlBlueprint,为应用、API 和自定义 LLM 代理提供服务,该代理将每个 Gemma 回复流式传输到 ElevenLabs,并使用持久磁盘存储使用限制和分析数据,密钥不进入仓库,信用额度限制作为环境变量。
独立构建。
如果你住在日本,也总是拖延打电话,那就试试它,并在评论中告诉我还缺什么。





















