1. 为什么我要把 Codex Skills 接到 Home Assistant 上小智语音助手能听懂“把客厅灯打开”但这句话要真正落到物理开关上中间缺一个能调用 Home Assistant 的 Tool。Codex 的 Skills 机制正好补上这一环它让模型在对话中生成结构化参数再由我们写的 Python Tool 去执行 HA 的 REST API 调用。整套链路是语音输入 → 小智解析意图 → Codex Skill 生成 action/entity_id → Python Tool 调 HA → 物理设备动作。我试过直接在 HA 里写自动化规则一多就变成 if-else 地狱加一个新设备要改三处配置。换成 Codex Skills 之后新增设备只需要在 Tool 的实体映射表里加一行模型自己会根据 friendly_name 去匹配。这篇就把 config.toml、Python Tool 骨架、TaoToken 统一 Key 接入、以及从语音指令到设备动作的验证步骤完整走一遍。适合已经在跑 HA、想让小智语音助手控制真实家电的开发者Python 基础够用就行。核心检索词先摆出来Codex Skills 是 Codex 的可复用技能包机制Home Assistant Tool 是把 HA API 封装成模型可调用的函数小智语音助手负责语音入口Python 负责执行层。四者串起来就是一套可复制的声控物理家电方案。2. 前置准备TaoToken 统一 Key 与 HA 长寿命令牌在写 Tool 之前有两套凭证要准备好。一套是给 Codex 用的模型调用 Key一套是给 HA 用的访问令牌。分开管理互不污染。2.1 TaoToken 统一 Key 的获取与配置Codex Skills 在生成 Tool 调用参数时需要调用模型这里用 TaoToken 做统一入口一个 Key 覆盖对话和编码场景。操作路径是登录官网后进入控制台在 API Keys 页面创建一个新 Key复制保存。这个 Key 后面会写进 config.toml 的api_key字段。需要区分两个地址官网入口是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 基址是https://taotoken.net/api注意 API 地址不加 UTM 参数避免请求签名被多余 query 干扰。如果你后面要跑长期编码或 Agent 任务可以单独看 Coding Plan 页面按量或包月按自己调用频率选。注意API Key 只写进本地 config.toml不要提交到 Git。建议在.gitignore里加上config.toml和*.token。2.2 Home Assistant 长寿命访问令牌HA 这边不要用用户名密码调 API用长寿命令牌。登录 HA 网页 → 点左下角用户资料 → 拉到最底部“长寿命访问令牌” → 创建命名比如codex_skill复制生成的 JWT。这个令牌只在创建时显示一次丢了只能重建。HA 的 API 基址通常是http://你的HA地址:8123/api局域网内直接用 IP不要暴露到公网。如果 HA 跑在 Docker 里确认 8123 端口映射正确。2.3 Python 依赖Tool 执行层只需要requests如果要做实体模糊匹配再加difflib标准库自带。建虚拟环境后python -m venv venv source venv/bin/activate pip install requests3. 可复制配置config.toml 与 Python Tool 骨架这一节是全文核心配置和代码都能直接抄。先给 config.toml再给 Tool 骨架最后讲 Codex Skill 怎么声明这个 Tool。3.1 config.toml 完整配置[taotoken] api_base https://taotoken.net/api api_key sk-你的TaoTokenKey model claude-sonnet-4-20250514 timeout 30 [home_assistant] base_url http://192.168.1.100:8123 access_token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.你的HA令牌 timeout 10 [skill] name ha_control_device description 控制 Home Assistant 中的物理设备支持开关、调光、调温api_base用 TaoToken 的 API 地址model按你实际可用的填。HA 的base_url换成你自己的局域网地址。3.2 Python Tool 骨架Tool 分两层Client 负责 HTTP 通信Tool 负责把模型参数翻译成 HA 服务调用。先看 Clientimport requests import logging from typing import Dict, Optional logger logging.getLogger(__name__) class HomeAssistantClient: def __init__(self, base_url: str, access_token: str, timeout: int 10): self.base_url base_url.rstrip(/) self.api_base f{self.base_url}/api self.timeout timeout self.session requests.Session() self.session.headers.update({ Authorization: fBearer {access_token}, Content-Type: application/json, }) def _request(self, method: str, endpoint: str, data: Optional[Dict] None) - Dict: url f{self.api_base}{endpoint} try: if method GET: resp self.session.get(url, timeoutself.timeout) else: resp self.session.post(url, jsondata, timeoutself.timeout) resp.raise_for_status() if resp.status_code 204: return {} return resp.json() except requests.exceptions.HTTPError as e: code e.response.status_code if code 401: raise Exception(HA 认证失败检查 access_token) if code 404: raise Exception(f实体或服务不存在: {endpoint}) raise Exception(fHA API 错误 {code}: {e.response.text}) except requests.exceptions.ConnectionError: raise Exception(无法连接 HA检查 base_url 和网络) except requests.exceptions.Timeout: raise Exception(HA 请求超时) def get_state(self, entity_id: str) - Optional[Dict]: try: return self._request(GET, f/states/{entity_id}) except Exception as e: logger.warning(f获取 {entity_id} 状态失败: {e}) return None def call_service(self, domain: str, service: str, data: Dict) - Dict: return self._request(POST, f/services/{domain}/{service}, data) def list_entities(self, domain: Optional[str] None) - list: states self._request(GET, /states) ids [s[entity_id] for s in states] if domain: return [i for i in ids if i.startswith(domain .)] return ids再看 Tool 层重点是execute方法把 action 映射到 HA 服务import json from typing import Dict, Optional class HomeAssistantTool: def __init__(self, client: HomeAssistantClient): self.client client self.metadata { name: ha_control_device, description: 控制 Home Assistant 物理设备支持 turn_on/turn_off/toggle/set_temperature/get_status, parameters: { type: object, properties: { action: { type: string, enum: [turn_on, turn_off, toggle, set_temperature, get_status], }, entity_id: {type: string}, parameters: {type: object, default: {}}, }, required: [action, entity_id], }, } def execute(self, action: str, entity_id: str, parameters: Optional[Dict] None) - str: parameters parameters or {} if action get_status: state self.client.get_state(entity_id) if not state: return f未找到实体 {entity_id} return f{entity_id} 当前状态: {state.get(state)} parts entity_id.split(.) if len(parts) 2: return f实体 ID 格式错误: {entity_id} domain parts[0] service_data {entity_id: entity_id} service_data.update(parameters) try: self.client.call_service(domain, action, service_data) return f已对 {entity_id} 执行 {action} except Exception as e: return f执行失败: {e} def get_metadata(self) - Dict: return self.metadata3.3 Codex Skill 声明Codex Skills 里声明这个 Tool 时把metadata直接喂给模型模型就会在需要控制设备时生成对应的 JSON 参数。Skill 的入口函数负责读 config.toml、初始化 Client 和 Tool、调用execute。这样模型侧不需要知道 HA 的 API 细节只认 action 和 entity_id。4. 验证请求从语音指令到设备动作配置写完必须验证链路通不通。分三步先验 HA API再验 Tool 执行最后验语音到动作的完整闭环。4.1 直接验 HA API先用 curl 确认 HA 本身能通curl -X GET http://192.168.1.100:8123/api/states/light.living_room \ -H Authorization: Bearer 你的HA令牌 \ -H Content-Type: application/json返回 JSON 里有state和attributes.friendly_name就说明 HA 侧没问题。如果 401回去检查令牌如果连接拒绝检查 HA 是否在跑、端口是否对。4.2 验 Tool 执行写个临时脚本调 Toolfrom ha_client import HomeAssistantClient from ha_tool import HomeAssistantTool client HomeAssistantClient(http://192.168.1.100:8123, 你的HA令牌) tool HomeAssistantTool(client) print(tool.execute(turn_on, light.living_room, {brightness: 200})) print(tool.execute(get_status, light.living_room))预期输出第一行返回“已对 light.living_room 执行 turn_on”第二行返回当前状态on。如果返回“执行失败”看错误信息里是 404 还是超时分别对应实体 ID 错和网络问题。4.3 验语音闭环小智语音助手侧说“打开客厅灯”。小智把这句话交给 Codex Skill模型生成{ action: turn_on, entity_id: light.living_room, parameters: {brightness: 200} }Skill 执行后返回结果小智播报“好的客厅灯已打开”。物理灯亮起链路就通了。如果模型生成的 entity_id 不对说明 friendly_name 映射没做好回到 4.4 的模糊匹配。4.4 实体模糊匹配用户不会说light.living_room会说“客厅灯”。加一层匹配from difflib import get_close_matches def find_entity(self, keyword: str) - Optional[str]: entities self.client.list_entities() names {} for eid in entities: state self.client.get_state(eid) if state: fn state.get(attributes, {}).get(friendly_name, ) names[fn] eid matches get_close_matches(keyword, names.keys(), n1, cutoff0.6) return names[matches[0]] if matches else None把用户说的“客厅灯”传进去返回light.living_room。这一步放在 Skill 入口模型生成的 entity_id 如果是中文名先过一遍匹配再执行。5. 本篇常见错排查链路跑不通九成是下面几个问题。按现象对号入座。现象可能原因解决401 UnauthorizedHA 令牌错或过期重新生成长寿命令牌更新 config.toml404 Not Foundentity_id 写错或服务名不对用list_entities确认实体 ID服务名对照 HA 文档Connection RefusedHA 没跑或端口没映射检查 HA 服务状态确认 8123 端口可达请求超时网络延迟或 HA 负载高调大 timeout检查 HA 日志模型生成的 entity_id 是中文没做模糊匹配在 Skill 入口加 find_entity 转换TaoToken 调用 401api_key 写错或 api_base 带了 UTM确认 api_base 是https://taotoken.net/api不带 query设备状态不同步HA 缓存未刷新调用后重新 get_state或等 HA 推送事件还有一个容易踩的坑config.toml 里api_base如果误写成带 UTM 的官网地址请求会打到错误路径。记住 API 基址就是https://taotoken.net/api干净路径。6. 继续往下走把 Tool 接进你的工作流到这一步Codex Skill 已经能通过 TaoToken 调模型、通过 Python Tool 调 HA、通过小智语音助手接收指令。如果你只是想让家里的灯和空调听语音这套配置够用了。如果你后面要跑更长的编码任务或 Agent 循环比如让 Codex 连续生成多个 Tool、自动测试、自动修错建议单独配一个 Coding Plan把模型调用和 Tool 执行分开管理避免 Key 混用导致额度不好追踪。接入文档里有完整的参数说明和示例遇到 Tool 注册或参数映射的问题可以直接对照。验证模型是否按预期生成 action 和 entity_id可以在模型对话页面手动发一条“打开客厅灯”看返回的 JSON 结构对不对。这一步能快速定位是模型侧参数生成的问题还是 Python 执行侧的问题。排障时先分层再定位比一股脑改代码快得多。