“跑一个工况测试,把结果发我群里。”——这句话从提需求到真正看到报告,以前至少要折腾三个人:测试工程师手动搭环境、执行脚本、截图留证、整理Excel,然后等开发有空了再去群里口头同步。如果我们把这些动作全部交给一个“听得懂人话”的机器人,让它在飞书群里自己接下指令、自己调度测试设备、自己跑完车载HMI全流程用例,再把结果整理成表格卡片推回来,这件事就成了。本篇文章要讲的,就是我自己落地这套“飞书机器人 × AI Agent”组合的车载HMI自动化测试平台全过程,从架构选型、系统分层到每个核心模块的实现细节,以及那些不跑一遍根本意识不到的坑。
这套平台的核心思路一句话就能说清:把飞书当成人机交互入口,把 AI Agent 当成大脑,把车载HMI自动化测试框架当成手脚。用户不需要学命令行、不需要打开测试平台网页、甚至不需要记住测试用例编号,直接在企业IM里用自然语言下指令,机器人就会自动拆解任务、调用对应的测试工具链、实时播报进度、最终生成结构化报告。整套体系我自己跑了几个月,现在团队里测试同学、开发同学、项目经理都在用,日均执行任务量比之前手动操作翻了四倍左右。下面直接进入正题,说一下整体设计和落地细节。
1. 系统定位与整体设计思路
1.1 传统车载HMI测试流程的痛点
车载HMI(Human Machine Interface)测试和普通App测试差距非常大。它不光是点按钮、看页面,还要联动CAN总线信号模拟、ECU状态切换、仪表盘UI刷新、语音交互、触控响应等一系列硬件在环操作。
最典型的一个场景:测试“车辆速度变化时仪表盘数字刷新是否正常”,这个用例需要先通过CAN工具发送车速信号,比如0 → 30km/h → 60km/h → 0,同时在每个节点截图验证仪表盘指针和数字显示。手动执行的话,一个人一天最多跑几十条这样的用例,而且极度枯燥——盯着仪表盘截图、比对像素、记录日志,一个不留神就看漏了。
传统流程还有两个附加问题。其一,环境准备非常重:CANoe或者PCAN的硬件连接、电源序列上电、HMI模拟器启动、诊断仪的会话建立,每一步都要人工确认。其二,结果反馈链路长:测试执行完还要整理截图、录屏、日志和结论,写出一份让人看得懂的报告,再通过IM或者邮件发出去。等到开发真正看到问题的时候,可能已经过去大半天了。这些痛点叠加在一起,让我意识到测试平台必须“主动服务”而不是“被动等待”——于是有了让机器人自己在群里跑测试的想法。
1.2 为什么是飞书机器人 + AI Agent 的组合
我最早考虑的其实不是AI Agent,而是传统关键词匹配机器人:用户输入命令字,比如“跑TC001”,机器人解析后调测试框架API。这个方案简单,但有个硬伤——团队成员根本记不住用例编号和参数格式。他们真正想要的是说一句“帮我把空调温度从22度调到26度,看看触控响应有没有卡顿”,然后机器就能自己去找对应用例、组合参数、执行并给结论。
后来决定引入大模型来做意图理解,这正好点到 AI Agent 的核心能力:让模型不仅会“聊天”,还会“做事”。一个Agent的基本结构是:大模型负责理解意图、规划步骤、决定调用哪个工具,工具层负责真实执行,执行结果再反馈给模型做汇总和汇报。这个链路放在自动化测试平台上是天然的——测试平台就是工具,飞书就是交互界面。
选飞书主要是基于实际环境:公司内部IM就是飞书,所有人在一个IM里沟通,不需要额外安装客户端。而且飞书开放平台提供了完整的机器人能力和消息卡片能力,支持长连接事件订阅,回调不需要暴露公网端口,这对内网部署非常友好。更关键的是飞书消息卡片支持表格、按钮交互、分栏展示,测试报告可以直接结构化呈现,不需要跳转网页。
1.3 设计目标与边界切分
这个平台我在第一版就明确了几条边界,避免变成“什么都做”的大杂烩:
- 只负责HMI相关测试任务的解析、调度、执行和结果回传,不承担测试用例的编辑器功能;
- 不强行全自动化环境搭建,电源上电、CAN通道初始状态校验这类高危动作保留人工确认位;
- 不做大而全的测试管理平台,原有Jira、禅道等缺陷系统继续存在,机器人只做“执行与汇报”环节;
- 系统单机可部署,不引入K8s等重调度组件,保证一台服务器能跑起来。
边界切好以后,整个架构做起来就很顺了。接下来拆解一下核心架构。
2. 核心架构解析
2.1 系统分层架构
整个平台分为七层,每一层职责尽量单一,避免互相耦合。实际部署时全部跑在一台物理服务器上,通过Docker Compose管理容器,整体拓扑如下:
- 接入层:负责与飞书服务器建立长连接,接收消息事件、回调事件,负责发送消息卡片与文件消息。
- Agent层:核心大脑,基于大型语言模型做意图识别、任务拆解、工具选择,并通过函数调用(Function Calling)机制触发后续动作。
- 调度层:负责任务排队、并发控制、设备锁管理、链路追踪。调度层是Agent和测试执行之间的“防抖”模块,避免大模型直接操作硬件。
- 工具层:把测试平台能力封装成一个个可被Agent调用的工具,比如“查询可用设备”“执行指定用例”“读取当前执行状态”“获取测试报告数据”。
- 执行层:车载HMI自动化测试框架,负责真实控制CAN卡、电源、HMI模拟器等硬件执行用例。
- 数据层:存储每次执行的记录、日志、截图、结构化结果,并提供检索API。
- 展示层:基于飞书消息卡片动态生成测试摘要、表格、按钮操作,使用户能直接在IM内完成查看和操作。
一句话概括:Agent 不直接碰硬件,调度层也不直接回消息,所有交互都通过统一的API网关走。
这样做的好处是每一层可以独立升级替换。比如今天接入层绑的是飞书,明天换成钉钉或企业微信,只需要重写接入层适配器,Agent和调度层完全不用动。同理,执行层今天用的自研HMI脚本,明天换成基于CANoe的框架,也只隔离在工具层内部。
2.2 飞书机器人接入细节
飞书机器人的接入门槛其实很低,麻烦的是权限和事件订阅。我在开放平台创建了一个企业自建应用,拿到App ID和App Secret,然后申请了以下关键权限:
| 权限 | 用途 |
|---|---|
im:message | 接收单聊和群聊消息 |
im:message.send_as_bot | 以机器人身份发送消息 |
im:resource | 获取消息中的图片资源(截图回传用) |
im:chat | 获取群信息、成员列表(判断谁有权限下指令) |
权限申请后需要发布应用版本,否则权限不生效,这个细节坑了不少人。
事件订阅方式我强烈建议用长连接(WebSocket)而不是Webhook回调。Webhook要求暴露一个公网可访问的HTTPS地址,在内网环境还得做内网穿透,而且飞书的回调机制要求请求后在3秒内响应,处理逻辑稍复杂就容易超时。长连接是飞书主动向你的服务端发起WebSocket连接,事件被动推给你,无需暴露地址,断线后SDK自动重连,对部署环境非常友好。飞书官方Python SDK(feishu或叫larksuite-oapi)集成了长连接模式,大概二十行代码就能跑起来。
机器人的交互能力我用了两类:一类是纯文本消息,适合简单指令确认;另一类是交互卡片,包含按钮和表格。卡片的高级功能后面实操部分细讲。
2.3 AI Agent 内部结构与关键机制
如果你接触过 AI Agent,应该知道现在比较主流的架构基本逃不开“感知—规划—行动—反馈”这个循环。落到这个项目里,我的Agent内部结构如下:
- System Prompt:定义了机器人的角色边界、可使用的工具清单、输出格式规范。这是最重要的部分,一个措辞不严谨的Prompt会让大模型去调用不存在的工具或者用错误格式返回。
- 大模型推理:使用Long Context窗口能力较强的模型,我这边用的是一个私有化部署的中等参数模型配合云端模型做互补(机密场景走私有化,常规场景走云端)。
- Function Calling机制:模型输出结构化的工具调用请求,而不是自由文本,这样能保证稳定性。工具层会严格校验参数,参数缺失就自动反馈给模型补充。
- 状态记忆:由于单轮对话有上下文窗口限制,长任务的状态不能全靠对话记录,所以Agent在执行完一步后会把状态写入数据层的
task_state表,下次继续时就从这个表中恢复。
这里要特别说一下AI Agent token 是什么意思。Token是模型理解和生成文本的最小单元,大致相当于一个字或小半句话。一次完整执行链路可能消耗几千甚至上万token——因为要把工具返回的结果全部给模型重新阅读一遍。如果不做上下文管理,一个跑了半小时的测试任务会把模型输入撑爆,费用也水涨船高。我的策略是:所有工具返回的数据进入模型前先做摘要提取,只保留关键字段和结论;截图和完整日志不直接进token,而是以文件消息的形式推送到飞书群。这个策略后面还会细讲。
2.4 端到端指令流转链路
整个平台能跑通,核心是一条清晰的调用链路。以一条真实指令为例:“帮我在模拟器上跑一遍仪表盘基础显示用例,结果发群里”。
- 用户在飞书群输入消息,飞书服务器通过长连接推送到接入层。
- 接入层判断这是不是机器人可处理的消息(比如判断是否@机器人,或群会话中是否包含关键词)。
- 消息送入Agent层,大模型解析出意图:目标是为仪表盘基础显示测试,动作是执行用例,结果是发送到群聊。
- Agent通过Function Calling调用
search_testcases工具查找匹配用例列表;再调用check_device_available查询当前是否有空闲的HMI模拟器;确认有资源后调用execute_test_task创建任务。 - 调度层收到创建任务请求后,将任务写入队列,并分配设备锁,防止其他任务抢用同一台设备。
- 执行层拿到任务后开始控制CAN卡发送信号、控制HMI模拟器截屏,跑完每一条用例后生成结构化结果。
- 执行结果回传到调度层,并通知Agent层已经完成。
- Agent层汇总执行摘要,调用
send_feishu_card工具,将结果以摘要卡片+完整表格的形式发送到原群。 - 如果中途有失败项,Agent还会自动判断是否需要重试,重试完成后再次发送结果。
这个链路里最容易被忽略的是第4步“工具选择”。大模型不是万能的,它可能选错工具甚至自己编造一个工具名,所以在工具层做了一个“工具描述长度限制+Schema校验”,每次模型返回的调用请求如果不符合预定义Schema,直接拒绝并要求重新生成。这比让模型自己“发挥”可靠得多。
3. 实操过程与核心环节实现
3.1 从零搭建飞书机器人服务
先说一下具体的代码实现。我的机器人服务用 Python + FastAPI + 飞书官方SDKlarksuiteoapi编写。初始化长连接的方式如下:
from larksuiteoapi import Config, new_ws_client from larksuiteoapi.event import EventDispatcherHandler config = Config("your_app_id", "your_app_secret", log_level="INFO") def on_message(ctx, event): # 处理消息 return {} dispatcher = EventDispatcherHandler(config) dispatcher.register_event("im.message.receive_v1", on_message) ws_client = new_ws_client(config, event_handler=dispatcher) ws_client.start()这样启动后,机器人就以长连接模式接入飞书,只要进程不退出,就能持续接收消息。注意飞书SDK的版本差异,有些老版本的长连接API名不一样,优先用新版本(v2.x)。
收到消息后,第一步不是直接送给大模型,而是先做基础过滤:忽略机器人自己发的消息;如果是群聊且没有@机器人,可以选择忽略或者按配置响应;判断消息类型是文本还是图片等。我当时在这里踩过一个坑——飞书SDK返回的event结构里,消息内容是一个JSON字符串,里面还有一层结构,直接当字典取文本会拿不到。后来统一封装了一个parse_message_text函数来处理。
机器人回复消息有两种方式:同步回复和异步主动发消息。对于慢任务(测试要跑几分钟到几十分钟),我选择了“先回复收到,再异步发送结果”的模式,避免长连接处理超时。这也是飞书事件响应的最佳实践——服务器必须在3秒内给出ack,否则飞书会重试推送事件。
3.2 Agent 层的工具化封装与函数调用
Agent层是整个平台的智商所在。我用的是OpenAI兼容的接口协议,这样可以灵活切换不同的模型服务商。核心代码如下:
from openai import OpenAI client = OpenAI(base_url="http://your-llm-server/v1", api_key="unused") tools = [ { "type": "function", "function": { "name": "execute_test_task", "description": "创建并执行一条车载HMI测试任务", "parameters": { "type": "object", "properties": { "testcase_ids": {"type": "array", "items": {"type": "string"}}, "device_name": {"type": "string"}, "priority": {"type": "string", "enum": ["high", "normal", "low"]} }, "required": ["testcase_ids"] } } }, # ...其他工具定义 ] resp = client.chat.completions.create( model="your-model", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_msg} ], tools=tools, tool_choice="auto" )模型如果判断需要调用工具,会在返回内容里带一个tool_calls字段,你只需解析里面的function.name和arguments,然后执行对应逻辑。执行完再把结果作为新的消息回传给模型,让模型把结果整理成适合飞书展示的文本或卡片数据。
这里最关键的是System Prompt的写法。我第一版写得太宽泛,模型经常自己脑补测试结论,后来我把Prompt改成了非常强约束的结构:
- 明确“你只能使用提供的工具,禁止编造未提供的工具”;
- 明确“工具调用结果必须真实,如果数据缺失要明确说无法获取”;
- 明确“如果用户指令模糊,必须先调用
ask_clarification工具向用户确认,不要自己猜”; - 明确“收到测试执行结果后,必须以Summarize格式输出摘要,不得添加未提供的统计信息”。
这样约束下来,模型输出质量提升非常明显。建议读者在构建自己的Agent时,把System Prompt当作工程代码来维护,而不是一段随手写的“聊天人格”。
3.3 车载HMI测试执行层的接入
HMI自动化测试执行层是整条链路里最“硬核”的部分。我的执行层基于Pytest构建了一套扩展框架,硬件控制封装成Fixture,用例以Python函数形式编写。硬件方面用了三件套:一个CAN卡(PCAN)、一个可编程电源、一个HMI模拟器(实际是一个带触摸屏的开发板,运行着仪表盘应用)。
核心思路是:把测试用例拆成“信号激励+UI验证+数据采集”三段。比如一个基础显示测试用例:
def test_display_basic_speed(): # 1. 发送车速信号 can_bus.send_signal('VehicleSpeed', value=30) sleep(2) # 2. 截图HMI界面 hmi.screenshot("speed_30.png") hmi.ocr_verify("30", region=(1200, 400, 1320, 460)) # 3. 记录日志 can_bus.send_signal('VehicleSpeed', value=0)车载HMI自动化有一个难点是UI元素的识别。传统App自动化有包名、控件ID、xpath可以用,但HMI很多场景是闭源图形界面,没有控件树可查。我采用的方案是截图 + OCR + 像素级模拟对比三管齐下:对于数字显示这类精确内容,用OCR识别数字文本;对于告警图标这类图形,用预先采集的基准图做像素相似度比对;对于需要验证“动画流畅度”的场景,录制屏幕后逐帧分析时间戳差值。
执行层做好后,封装成工具API给Agent调用。这里要注意接口的幂等性:同一个测试任务如果因为超时被重复调度,不允许同一台设备同时跑同一个用例。我在任务表里加入了status字段(排队中/执行中/完成/失败),调度器每次取任务前先更新设备锁记录,设备被锁时其他任务直接排队等待,不并行。
3.4 测试结果汇总与飞书表格消息卡片
现在到了很多人感兴趣的“飞书机器人发送表格”。飞书有两种发送表格的方式:一种是以文件消息发送Excel或CSV;另一种是把表格渲染进消息卡片。这两种我都用了,适用场景不太一样。
- 文件消息:适合测试结果数据量大、需要开发自己筛选分析的场景。Post一个
file类型的消息,把生成的Excel或CSV文件上传即可,飞书可以直接在聊天窗口里预览。 - 消息卡片:适合在群里快速总览的场景,比如用例总数、通过率、失败项列表。
消息卡片的结构化展示是这套平台体验的关键。下面是一个典型卡片JSON片段:
{ "config": {"wide_screen_mode": true}, "elements": [ { "tag": "div", "text": { "tag": "lark_md", "content": "**测试完成** 结果:通过 38/40" } }, { "tag": "div", "text": { "tag": "lark_md", "content": "执行时长:12分36秒 设备:HMI-01" } } ] }如果需要更复杂的表格,飞书消息卡片支持table标签,但布局和字段数量有限制。我实际开发中发现,卡片里直接塞一个大表格容易在手机上显示得很窄,所以我的做法是:摘要卡片里只放总览和失败用例列表,完整数据以CSV文件消息发送,两相结合。
实现上,Agent会先调用一个fetch_test_summary工具从数据层拿汇总数据,然后用一段生成卡片的Python函数构造消息,最终通过im.message.create接口发到群里。注意自定义卡片要用interactive消息类型,不是text。
3.5 上下文窗口与Token成本控制
前面说过token是很多人刚接触Agent时的盲区。我可以提供一个实际数据:一次包含“查询设备→执行任务→获取结果→汇总发送”的完整链路,如果全部用原始内容传输,差不多会消耗3000到5000个token。在测试任务频繁触发的场景下,一天下来token开销不小,而且还会拖慢响应速度。
我的处理策略有三板斧:
- 工具返回内容压缩:给每个工具定义
summary_fields,只有网络地址、核心数值、结果代码等字段进入模型上下文,长文本直接截断为前200字符。 - 分阶段上下文重建:对于执行时间很长的任务,不把完整的中间过程串进同一个模型会话,而是等到执行阶段结束后重新发起一个“汇总会话”,只把最终结构化数据和用户原始指令传入。核心代码思路是:任务执行期间模型会话已经结束,执行结果暂存在数据库中,拿到结果后新起一次completion。这个方案的token开销平均下降了60%。
- 设置模型输出长度上限:调用参数中显式设置
max_tokens,不允许模型生成过长的自夸文本,强制简洁输出。
同时我在Agent层做了一层“成本保护”:如果单个任务token消耗超过预设阈值,自动切换到更小参数模型来完成汇总工作。小模型在这个职责下效果完全够用。
4. 常见问题与排查技巧实录
4.1 飞书机器人收不到消息
这个问题排在踩坑榜第一位。统计下来90%的原因是应用权限没配置好或没发布版本。飞书开放平台里修改权限之后,必须到“版本管理”中创建一个新版本并发布,权限才会真正生效。第二个原因是事件订阅没配置成功——如果用长连接,需要确认启动日志里出现了“ws connected”字样,否则事件不会推送。第三点:群聊中必须把机器人拉进群,且需要@机器人(或使用关键词),否则机器人收不到。
收不到消息时先用飞书提供的调试工具模拟发送事件,确认SDK能收到,再去查业务逻辑,不要一上来就翻业务代码。
4.2 Agent调用了不存在的工具或参数格式错误
大模型在Function Calling场景下偶尔会“幻觉”。最常见的情况是模型自己发挥了一个工具名,比如我明明定义了execute_test_task,它却输出成execute_task,然后参数也随便填。
我的对策是:
- 工具名设计为“动词+名词”的明确组合,减少歧义;
- 在SDK侧做严格校验:工具名不在白名单内则直接报错,并把可用的工具列表重新塞入下一条消息;
- 参数校验失败时不直接结束对话,而是把错误信息反馈给模型,要求它重新生成正确的调用请求。实测下来第二次生成的成功率极高。
另外要设置重试上限,比如一个工具连续三次调用失败就终止会话并让用户联系管理员,防止模型死循环。
4.3 长任务执行状态丢失与并发冲突
第一次上线时遇到一个经典问题:模型等待执行结果期间,WebSocket连接断了几十秒,执行完成后Agent层进程重启了,结果状态丢了,最终群里没人收到报告。
后来我引入了状态持久化机制。所有长期运行任务在调度层都有独立的状态记录,并且守护进程在重启后会扫描status=执行中但超时未完成的任务,主动标记为失败并发通知。设备并发锁是另一个容易被忽略的细节:两辆车的路测任务可能同时到达,调度器必须给每台HMI设备建立互斥锁,否则会看到两台测试机画面被同时操控的诡异现象。
4.4 车载HMI执行层的环境稳定性坑
车载环境硬件状态比纯软件复杂得多。最常见的问题是CAN卡驱动崩溃——长时间运行后PCAN驱动偶发失去响应,导致测试卡在“等待信号确认”环节。我的方案是增加硬件看门狗:执行层每个用例开始前先调用can_bus.ping()确认链路健康,如果响应超时就自动重启CAN卡驱动,并把异常记入日志重试当前用例。
还有电源序列的问题。有些HMI设备对上下电时序敏感,快速反复启动容易导致系统异常,我给执行层做了严格的功率状态机——空闲时下电、测试时上电、结束后延迟5秒再下电。这套状态机写在调度层里的好处是统一管理,不允许工具绕过。
5. 一些后续可以继续扩展的方向
这套平台目前在一台服务器上跑得很稳,但后面要扩展的路其实还不少。比如多车型并行测试——现在设备锁是单机内的,如果未来实验室上了多台HMI台架,就得给调度层引入分布式锁,或者直接迁移到Kubernetes管理,让多个执行Pod可以同时跑用例。
AI侧也有升级空间。现在的Agent只是被动接指令,下一步可以做成主动巡检:每天早上自动跑一遍冒烟用例,发现失败直接通知到人,而不是等人来问。另外可以结合历史缺陷库做智能分析:当测试失败时,Agent自动对比历史日志和缺陷说明,推荐最可能的原因和修复建议。这些能力基于现在的工具层和数据结构完全可以加上去,只是需要在Prompt层增加对应的任务模板。
还有一件事是个人建议:不要一上来就追求全自动。我整个开发和上线周期里,前两周都在做人机确认机制——比如机器人执行高危操作前必须等待用户回“确认”才会继续。自动化不是把人的判断力去掉,而是把重复劳动去掉,人只需要在关键节点做决策。这个思路放到任何自动化平台都是通用的。