Playbook 高级特性完整指南

February 14, 2026 · View on GitHub

本文档介绍 Active-Call Playbook 系统中的高级特性,包括环境变量支持、SIP Headers 提取、变量管理、HTTP 调用等。

目录


环境变量支持 (Universal)

✨ 新特性:所有配置字段支持 ${VAR_NAME} 语法

从 v0.3.37+ 开始,所有 Playbook 配置字段都支持环境变量模板语法。

语法

# 字符串字段
provider: "${MY_PROVIDER}"
api_key: "${OPENAI_API_KEY}"

# 数值字段(不需要引号)
speed: ${TTS_SPEED}
temperature: ${LLM_TEMPERATURE}
max_tokens: ${LLM_MAX_TOKENS}

# 嵌套字段
base_url: "${OPENAI_BASE_URL}"
language: "${ASR_LANGUAGE}"

示例

---
asr:
  provider: "${ASR_PROVIDER}"      # sensevoice, tencent, aliyun
  language: "${ASR_LANGUAGE}"      # zh, en, auto
  
tts:
  provider: "${TTS_PROVIDER}"      # supertonic, cosyvoice
  speaker: "${TTS_SPEAKER}"        # F1, M1, M2, F2
  speed: ${TTS_SPEED}              # 0.8, 1.0, 1.2
  
llm:
  provider: "${LLM_PROVIDER}"      # openai, azure, dashscope
  model: "${LLM_MODEL}"            # gpt-4o, gpt-4o-mini
  apiKey: "${LLM_API_KEY}"
  baseUrl: "${LLM_BASE_URL}"
  temperature: ${LLM_TEMPERATURE}  # 0.0 - 2.0
  max_tokens: ${LLM_MAX_TOKENS}    # integer
---

优势

  1. 安全性:API keys 不会被提交到代码仓库
  2. 灵活性:同一个 playbook,不同环境不同配置
  3. 动态性:可以在运行时切换模型、语言等参数
  4. 通用性:支持所有字段类型(字符串、数值、嵌套对象)

回退行为

  • 如果环境变量未定义,${VAR_NAME} 会被保留(原样输出)
  • YAML 解析器可能会因为无效值而失败
  • 建议:始终设置必需的环境变量

完整示例

参见:Environment Variables Example

⚠️ 与运行时变量 {{var}} 的区别

重要${VAR}{{var}} 是两个不同的能力,不会冲突!

语法用途时机作用域来源
${VAR}环境变量Playbook 加载时(静态)YAML 配置系统环境变量
{{var}}运行时变量对话运行时(动态)Prompt 文本SIP Headers、set_var

示例

---
# 配置中使用 ${VAR} - 从环境变量读取
llm:
  apiKey: "${OPENAI_API_KEY}"  # ← 加载时替换
  model: "${LLM_MODEL}"
  
sip:
  extract_headers:
    - "X-Customer-Name"
---

# Prompt 中使用 {{var}} - 运行时替换
# Scene: main
你好,{{ sip["X-Customer-Name"] }}!     # ← 每次通话不同

详细对比:参见 Template Syntax Comparison


SIP Headers 提取与使用

1. 配置提取规则

在 Playbook 的 YAML 配置部分指定要提取的 SIP Headers:

---
sip:
  extract_headers:
    - "X-CID"           # 客户ID
    - "X-Session-Type"  # 会话类型
    - "X-Agent-ID"      # 坐席ID
llm:
  provider: "aliyun"
  model: "qwen-turbo"
---

2. 在 Playbook 中使用

提取的 Headers 会自动注入到 Playbook 的变量上下文中。由于 Header 名称通常包含连字符(如 X-Customer-ID),而 Jinja2 会将连字符解析为减法运算符,因此需要使用 字典访问语法

你好!您的客户编号是 {{ sip["X-CID"] }}。
本次会话类型:{{ sip["X-Session-Type"] }}。

关键说明

  • 推荐{{ sip["X-Header-Name"] }} - 使用 sip 字典访问,支持包含连字符的变量名
  • 错误{{ X-Header-Name }} - 会被解析为 X 减 Header 减 Name,导致错误
  • 📋 sip 字典范围:只包含 Headers(比如以 X-x- 开头的变量)
  • 普通变量:对于不含连字符的变量(如 customer_id),可以直接使用 {{ customer_id }}

3. LLM 访问方式

LLM 可以通过系统消息获取这些变量(自动注入到上下文):

用户: 我的编号是多少?
LLM: 根据系统记录,您的客户编号是 {{ sip["X-CID"] }}。

4. 使用 set_var 更新 SIP Headers

在对话过程中,LLM 可以使用 <set_var> 动态设置或更新单个 SIP Header:

