Call_Workflow_V2

外呼打电话全流程梳理 v2

本文基于 waihu_log 目录下的样本日志,以及 interactive-controller 当前代码整理。重点不是只描述模块,而是把一通电话里每个阶段的数据如何产生、如何存储、如何在下一轮 VAD/ASR 交互中被带回、如何传给 LLM/NLU/flow-agent/TTS 串起来。

1. 样本和关键文件

主要样本:

关键代码:

2. 一通电话里的三个上下文

当前链路里至少有三类上下文,不能混在一起看。

| 上下文 | 存储/来源 | 下一轮如何使用 | 典型字段 | | --- | --- | --- | --- | | TS 多轮识别/语义上下文 | ts-ex 写 Redis,key 形如 multi_round_session_{cuid}{pid} | 下一轮 TaskLongLlm::fill_controller_recog_result 放进 BDVS payload 的 nlu_session | finalSessionsemantic_sessionnluInfostts_text | | IC 历史 QA | TaskBaishengLLM 写 Redis,key 形如 baisheng_history_qa_prefix_akw3ob_{pid}{cuid}{sessionId} | 下一轮 IC 初始化读取,用于 RAG session_context、安全上下文、部分 LLM param | query/answer 成对保存 | | flow-agent/ICE 会话 | flow-agent 按 pid:cuid:session_id 维护,IC 的 RPCFlowagentHandler 初始化时读取 | 取上一轮 prefetch_rag,并在本轮请求里注入 session.ice_clarify_session | clarify_responseretrieval_info、业务 memory |

几个容易混的字段:

3. 总体时序

sequenceDiagram
    participant Tel as 电话/ACG
    participant SC as speech-controller
    participant BTS as bdvs-ts
    participant TS as ts-ex
    participant IC as interactive-controller
    participant LLM as LLM backend
    participant NLU as rpc_nlu
    participant FA as flow-agent
    participant ICE as ICE/百胜接口
    participant TTS as TTS

    Tel->>SC: 建立 ASR/TTS 流,传入 pid/cuid/session/ext
    SC->>Tel: prologue 事件
    Tel-->>SC: play_prologue + tts_text + personalized_info
    SC->>BTS: SynTextInput,播放开场白
    BTS->>TTS: 合成开场白音频
    SC->>TS: q1 开场白参考信息,写入多轮上下文

    Tel->>SC: 用户音频/VAD
    SC->>TS: 音频包 + vad_info
    TS->>TS: ASR/ITGP/多轮 session 读写
    TS->>IC: TextInput BDVS JSON,带 nlu_session/text_param/interactionOutputData
    IC->>IC: 解析 nlu_session/raw_nluinfo,读取 IC 历史 QA
    IC->>LLM: 第一次 LLM,预取/垫话/label
    LLM-->>IC: padding 文本 + nlu_txt
    IC->>NLU: asr_result + llm_answer + history + nlu_txt
    NLU-->>IC: 结构化 nlu_result
    IC->>FA: 注入 nluInfos,调用 flow-agent
    FA->>ICE: 业务策略/接口
    ICE-->>FA: 业务结果
    FA-->>IC: polish_rag / prefetch_rag / flow session
    IC->>LLM: 第二次 LLM,follow_up_content=polish_rag
    LLM-->>IC: 润色结果
    IC-->>TS: Speak directive 文本
    TS->>TTS: 合成最终回复
    TS-->>SC: TTS 音频
    SC-->>Tel: 播放回复
    SC->>Tel: dialog_record / client_silence 等事件

4. 建连和开场白

4.1 speech-controller 建立电话会话

电话侧会建立 ASR/TTS 两路流。样本 3303326126 的汇总日志显示:

对应日志:waihu_log/speech-controller.log:46

4.2 向 ACG 请求开场白

speech-controller 先向 ACG 发 prologue 事件。ACG 返回:

