1. 前言
做完一个机器人项目,往往会留下几份开发笔记:接入方式记在一处,运行顺序放在另一处,不同版本的说明也常常同时保存。再查“这个版本要不要唤醒”“录制前需要准备什么”,仍得重新翻文件。如果能直接问身边的 T1,并在电脑上看到它查到的原文,查资料会方便一些。
这次做的就是一个资料问答 Skill。这里的 Skill 是运行在机器人上的小程序,负责收音、提示音和播报;电脑端负责把语音识别成文字,从自己的笔记里查找内容,再请 DeepSeek 组织回答。项目在已有语音能力上增加资料导入、版本查询和原文查看,真机交互以语音为主,不需要机器人移动。
下面使用已经完成真机问答的同一组资料,从实际效果讲起,再展开环境准备、资料查询和原生 Skill 的实现。最后给出完整运行顺序,并补充网页操作与真机问答演示。
2. 先看资料问答怎样使用
2.1 给 T1 准备什么资料
演示资料来自前一个技术面试官项目,包括两份开发笔记和一份录制检查表,全部随源码保存在 sample_docs 目录。先看两份笔记里与本次提问直接有关的原句:
资料 | 笔记中的内容 |
技术面试官开发笔记 V2 | 机器人完整播完问题并响两声提示音后,使用者直接回答,不需要再次叫唤醒名字。 |
技术面试官开发笔记 V1 | 使用者需要先唤醒机器人,再说出回答。 |
V1、V2 是这两份笔记的版本,分别描述技术面试官项目早期和后来的接法。它们不是 T1 固件版本。选择 V2,就是让程序按 V2 笔记回答;问旧版,则应重新查 V1,不能把两份笔记的句子混在一起。
第三份 recording-guide-v2.txt 记录录制准备和使用顺序,归在 V2 下。它和 V2 开发笔记可以共同参与一次查询。这样既能问“要不要唤醒”这种具体问题,也能查询同版本资料中其他已记下的步骤。
2.2 问完整问题,再接着追问
真机运行时,先选择 V2 并建立会话。听到两声提示音后,说:
第二版需要唤醒机器人吗?
T1 当时的实际回答是:“不需要。机器人完整播完问题并响两声提示音后,直接回答即可,不用再叫唤醒名字。”电脑页面同时保留 V2 标签和所引用的原句。点击“查看原文”,可以展开相应笔记,直接看到回答依据。
等它答完并再次响提示音,再问“那旧版呢?”。这一轮回答变成:“需要。资料提到早期方案中,使用者要先唤醒机器人,再说出回答。”程序沿用上一句“需要唤醒机器人吗”这个问题,改查 V1;使用者不用把整句话再说一遍。
这两轮来自同一次真机连续问答,原始识别文字与采用的问题一致,没有人工改字,现场也确认了完整播报和没有原来的闲聊插话。后面还会单独演示“机器人官方售价是多少”:示例资料没写价格,程序就说明资料不足,不补一个数值。这个问题在另一场 V2 会话中完成,不与前两轮拼成一次历史运行。
2.3 一句话经过哪些步骤
以“第二版需要唤醒机器人吗”为例,声音先从 T1 麦克风进入程序,在电脑上识别成文字。程序把“第二版”对应到 V2,再从 V2 笔记中找与唤醒有关的段落。DeepSeek 根据这些段落生成回答和引用,最后由 T1 读出回答,网页展示原文。
开发时可以把它分成两部分。先让电脑能够根据资料回答文字问题,再把真机收音和播报接进来。两边共用同一个问答核心:网页输入和麦克风识别出的文字,最后都会进入 Knowledge.ask()。这样排查时也有顺序,资料查错了就先看查询结果,声音没收完整则检查语音采集,不必同时改动所有环节。
这里没有把模型和资料库都塞进机器人。T1 保留现场交互,开发电脑运行识别模型、文本检索和网页;当前问题、检索到的片段及同版本少量问答历史会发送到 DeepSeek。自己的资料是否适合发给文本服务,需要在导入之前确定。
3. 准备开发环境与项目资料
3.1 先分清使用哪一种开发方式
启元提供的资料里,有 ROS2 SDK,也有原生 Skill 的开发与调试方式。两者的运行入口不同,环境不能混着装。T1 ROS2 SDK 快速入门推荐 Ubuntu 22.04 与 ROS2 Humble;本文使用的是机器人机内已有的 Lumina 运行时,让它执行 Skill 包中的 Python 程序,电脑另外运行资料服务。
位置 | 本文使用的环境与职责 |
开发电脑 | Windows、Python 3.12.14,运行资料服务、中文识别、DeepSeek 请求与网页 |
T1 机器人 | 机内 Python 3.10.12、已有 Lumina 服务,执行原生 Skill 并访问机器人音频能力 |
连接 | 电脑与 T1 局域网互通,通过已核验主机指纹的 SSH 建立开发连接 |
PrimeHub Dev Kit 手册介绍了原生 Skill 结构与 robot 对象;官方 PrimeConsole 下载页可获取调试工作台及资料包。本文按已经使用的源码调试入口安装并启动任务。机器人语音接管部分还适配了本台设备的机内对象和消息类型,换固件时需先核对这一部分,不能把它当作所有 T1 都相同的公开方法。
3.2 安装电脑端依赖
取得项目源码后,进入 projects/02-private-knowledge。下面的命令在 Windows CMD 中执行,先建立独立环境,再安装项目依赖:
暂时无法在飞书文档外展示此内容
激活后,命令提示符前会出现 (.venv),随后使用的 python 就是这个环境里的解释器。--quiet 只是减少安装日志,最后的 pip check 用于检查依赖关系。本次安装后显示 No broken requirements found,同时核对了 Paramiko、Vosk、FunASR ONNX 和 ONNX Runtime 的版本。
电脑还需要准备三份本地模型。Paraformer 用来把中文录音转为文字;FSMN VAD 判断一句话是否已经说完;Vosk 保留为同段录音的识别对照。按 README 的文件清单下载后,放成下面的目录,或在启动命令里换成自己的路径:
暂时无法在飞书文档外展示此内容
Paraformer 和 FSMN 目录需要相应的 model_quant.onnx、配置及辅助文件,不能只有一个空文件夹。具体文件名及模型链接见 README 的运行前准备。DeepSeek Key 可保存在项目的 private/deepseek.key,也可以使用当前终端的 DEEPSEEK_API_KEY 环境变量;它由电脑读取,不写进机器人技能包。
3.3 启动资料控制台
仍在这个 CMD 窗口执行:
暂时无法在飞书文档外展示此内容
18882 是本机网页端口。vad 表示说完并停顿后结束收音,20 是一次收音窗口的上限,review-seconds 0 表示默认直接采用识别文字。命令中的 ^ 是 CMD 的续行符,不要把它换成 PowerShell 的写法。
终端出现本地地址后,浏览器打开 http://127.0.0.1:18882/。先确认页面能够加载、识别模型就绪、资料列表里有三份示例文本,再进行下一步。
此时打开的是开发电脑上的页面,127.0.0.1 不是 T1 的设备地址。浏览器能显示控制台,说明电脑服务可以访问;后面部署命令的 --host 才填写机器人在局域网中的地址。把两个地址分清,可以避免拿网页端口去测试机器人是否在线。
控制台的版本选择决定本轮从哪组资料查找内容,“新建问答”建立一次会话,问题与回答显示在当前会话中。真机尚未启动时,也可以先用文字输入核对资料查询。文字输入和麦克风输入会分别记录,方便后面确认一个答案究竟来自哪次操作。
3.4 导入自己的 Markdown 或 TXT
示例文本由 sample_docs/index.json 自动载入,复现本文时不必重新添加。换成自己的笔记时,展开“添加自己的资料”,填写资料 ID、标题、版本,再选择文件或粘贴正文。
资料 ID 用来关联同一份笔记,例如两版开发笔记都叫 interview-guide;标题用于在回答旁边辨认出处;版本用于限定查询范围。初次导入前把这三个字段定好,后面引用就能找到确定的一份正文。
版本标签也需要在同一批资料里保持一致。例如开发笔记和录制检查表都属于当前方案,就一起归到 V2。程序不会按文件修改日期自动推断最新版,也不会把只有 V1 的段落补进 V2。整理资料时先定版本,再提问,结果才容易核对。
目前支持 UTF-8 Markdown 和 TXT,单份正文不超过 64 KiB。同一资料、同一版本和相同正文重复导入,会保持原记录;内容变了则应使用新版本。这样昨天回答所引用的那句话,今天还能查回原来的内容。PDF 或网页资料需要先整理成文本,本项目没有把文档提取一并实现。
4. 实现“按资料回答”的核心逻辑
4.1 先把笔记整理成可检索的段落
阅读源码时,先找到这几个文件,不必从语音底层一路追起:
文件 | 本例中负责的工作 |
knowledge_core.py | 导入资料、查询版本、检索原文与生成回答 |
server.py | 给网页和原生 Skill 提供同一套问答接口 |
buffered_asr.py | 在电脑上完成整段中文识别与句尾检测 |
skill/src/main.py | 串起提示音、收音、请求答案和播报 |
run_robot.py | 打包、建立开发连接、安装及启动原生任务 |
先读资料核心,再看原生入口如何调用它,能够把问题分开:即使暂时没有机器人,仍可检查资料是否正确导入、同一问题在两版笔记里找到什么;接上 T1 后,再检查声音和播报是否完整。
资料由 knowledge_core.py 中的 Knowledge.import_document() 接收。导入时先检查文件和正文,再调用 split_chunks(),按照 Markdown 标题与段落拆分内容。每段同时保存标题路径、原文行号和版本,后面既能检索,也能定位出处。
这里没有逐句随意切开正文。例如 V2 笔记中的“提示音后直接回答”带有使用条件,应该与同一段的上下文一起交给模型。保留段落,可以减少只拿到“不需要”三个字、却不知道在什么条件下不需要的问题。较长段落会继续按长度拆分,完整文档仍单独保存,方便页面展开。
资料并不是发一次问题就重新从文件读一遍。电脑将规范化文本和段落存进本地 SQLite 数据库,后续查询在这份已导入内容上进行。新增版本时保留旧文本,因此一次问答始终能对应到当时使用的资料。
4.2 在选定版本里找相关内容
Knowledge.retrieve() 接收问题和资料版本,先限定 V2,再给 V2 下的段落计算相关程度。本项目资料量小,采用中文相邻双字片段与英文标识符的词法匹配,没有额外部署向量数据库。返回的是最多四段值得进一步查看的原文,不是最终答案。
可以先运行下面的独立示例,看看“需要唤醒机器人吗”会找到哪些内容。它使用临时数据库,只查本地文本,不请求模型,也不连接机器人:
暂时无法在飞书文档外展示此内容
输出里应同时看文件名、版本和正文。即使结果里出现两段不同资料,只要它们都属于 V2,也可能共同说明同一个操作;但某段只提到“机器人”却没有回答唤醒问题,就还不能据此下结论。若没有找到预期原句,先检查导入正文和所选版本,再调整问法。
检索的作用是缩小阅读范围。如果问题与“技术面试官”有关,就先找到这个项目的相关段落;段落里是否真的回答了问题,还要继续判断。因此,代码不会把得分第一的原文直接当作答案读出。具体特征、分数和阈值可以在调试时查看,正常使用先关注找到的原文是否相关。
4.3 把问题和原文交给 DeepSeek
网页和语音入口最后都调用 Knowledge.ask()。它先检查会话是否仍在进行,再进入 _ask_snapshot():识别问题中的版本,补全短追问,调用检索,最后请求模型。
第一次说“第二版需要唤醒机器人吗”,程序得到 V2 和“需要唤醒机器人吗”。下一次只说“那旧版呢”,就沿用刚才的实质问题,改用 V1 重新检索。这个补全来自会话保存的上一条问题,不是把上一句答案里的“不需要”改成“需要”。
找到段落后,发送给 DeepSeek 的数据包含当前问题、所选版本、相关原文,以及同版本最近少量已回答内容。提示要求只依据这些资料回答;片段没有提供被问事实时,就说明资料不足。模型负责把原文组织成可播报的回答,不负责替程序选一个未经查询的资料版本。
历史问答也要跟随当前资料版本。上一轮问 V2、下一轮改查 V1 时,不能把 V2 的答案继续当作 V1 的事实。这里保留的是“上一轮问了什么”,让短句能够补全;用于回答的原文仍要从本轮版本重新取得。这个区别很小,却决定了连续问答是在继续查资料,还是只在改写上一句话。
项目通过 DeepSeek JSON Output接收结构化结果。其中 answer 是要读出来的话,citations 保存答案使用的原句和片段编号,status 区分正常回答与资料不足。先把这几项分开,网页就能同时显示回答和出处,机器人只播报适合听的正文。
4.4 给回答留下可以打开的出处
模型返回内容后,Knowledge.validate_answer() 做三项检查:引用的片段确实来自本次检索;引用版本与当前选择一致;引用文字逐字存在于原文。有答案却没有引用,或者引用了一段不存在的文字,都不能直接保存为正常回答。
通过检查后,程序从原始候选中补上资料标题、文件名和行号。模型只负责指出用了哪一段、哪一句,不自由填写一个看似可信的来源。网页点击“查看原文”时,再通过 get_document() 取回同一版本的完整正文,并高亮对应文字。
回答旁边的信息 | 读者用它核对什么 |
资料标题与版本 | 回答使用的是哪份笔记 |
原文引句 | 结论直接依据哪一句话 |
行号与完整正文 | 引句前后有没有适用条件 |
这些检查能防止部分错引文,仍需要人阅读原文判断结论有没有概括过头。查询重要内容时,应核对原句讨论的是不是同一个对象,而不是只看回答说得顺不顺。更细的引用测试与检索分析放在 FAQ,不影响这里的使用流程。
模型服务出错时,程序也要与“资料没写”分开处理。前者需要检查接口或重新尝试,后者需要补充资料;如果都回复“资料不足”,使用者就分不清是笔记缺内容,还是服务根本没有正常工作。本项目在生成请求失败时保留错误,不把这一轮保存为正常回答。
5. 将问答程序接入 T1 Skill
5.1 技能包里装什么
原生 Skill 的最小结构是 cfg/pkg.yaml 和 src/main.py。Lumina 启动入口脚本时,会注入全局对象 robot;脚本通过它使用播报和音频能力,不在入口里自行 import robot。这个约定可对照 Dev Kit 运行时契约。
本项目把语音流程、采集器和模式适配分别维护在 skill/src。run_robot.py 构建时将这些模块合并成入口,连同包配置写入 RPK。机内程序只使用标准库和现有运行时,Paraformer、检索数据库及 DeepSeek Key 留在电脑。
暂时无法在飞书文档外展示此内容
先在第二个 CMD 窗口进入同一项目并激活环境,检查仓库附带的包和部署参数:
暂时无法在飞书文档外展示此内容
仓库附带的包在 artifacts;截图检查的是本机构建目录 dist 下的同一份包。压缩包检查与 --help 都只在电脑执行。真正安装、启动任务和收音播报,还要在连接 T1 后分别完成。RPK 结构与打包方式可参考工具包的 pack.py。
5.2 把文字回答变成机器人播报
问答服务返回后,原生入口从结果中取出 answer,把这段文字交给播报函数。机器人发声用到的是 Lumina 注入的 robot 对象,关键调用都在 skill/src/main.py 中:
实际调用 | 在本例中的作用 |
robot.play_tts(text) | 提交要说的文字,检查请求是否被接受 |
robot.get_audio_play_state() | 读取播放状态与所属技能,确认本轮确实开始播放并进入稳定空闲 |
robot.play_audio_file(path, pkg_name=PACKAGE, priority=6) | 播放两声本地 WAV 提示音,告诉使用者可以提问 |
speak() 先提交文字,再在循环里读取音频状态。本项目使用的运行时中,状态值 1 表示正在播放,2 表示空闲;程序先观察本技能的播放,再等待连续空闲。这样才能判断这一句已经播完,随后安排提示音和收音。若播报被其他语音抢占,或者没有观察到完成,程序会记录原因并停止本轮。
长答案由 speak_answer() 按句子边界分成不超过 100 字的片段,再依次交给 speak()。这个长度是本项目的分段策略。完整逻辑可直接查看原生入口源码,资料服务、播报函数和入口代码分别负责查询、发声和流程控制。
下面这段实际入口把两端接在一起:向电脑的 /api/ask 请求资料答案,取回回答与引用,最后调用 speak_answer(result['answer'])。前面第 4 章的资料查询,到这里才变成 T1 扬声器里的声音。
两声提示音同样需要检查播放状态。完整播报、提示音、收音依次进行,使用者听到提示后再说一句。音频能力可对照官方 SDK 的接口说明和 Dev Kit 运行时契约;本文执行的具体方法名来自后者的 robot 对象。
5.3 麦克风音频怎样送到电脑
采集部分参考官方 SDK 的 record_audio.py,调用机内已有的 parec 工具,从机器人本机 PulseAudio Unix socket 读取内置麦克风。本次设备是八通道、48 kHz,代码混合第 5、6 通道并转换为 16 kHz 单声道,再通过开发连接发送给电脑识别。
SdkMicrophone 管理这一段采集,电脑端 BufferedAsr 判断句尾并识别完整录音。识别完成后,原生入口将文字交给 /api/ask;电脑返回答案,入口再调用上一节的播报函数。底层音频与资料查询因此可以分开检查:前者要拿到完整问题,后者要找对版本和原文。
为了避免原来的聊天助手同时抢答,程序在交互阶段管理临时语音接管,并在采集收尾时恢复、读回状态。当前 only_voice 适配写在 voice_mode.py,依赖本台机内服务;它与资料检索逻辑分开。语音底座的前期接入过程可参照技术面试官项目,本篇不再重复整套收音调试。
6. 在真机上运行同一组问答
6.1 建立连接并启动原生任务
先确认 T1 已开机联网,电脑能够通过局域网访问它。手机 App 显示蓝牙连接,不能代替这里的开发连接。先取得本台 T1 可用的 SSH 登录资料,并核验主机指纹,再运行部署器:
暂时无法在飞书文档外展示此内容
大写参数换成自己的设备信息。--known-hosts 指向已核验的主机指纹文件,用于确认连接对象;密码由终端交互读取,使用 SSH 私钥时增加 --key-file。部署器通过 SSH 访问机器人本机的 Lumina 服务,向 /packages/script 装入主 Skill 与恢复 Skill,并建立回环转发,让机器人能访问电脑的 18882 服务。它采用的源码调试方式可对照 deploy_test.py。
--prepare-only 会安装技能,但暂不开始问答。看到 ready: true 后,先在网页选 V2、点击“新建问答”,再回到部署终端输入 run。这时程序向 /tasks 发起任务,机器人开始执行原生入口。后面要分别看是否听到开场、提示音、回答及结束提示,而不只看安装返回值。
本文完成的是这条开发入口的安装和真机运行,没有把 PrimeConsole 图形导入或 PrimeHub 技能市场上架算作已经完成。
机器人脚本里的 127.0.0.1 和浏览器里的本地地址原本指向不同设备。本项目通过 SSH 回环转发,把机器人发到 18882 的请求送回电脑端服务,所以原生入口可以调用同一套问答接口。连接中断后,这条转发也随之停止;运行期间保留控制台和部署终端,等任务实际结束再关闭连接。
6.2 完成两轮连续提问并查看原文
第一段按以下顺序操作,保留机器人声音、使用者提问和电脑页面之间的对应关系:
顺序 | 实际操作 | 需要看到或听到的结果 |
1 | 在 V2 新会话中启动原生任务 | 开场完整,随后响两声提示音 |
2 | 提示音后问“第二版需要唤醒机器人吗” | 回答不需要;页面显示 V2 和原文引用 |
3 | 等回答结束及下一次提示音后问“那旧版呢” | 回答早期方案需要唤醒;本轮资料变为 V1 |
4 | 第二答播完后,在页面点击“结束问答” | 等任务结束、robot.active_session 为空并确认语音模式恢复 |
5 | 展开这两轮的“查看原文” | 分别看到 V2 第 9 行和 V1 第 7 行对应的句子 |
6 | 在部署终端输入 close | 关闭本次开发连接,保留技能和已保存的问答 |
每次原生任务最多使用四个收音窗口。没有选版本时,程序可能先问要查哪一版,这段澄清也会占一个窗口;复现上述两问时先选 V2,流程更直接。每个窗口只说一句,等下一次提示音再继续。
第二答后先结束问答,再查原文,可以避免翻阅时第三个收音窗口等待超时。结束会话会保留当前页面记录;查完后再新建下一场会话。回答措辞可能随模型返回变化,核对重点是版本、核心事实和引文,不要求逐字重演历史播报。
6.3 另开一轮查询资料没有提供的内容
结束前一段后,重新建立开发连接,在页面选择 V2 并新建问答,再启动一次原生任务。提示音后问“机器人官方售价是多少”。这组笔记只记录项目接入,没有售价;保存的真机结果为资料不足,引用列表为空。
这个例子检查的是资料范围。给机器人导入自己的笔记,并不意味着它就有了与项目有关的全部事实。想让它回答价格,应先加入确实包含价格及适用条件的资料,再进行新的验证。已有问答仍保留当时使用的版本和原文,不随新增资料改写。
6.4 网页操作与真机演示
这次选择“技术面试官更新后的开发笔记”,把问题上限设为 1。开始真机问答后,直接说出“模型密钥应该保存到哪里”。T1 根据笔记回答:密钥留在电脑本地,不放进机器人 Skill,也不放进公开仓库;网页同步保存答案、原文引用和本次知识整理。
网页操作与结果
资料专家_网页操作与结果
真机语音演示
资料专家_真机语音演示
7. 联调中遇到的问题
7.1 原来的聊天助手也在回答怎么办
早期直接使用原生聊天转写时,原有助手也会处理同一份语音,出现插话或抢占播放。当前方案将原生 Skill 的采集与播报作为一个受控交互过程,临时管理语音接管,采集结束后检查恢复状态。资料问答只接受这一轮送来的文字,不另拼原聊天结果。
排查时先看问题有没有完整收到,再看问答结果,最后确认播放有没有被其他语音打断。不能因为电脑已经生成答案,就忽略现场只听到半句的情况。第一篇与本篇也要串行使用,不能同时让两个 Skill 占用相同的语音状态。
7.2 说完一句为什么还要等很久
最初的固定录音时间加上文字确认,会让短问题也等待较长时间。现在默认采用句尾检测,说完并停顿约 800 毫秒后结束本轮,上限 20 秒;识别完成后直接采用文字。需要人工核对技术词时,再打开页面的十秒确认选项。
提示音前的等待则通过提前准备麦克风、确认上一句播完后直接播放提示音来减少。收音等待、识别耗时和模型请求是不同环节,调试时分别看;具体测量与对照保存在等待时间记录,不把少数轮次当作普遍时延保证。
7.3 当前录制前还需要核对什么
完整问题、旧版追问和资料不足已有真机记录。短结束口令曾经漏收,当前代码增加了录音流低延迟请求,但这项修改后的完整“结束问答”尚未完成真机复测。录制当前案例可以先用页面按钮结束,并等待任务结束和语音恢复,不能把安装成功当作短口令已经修好。
引用检查、版本处理和页面接口另有本地受控测试,详细范围见真机验证记录与 FAQ。遇到问题时按实际发生的环节查看,不需要先读完全部日志和测试脚本才能使用这个 Skill。
8. 总结
这次给 T1 增加的能力,是把自己已有的项目笔记接进语音问答。电脑保存资料,T1 接收问题并播报回答;问到另一版,就重新查对应版本;想确认答案,则在页面打开原文。资料查询、现场交互和结果核对由此能连成一次实际操作。
工程目前适合小型 Markdown、TXT 资料集,运行时需要电脑参与识别和文本问答。后续替换成自己的笔记,可以先用文字问题检查内容,再用同样的问题接真机验证。完整源码、示例资料、技能包和运行说明均保存在本项目目录,前面的操作顺序也可用于替换自己的资料后复现问答。