LLM: 您的工单已创建 <set_var key="X-Ticket-ID" value="TKT-12345" />
LLM: 通话评分为优秀 <set_var key="X-Call-Rating" value="excellent" />

这些设置的 Headers 会:

  • 立即写入 ActiveCall.extras
  • 在 BYE 请求的 render_sip_headers 中可用
  • 可被后续的模板引用

5. BYE Headers 渲染

挂断时,可以配置 hangup_headers 模板,访问所有变量(包括 SIP headers 和普通变量):

---
sip:
  extract_headers:
    - "X-Customer-ID"
  hangup_headers:
    X-Call-Result: "{{ call_result }}"          # 普通变量
    X-Customer: "{{ sip["X-Customer-ID"] }}"     # SIP Header
    X-Agent: "{{ agent_name }}"                  # 普通变量
---

在对话中设置变量:

<set_var key="call_result" value="successful" />
<set_var key="agent_name" value="Alice" />

6. 完整示例

---
sip:
  extract_headers:
    - "X-Customer-ID"
    - "X-Customer-Name"
    - "X-Session-Type"
llm:
  provider: "aliyun"
  model: "qwen-turbo"
  greeting: "{{ sip["X-Customer-Name"] }}您好!"
---
# Scene: main
您的客户编号是 {{ sip["X-Customer-ID"] }},会话类型为 {{ sip["X-Session-Type"] }}。
请问有什么可以帮您?

内置会话变量

系统会在通话建立时自动注入一组内置变量到 ActiveCall.extras 中,无需手动设置即可在 Prompt 和模板中使用。

内置变量列表

变量名说明示例值
session_id通话会话唯一标识"abc123-def456"
call_type通话类型"sip" / "websocket" / "webrtc" / "b2bua"
caller主叫方 SIP URI"sip:13800138000@domain.com"
callee被叫方 SIP URI"sip:10086@domain.com"
start_time通话开始时间(RFC 3339 格式)"2025-01-15T10:30:00+08:00"

在 Prompt 中使用

# Scene: main
当前会话ID:{{ session_id }}
通话类型:{{ call_type }}
开始时间:{{ start_time }}
主叫号码:{{ caller }}
被叫号码:{{ callee }}

注意事项

  • 内置变量使用 entry().or_insert_with() 模式注入,不会覆盖外部传入的同名变量
  • callercallee 仅在 SIP 通话中可用
  • 所有内置变量都可以在场景切换时通过动态渲染在 Prompt 中使用

动态场景 Prompt 渲染

问题背景

在多场景 Playbook 中,Prompt 模板在 Playbook 加载时就会被渲染({{var}} 被替换)。这意味着通过 <set_var> 在对话过程中设置的变量无法在其他场景的 Prompt 中被引用。

例如

# Scene: collect_info
请收集用户的意向信息。

# Scene: confirm
用户的意向是:{{ intent }}   ← 加载时 intent 尚不存在,渲染为空

解决方案

从 v0.3.38 开始,系统支持动态场景 Prompt 渲染

  1. 解析 Playbook 时,原始模板保存在 Scene.raw_prompt
  2. 每次切换场景(<goto>)时,使用当前 extras 中的变量重新渲染 Prompt
  3. 渲染失败时自动回退到已有的 Prompt,确保不影响正常对话

使用示例

---
sip:
  extract_headers:
    - "X-Jobid"
llm:
  provider: "openai"
  model: "gpt-4o"
---

# Scene: collect
你是一个意向收集助手。
请收集用户的购买意向。收集完毕后输出 <set_var key="intent" value="用户意向" /> 然后输出 <goto scene="confirm" />

# Scene: confirm
你是一个确认助手。
工单编号:{{ sip["X-Jobid"] }}
会话ID:{{ session_id }}
用户意向:{{ intent }}

请向用户确认以上信息是否正确。

执行流程

  1. 通话开始,进入 collect 场景
  2. LLM 收集信息后执行 <set_var key="intent" value="买零食" />
  3. LLM 输出 <goto scene="confirm" />
  4. 系统切换场景时,读取 confirm 场景的 raw_prompt,使用当前 extras(含 intent="买零食"session_id 等)重新渲染
  5. confirm 场景的 Prompt 变为:"用户意向:买零食","会话ID:abc123"

支持的变量类型

动态渲染时可以使用以下所有变量:

类型示例说明
<set_var> 变量{{ intent }}对话中设置的变量
SIP Headers{{ sip["X-Jobid"] }}通过 sip 字典访问
内置变量{{ session_id }}系统自动注入

容错机制

  • 引用不存在的变量时,渲染为空字符串(MiniJinja 默认行为)
  • 模板语法错误时,自动回退到原有 Prompt,不会中断通话
  • 日志记录渲染失败的详细信息,便于排查