{
  "action_type": "play_prologue",
  "data": {
    "tts_text": "喂,您好",
    "silence_detect_ms": 10000,
    "personalized_info": {
      "sys_phonenum": "15162115538",
      "sys_date": "2026-05-11",
      "sys_time": "19:43:49"
    },
    "allow_interrupt": false
  }
}

对应日志:waihu_log/speech-controller_debug.log:22waihu_log/speech-controller.log:29

这里有两条并行作用:

  1. BdvsEventClient 走事件链路,把 SynTextInput 发到 bdvs-ts,用于真正播放开场白。
  2. BdvsClient 走普通 TS 链路,发一个 q1 请求,文本是 任务=开场白 的参考信息,用于把开场白写入对话上下文。

样本中 speech-controller.log:30 是事件 TTS 完成,speech-controller.log:31 是 q1 开场白参考信息完成。

4.3 开场白也会写上下文

q1 进入 TS 后,TS 解析出 interaction_output_data.finalSessionnluInfos,然后写 Redis:

key=multi_round_session_2_9ab7..._38082510323712004144826

相同逻辑在另一个样本里能看到:

IC 侧也会通过 TaskConversationInjection 把开场白作为 QA 写入自己的历史:

对应日志:waihu_log/interactive-controller_debug.log:3121

5. 用户说话、VAD、ASR 和 TS 上下文

5.1 speech-controller 发送音频和 VAD

电话侧音频进入后,speech-controller 把音频分块发给 TS。日志里能看到每个音频包带 vad_info,例如:

{
  "audio_packet_index": 2,
  "audio_len": 2560,
  "vad_info": [0,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0]
}

对应日志:waihu_log/speech-controller_debug.log:172speech-controller_debug.log:180

这一步还没有进入 IC,它只是把用户音频和 VAD 标记送到 TS,TS 后续根据 ASR/完整性/VAD 结果形成 TextInput

5.2 TS 读取上一轮多轮 session

每个 query 进入 TS 后,会先读 Redis 中的多轮识别/语义上下文。第一次通常读不到:

GET multi_round_session_{cuid}{pid}
read_redis failed

对应日志:waihu_log/ts_debug.log:240waihu_log/ts_debug.log:241

后续轮次会读到上一轮写入的 session,并在 TaskLongLlm::fill_controller_recog_result 里塞进发给 IC 的 BDVS JSON,字段通常表现为:

样本 2001844932 的第一条 IC 输入就带了上一轮的 nlu_session,里面已有:

对应日志:waihu_log/2001844932:3

5.3 TS 写回本轮 interaction session

ASR/ITGP 产生本轮结果后,TS 会写:

TSContext::write_interaction_session_to_redis
key=multi_round_session_{cuid}{pid}
sessionsize=...

样本 今天天气怎么样 的写入在 waihu_log/ts_debug.log:10078。紧接着,TS 构造给 IC 的请求体,见 waihu_log/ts_debug.log:10113

6. IC 收到 TS 请求后的初始化

6.1 Context 解析 nlu_session

IC 收到 BDVS JSON 后,Context::init_with_bdvs_param() 会提取 nlu_session

代码:interactive-controller/context/context.cpp:256context.cpp:273

6.2 TaskBaishengLLM 解析 BDVS JSON

TaskBaishengLLM::process_task_req_param() 调用:

bdvs_json_to_pb_with_dialog(param_str, bdvs_pb_ptr, err_msg, _context->handshake_request())

核心解析在 bdvs_parser.cpp

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:176interactive-controller/util/bdvs_parser.cpp:84bdvs_parser.cpp:145

6.3 IC 构造 _history_dialog

如果 bdvs_pb_ptr->nlu_session_size() > 0,IC 会把每个 ConversationSessionItem 转 JSON,组成 _history_dialog。后续 rpc_nlu 请求会带这个字段。

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:192task_baisheng_llm.cpp:199

样本 2001844932 中,rpc_nlu 请求里的 history 包含前两轮:

对应日志:waihu_log/2001844932:250

