数据截至 (上游 commit 689ca048bb0a)
图即工具:PipecatEngine 状态机(核心)
30 秒导读: 一张对话流程图(节点 = 对话阶段,边 = 阶段之间的跳转条件)怎么变成一通"会说话、会自己决定往哪走"的电话?答案是一句话:把每条出边包装成一个 LLM 能调用的函数。LLM 每次生成时,看着"当前节点的 system prompt + 当前节点的出边函数列表",它调用哪个函数,就等于选择走哪条边。函数被调用时,引擎抽取变量、播报过渡语、切到新节点、换上新节点的 prompt 和工具,再触发下一轮生成。本章讲这套"图 → 工具 → 跳转"的运行时机制,以及它最精妙的时序细节。
本章聚焦 PipecatEngine 内部的运行时逻辑。相关分工:
- 图的静态结构(节点/边的数据模型与校验)在 01-workflow-model。
- 帧在处理器之间怎么流动(管线本身)在 02-voice-pipeline。
- 引擎的装配与生命周期(谁创建它、怎么接上管线)、以及引擎旁路能力在 04-call-orchestration。
- transition 函数之外的可扩展工具(自定义工具、MCP、知识库)在 05-extensibility-registries。
1. 这是什么(先建直觉)
一句话定义
PipecatEngine 是一台由 LLM 函数调用驱动的对话状态机:它把工作流图跑起来,让语言模型在每个节点上"边说话边决定下一步去哪个节点"。
它要解决的问题
假设你要搭一个外呼电话机器人:先问候 → 确认身份 → 介绍产品 → 如果感兴趣就预约、不感兴趣就礼貌挂断。
传统做法是写一堆 if/else 状态机,手工判断用户说了什么、该跳哪。但用户的话是自然语言,判断条件千变万化,硬编码根本写不完。
Dograh 的思路是把"判断走哪条边"这件事外包给 LLM:
- 你在图里给每条边写一个条件描述(例:"用户表示有兴趣了解更多")。
- 引擎把这条边变成一个 LLM 可调用的函数,函数的描述就是那句条件。
- LLM 在对话中觉得"用户确实感兴趣了",就去调用那个函数——于是对话跳到下一个节点。
一句话类比
把它想成带对讲机的导游:导游(引擎)站在某个展厅(节点)里,手上有几张写着"去往 X 厅"的门卡(transition 函数)。游客(用户)说的话让导游判断该带去哪个厅,导游刷对应门卡,一行人就走到新展厅,墙上的讲解词(system prompt)和可用设施(工具)也随之全换成新厅的。
用起来什么样(一次跳转的直观样子)
[节点: 确认身份] system prompt: "你在确认对方是不是本人……"
可调用函数:
- identity_confirmed (描述: 用户确认了自己是本人)
- wrong_person (描述: 接电话的不是目标本人)
用户: "对,我就是张先生本人。"
LLM 决策: 调用 identity_confirmed()
│
▼
引擎: 抽取变量 → 播过渡语"好的,谢谢确认" → set_node(产品介绍节点)
→ 换上"产品介绍"的 prompt 和函数 → 触发下一轮 LLM 生成
[节点: 产品介绍] ……LLM 开始用新 prompt 说话
本节不谈代码细节。记住一个核心等式即可:LLM 调用哪个 transition 函数 = 对话走哪条边。
2. 顶层全景(引擎大概怎么转)
部件职责
| 部件 | 干什么 | 在哪 |
|---|---|---|
set_node | 进入一个节点的总入口:更新当前节点、发跳转事件、按类型分派 | pipecat_engine.py:614 |
_setup_llm_context | 把当前节点的出边、工具、prompt 全部装进 LLM | pipecat_engine.py:571 |
_create_transition_func | 为一条边生产出真正被 LLM 调用的 transition_func | pipecat_engine.py:248 |
compose_system_prompt_for_node | 拼系统提示(全局 prompt + 节点 prompt + 录音模式指令) | pipecat_engine_context_composer.py:49 |
compose_functions_for_node | 拼函数列表(知识库 + 自定义工具 + 出边 transition schema) | pipecat_engine_context_composer.py:86 |
end_call_with_reason | 结束节点收尾:排 EndFrame、落库 disposition/tags | pipecat_engine.py:825 |
queue_node_opening / get_node_greeting | 节点开场:播文本 TTS、预录音频,或触发首轮生成 | pipecat_engine.py:725 / :614 |
should_mute_user | 判定此刻要不要静音用户输入 | pipecat_engine.py:940 |
pipecat_engine_callbacks.py | 各种回调工厂(用户静默、超时、聚合纠错) | pipecat_engine_callbacks.py |
主线走一遍(高层)
一次"从当前节点跳到下一个节点"的完整流程,只有一条主干:
┌─────────────────────────────────────────┐
进入某节点 ──────► │ set_node(node_id) │
│ • _current_node = 新节点 │
│ • 发 node_transition 事件 │
│ • 按类型分派 ↓ │
└───────┬─────────────┬────────────┬───────┘
is_start │ is_end │ agent │
▼ ▼ ▼
_handle_start_node _handle_end_node _handle_agent_node
└─────────────┴────────────┘
│ 都调用
▼
┌────────────────────── ───────────────────┐
│ _setup_llm_context(node) │
│ ① 为每条出边注册 transition 函数 │
│ ② 注册自定义工具 / 知识库函数 │
│ ③ compose_system_prompt_for_node │
│ ④ compose_functions_for_node │
│ ⑤ _update_llm_context(prompt, funcs) │
└───────────────────┬─────────────────────┘
▼
LLM 带着"新 prompt + 新函数列表"生成下一句
│
┌───────────────────┴─────────────────────┐
│ LLM 调用了某个 transition 函数 │
│ → transition_func 执行 → 又一次 set_node │ ← 回到顶部,循环
└─────────────────────────────────────────┘
一句话:set_node 装填节点 → LLM 调 transition 函数 → transition 函数再 set_node,如此循环,直到走进结束节点。
3. 核心机制一:图 → 工具(每条出边变成一个函数)
3.1 它要解决的小问题
图里的"边"是静态数据,LLM 看不见。要让 LLM 能"选边",必须把每条出边翻译成 LLM 世界里唯一能"主动触发外部动作"的东西——函数调用(tool call)。