变量管理 (<set_var>)

<set_var> 标签允许 LLM 在对话过程中动态设置或修改变量。

语法

<set_var key="变量名" value="变量值" />

使用场景

1. 记录用户信息

LLM: 请问您贵姓?
用户: 我姓张
LLM: 张先生您好 <set_var key="user_name" value="张先生" />,请问有什么可以帮您?

2. 保存业务状态

LLM: 已为您创建工单,工单号 12345 <set_var key="ticket_id" value="12345" />

3. 设置 BYE Headers(用于 SIP)

LLM: 通话结束,感谢您的来电 <set_var key="_hangup_headers" value='{"X-Hangup-Reason":"normal","X-Duration":"180"}' />

特性说明

  • 实时更新: 变量立即写入 ActiveCall.extras
  • 持久化: 变量在整个通话会话中保持
  • 可传递: 可用于后续的 BYE 请求 Headers
  • 类型: 所有值都存储为字符串(JSON Value::String)

注意事项

⚠️ 变量值中的特殊字符需要转义

<!-- 正确 -->
<set_var key="note" value='{"status":"ok"}' />

<!-- 错误 - 双引号冲突 -->
<set_var key="note" value="{"status":"ok"}" />

HTTP 外部调用 (<http>)

<http> 标签允许 LLM 在对话中调用外部 HTTP API。

语法

<http url="API地址" method="方法" body="请求体" />
  • url: 必需,API 完整 URL
  • method: 可选,默认 GET(支持 GET/POST/PUT)
  • body: 可选,请求体内容

使用场景

1. 查询外部数据

LLM: 我来帮您查询天气 <http url="https://api.weather.com/v1/current?city=beijing" />

响应处理:HTTP 响应会自动添加到对话历史中:

系统: HTTP GET https://api.weather.com/v1/current?city=beijing returned (200): {"temp":15,"condition":"sunny"}
LLM: 北京当前温度 15 度,天气晴朗。

2. 提交数据

LLM: 正在为您创建工单 <http url="https://api.crm.com/tickets" method="POST" body='{"subject":"咨询","customer":"123"}' />

3. 多步骤交互

用户: 帮我订一张去上海的机票
LLM: 好的,我先查询一下航班信息 <http url="https://api.flight.com/search?dest=shanghai" />

[系统返回航班列表]

LLM: 有 MU5180 和 CA1234 两个航班,您选择哪个?
用户: MU5180
LLM: 正在为您预订 <http url="https://api.flight.com/book" method="POST" body='{"flight":"MU5180","passenger":"user123"}' />

特性说明

  • 同步执行: HTTP 请求会阻塞流式输出,等待响应
  • 自动注入: 响应内容自动添加到对话历史
  • 错误处理: 失败时错误信息也会添加到历史
  • 安全性: 建议只调用可信的内部 API

注意事项

⚠️ 性能考虑

  • HTTP 调用会增加响应延迟
  • 建议设置合理的超时时间
  • 避免在流式响应中频繁调用

SIP BYE Headers 定制

在 SIP 通话结束时,可以附加自定义 Headers 到 BYE 请求。

1. 配置模板

在 Playbook 配置中定义 BYE Headers 模板:

---
sip:
  hangup_headers:
    X-Hangup-Reason: "{{ hangup_reason }}"
    X-Call-Duration: "{{ call_duration }}"
    X-User-Rating: "{{ user_rating }}"
    X-Ticket-ID: "{{ ticket_id }}"
---

2. LLM 设置变量

在对话中通过 <set_var> 设置这些变量:

LLM: 问题已解决,请对本次服务评分(1-5)
用户: 5分
LLM: 感谢您的好评 <set_var key="user_rating" value="5" /> <set_var key="hangup_reason" value="satisfied" />,再见!<hangup/>

3. 自动渲染

系统会在发送 BYE 时:

  1. 读取 ActiveCall.extras 中的所有变量
  2. 使用 Jinja2 渲染 hangup_headers 模板
  3. 将渲染结果作为 SIP Headers 发送

直接设置 Headers

也可以直接设置 _hangup_headers 变量:

<set_var key="_hangup_headers" value='{"X-Custom":"value","X-Status":"completed"}' />

注意:直接设置会覆盖模板渲染结果


完整流程示例

场景:客服呼叫中心

---
asr:
  provider: "aliyun"
llm:
  provider: "openai"
  model: "gpt-4o"
  apiKey: "${OPENAI_API_KEY}"
  prompt: |
    你是一个客服机器人。可以使用以下工具:
    - 查询工单: <http url="https://api.crm.com/tickets/{ticket_id}" />
    - 创建工单: <http url="https://api.crm.com/tickets" method="POST" body='...' />
    - 记录信息: <set_var key="变量名" value="值" />
    
    当用户要求转人工时,输出: <refer to="sip:agent@domain.com"/>
    对话结束时,务必记录通话原因。