6.4 IC 读取自己的历史 QA

TaskBaishengLLM 初始化时生成:

baisheng_history_qa_prefix_akw3ob_{pid}{cuid}{sessionId}

然后 read_history_qa_from_redis() 读取历史 query/answer。样本中:

[我要修改订单][您好!请问您需要修改送餐地址里的楼层或者房间号吗?...]
[是的][请您稍等一下。麻烦您提供一下订单上收餐人的手机号哦...]

对应日志:waihu_log/2001844932:9waihu_log/2001844932:15。代码:interactive-controller/task/llm/task_baisheng_llm.cpp:339task_baisheng_llm.cpp:363

这份历史会进入:

7. IC 内部一轮用户输入的处理

以用户提供手机号 15162115538 为例,完整路径如下。

7.1 判断是否是 NLU 轮

IC 用:

has_nluinfo = bdvs_msg->is_speech_mode() && bdvs_msg->has_raw_nluinfo()

如果是 NLU 轮,process_task_req_param() 会:

  1. push_bdvs_req_into_list() 插入第一次 LLM 请求,is_follow_req=false,用于预取和垫话。
  2. push_followup_bdvs_req_into_list() 再插入第二次 LLM 请求,is_follow_req=true,先挂起,等 flow-agent 返回 polish_rag
  3. process_nluinfo() 发 RAG、安全请求,并记录 _lastest_nlu_bdvs_req
  4. process_completeness() 在完整性满足后放行第一次 LLM。

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:223task_baisheng_llm.cpp:240task_baisheng_llm.cpp:933task_baisheng_llm.cpp:970

7.2 预取 RAG 和意图压缩

TS 给 IC 的 text_param.default_rag_data 是一个大字符串,常见结构:

{
  "retrieval_results": [
    {
      "content": {
        "任务": "预取",
        "参考回复": {
          "提供手机号": {"候选回复": "...<placeholder no 436>..."},
          "提供订单号": {"候选回复": "...<placeholder no 433>..."},
          "其他": {"候选回复": "...<placeholder no 420>..."}
        }
      }
    }
  ]
}

如果候选太多,IC 会做意图压缩:

  1. fill_rag_data_str() 从 flow-agent 上轮 prefetch_rag 或本轮 text_param.default_rag_data 取预取 RAG。
  2. need_compress_prefectch_rag_data() 抽取 参考回复 的 key。
  3. send_intents_rag_request_to_rag_handler() 把候选 key 作为 intends 发给 intents RAG。
  4. rebuild_retrieval_results() 只保留召回到的候选,同时补一个 其他 fallback。

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:2868task_baisheng_llm.cpp:3175

注意:意图压缩不调用 rpc_nlu。它只影响第一次 LLM 能看到哪些预取候选,进而影响 LLM 输出哪个 placeholder label。

7.3 第一次 LLM:预取 + 垫话 + label

第一次 LLM 请求由 send_data_request_to_llm_handler() 构造:

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:1487task_baisheng_llm.cpp:1585

LLM 回复文本里可能带 placeholder label,例如:

<|place▁holder▁no▁436|>请您稍等片刻。

LLMHandler::process_nlu_label() 会解析这个 label,查 label mapping,得到:

GENERAL_INFO#GET_ORDER_INFO#{phone_num}

并把去掉 label 后的文本作为垫话下发给 TS/TTS。样本中垫话是:

请您稍等片刻。

对应日志:waihu_log/2001844932:250 中 rpc_nlu 请求的 llm_answernlu_txt

7.4 垫话完成后调用 rpc_nlu

触发条件在 process_llm_data_response()

resp.has_padding_is_complete() && resp.padding_is_complete() == 1

如果 FLAGS_need_current_answer_to_nlu=true,IC 会调用 send_input_to_rpc_nlu_handler(),请求字段是:

| 字段 | 来源 | 样例 | | --- | --- | --- | | asr_result | bdvs_msg->ori_query() | 幺五幺六二幺幺五五三八 | | llm_answer | 第一次 LLM 已输出垫话 | 请您稍等片刻。 | | history_dialog | BDVS nlu_session 转成的历史 | 前两轮用户/机器人记录 | | index | decoder_idx | 23 | | nlu_txt | LLM label 映射结果 | GENERAL_INFO#GET_ORDER_INFO#{phone_num} |

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:1808task_baisheng_llm.cpp:1818task_baisheng_llm.cpp:2542task_baisheng_llm.cpp:2568

RPCNLUHandler 再转成后端 speech::NluRequest

代码:interactive-controller/component/rpc_nlu_handler.cpp:305rpc_nlu_handler.cpp:324。协议:interactive-controller/protocol/decoder.proto

样本请求和返回:

7.5 rpc_nlu 结果进入 flow-agent

TaskBaishengLLM::process_rpc_nlu_handler_output() 收到 nlu_result 后,调用:

send_input_to_rpc_flowagent_handler(nlu_out->index(), nlu_out->nlu_result(), nlu_out->is_last())

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:2571task_baisheng_llm.cpp:2585

RPCFlowagentHandler::construct_flow_request() 会把 rpc_nlu 的结果注入回 BDVS JSON:

代码:interactive-controller/component/rpc_flowagent_handler.cpp:388rpc_flowagent_handler.cpp:440

flow-agent 日志里能看到请求已经包含:

例如 waihu_log/flow-agent.log:3

7.6 flow-agent 返回待润色文本和下一轮预取信息

flow-agent 返回 FlowResponse.response,里面的 session.clarify_response.retrieval_info 是一个二元数组:

  1. 第一个元素:polish_rag,给第二次 LLM 润色使用。
  2. 第二个元素:prefetch_rag,给下一轮预取使用。

RPCFlowagentHandler::process_response() 会解析 clarify_response,再由 parse_flow_session() 拆出 polish_ragprefetch_rag

代码:interactive-controller/component/rpc_flowagent_handler.cpp:448rpc_flowagent_handler.cpp:585

样本 2001844932 中,flow-agent 返回的待润色文本是:

系统忙不过来了,请稍后重试

对应日志:waihu_log/2001844932:286

7.7 第二次 LLM:润色

IC 收到 flow-agent 输出后,不直接把 flow-agent 的文本给用户,而是:

set_bdvs_req_ready_in_list(flowagent_out->index(), &flowagent_out->polish_rag());
process_bdvs_req_list();

这会唤醒之前挂起的 follow LLM 请求:

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:2685task_baisheng_llm.cpp:2708task_baisheng_llm.cpp:1119task_baisheng_llm.cpp:1140

样本第二次 LLM 请求中:

follow_up_content=任务:润色, 待润色文本:系统忙不过来了,请稍后重试

对应日志:waihu_log/2001844932:291

最终输出:

请您稍等片刻。系统现在有点忙不过来啦,请您稍后再重试哦

对应日志:waihu_log/2001844932:533

8. 数据如何传出到用户和外部系统

8.1 IC 到 TS

LLM 的流式结果会由 TaskBaishengLLM::generate_bdvs_speak_directive() 包成 BDVS Speak directive,然后通过:

_context->write_stream_to_client(CONTROLLER_RSP_TYPE_TTS, ...)

回给 TS。第一次 LLM 垫话和第二次 LLM 润色都会走这个路径。

代码:interactive-controller/task/llm/task_baisheng_llm.cpp:1833task_baisheng_llm.cpp:2029task_baisheng_llm.cpp:2332

8.2 TS 到 TTS,再到 speech-controller

TS 的 TaskWenxinDialog 收到 IC 的 TTS 文本后,调用本地 TTS 服务 127.0.0.1:8853 合成音频。样本 今天天气怎么样 的 TS 汇总中能看到:

对应日志:waihu_log/ts_debug.log:10540

8.3 speech-controller 到 ACG/电话侧

