- 开发工具
- 物联网
- 消息队列
【免费下载链接】MQTT-Explorer
An all-round MQTT client that provides a structured topic overview
LLM_TESTS_DEBUG.md 记录了 MQTT-Explorer 在接入 LLM(AI Assistant)集成测试时遇到的一个典型 CI 故障——GitHub Actions 工作流中.env.llm-tests环境文件因「先写文件、后 Checkout」的步骤顺序错误而被工作区覆盖丢失,以及后续的环境变量注入、测试开关与 jsdom 网络限制的排查与修复过程。读完本文,你将掌握该仓库 LLM 集成测试的完整调试思路、可复用的修复模式,以及本地开发与 CI/CD 中运行「离线测试 + 在线 LLM 实测」两套测试体系的实操方法。
一、问题背景:LLM 集成测试的运行前提
MQTT-Explorer 的 AI Assistant(LLM 集成)在 LLM_INTEGRATION.md 中已有完整设计:前端通过 WebSocket RPC 与后端通信,后端持有 API Key,代理所有 LLM 请求。为了保证这个功能的质量,仓库在 app/src/services/spec/ 目录下维护了三类测试:
- 单元测试(
llmService.spec.ts):覆盖parseResponse()、getQuickSuggestions()、hasApiKey()等 LLM Service 方法; - 提案校验测试(
llmProposals.spec.ts):校验 Topic 格式、Payload 合法性、QoS 取值、Description 质量; - 实时 LLM 集成测试(
llmIntegration.spec.ts):真实调用 OpenAI/Gemini API,验证系统识别(zigbee2mqtt、Home Assistant、Tasmota)、提案质量、边界情况与问题生成质量。
其中第三类测试是**选择性(opt-in)**的,需要同时满足两个条件才会真正执行(见 app/src/services/spec/llmIntegration.spec.ts):
const shouldRunLLMTests = process.env.RUN_LLM_TESTS === 'true' const hasApiKey = !!process.env.OPENAI_API_KEY || !!process.env.GEMINI_API_KEY || !!process.env.LLM_API_KEY也就是说:必须有RUN_LLM_TESTS=true环境变量,且至少注入一个 API Key。这看似简单,却正是本次调试的核心战场——如何把 API Key 安全、可靠地注入到 CI 环境并让测试真正跑起来。
二、核心问题:GitHub Workflow 步骤顺序缺陷
文档明确指出,.github/workflows/copilot-setup-steps.yml存在一个致命步骤顺序问题。修复前的执行流程是:
- 创建
.env.llm-tests文件(写入 API Key 与测试开关) - Checkout 代码 ←这一步会用仓库内容覆盖工作目录,导致刚创建的
.env.llm-tests被整体清除 - 运行测试
GitHub Actions 的actions/checkout会重置工作目录,任何在 checkout 之前写入的未跟踪文件都会丢失。于是测试运行时环境文件已不存在,RUN_LLM_TESTS与 API Key 自然全部缺失,LLM 集成测试被静默跳过,而 CI 日志往往只会显示一个含糊的 "skipped",排查难度极高。
修复后的正确顺序是:
- 先 Checkout 代码
- 再创建
.env.llm-tests(此时文件才能在工作区中持久存在) - 运行测试
从当前仓库 .github/workflows/copilot-setup-steps.yml 可以看到修复后的实际形态——Checkout code步骤位于最前,紧随其后的Persist Secrets to Agent Environment步骤负责写入环境文件:
steps: - name: Checkout code uses: actions/checkout@v6 - name: Persist Secrets to Agent Environment run: | echo "export OPENAI_API_KEY=${{ secrets.OPENAI_API_KEY }}" > .env.llm-tests echo "export RUN_LLM_TESTS=true" >> .env.llm-tests chmod 600 .env.llm-tests echo "✅ Created .env.llm-tests file" ls -la .env.llm-tests修复中引入的四项关键改动
文档总结的改动点逐一对应到上述 YAML,每一项都有其明确目的:
| 改动 | 目的 | 实现位置 |
|---|---|---|
| 将「Persist Secrets」步骤移到「Checkout code」之后 | 避免 checkout 覆盖工作区、丢失环境文件 | .github/workflows/copilot-setup-steps.yml |
为环境变量添加export前缀 | 使文件可通过source .env.llm-tests正确注入当前 shell,而不是以普通赋值形式存在 | 同上echo "export OPENAI_API_KEY=..." > .env.llm-tests |
追加RUN_LLM_TESTS=true | 显式启用 live 测试,避免「有 Key 但测试仍跳过」的隐性失效 | 同上echo "export RUN_LLM_TESTS=true" >> .env.llm-tests |
chmod 600 | 将文件权限收紧为仅属主可读写,防止敏感 Key 被同环境其他用户读取 | 同上chmod 600 .env.llm-tests |
验证日志(ls -la+ echo) | 确认文件确实创建成功,为后续排障提供证据 | 同上 |
值得注意的是,工作流环境块中还配置了TESTS_MQTT_BROKER_HOST: localhost、TESTS_MQTT_BROKER_PORT: 1883与OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }},其中 secrets 通过环境变量传入,再被第二步固化到.env.llm-tests中,形成「GitHub Secrets → 环境变量 → 文件 → shell source」的注入链。
三、环境变量注入机制验证:从手动创建到一键脚本
文档用一段最小化 bash 验证了.env.llm-tests的创建与 source 机制:
# 创建 .env 文件 echo 'export OPENAI_API_KEY=sk-your-key' > .env.llm-tests echo 'export RUN_LLM_TESTS=true' >> .env.llm-tests # Source 并验证 source .env.llm-tests echo $OPENAI_API_KEY # 输出 Key,说明注入成功仓库在此基础上提供了两个一键脚本,把上述流程产品化:
1. 环境搭建脚本 scripts/setup-llm-env.sh
该脚本的核心逻辑是读取当前环境中的注入 secrets,按优先级写入$REPO_ROOT/.env.llm-tests:
- 检测到
OPENAI_API_KEY→ 写入export OPENAI_API_KEY='...'+export RUN_LLM_TESTS=true; - 检测到
GEMINI_API_KEY→ 写入对应的 Gemini 配置 + 测试开关; - 检测到通用
LLM_API_KEY→ 额外写入export LLM_PROVIDER='${LLM_PROVIDER:-openai}'(generic key 需要显式指定 provider); - 三种 Key 都不存在 → 打印手工创建指引并以退出码 1 结束,避免静默失败。
写入完成后同样执行chmod 600 "$ENV_FILE",并在末尾提示:永远不要把这个文件提交到版本控制(已加入.gitignore)。
2. 测试运行脚本 scripts/run-llm-tests.sh
该脚本负责完整的前置校验与执行:
set -e:任何一步失败立即中断,防止"半失败"状态被误判为成功;- 校验
OPENAI_API_KEY/GEMINI_API_KEY/LLM_API_KEY至少存在其一,否则打印三种 Key 的用法并exit 1; - 提供方自动识别:检测到 OpenAI Key 时默认
LLM_PROVIDER=openai,检测到 Gemini Key 时默认LLM_PROVIDER=gemini,通用 Key 默认走openai; - 强制
export RUN_LLM_TESTS=true; - 进入
app/目录执行yarn test。
这也解释了文档中「Provider auto-detection (OpenAI/Gemini)」这一已验证项的来源——两个脚本与测试入口 app/src/services/spec/llmIntegration.spec.ts 中的getProvider()逻辑相互印证:
const getProvider = (): 'openai' | 'gemini' | null => { if (process.env.OPENAI_API_KEY) return 'openai' if (process.env.GEMINI_API_KEY) return 'gemini' if (process.env.LLM_API_KEY && process.env.LLM_PROVIDER) { return process.env.LLM_PROVIDER as 'openai' | 'gemini' } return null }三个层级(shell 脚本、测试文件、服务层)对提供方识别逻辑保持了一致,这也是「Provider auto-detection 已验证」能被端到端确认的原因。
四、测试检测与跳过行为的源码级验证
文档描述的「测试能正确检测 API Key、启用 live 执行(不跳过)、输出 provider 识别日志」可以直接在测试代码中找到对应实现(app/src/services/spec/llmIntegration.spec.ts):
before(function () { if (!shouldRunLLMTests) { console.log('Skipping LLM integration tests: RUN_LLM_TESTS not set to "true"') this.skip() } if (!hasApiKey) { console.warn('Skipping LLM integration tests: No API key found') this.skip() } if (!provider) { console.warn('Skipping LLM integration tests: Could not determine provider') this.skip() } console.log(`Running LLM integration tests with provider: ${provider}`) })可见跳过逻辑是分层守卫的:RUN_LLM_TESTS未设置、无 API Key、无法识别 provider,三种情况任一成立都会this.skip(),并打印明确的原因日志。这解释了调试时的核心排查原则——先看测试是否在跑,再看跑的是离线还是在线。若 CI 日志中出现 skip 提示,第一反应应是检查RUN_LLM_TESTS与 Key 注入是否真正落到了执行测试的那个 shell 环境。
在线测试的判定标准与耗时预期可见 docs/LLM_TEST_RESULTS.md:离线测试 100 个用例全部通过、约 2 秒完成;在线 LLM 集成测试 11 个用例、约 20-30 秒完成,单用例耗时普遍在 1.5-2.3 秒区间(真实 API 往返),因此测试套件把this.timeout(60000)放宽到 60 秒以容纳 API 延迟。
五、当前限制:jsdom 环境中的网络错误
文档记录了当前已知限制——在 jsdom 测试环境中调用真实 LLM API 会失败:
Error: Cross origin null forbidden Error: LLM API call failed: Network Error原因分析是明确的:
- 测试运行在 jsdom 模拟的 DOM 环境,而非真实浏览器;
- 测试代码通过
axios直接发起 HTTP 请求(见 app/src/services/spec/llmIntegration.spec.ts),jsdom 对跨域请求施加 CORS 限制,导致Cross origin null forbidden; - 因此实时 API 测试需要真正的 Node.js 环境或网络请求 mock。
这一点值得特别注意:生产架构中 LLM 请求是通过后端 WebSocket RPC 代理的(见 app/src/services/llmService.ts 中backendRpc.call(RpcEvents.llmChat, ...)),而测试为了独立于后端、直接验证 LLM 输出质量,选择了直连 API 的方式,所以在 jsdom 下必然受限。这是"测试直连"与"生产代理"两种路径的架构差异所致,不是缺陷而是设计取舍。
六、推荐的运行方案
本地开发:在 Node 环境中运行
文档给出的本地运行方式与仓库脚本一致:
source .env.llm-tests cd app && yarn test更推荐直接使用一键脚本(脚本内部已处理 provider 识别与测试开关):
OPENAI_API_KEY=sk-your-key ./scripts/run-llm-tests.sh手动方式的完整等价命令(与 app/src/services/spec/README.md 中的说明一致):
export OPENAI_API_KEY=sk-your-key export RUN_LLM_TESTS=true cd app && yarn test执行后应能在控制台看到 provider 识别日志Running LLM integration tests with provider: openai,随后是 zigbee2mqtt 系统识别、提案质量校验、边界情况、问题生成四组在线用例的逐个执行与通过结果。
CI/CD 中的分层策略
文档建议 CI 采用分层策略,app/src/services/spec/README.md 给出了对应的 GitHub Actions 示例:
- 常规 CI(默认):只跑离线测试,无需 API Key,快速、确定性、零成本;
- 定时任务(如 nightly):通过
secrets.OPENAI_API_KEY注入 Key 并设置RUN_LLM_TESTS: true,运行在线实测; - 网络与 mock 兜底:若必须在 jsdom 环境跑在线逻辑,考虑用 nock 或 msw mock HTTP 请求,或在有真实网络访问权限的 Job 中执行。
关键运维纪律(同样适用于本仓库任何含 Key 的流程):
- API Key 一律走 secrets 管理,严禁硬编码或提交;
.env.llm-tests不纳入版本控制(.gitignore已排除);- 定期轮换 Key 并在供应商控制台监控用量与设置计费告警(可参考 ENV_VARS_EXAMPLE.md 的 Security Recommendations)。
七、已验证工作项与调试结论
文档末尾的 Verified Working 清单,结合本次源码核对可以给出如下对应关系:
| 已验证项 | 对应实现/证据 |
|---|---|
✅ 工作流创建.env.llm-tests(步骤顺序已修复) | .github/workflows/copilot-setup-steps.yml |
✅setup-llm-env.sh创建环境文件 | scripts/setup-llm-env.sh |
| ✅ 环境变量 source 机制 | export前缀 +source .env.llm-tests |
| ✅ 测试检测 API Key | app/src/services/spec/llmIntegration.spec.ts |
| ✅ Provider 自动识别(OpenAI/Gemini) | app/src/services/spec/llmIntegration.spec.ts 与 scripts/run-llm-tests.sh |
| ✅ 无 API Key 时的正确跳过行为 | app/src/services/spec/llmIntegration.spec.ts 三层守卫 |
最终结论:LLM 测试基础设施现已正常工作。核心修复只需把「Persist Secrets」步骤移到「Checkout code」之后,配合export前缀、RUN_LLM_TESTS=true开关、chmod 600权限收紧与创建验证日志,即可让 API Key 稳定穿越 checkout 阶段并到达测试进程。而 jsdom 网络限制属于环境能力边界,不是基础设施故障——线上实测应放在 Node 环境或具备真实网络访问的 CI Job 中执行。
八、调试思路沉淀:三个可复用原则
- 步骤顺序先于脚本内容:GitHub Actions 中 checkout、缓存恢复、secrets 写入的先后顺序,决定了文件能否存活到后续步骤。凡是在 checkout 之前写工作区文件的做法,都应视为反模式。
- 跳过不是通过:测试静默 skip 时 CI 依然是绿色,但功能质量无人把关。
RUN_LLM_TESTS=true显式开关 + skip 原因日志,是让「该测没测」变得可见的关键设计。 - 注入链每一环都要有证据:从 Secrets 环境变量到
.env文件再到 shell source,每一环都应像工作流中的ls -la那样留有验证输出,排障时才能快速定位断点。
如需进一步了解 LLM 功能的完整配置(LLM_PROVIDER、OPENAI_API_KEY、GEMINI_API_KEY、LLM_API_KEY、LLM_NEIGHBORING_TOPICS_TOKEN_LIMIT及其优先级、Token 上下文截断对提案质量的影响),可继续阅读 LLM_INTEGRATION.md 与 ENV_VARS_EXAMPLE.md;在线测试的完整示例输出与验收标准见 docs/LLM_TEST_RESULTS.md。
- 开发工具
- 物联网
- 消息队列
【免费下载链接】MQTT-Explorer
An all-round MQTT client that provides a structured topic overview
相关推荐
Tsuru环境变量注入工具集成:CI/CD流程的终极指南
Tsuru环境变量注入工具集成:CI/CD流程的终极指南 在现代软件开发中,持续集成和持续部署(CI/CD)已成为不可或缺的环节。Tsuru作为开源的可扩展平台
后端云原生容器编排DevOpsWoodpecker CI 环境变量完全指南:步骤级注入、内建 CI 变量与字符串替换
Woodpecker CI 环境变量完全指南:步骤级注入、内建 CI 变量与字符串替换 本篇技术指南以 Woodpecker(一个简单而强大的 CI/CD 引擎
CI/CDDevOpsAkka远程通信:构建分布式系统的通信机制和网络配置
Akka远程通信:构建分布式系统的通信机制和网络配置 Akka远程通信是构建分布式系统的核心组件,它允许不同JVM中的Actor通过网络进行高效通信。本文将详细
示例工程后端软件架构
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考