tts:
  provider: "aliyun"
sip:
  extract_headers:
    - "X-Customer-ID"
    - "X-Call-Source"
    - "X-Priority"
  hangup_headers:
    X-Hangup-Reason: "{{ hangup_reason }}"
    X-Resolved: "{{ is_resolved }}"
    X-Ticket-ID: "{{ ticket_id }}"
    X-Sentiment: "{{ user_sentiment }}"
---

你是客服助手小智。请注意:

1. 首先使用客户ID查询历史工单: <http url="https://api.crm.com/customers/{{ sip[\"X-Customer-ID\"] }}/tickets" />
2. 处理问题时创建工单并记录ID
3. 对话中识别用户情绪(positive/neutral/negative)
4. 结束时记录必要信息

礼貌、专业、简洁回复。

实际对话流程

[SIP INVITE with X-Customer-ID: CUST001]

机器人: 您好,我是小智客服。<http url="https://api.crm.com/customers/CUST001/tickets" />

[API返回: 该客户有1个未处理工单 #TK123]

机器人: 看到您有一个进行中的工单 #TK123,是关于这个问题吗?

客户: 不是,我有新问题,网络连不上

机器人: 好的,我帮您创建新工单 <http url="https://api.crm.com/tickets" method="POST" body='{"customer":"CUST001","subject":"网络连接故障","priority":"high"}' /> 
      <set_var key="user_sentiment" value="neutral" />

[API返回: 创建成功,工单号 TK456]

机器人: 工单 TK456 已创建 <set_var key="ticket_id" value="TK456" />。现在帮您排查...

[技术支持对话...]

机器人: 问题解决了吗?

客户: 好了,谢谢!

机器人: 太好了 <set_var key="is_resolved" value="true" /> <set_var key="user_sentiment" value="positive" /> <set_var key="hangup_reason" value="problem_solved" />!
        祝您使用愉快,再见!<hangup/>

[SIP BYE with Headers:]
X-Hangup-Reason: problem_solved
X-Resolved: true
X-Ticket-ID: TK456
X-Sentiment: positive

完整测试用例

参见 tests.rs 中的:

  • test_set_var_in_streaming
  • test_set_var_with_sip_headers
  • test_http_command_in_stream
  • test_http_command_post_with_body
  • test_multiple_commands_in_sequence

最佳实践

1. 性能优化

推荐

查询中 <http url="..." /> 处理中...

避免

<http url="...1" /> <http url="...2" /> <http url="...3" />

2. 错误处理

在 LLM Prompt 中说明错误处理:

如果 API 调用失败,礼貌告知用户并提供备选方案。
不要暴露技术错误细节。

3. 安全建议

  • ✅ 只调用内部可信 API
  • ✅ 使用环境变量存储敏感信息(如 API Key)
  • ✅ 在 API 网关层做访问控制
  • ❌ 不要在 Playbook 中硬编码密钥
  • ❌ 不要调用不可控的外部 URL

4. 变量命名规范

  • 使用下划线分隔:user_name, ticket_id
  • 保留前缀 _sip_ 用于系统变量
  • 避免使用特殊字符和空格

故障排查

问题:变量未生效

检查

  1. <set_var> 语法是否正确(注意引号)
  2. 变量名是否在模板中拼写一致
  3. 查看日志确认变量已写入 extras

问题:HTTP 调用无响应

检查

  1. URL 是否可访问(网络/防火墙)
  2. API 是否有超时限制
  3. 查看对话历史中的系统消息确认响应内容

问题:BYE Headers 未携带

检查

  1. sip.hangup_headers 配置是否正确
  2. 变量在 hangup 前是否已设置
  3. 确认是 SIP 类型通话(WebRTC 不支持)

API 参考

ActiveCall.extras

类型:Option<HashMap<String, serde_json::Value>>

存储通话会话中的所有变量,包括:

  • 提取的 SIP Headers
  • <set_var> 设置的变量
  • 系统注入的变量

Playbook 配置

sip:
  extract_headers: [String]        # 要提取的 Headers 列表
  hangup_headers: HashMap<String, String>  # BYE Headers 模板

相关文档


版本历史

  • v0.3.38: 内置会话变量(session_id, call_type, caller, callee, start_time);动态场景 Prompt 渲染(set_var 变量可在场景切换时应用到 Prompt)
  • v0.3.37: 通用 ${VAR_NAME} 环境变量支持
  • v0.3.36: <http> 响应注入
  • v0.3.35: SIP BYE Headers 定制
  • v0.3.34: <set_var> 单/双引号支持
  • v0.3.30: SIP Headers 提取