speech-controller 收到 TS/TTS 音频后回送电话侧,同时把每轮对话上报给 ACG:

{
  "event_type": "dialog_record",
  "trigger_event": "user_interaction",
  "data": {
    "records": [
      {"role": "user", "type": "asr", "text": "..."},
      {"role": "robot", "type": "tts", "text": "..."}
    ]
  }
}

样本中每轮完成后都有 dialog_record

如果用户静默,speech-controller 会向 ACG 发 client_silence。样本中:

event=client_silence
action=play_silence

对应日志:waihu_log/speech-controller.log:52

9. 当前样本的完整轮次时间线

logid=3303326126 为主:

| 时间 | query_idx | sn/logid | 输入 | 关键处理 | 输出 | | --- | ---: | --- | --- | --- | --- | | 19:45:31 | - | 3303326126 | 呼入 | ACG prologue 返回 play_prologue | 喂,您好 | | 19:45:32 | 1/2 | 516246244 | 开场白参考信息/q1 | TS 写 multi_round_session;IC TaskConversationInjection 写开场白 QA | 开场白进入上下文 | | 19:45:41 | 3 | 1258119147 | 我要修改订单 | 预取 LLM -> rpc_nlu -> flow-agent -> 润色 LLM | 询问是否修改楼层/房间号 | | 19:45:51 | 4 | 1273919548 | | LLM label <416> -> GENERAL_CONTROL#CONFIRM#{};flow-agent 要手机号;润色 | 要求提供订单手机号 | | 19:46:07 | 5 | 1289719949 | 15162115538 | rpc_nlu 抽 phone_num;flow-agent 业务查询异常;润色 | 系统忙,稍后重试 | | 19:46:15 | 6 | 1305520350 | 重试什么 | unknown#unknown;兜底 | 暂不会这项技能 | | 19:46:29 | 7 | 1321320751 | 你会什么 | flow-agent 业务链路耗时偏长/异常 | 系统忙,稍后重试 | | 19:46:37 | 9 | 1352921553 | 哎今天天气 | CHAT#GREET | 问候类回复 | | 19:46:49 | 10 | 1303045849 | 今天天气怎么样 | CHAT#GREET | 问候类回复 | | 19:47:29 | 8 | 1337121152 | 今天天气 | controller 等待 60s 后补结束 | 补发 dialog_record / 静默事件 |

注意 query_idx=8 晚于 9/10 结束,是因为该轮 TaskLongLlm timecost=60000,后续补报。对应日志:waihu_log/ts_debug.log:10585waihu_log/speech-controller.log:49

10. 一轮手机号输入的字段流转

样本:logid=2001844932sn=3a6fd312-a998-4c16-a713-9221c7e9aa14_6

10.1 TS -> IC

上行字段:

对应日志:waihu_log/2001844932:3waihu_log/2001844932:126

10.2 IC -> 第一次 LLM

第一次 LLM 请求:

对应日志:waihu_log/2001844932:160

10.3 第一次 LLM -> rpc_nlu

第一次 LLM 输出:

rpc_nlu 请求:

{
  "asr_result": "幺五幺六二幺幺五五三八",
  "llm_answer": "请您稍等片刻。",
  "nlu_txt": "GENERAL_INFO#GET_ORDER_INFO#{phone_num}"
}

对应日志:waihu_log/2001844932:250

10.4 rpc_nlu -> flow-agent

rpc_nlu 返回:

{
  "domain": "GENERAL_INFO",
  "intent": "GET_ORDER_INFO",
  "slots": {
    "phone_num": [{"value": "15162115538"}]
  }
}

对应日志:waihu_log/2001844932:256

IC 注入 flow-agent 请求:

对应代码:interactive-controller/component/rpc_flowagent_handler.cpp:388rpc_flowagent_handler.cpp:440

10.5 flow-agent -> 第二次 LLM

flow-agent 输出 polish_rag

