智能家居助手演示
一个智能家居助手要把“把全屋的灯都关掉”“帮我开一下门,快递到了”“我要睡觉了”这样的话变成设备动作。本演示用象信一号完成这一步:每条指令只发一次请求,请求里带着 20 个问题,程序可能用到的都在里面。答案回来后,由代码决定用哪几个。
在 44 条示例指令上,这样做类别、设备、房间、动作、是否需确认等字段全部判对的有 43 条。与“先分类、再追问”的顺序调用相比,端到端耗时的中位数是后者的约 1/2.6(测量时服务负载较高,见下文)。
看看效果
页面左边输入指令或点示例,右边的设备面板会随答案更新;下方表格列出全部 20 个问题的答案,其中被代码用到的标 ●,推测提问但本次没用上的标 ○。
页面本身不含任何 API 密钥。在线模式把请求发往同源代理 /demo-api/smart-home;代理不可用时自动切换为离线回放,回放的是我们在真实运行中录下的 44 条象信响应(录制于 2026-09-25,模型 xiangxin-1.0.0),只覆盖页面上的示例指令。
工作原理
推测式扇出
核心模式是推测式扇出:每条指令都拿一长串问题去问,其中很多对这条指令来说根本用不上。
以“把全屋的灯都关掉”为例。这是一条很简单的指令,代码最终只需要四个答案:
| 问题 | 答案 | 置信度 |
|---|---|---|
category:这句话属于哪一类请求? | 设备控制 | 0.79 |
room:针对哪个房间? | 全屋 | 0.93 |
device:针对哪种设备? | 灯 | 0.93 |
light_action:如果这是对灯的指令,要执行什么动作? | 关闭 | 0.99 |
注意最后一个问题的写法:它假设指令是对灯说的,而我们在知道用户说的是灯之前就问了它。这就是“推测式问题”:先问了再说,用不上就扔。同一个请求里还有“如果这是对空调的指令,要执行什么动作?”,它在这条指令上也给出了答案(关闭,0.81),“如果这是场景类请求,对应哪个场景?”也给出了离家模式(0.41)。这些答案都不会被用到,因为代码先看了 category 和 device。推测式问题总会有答案,是否采用由代码决定。
全部 20 个问题:
| 类型 | 问题 |
|---|---|
| Choice | category(设备控制 / 场景模式 / 状态查询 / 闲聊或常识 / 其他)、room(7 个房间 + 未指明)、device(9 种设备 + 无)、scene(5 个场景 + 无) |
| Choice | 每种设备一道动作题:light_action、ac_action、curtain_action、tv_action、lock_action、oven_action、heater_action、sweeper_action、speaker_action |
| Choice | 参数:ac_temp(16–30 度 + 未指定)、heater_temp(35–75 度 + 未指定) |
| Score | 参数:light_level(最暗到最亮 5 档)、curtain_open(全关到全开 5 档)、volume(静音到最大声 5 档) |
| Noul | is_compound(是否包含多个独立操作)、needs_confirmation(是否高风险操作) |
问题定义节选(完整定义见 main.py 的 build_questions()):
from xiangxin import Choice, Noul, Score, XiangxinClient
questions = {
"category": Choice(
instructions="这句话属于哪一类请求?",
criteria={
"设备控制": "让某个设备做某件事",
"场景模式": "一句话触发一组设备,如睡觉、出门、回家、看电影",
"状态查询": "询问某个设备当前的状态,不要求改变它",
"闲聊或常识": "聊天、问天气、问常识,与家里设备无关",
"其他": "超出智能家居能力范围的事务,如订票、购物",
},
),
"device": Choice(
instructions="这条指令针对哪种设备?",
criteria={d: None for d in ["灯", "空调", "窗帘", "电视", "门锁", "烤箱", "热水器", "扫地机器人", "音箱"]}
| {"无": "没有涉及具体设备"},
),
# 推测式:每种设备一道,不管指令是不是对它说的
"light_action": Choice(
instructions="如果这是对灯的指令,要执行什么动作?",
criteria={"打开": "开灯", "关闭": "关灯", "调亮": "比现在亮一些", "调暗": "比现在暗一些",
"设定亮度": "调到某个明确的亮度,如最亮、夜灯、一半"},
),
"light_level": Score(
instructions="如果指令给出了灯的目标亮度,是哪一档?",
criteria=["最暗(夜灯)", "较暗", "一半亮度", "较亮", "最亮"],
),
"is_compound": Noul(
instructions="这句话是否包含两个或更多彼此独立的操作?",
criteria={"true": "要做两件及以上不同的事,如关灯并开空调",
"false": "只有一个操作(可以作用于多个房间的同一设备)"},
),
# ……其余 15 个问题
}
resp = XiangxinClient().system_one(state="把全屋的灯都关掉", questions=questions)代码按类别和设备取答案(plan(),Python 与页面里的 JavaScript 逻辑相同):
def plan(a):
if a["is_compound"]["noul"] > 0.5:
return 交给大模型拆分()
cat = a["category"]["choice"]
if cat == "闲聊或常识":
return 交给大模型回答()
if cat == "场景模式":
return 启动场景(a["scene"]["choice"])
device = a["device"]["choice"]
action = a[DEVICES[device]]["choice"] # 只看这个设备的动作题
if device == "灯" and action == "设定亮度":
level = round(a["light_level"]["score"]) # 只在需要时看参数题
confirm = a["needs_confirmation"]["noul"] > 0.5
...错误做法:顺序调用
另一种写法是把问题拆成几次请求,确定需要时才问下一步:
- 先问
category(以及is_compound):是设备控制; - 确定是设备控制后,再问
room和device:全屋、灯; - 确定是灯之后,再问
light_action和参数题:关闭。
这样问的问题最少,但要等三轮网络往返。我们对同一批 44 条指令两种做法都跑了一遍,每条指令先扇出、紧接着顺序调用,让两者面对相近的服务负载:
| 做法 | 请求数 | 服务端总耗时中位数 | 输入 token 中位数 |
|---|---|---|---|
| 推测式扇出(1 次请求,20 个问题) | 44 | 5,344 ms | 1,652 |
| 顺序调用(1–3 次请求) | 111 | 15,481 ms | 1,019.5 |
逐条配对看,顺序调用的耗时中位数是扇出的 2.58 倍;需要走满三轮的 31 条指令上是 2.63 倍。44 条里有 7 条顺序调用更快,其中 6 条是闲聊或复合指令,第一轮之后就结束了,只问了 2 个问题。
关于这组耗时
测量时(2026-09-25)同一服务上有大批评测任务在并发运行,单次请求的耗时远高于空闲时:同一批请求里最快的一次扇出只用了 746 ms。绝对数值请以你自己的测量为准;两种做法交替测得,相对比较是公平的。
扇出多花了约 60% 的输入 token(每个问题的选项都要读一遍),按 ¥0.042 / 百万输入 token 计,每条指令约 ¥0.00007。换一轮网络往返,这个价钱很划算。
让模型判断事实,让代码决定策略
needs_confirmation 决定是否在执行前弹出确认,开门和启动烤箱必须确认,关灯不需要。我们第一版这样问:
执行这条指令前,是否应当先向用户确认?
结果不理想:“帮我开一下门,快递到了”只得到 0.43,“烤箱预热到200度”0.35,而本不需要确认的“把门锁上”反而有 0.47。30 条设备控制指令中判错 2 条,而且找不到能把两类分开的阈值(数据见 results/v1/)。
“是否应当确认”是一个策略问题,答案取决于产品设计,模型只能猜。第二版改问一个事实,把“哪些操作算高风险”直接写进问题和 criteria:
"needs_confirmation": Noul(
instructions="这条指令是否要求执行高风险操作:打开门锁(开门、解锁),或启动烤箱等加热设备(开启、预热)?",
criteria={
"true": "要求开门/解锁,或要求开启、预热烤箱",
"false": "其他操作,包括上锁、关闭设备、查询状态、开关灯、调空调、放音乐",
},
),开门升到 0.75,预热烤箱升到 0.89;其余 28 条中最高的是 0.27。30 条全部判对,0.5 的阈值两边都留有余量。本页其余数字和离线回放用的都是第二版。
象信与大模型配合
象信一号不生成文本(见已知短板),所以本演示在两处需要生成式大模型:
- 拆分复合指令。
is_compound判定一句话里有多个独立操作时(“关掉客厅灯,再把空调调到24度”),交给大模型拆成单条指令,再逐条交给象信。3 条复合指令的is_compound都判对了,44 条上这道题没有错误。 - 闲聊兜底。 类别为闲聊或常识时(“给我讲个笑话”),交给大模型自由回答。
绝大多数指令是简单的设备控制,由象信一次请求处理完;只有少数请求需要付大模型的时间和钱。
大模型部分尚未接入
本演示暂未接入大模型:命令行版本检测到 LLM_BASE_URL、LLM_API_KEY、LLM_MODEL 环境变量时才会调用,页面上遇到复合指令和闲聊会显示“需要大模型(演示未接入)”。上面的结果都只涉及象信的判断,不包括大模型的输出。
在 44 条指令上的结果
示例指令为本文构造(data/requests.jsonl),覆盖 9 种设备、4 个场景、状态查询、复合指令、闲聊和超出范围的请求,每条都标注了期望答案。没有指明房间的指令(如“打开电视”),期望答案同时接受“未指明”和设备所在的房间。
| 字段 | 正确 / 总数 |
|---|---|
请求类别 category | 40 / 41 |
复合指令 is_compound | 44 / 44 |
场景 scene | 4 / 4 |
设备 device | 31 / 32 |
房间 room | 32 / 32 |
| 设备动作 | 31 / 31 |
需确认 needs_confirmation(第二版) | 30 / 30 |
| 参数(温度、水温、亮度档、开合档) | 5 / 5 |
| 所有字段都正确的指令 | 43 / 44 |
唯一出错的是“有点暗,看不清”:没有提到任何设备,模型把它归为状态查询(置信度 0.43),设备选了“无”(0.41)。代码因此回复“没听出是哪个设备,请再说一遍”。按标注它应当是“把灯调亮”,但对这样一句话追问一次也说得过去。低置信度本身就是信号:页面在房间、设备或动作的置信度低于 0.5 时会提示“真实产品里应追问”。
44 条指令、44 次请求,只是一个演示规模的样本,不足以代表真实流量。上线前请用自己的指令日志评测。
自己运行
完整代码:github.com/xiangxinai/xiangxin-cookbooks/tree/main/demos/smart-home(仓库稍后建立)。
pip install xiangxin-sdk openai
export XIANGXIN_API_KEY="sk-xx-..."
python main.py "把客厅的灯调暗一点" # 解析一条指令
python main.py --eval # 跑 44 条示例,写 results/
python analyze.py # 由 results/ 算出本页的统计
python build_page.py # 把录制的响应嵌入静态页面一次真实运行的输出(results/cli_example.txt;运行时服务负载很高,耗时偏大):
指令:把客厅的灯调暗一点
象信:20 个问题,一次请求,模型 12267.0 ms / 总计 13102.0 ms,输入 1670 tokens
计划:客厅·灯·调暗
用到的答案:category, is_compound, device, room, light_action, needs_confirmation目录结构:
| 文件 | 作用 |
|---|---|
main.py | 问题定义 build_questions()、取答案逻辑 plan()、评测 --eval |
data/requests.jsonl | 44 条示例指令与期望答案 |
results/ | 真实运行结果:recorded.json(每条指令的完整响应)、summary.json、graded.json、analysis.json;v1/ 为第一版确认问题的结果 |
page_template.html、build_page.py | 生成 docs/public/demos/smart-home/index.html |
给页面接上在线模式
页面向同源的 /demo-api/smart-home 发送 POST,请求体为 {"request": 指令, "state": 指令, "questions": {…20 个问题…}, "model": "xiangxin-latest"},期望收到与 /v1/systemone 相同格式的响应体,并读取 x-xiangxin-model-ms、x-xiangxin-total-ms 响应头显示耗时。代理在服务端附上 API 密钥后转发即可。为避免代理被当成任意请求的中转,代理应当忽略请求里的 questions,改用服务端保存的同一份问题定义(results/questions.json),并按 IP 限流。

