前言
SmartCall 的技能编排引擎提供了配置化的通用能力:通过页面配置即可实现关键词转人工、定时提醒、HTTP 接口调用等常见逻辑,覆盖了 80% 的标准业务场景。 但在实际落地中,很多场景需要更深的定制:比如对话情绪识别、自研业务系统对接、专属质检规则、复杂状态机控制等,仅靠配置化技能无法满足。
得益于插件化的扩展架构,SmartCall 预留了标准扩展接口,开发者只需实现接口并注册为 Spring Bean,即可将自定义逻辑无缝织入通话生命周期,全程零侵入核心通话代码,也不影响原有配置化能力。
源码核心包路径:smart-aster/src/main/java/com/qidiangk/smart/aster/realtime
- 拦截器扩展:
intercept.RealtimeTurnInterceptor - 工具扩展:
function.FunctionCallHandler
一、扩展体系基础
1.1 两大标准扩展接口
整个扩展体系围绕两个顶层接口展开,分别对应「平台主动触发」和「模型自主调用」两类场景:
| 接口 | 定位 | 运行时机 |
|---|---|---|
RealtimeTurnInterceptor | 拦截器扩展点 | 通话生命周期各挂载点主动触发 |
FunctionCallHandler | 工具扩展点 | 大模型通过 Function Calling 自主调用 |
1.2 自动注册与装配机制
SkillRegistry 在服务启动时,会自动扫描 Spring 容器中所有上述接口的实现类,纳入全局注册表。 每次通话建立时,再通过会话级装配将全局扩展与智能体私有配置技能合并,形成本次通话的执行链。
- 全局扩展(编码实现的 Bean):对所有智能体、所有通话生效;
- 配置化技能(页面配置):仅对当前智能体生效;
- 两者合并排序,互不污染,扩展开发完全不影响原有配置功能。
1.3 内置通用实现(简览)
页面配置的技能,底层也基于同一套接口实现,是通用化的配置驱动实现:
DynamicTurnInterceptor:数据库配置驱动的通用拦截器,对应页面「拦截器型技能」;DynamicFunctionHandler:配置驱动的通用工具处理器,对应页面「工具型技能」。
本文重点讲解编码级自定义扩展,通用实现不做展开。
二、实战一:自定义拦截器扩展
2.1 接口说明
RealtimeTurnInterceptor 是拦截器的顶层接口,提供 5 个生命周期钩子和 1 个排序方法:
public interface RealtimeTurnInterceptor {
// 执行顺序,数值越小越优先
default int order() { return 0; }
// 会话启动
default void onSessionStart(TurnContext context) {}
// 用户说完话时触发,返回true表示认领本轮
default boolean interceptUserText(TurnContext context) { return false; }
// 模型回复前
default void beforeTurn(TurnContext context) {}
// 模型回复后
default void afterTurn(TurnContext context) {}
// 会话结束
default void onSessionEnd(TurnContext context) {}
}
2.2 扩展步骤
- 新建类实现
RealtimeTurnInterceptor接口; - 类上标注
@Component,由 Spring 容器管理; - 重写对应生命周期钩子方法,写入业务逻辑;
- 服务启动自动注册,所有智能体通话生效。
2.3 完整实战案例:敏感词质检拦截器
需求:每轮对话结束后,检测用户与 AI 回复是否包含敏感词;命中则记录质检日志,命中次数达到阈值自动转人工坐席。
完整可运行代码:
package com.qidiangk.smart.aster.realtime.intercept.impl;
import com.qidiangk.smart.aster.realtime.intercept.RealtimeTurnInterceptor;
import com.qidiangk.smart.aster.realtime.intercept.TurnContext;
import com.qidiangk.smart.aster.realtime.skill.engine.SkillActionExecutor;
import com.qidiangk.smart.aster.realtime.skill.dto.ActionDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
/**
* 自定义敏感词质检拦截器
* 每轮对话后检测敏感词,命中阈值自动转人工
*/
@Slf4j
@Component
public class SensitiveWordInterceptor implements RealtimeTurnInterceptor {
// 执行顺序,放在通用配置拦截器之后
@Override
public int order() {
return 200;
}
// 模拟敏感词库,实际项目可接入外部词库
private static final List<String> SENSITIVE_WORDS = Arrays.asList(
"投诉到监管", "曝光", "媒体", "起诉"
);
private static final String ATTR_HIT_COUNT = "sensitive_hit_count";
private static final int THRESHOLD = 2;
@Resource
private SkillActionExecutor actionExecutor;
@Override
public void afterTurn(TurnContext context) {
// 耗时检测异步执行,不阻塞音频线程
ThreadUtil.execAsync(() -> checkSensitive(context));
}
private void checkSensitive(TurnContext context) {
String userText = context.getUserText();
String botText = context.getBotText();
int hitCount = getHitCount(context);
// 检测用户侧
for (String word : SENSITIVE_WORDS) {
if (userText != null && userText.contains(word)) {
hitCount++;
log.warn("[敏感词质检] 用户侧命中:{},通话ID:{}", word, context.getChannelId());
}
if (botText != null && botText.contains(word)) {
hitCount++;
log.warn("[敏感词质检] AI侧命中:{},通话ID:{}", word, context.getChannelId());
}
}
// 更新命中次数到会话变量
context.setAttr(ATTR_HIT_COUNT, hitCount);
// 达到阈值触发转人工
if (hitCount >= THRESHOLD) {
log.info("[敏感词质检] 命中阈值,自动转人工,通话ID:{}", context.getChannelId());
// 复用标准动作执行器执行转人工
ActionDTO.Transfer transfer = new ActionDTO.Transfer();
transfer.setSeatName("complaint_group");
transfer.setAnnounceContent("检测到对话风险,正在为您转接专员,请稍候。");
ActionDTO action = new ActionDTO();
action.setType(SkillActionTypeEnum.TRANSFER);
action.setTransfer(transfer);
actionExecutor.execute(action, context, SkillHookPointEnum.AFTER_TURN);
}
}
private int getHitCount(TurnContext context) {
Object count = context.getAttr(ATTR_HIT_COUNT);
return count == null ? 0 : (int) count;
}
}
验证方式:
- 启动服务,查看启动日志,确认
[Enrollment]列表中出现该拦截器; - 拨打测试电话,对话中触发敏感词,观察是否自动转人工;
- 查看服务日志,确认质检日志输出正常。
三、实战二:自定义工具处理器扩展
3.1 接口说明
FunctionCallHandler 是工具型扩展的顶层接口,大模型发起 Function Calling 时按工具名路由到对应处理器。
public interface FunctionCallHandler {
// 返回工具定义(下发给模型的描述与参数结构),返回null表示不暴露
default Map<String, Object> toolDefinition() { return null; }
// 是否处理该工具调用
default boolean supports(FunctionCallRequest request) { return true; }
// 执行工具调用,callback 必须且仅能调用一次
void handle(FunctionCallRequest request, TurnContext context, Consumer<FunctionCallResult> callback);
}
3.2 扩展步骤
- 新建类实现
FunctionCallHandler接口; - 标注
@Component,Spring 自动扫描注册; - 实现
toolDefinition()返回工具元数据; - 实现
supports()按工具名路由; - 实现
handle()编写业务逻辑,通过 callback 返回结果。
3.3 完整实战案例:自定义内部订单查询工具
需求:对接企业内部订单系统,让大模型可以根据订单号查询订单状态、物流信息,并用自然语言回复用户。
完整可运行代码:
package com.qidiangk.smart.aster.realtime.function.impl;
import com.qidiangk.smart.aster.realtime.function.FunctionCallHandler;
import com.qidiangk.smart.aster.realtime.function.FunctionCallRequest;
import com.qidiangk.smart.aster.realtime.function.FunctionCallResult;
import com.qidiangk.smart.aster.realtime.intercept.TurnContext;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 自定义内部订单查询工具
* 对接企业内部订单系统,支持按订单号查询详情
*/
@Slf4j
@Component
public class InnerOrderQueryHandler implements FunctionCallHandler {
private static final String TOOL_NAME = "inner_order_query";
@Override
public Map<String, Object> toolDefinition() {
Map<String, Object> tool = new LinkedHashMap<>();
tool.put("type", "function");
tool.put("name", TOOL_NAME);
tool.put("description", "查询企业内部订单的状态、物流、金额等详情,需要用户提供订单号");
// 参数定义
Map<String, Object> properties = new LinkedHashMap<>();
Map<String, Object> orderNo = new LinkedHashMap<>();
orderNo.put("type", "string");
orderNo.put("description", "要查询的订单编号,如ORD20261001001");
properties.put("orderNo", orderNo);
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("type", "object");
parameters.put("properties", properties);
parameters.put("required", List.of("orderNo"));
tool.put("parameters", parameters);
return tool;
}
@Override
public boolean supports(FunctionCallRequest request) {
// 按工具名精确路由
return TOOL_NAME.equals(request.getName());
}
@Override
public void handle(FunctionCallRequest request, TurnContext context, Consumer<FunctionCallResult> callback) {
// 必须切异步线程,不阻塞模型事件线程
ThreadUtil.execAsync(() -> {
try {
// 解析入参
JSONObject args = parseArgs(request.getArguments());
String orderNo = args.getString("orderNo");
log.info("[内部订单查询] 开始查询,订单号:{},通话ID:{}", orderNo, context.getChannelId());
// 调用内部业务接口(示例)
// 实际项目替换为真实内部服务调用:RPC、HTTP、数据库查询等
JSONObject orderInfo = queryInnerOrder(orderNo);
// 返回结果给大模型
callback.accept(FunctionCallResult.success(
request.getCallId(),
orderInfo.toJSONString()
));
} catch (Exception e) {
log.error("[内部订单查询] 执行失败,通话ID:{}", context.getChannelId(), e);
// 失败也必须回调,否则模型会一直等待
callback.accept(FunctionCallResult.fail(
request.getCallId(),
"订单查询失败,请稍后重试"
));
}
});
}
private JSONObject parseArgs(String arguments) {
if (arguments == null || arguments.isBlank()) {
return new JSONObject();
}
return JSONObject.parseObject(arguments);
}
/**
* 模拟内部订单查询接口
* 实际项目替换为真实业务调用
*/
private JSONObject queryInnerOrder(String orderNo) {
JSONObject result = new JSONObject();
result.put("orderNo", orderNo);
result.put("status", "运输中");
result.put("logistics", "顺丰速运,预计明天送达");
result.put("amount", "1299.00");
result.put("createTime", "2026-10-01 14:30:00");
return result;
}
}
验证方式:
- 启动服务,确认启动日志中该处理器已注册;
- 在智能体配置中,无需额外配置,工具自动对所有智能体可用;
- 拨打测试电话,让 AI 查询订单号,确认是否正确调用工具并返回结果。
四、扩展最佳实践
1. 线程约束
拦截器的钩子方法、工具的 handle 方法,默认运行在模型事件线程。 严禁在此线程执行耗时操作(数据库、HTTP、复杂计算),必须切换到业务线程池异步执行,否则会阻塞音频流,导致通话卡顿。
2. 回调幂等
FunctionCallHandler 的 callback 必须且仅能调用一次。 成功、失败都要回调,否则大模型会一直等待工具返回,导致对话卡死。
3. 异常隔离
自定义扩展内部的异常尽量捕获处理,不要抛到通话主线程,避免因扩展逻辑异常导致通话中断。 日志建议加上统一前缀,便于排查问题。
4. 状态隔离
不要在实现类中持有跨会话的可变状态。每次通话、每次工具调用都是独立的,状态数据存入 TurnContext 的属性中,随会话生命周期销毁。
5. 顺序控制
拦截器通过 order() 控制执行顺序:
- 数值越小越先执行;
- 通用前置校验建议设小值,后置业务处理建议设大值;
- 配置化技能与编码扩展混排,统一按 order 排序。
五、总结
SmartCall 的插件化扩展体系,真正做到了「配置解决通用问题,编码解决定制问题」:
- 普通业务:运营通过页面配置技能,分钟级上线;
- 复杂业务:开发通过标准接口扩展,零侵入核心代码;
- 两者共用同一套执行器、同一套上下文、同一套生命周期,体验一致、维护成本低。
对于需要深度对接内部系统、有专属业务规则、追求差异化能力的团队,这套扩展机制既能保留开源项目的自主可控,又能快速支撑业务定制,是企业级落地的重要技术保障。
资源地址
📘 官方技术文档:SmartCall — 大模型 AI 客服呼叫中心
⭐ Gitee 开源仓库:gitee.com/gdzWork/Sma…
🌐 官方网站:qidiangk.com