任务=润色
待润色文本=系统忙不过来了,请稍后重试

对应日志:waihu_log/2001844932:286

第二次 LLM 请求把它放到 follow_up_content

follow_up_content=任务:润色, 待润色文本:系统忙不过来了,请稍后重试

对应日志:waihu_log/2001844932:291

10.6 最终输出和历史保存

最终文本:

请您稍等片刻。系统现在有点忙不过来啦,请您稍后再重试哦

对应日志:waihu_log/2001844932:533

IC 结束时写历史 QA:

query=15162115538
answer=请您稍等片刻。系统现在有点忙不过来啦,请您稍后再重试哦

对应日志:waihu_log/2001844932:548。代码:interactive-controller/task/llm/task_baisheng_llm.cpp:2068task_baisheng_llm.cpp:2106,结束回写在 task_baisheng_llm.cpp:2769task_baisheng_llm.cpp:2797

11. 对后续协议调整的关键影响点

当前流程里,LLM、rpc_nlu、flow-agent 不是简单串行文本调用,存在几处隐式契约:

  1. 第一次 LLM 不只是出垫话,还负责输出 label,IC 解析成 nlu_txt,再传给 rpc_nlu。
  2. rpc_nlu 不只看 ASR,还看 llm_answerhistory_dialognlu_txt
  3. flow-agent 不直接面向用户输出,至少在 TaskBaishengLLM 链路里,它输出的是 polish_rag,再由第二次 LLM 润色。
  4. 预取候选来自 text_param.default_rag_data 或上一轮 flow-agent 的 prefetch_rag,并可能被 IC 做意图压缩。
  5. TS 的 nlu_session 和 IC 的 baisheng_history_qa 是两套上下文,后续如果改成 stateless HTTP/sglang,需要明确每次请求重新组装哪些上下文。
  6. 旧 brpc 流式 LLM 里有“新 NLU 到来后打断旧 LLM”的控制消息;如果改成每个 NLU 新建 HTTP 连接,需要明确旧连接废弃、结果丢弃、decoder_idx 乱序处理的规则。
  7. padding_is_complete 是触发 rpc_nlu 的关键开关。如果新协议没有等价字段,需要定义“垫话完成”的判断条件。
  8. follow_up_content 是第二次 LLM 的润色输入。如果改 OpenAI/sglang 协议,需要把它显式组装成 message 或结构化字段。

12. 快速定位表

| 主题 | 日志/代码 | | --- | --- | | ACG prologue 返回 | waihu_log/speech-controller_debug.log:22 | | 开场白事件 TTS | waihu_log/speech-controller.log:30 | | q1 开场白参考信息 | waihu_log/speech-controller.log:31 | | TS 写 multi_round_session | waihu_log/ts_debug.log:274waihu_log/ts_debug.log:10078 | | TS 构造发 IC 请求 | waihu_log/ts_debug.log:10113 | | IC 解析 nlu_session | interactive-controller/util/bdvs_parser.cpp:37interactive-controller/util/bdvs_parser.cpp:137 | | IC 读取历史 QA | interactive-controller/task/llm/task_baisheng_llm.cpp:339 | | 第一次/第二次 LLM 队列 | interactive-controller/task/llm/task_baisheng_llm.cpp:933interactive-controller/task/llm/task_baisheng_llm.cpp:957 | | 预取意图压缩 | interactive-controller/task/llm/task_baisheng_llm.cpp:2868 | | LLM label 解析 | interactive-controller/component/llm/handler/llm_handler.cpp:1718 | | rpc_nlu 请求组装 | interactive-controller/component/rpc_nlu_handler.cpp:305 | | flow-agent 请求注入 nluInfos | interactive-controller/component/rpc_flowagent_handler.cpp:388 | | flow-agent 解析 polish_rag | interactive-controller/component/rpc_flowagent_handler.cpp:448 | | IC 写历史 QA | interactive-controller/task/llm/task_baisheng_llm.cpp:2068 |