ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GPT-4+Whisper+Weaviate:从零搭建语音问答助手的技术实践

GPT-4+Whisper+Weaviate:从零搭建语音问答助手的技术实践 这个系列写到第三篇前两篇我们基本把 GPT 应用开发的地基讲透了从 API 调用到提示词工程再到上下文管理按说已经能写点像样的东西了。但这篇我想换个玩法把GPT-4、Whisper和Weaviate真正串成一条流水线做一个能落地、能扛住真实数据量的AI 驱动应用而不是那种调用一次接口返回一段话的玩具 demo。过去两个月我一直在折腾一套语音问答助手用户对着麦克风说话系统先转成文字再从知识库里检索相关内容最后交给 GPT-4 生成一段有依据的回答。整个链路看起来简单但真正跑起来才发现每个环节都在考验你对工具的掌握程度。这篇就把我的设计思路、代码细节、踩过的坑全部摊开讲适合已经会基础调用 OpenAI 接口、想往工程化方向走的 Python 开发者也适合正在纠结向量数据库到底怎么选Whisper 到底怎么部署的朋友。1. 先搞清楚这三个东西为什么要凑在一起1.1 单靠 GPT-4 搞不定的事很多人刚开始做 AI 应用时有个错觉GPT-4 这么强我啥都不用管把所有问题丢给它就行了。实际跑一跑就会发现单模型至少有四个绕不过去的坎。第一是知识时效性。GPT-4 的训练数据有截止日期你问它最近发生的事、你公司内部的规章制度、你自己的一堆文档它要么瞎编要么直接告诉你不知道。第二是幻觉问题。就算它知道一点也会一本正经地胡说八道尤其在你追问细节的时候。第三是上下文长度限制。虽然现在的模型窗口越来越大但你不可能把一本几十万字的资料全塞进去成本和时间都受不了。第四是输入模态单一。用户很多时候不想打字尤其在做会议记录、语音笔记这类场景你得先解决声音变文字的问题。所以真正工程化的做法不是让 GPT-4 一个人干所有活而是让它当一个聪明的答题员背后有语音转写模块负责听有向量数据库负责查资料。这三者各司其职才算一个合格的 AI 驱动应用。1.2 Whisper 和 Weaviate 在架构里的真实角色先看Whisper。它是 OpenAI 开源的语音转文字模型支持多语言中文识别效果在开源模型里属于第一梯队。我的经验是它在有口音、背景噪声的场景下表现明显好于很多国产免费 API。在这个架构里它不负责理解语义只负责把音频变成文本相当于人类的耳朵。再看Weaviate。这是一个开源向量数据库专门干一件事把文本变成向量然后帮你快速找语义上最接近的内容。为什么需要它因为用户的提问和知识库里的原文往往不是一字不差的。比如用户问上个月的销售额怎么样文档里写的是6 月营收数据关键词完全不同但语义相近。传统数据库做不了这种模糊匹配得靠向量检索。我经常用一个类比Weaviate 像一个超大图书馆的索引员你告诉他一个大概意思他能从海量书架里把最相关的几本书抽出来递给你。GPT-4 则是那个拿着书帮你总结、提炼、组织语言的讲解员。Whisper 是前台接待负责听懂你的话。三者分工明确各管一段。1.3 这条技术栈适合什么场景这套组合最适用的场景是私有知识库问答和语音交互类应用。我做那套语音问答系统时用户上传的是一批产品手册、运维文档和会议录音总量大概有 2 万多条文本记录几十 GB 的音频。用 Weaviate 存向量用 Whisper 转写录音用 GPT-4 做最终回答效果比单用 GPT-4 好得多——回答里引用的内容都能在原始文档里找到出处。如果你是做客服机器人、企业内部知识助手、会议纪要工具、学习辅助应用或者任何需要让用户用自然语言查询私有数据的场景这套技术栈都值得参考。哦对如果你只是做那种陪聊机器人不需要知识库那 Weaviate 可以省掉但 Whisper 照样能用来做语音输入。2. 环境搭建和依赖管理这些坑我先替你踩了2.1 Python 环境与版本选择的现实考量聊到具体实施第一步自然是 Python 环境。我知道很多初学者喜欢装最新的 Python但我建议在 AI 项目里稳一点Python 3.10 或 3.11 是当前最稳的选择。Whisper 的某些依赖比如 ONNX、torch在太新的 Python 版本上可能出现兼容问题而 3.8 又太老有些库的新版本已经放弃支持。装环境时强烈建议用虚拟环境别图省事直接装到全局。我用python -m venv .venv建虚拟环境然后激活。Windows 下激活命令是.venv\Scripts\activateLinux 和 macOS 是source .venv/bin/activate。这一步能救你无数次尤其是当你同时维护多个项目、每个项目依赖的 torch 版本还不一样时。另外装完 Python 后记得检查 pip 是否正常python -m pip --version。如果提示找不到命令多半是安装时没勾选Add Python to PATH。这个问题在 Windows 上特别常见解决办法是重新运行安装程序选择 Modify然后把 PATH 选项勾上或者手动把 Python 的 Scripts 目录加到系统环境变量里。2.2 安装 openai、whisper、weaviate-client 时的版本陷阱接下来装核心依赖我用一个 requirements.txt 管理openai1.0.0 openai-whisper20231117 weaviate-client3.26.0 torch2.0.0 numpy python-dotenv这里有一个特别容易踩的坑openai 库从 1.0 版本开始API 的调用方式发生了大变化。网上大量老教程用的是 0.28 之前的写法比如openai.Completion.create如果你装的是新版本照抄必然报错。我的做法是统一用 1.0 之后的新写法后面代码里会详细展示。whisper 这个包在 PyPI 上的名字叫openai-whisper不是whisper装错的话 import 都失败。另外whisper 依赖ffmpeg很多人在这里卡住。Windows 上可以用winget install ffmpeg或者去官网下载二进制包然后把 ffmpeg 的路径加到环境变量里。装完后跑一下ffmpeg -version能输出版本信息再往下走。weaviate-client 的版本也要注意。3.x 和 4.x 的 API 风格差异很大我用的是比较稳定的 3.x 系列集群 API 和 Collection API 都能用。如果你看官方文档看到的是weaviate.collections开头的写法那就是 4.x 的语法别混用。2.3 本地 whisper 前置依赖ffmpeg 和模型下载Whisper 处理音频本质上要先解码成 PCM 数据这个过程全靠 ffmpeg。没有它你会遇到RuntimeError: ffmpeg is not installed之类的报错。装完 ffmpeg 后第一次运行 whisper 时它会自动下载模型文件比如base模型大概 140MBlarge-v3要 3GB 左右。我建议提前手动下载模型免得运行时慢慢等也方便网速不稳时重试。在 Python 里跑一行import whisper model whisper.load_model(base)这会从 Hugging Face 下载并缓存到本地。如果下载慢或失败可以设置环境变量HF_ENDPOINT指向镜像站这是 Hugging Face 官方支持的国内镜像做法。下载完成后后续每次加载都会直接走本地缓存速度很快。还有个细节whisper 的输入音频格式。我实测下来wav 和 mp3 都没问题但 flac 在某些版本上有小概率解码异常建议统一转成 16kHz 或 32kHz 的 wav 再喂给模型准确率和稳定性都会更好。3. Whisper 本地部署和调用从模型参数到服务化3.1 模型大小怎么选别一上来就 largeWhisper 官方提供了多个尺寸tiny、base、small、medium、large还有一个 large-v3。很多新手一上来就选 large觉得越大越准结果发现跑一段 10 分钟的音频要等两三分钟显存不够还直接 OOM。这里有个经验法则先按你的硬件来选再按实际效果微调。我自己的基准测试结果大概这样CPU 环境下转写 10 分钟英文音频模型显存需求GPU转写耗时约中文识别准确率tiny~1 GB1-2 分钟一般base~1 GB3-5 分钟尚可small~2 GB8-10 分钟良好medium~5 GB15-20 分钟优秀large~10 GB30 分钟最佳如果你只有一块 8GB 显存的普通显卡medium 几乎就是天花板了。我自己的服务器是 16GB 显存最终选了 large-v3因为我对中文转写准确率要求很高尤其是涉及专业术语时。但如果你做的是实时性要求高的场景比如语音对话small 或 base 就已经够用延迟能控制在几秒内。3.2 本地部署还是 API 调用Whisper 有两种用法直接调 OpenAI 的 API或者在本地部署服务。我的立场很明确能用本地就本地除非你完全不在乎隐私和数据合规。本地部署的好处有三个。第一是免费跑一次转写不用付 token 费。第二是隐私音频不必上传到云端这对处理客户数据、内部会议录音来说特别重要。第三是可控你可以自定义模型参数调整热词甚至做后处理逻辑。坏处也有你需要一台像样的机器。我实测纯 CPU 跑 medium 模型转写 1 小时的音频大概要 2 个多小时基本不可用。所以至少需要一张支持 CUDA 的 NVIDIA 显卡。如果没有 GPU我的建议是优先用 API或者租一台带 GPU 的云服务器。如果决定本地部署成 HTTP 服务我有一个比较轻量的做法用 FastAPI 包一层暴露一个/transcribe接口。这样前后端分离语音问答系统只需要 POST 一个音频文件接口返回 JSON 转写文本。代码不复杂from fastapi import FastAPI, UploadFile import whisper app FastAPI() model whisper.load_model(small) app.post(/transcribe) async def transcribe(file: UploadFile): tmp_path f/tmp/{file.filename} with open(tmp_path, wb) as f: f.write(await file.read()) result model.transcribe(tmp_path, languagezh, fp16False) return {text: result[text]}启动时用uvicorn main:app --host 0.0.0.0 --port 8000注意fp16False在 CPU 下必须加否则会有精度报错。3.3 异步转写与批处理如果你要处理一批音频比如几十段会议录音同步循环调用会非常慢。我的经验是做一个任务队列上传的音频先落盘丢进队列后台 worker 逐个转写前端轮询任务状态。这比同步等待用户体验好得多。批处理方面Whisper 支持从一段长音频里自动识别语音段落返回带时间戳的 segments。我在做会议纪要时会把每个 segment 单独拿出来后面做向量化时按段落切分而不是把整篇长文本丢给向量化模型——这一点在 Weaviate 章节会详细说。实操中还有个提速技巧把音频切成 30 秒左右的片段并行转写最后按时间戳拼接。whisper 的transcribe函数支持initial_prompt参数可以注入领域相关的热词列表帮助模型更准确地识别专有名词。我处理运维文档时会在initial_prompt里放上网关、熔断、QPS、限流这类词转写准确率明显提升。4. Weaviate把知识库变成可检索的向量空间4.1 为什么选 Weaviate 而不是裸上向量计算向量数据库现在选择很多Pinecone、Milvus、Chroma、Weaviate 都有人用。我在对比测试后选了 Weaviate三个原因。第一它支持混合检索BM25 关键词 向量相似度这个能力很重要。实际场景里用户问磁盘满了怎么办如果只靠向量匹配可能找到的是存储空间不足的处理方案语义虽然相关但跟运维文档里的磁盘使用率过高这种原句有距离。加上 BM25 关键词召回能两者取长补短。第二它自带模块化向量化能力。Weaviate 可以配置text2vec-openai、text2vec-huggingface之类的向量化模块插入数据时自动把文本变成向量不需要你在代码里手动调嵌入模型。省掉不少胶水代码。第三它对Python 客户端支持好schema 管理清晰而且数据量在百万级以内时性能完全够用。你如果只是想做一个 demo用 Chroma 也行但它缺少很多生产级的查询能力。Milvus 性能更强但部署运维复杂度高不适合个人项目或小团队快速迭代。4.2 schema 设计class、property、vectorizerWeaviate 里的核心概念叫 Class类比于关系数据库里的表。每个 Class 有一堆 Property类比于字段。我设计了一个Document类用来存转写出来的文本切片import weaviate client weaviate.Client(http://localhost:8080) schema { class: Document, vectorizer: text2vec-openai, moduleConfig: { text2vec-openai: { model: text-embedding-ada-002, modelVersion: 2, type: text } }, properties: [ {name: content, dataType: [text], description: 文本内容}, {name: source, dataType: [text], description: 来源文件}, {name: timestamp, dataType: [number], description: 录音或文档时间戳}, ] } client.schema.create_class(schema)这里最关键的是 vectorizer 的配置。如果你用的是text2vec-openai每次插入数据时 Weaviate 会调用 OpenAI 的 embedding 接口生成向量。这意味着你需要配置好 API key 和 base URL并且在向量化那一步就产生 API 费用。如果你不想依赖 OpenAI 的 embedding可以改用 Hugging Face 的本地模型比如all-MiniLM-L6-v2速度更快且免 API 费用。property 设计上我建议不要贪多。能用 text 类型就别用别的因为后面的混合检索和过滤都依赖它。timestamp 用 number 类型是为了支持按时间范围过滤比如只查最近一个月的文档。4.3 混合检索把关键词和语义结合起来Weaviate 的混合检索是把 BM25 关键词匹配和向量相似度做一个加权融合。我的查询代码大概是这样的results ( client.query .get(Document, [content, source, timestamp]) .with_hybrid( query磁盘空间不足导致服务异常, alpha0.75 ) .with_limit(5) .do() )这里alpha是向量和关键词的权重比例0 表示完全用关键词 BM251 表示完全用向量。我实测下来alpha 在 0.7 到 0.8 之间效果最好既能抓住语义相关又不会漏掉精确关键词。还有一个我踩过的坑with_hybrid的query参数如果太短比如就一两个字符向量化出来几乎没有语义信息检索结果会很差。所以查询前最好对用户输入做一点预处理去掉无意义的语气词适当扩充同义词。比如用户说机器卡了我会在后面补一句性能下降 运行缓慢 处理慢让向量检索有更多信号。4.4 批量导入数据和更新策略如果一条一条插入几万条数据速度会感人。Weaviate 支持 batch 导入我的做法是把文本切片按 100 条一批丢进去with client.batch as batch: batch.batch_size 100 for doc in docs: batch.add_data_object( { content: doc[content], source: doc[source], timestamp: doc[timestamp], }, Document )批量导入时有个教训别一个批次里塞太多字段太长的数据。embedding API 有请求体大小限制如果一条 content 超过几千 token批量导入会直接失败。我的切分策略是按段落切每段控制在 200-500 个汉字这样向量表达也更精准——段落太长语义会被稀释。数据更新上如果知识库里的文档变了我不会直接删了重插而是用client.data_object.delete_by_id先删掉旧的再插新的。还有个细节Weaviate 的timestamp字段是 number我用 Unix 时间戳存查起来方便也知道哪条数据过期了。5. GPT-4 的接入姿势RAG 和 function calling5.1 RAG 的完整闭环RAGRetrieval-Augmented Generation这个名字听着高深其实就是我前面说的那套流程先检索知识库再把检索结果和用户问题一起交给 GPT-4让它基于这些材料作答。这样回答有出处、有依据幻觉问题大幅减少。我实现 RAG 时prompt 是这样组织的system_prompt 你是一个知识助手请根据给定的参考资料回答用户问题。 如果你在资料中找不到答案请直接说资料中没有相关信息不要瞎编。 回答时尽量引用资料中的原话并在最后列出资料来源。 参考资料 {context} 然后context就是 Weaviate 检回来的几条 content 拼接。这里有一件很重要的事不要把检索回来的所有内容全塞进去。我试过塞得越多模型越容易抓错重点而且 token 费用直线上升。我的策略是只取 top 3 到 top 5 条每条控制在 200 字左右。如果这几条都不相关宁可让模型承认不知道也不要硬答——用户会明显感觉到这个助手靠谱。5.2 用 function calling 让模型学会查库单纯的 RAG 是每次必查但真实的对话不总是需要查资料。比如用户说你好谢谢你没必要去 Weaviate 里搜一遍。这时候就该用 GPT-4 的 function calling让模型自己决定要不要查库。我定义了一个函数{ name: search_documents, description: 在知识库中检索与问题相关的资料, parameters: { type: object, properties: { query: {type: string, description: 检索关键字或问题描述} }, required: [query] } }调用时先让 GPT-4 看用户消息和函数定义它会输出一个function_call请求我再执行真正的 Weaviate 搜索把结果作为role: function的消息传回去最后让 GPT-4 生成答案。这样模型能自己判断这问题不在我的知识范围内需要查库比每次硬查要聪明得多。我在这里踩过一个大坑function calling 里 query 参数的构造直接决定检索质量。如果模型把整个闲聊句子丢给我去做检索结果就是一坨垃圾。所以我给函数描述写得很严格请提取用户问题中的核心实体和意图构造一个简洁的检索关键词实测下来效果提升很明显。5.3 token 成本控制的几个野路子GPT-4 的 API 费用不便宜token 消耗大头在系统提示词和检索内容上。我用了几个办法把成本压下来不少。第一个是模型分级。简单闲聊、意图识别用gpt-3.5-turbo只有最终生成答案时用gpt-4。function calling 里让 3.5 做决策成本能砍一半。第二个是缓存。用户的提问如果和之前某次问题语义相近用向量相似度算直接复用上一次的回答不再调 API。我在 Weaviate 里单独建了一个ConversationCache类查询前先查缓存命中就直接返回。第三个是消息裁剪。多轮对话时早期轮次的信息可以压缩成摘要只保留最近几轮的完整内容。这一步对成本影响极大因为 token 是按输入总长度计的对话越长越贵。6. 端到端实操做一个语音问答小助手6.1 整体流程和目录结构前面讲了一堆原理现在我把完整的小项目跑一遍。我做的语音问答小助手流程是这样的用户上传音频文件wav/mp3Whisper 转写得到文本文本交给 function calling 判断是否需要检索需要检索时去 Weaviate 做混合检索取 top 3把检索内容和对话历史一起交给 GPT-4返回答案给前端目录结构很简单voice_qa/ ├── app.py # FastAPI 主入口 ├── whisper_service.py # Whisper 转写封装 ├── weaviate_store.py # Weaviate 检索封装 ├── gpt_service.py # GPT-4 调用与缓存 ├── requirements.txt └── .env # API key 等配置6.2 核心代码走读先看 whisper_service.py核心就是加载模型和转写import whisper from fastapi import UploadFile class WhisperService: def __init__(self, model_namesmall): self.model whisper.load_model(model_name) def transcribe(self, file: UploadFile) - dict: path f/tmp/{file.filename} with open(path, wb) as f: f.write(file.file.read()) result self.model.transcribe(path, languagezh, fp16False) return {text: result[text], segments: result[segments]}再看 weaviate_store.py核心是混合检索import weaviate class WeaviateStorage: def __init__(self, urlhttp://localhost:8080): self.client weaviate.Client(url) def search(self, query: str, top_k: int 3): result ( self.client.query .get(Document, [content, source]) .with_hybrid(queryquery, alpha0.75) .with_limit(top_k) .do() ) return result[data][Get][Document]最后是 gpt_service.pyRAG 加 function callingfrom openai import OpenAI import os client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) tools [{ type: function, function: { name: search_documents, description: 在知识库中检索与问题相关的资料, parameters: { type: object, properties: { query: {type: string, description: 检索关键词} }, required: [query] } } }] def ask(question: str, history: list): messages [{role: system, content: 你是知识助手。必要时调用 search_documents 查资料。}] messages.extend(history) messages.append({role: user, content: question}) resp client.chat.completions.create( modelgpt-4, messagesmessages, toolstools, tool_choiceauto, ) return resp.choices[0].message这里tool_choiceauto是关键让模型自己决定调不调函数。6.3 参数选择和自测我的参数选择写在 .env 里OPENAI_API_KEY你的key WEAVIATE_URLhttp://localhost:8080 WHISPER_MODELsmall VECTOR_MODELtext-embedding-ada-002自测时我会准备三段音频一段清晰的普通话一段带口音的一段有背景噪声的。因为 whisper 的 small 模型对带口音的识别有点吃力如果测试结果不佳我会切到 medium。另一个自测点是检索质量我会故意问一些知识库里没有的问题看系统会不会瞎编。合格的系统应该回答资料中没有相关信息。7. 我遇到的常见问题和排查记录7.1 故障速查表这半年我遇到过的坑不少整理成一张表方便你照着排查现象根本原因解决办法whisper 报ffmpeg not foundffmpeg 未安装或未加入 PATH安装 ffmpeg 并验证ffmpeg -versionwhisper CPU 下报fp16错误没有设置fp16Falsetranscribe 参数加fp16FalseWeaviate 连接超时Docker 容器没起或端口不对检查docker ps确认 8080 端口映射插入数据报 embedding API 错误OPENAI API key 未配置或额度用尽检查 .env 里的 key测试 embedding 接口混合检索结果为空索引没建立或数据未正确导入查询client.data_object.get验证数据存在GPT-4 返回内容明显跑题检索结果太杂或上下文太长减少检索条数裁剪消息体加系统提示约束批量导入速度极慢单条文本过长embedding 超时重试按段落切片控制在 500 字以内function calling 反复触发检索函数描述不明确在描述里写明仅当问题涉及知识库内容时调用7.2 值得注意的细节有几个问题不是报错但影响体验值得单独提醒。第一个是Weaviate 的 docker 容器要开对端口和卷。我的启动命令是这样docker run -d \ --name weaviate \ -p 8080:8080 \ -e AUTHENTICATION_ANONYMOUS_ACCESS_ENABLEDtrue \ -e PERSISTENCE_DATA_PATH/var/lib/weaviate \ -v weaviate_data:/var/lib/weaviate \ semitechnologies/weaviate:1.24.3第二个是Whisper 模型加载的时机。我一开始在 FastAPI 里每次请求都加载一次模型结果是第一次请求慢到爆炸因为它要重新加载一两 GB 的模型文件。正确做法是在服务启动时就把模型加载进内存用全局变量持有。第三个是并发安全。Whisper 的模型在 GPU 上做推理时多个请求同时进来会爆显存。我的方案是给转写接口加一个简单的信号量限制同时只能跑一个转写任务其他请求排队。第四个是日志。AI 应用不像传统接口输入是音频、输出是文本中间还有检索和模型调用出问题特别难排查。我在每个环节都加了一条结构化日志记录耗时时长、token 数、检索命中情况。排查问题时非常有用比如你发现某次回答很差看日志就知道是检索没搜到相关内容还是 GPT-4 没按规范回答。这篇文章基本上把我做这套语音问答助手从零到一的全过程写透了。我个人在实际操作中体会最深的一点是AI 应用开发和传统后端开发最大的区别在于你不能只盯着代码本身还要盯着模型的行为、数据的好坏、成本的波动甚至用户提问的姿势。Whisper 怎么调、Weaviate 怎么建索引、GPT-4 怎么写 prompt这些单独拿出来都有大量优化空间但真正考验人的是把它们组合起来时系统能不能稳定、可控、低成本地运转。最后再分享一个小技巧如果你准备在团队里推广这套方案不要一上来就接 GPT-4先用gpt-3.5-turbo验证完整链路逻辑通了再切到 GPT-4这样能省不少调试成本。毕竟模型贵但你的时间更贵。
返回列表