背景
Realtime 会话(intelligence/realtime/agent.py)为了让通话能跨多轮保持上下文,一个会话会被持续复用,直到用完 SESSION_MAX_TURNS=40 轮或 SESSION_MAX_AGE_SECONDS=240 秒的预算才重建(agent.py:46-47,_spent())。这套复用机制 push_to_talk 和 continuous 两种语音模式都会走到——_session_for()/_Held 按 (account_id, conversation_id) 复用同一个会话(agent.py:198 held.turns += 1),不是 continuous 独有的问题。但这两个预算只按轮次数和时间计量,不看会话里实际攒了多少上下文——工具调用结果一旦进入会话历史,会在同一个会话存活期内被反复带上下文重新处理,直接推高 audio_text_input_token 消耗。
schedule_tools.py 里返回给模型的工具结果目前没有做过任何裁剪:
_find()(list_schedules)对每条匹配日程调用 _for_model(),逐条 asdict(schedule) 把 ScheduleSnapshot 全部 21 个字段(business/calendar/contracts.py:128-153:id、account_id、schedule_type、schedule_kind、title、is_all_day、timezone、status、revision、created_at、updated_at、start_time、end_time、recurrence_rule、location_name、latitude、longitude、reminder_type、reminder_trigger_at、reminder_offset_minutes、reminder_strength、reminder_disposition_state、deleted_at)原样吐给模型,外加一个 starts_at_local(schedule_tools.py:478-491)。
_mutation_result()(create/update/delete 共用)同样通过 _for_model_dict() 把完整快照塞进 output(schedule_tools.py:463-476)。
模型确认一次日程创建、或者报一次"今天有哪些日程",实际只需要说出标题、时间、地点这几项,但现在整条 21 字段的快照连同没用的审计字段都进了会话上下文,且会在这个会话存活的每一轮里被重新计入 audio_text_input_token。
同时,阿里云 Realtime API 的 response.done 事件本身带用量信息,但 infrastructure/external/realtime/qwen_audio.py 里两处 response.done 处理分支(qwen_audio.py:271、qwen_audio.py:331)都只用它判断响应是否结束,从没读取或记录过用量字段。对照 infrastructure/external/llm/openai_compatible.py:97-172 里非 realtime 那条 LLM 管线,_parse_usage()/LlmUsage 早就在做同样的事——realtime 这条路径缺的是同一套实践,不是要新发明一套。
这三点合起来是 audio_text_input_token 占到约 70% 总成本的直接原因:上下文本身该精简的没精简,攒起来的量既不可见(没有用量日志),会话重建的时机也不看这个量(只看轮次/时间)。
建议方案
- 记录
response.done 用量:解析阿里云 Realtime API response.done 事件里的 usage 字段(若字段名/结构与协议不确定,需先对照实际线上事件确认),按 account_id/conversation_id 打日志或指标,参照 openai_compatible.py 的 LlmUsage 模式定义等价结构。没有这一步,后面两项改完有没有效果全靠猜。
- 精简工具结果:
_for_model()/_for_model_dict() 只回传模型确认/播报实际需要的字段(标题、时间、地点、日程类型等),account_id/created_at/updated_at/deleted_at/revision 这类审计字段模型不需要开口读出来,没必要进上下文(_snapshot_for_client() 已经在给客户端的那条路上做了同样的字段过滤,schedule_tools.py:493-500,工具结果这条路可以照抄同一个思路)。_find() 列表场景数量一多要单独考虑是否需要摘要/截断上限。
- 按上下文体量重建会话:
_spent() 除了轮次和时间,加一个基于累计 token(或输入字节数的粗略代理,如果拿不到精确 token 数)的第三个预算阈值,上下文变大到一定程度就主动重建,不等到轮次或时间耗尽。具体阈值需要基于第 1 步拿到的真实用量数据来定,不能拍脑袋。
验收标准
不做
- 不动 Composed Agent 那条工具注册表(
conversation/schedule_tools.py)——目前没有接入 /ws,不产生真实 token 成本
背景
Realtime 会话(
intelligence/realtime/agent.py)为了让通话能跨多轮保持上下文,一个会话会被持续复用,直到用完SESSION_MAX_TURNS=40轮或SESSION_MAX_AGE_SECONDS=240秒的预算才重建(agent.py:46-47,_spent())。这套复用机制 push_to_talk 和 continuous 两种语音模式都会走到——_session_for()/_Held按(account_id, conversation_id)复用同一个会话(agent.py:198held.turns += 1),不是 continuous 独有的问题。但这两个预算只按轮次数和时间计量,不看会话里实际攒了多少上下文——工具调用结果一旦进入会话历史,会在同一个会话存活期内被反复带上下文重新处理,直接推高audio_text_input_token消耗。schedule_tools.py里返回给模型的工具结果目前没有做过任何裁剪:_find()(list_schedules)对每条匹配日程调用_for_model(),逐条asdict(schedule)把ScheduleSnapshot全部 21 个字段(business/calendar/contracts.py:128-153:id、account_id、schedule_type、schedule_kind、title、is_all_day、timezone、status、revision、created_at、updated_at、start_time、end_time、recurrence_rule、location_name、latitude、longitude、reminder_type、reminder_trigger_at、reminder_offset_minutes、reminder_strength、reminder_disposition_state、deleted_at)原样吐给模型,外加一个starts_at_local(schedule_tools.py:478-491)。_mutation_result()(create/update/delete 共用)同样通过_for_model_dict()把完整快照塞进output(schedule_tools.py:463-476)。模型确认一次日程创建、或者报一次"今天有哪些日程",实际只需要说出标题、时间、地点这几项,但现在整条 21 字段的快照连同没用的审计字段都进了会话上下文,且会在这个会话存活的每一轮里被重新计入
audio_text_input_token。同时,阿里云 Realtime API 的
response.done事件本身带用量信息,但infrastructure/external/realtime/qwen_audio.py里两处response.done处理分支(qwen_audio.py:271、qwen_audio.py:331)都只用它判断响应是否结束,从没读取或记录过用量字段。对照infrastructure/external/llm/openai_compatible.py:97-172里非 realtime 那条 LLM 管线,_parse_usage()/LlmUsage早就在做同样的事——realtime 这条路径缺的是同一套实践,不是要新发明一套。这三点合起来是
audio_text_input_token占到约 70% 总成本的直接原因:上下文本身该精简的没精简,攒起来的量既不可见(没有用量日志),会话重建的时机也不看这个量(只看轮次/时间)。建议方案
response.done用量:解析阿里云 Realtime APIresponse.done事件里的 usage 字段(若字段名/结构与协议不确定,需先对照实际线上事件确认),按account_id/conversation_id打日志或指标,参照openai_compatible.py的LlmUsage模式定义等价结构。没有这一步,后面两项改完有没有效果全靠猜。_for_model()/_for_model_dict()只回传模型确认/播报实际需要的字段(标题、时间、地点、日程类型等),account_id/created_at/updated_at/deleted_at/revision这类审计字段模型不需要开口读出来,没必要进上下文(_snapshot_for_client()已经在给客户端的那条路上做了同样的字段过滤,schedule_tools.py:493-500,工具结果这条路可以照抄同一个思路)。_find()列表场景数量一多要单独考虑是否需要摘要/截断上限。_spent()除了轮次和时间,加一个基于累计 token(或输入字节数的粗略代理,如果拿不到精确 token 数)的第三个预算阈值,上下文变大到一定程度就主动重建,不等到轮次或时间耗尽。具体阈值需要基于第 1 步拿到的真实用量数据来定,不能拍脑袋。验收标准
response.done用量被解析并记录(日志或指标),可以在真实通话里观察到逐轮累积的audio_text_input_token走势schedule_tools.py里list_schedules/create/update/delete 的工具结果只包含模型确认/播报所需字段,审计字段被过滤agent.py的会话重建条件新增一个上下文体量维度(token 数或字节数近似),并有单测覆盖"上下文变大但轮次/时间预算未耗尽"时触发重建的分支audio_text_input_token消耗下降不做
conversation/schedule_tools.py)——目前没有接入/ws,不产生真实 token 成本