1. 项目概述与核心价值

最近在团队协作中,我发现自己和很多同事都面临一个共同的痛点:会议开得不少,但会后整理纪要、追踪待办事项、查找关键决策点却异常耗时费力。手动记录要么跟不上节奏,要么遗漏重点,最后往往变成“开完会就忘”。为了解决这个问题,我动手开发了一个“AI会议记忆助手”。这不仅仅是一个简单的录音转文字工具,而是一个集成了实时转录、智能摘要、任务提取和知识沉淀的自动化工作流。它的核心价值在于,将原本需要会后花费数小时的人工整理工作,压缩到几分钟内自动完成,并结构化地输出可搜索、可追溯的会议资产。

这个项目非常适合需要频繁参与跨部门会议、项目复盘、客户沟通或头脑风暴的团队和个人。无论你是项目经理、产品经理、工程师还是销售,只要你的工作离不开会议,这个工具就能显著提升你的信息处理效率。它基于Python构建,利用了当前成熟的AI语音和语言模型,技术栈清晰,部署灵活。接下来,我将从设计思路、技术选型、实现细节到避坑经验,完整地拆解这个项目,希望能为你构建自己的效率工具提供一份可直接参考的蓝图。

2. 整体架构设计与技术选型考量

2.1 核心需求与功能模块拆解

在动手写代码之前,我首先明确了工具必须解决的几个核心问题: 实时性 (能否在会议进行中提供辅助)、 准确性 (转录和理解的准确度)、 结构化 (输出的信息是否易于后续处理)以及 易用性 (接入和使用的门槛)。基于这些,我将系统划分为四个核心模块:

  1. 音频采集与预处理模块 :负责从麦克风或音频文件中获取原始音频流,并进行降噪、增益调节等预处理,为后续的语音识别提供干净的输入。
  2. 语音转文本(STT)模块 :这是技术的核心之一,需要将连续的语音流实时或准实时地转换为文本。
  3. 文本理解与后处理模块 :这是AI能力集中体现的部分,需要对转录文本进行智能处理,包括实时摘要、关键信息提取(如任务、决策、责任人)、说话人分离等。
  4. 输出与集成模块 :将处理后的结构化数据,以友好、实用的形式输出,如Markdown格式的会议纪要、同步到任务管理工具(如Jira、Trello)、或存入数据库以便全文检索。

2.2 关键技术选型与背后的逻辑

技术选型直接决定了项目的可行性、性能和开发效率。以下是几个关键决策点的深度分析:

语音识别(STT)引擎的选择:本地 vs. 云端 这是第一个需要权衡的决策点。云端API(如OpenAI Whisper API、Google Speech-to-Text)识别准确率高,尤其是对专业术语和不同口音的适应性好,且无需考虑本地计算资源。但缺点也很明显: 延迟、成本、隐私和网络依赖 。对于实时辅助场景,网络波动带来的延迟是难以接受的;会议内容通常涉及商业机密,上传到第三方云端存在隐私风险;长期使用,API调用费用也是一笔开销。

因此,我最终选择了 OpenAI Whisper 模型的本地化部署方案 。Whisper系列模型(特别是 medium large-v3 )在开源模型中表现出了接近商用的准确率。选择本地部署,虽然对硬件(尤其是GPU)有一定要求,但换来了 零延迟、数据完全私有、一次部署长期免费使用 的巨大优势。对于团队内部工具,数据安全永远是第一位的。

大语言模型(LLM)的集成:摘要与提取的核心 原始的转录文本是线性的、冗长的。要从中提取摘要、任务和决策,必须依靠大语言模型的理解能力。这里我选择了通过 API调用云端LLM 的方式,具体是 OpenAI的GPT-4系列或 Anthropic的Claude系列 。为什么不本地部署LLaMA等开源大模型?

核心原因在于 任务复杂度与成本效益 。会议摘要和任务提取属于“小样本、高理解度”的任务,需要模型有很强的指令遵循和上下文理解能力。目前,顶尖的闭源模型在这些任务上的表现显著优于同体量的开源模型。此外,一次会议的处理通常只需要1-2次API调用,成本极低(一次会议几分钱),却可以节省人工数小时的工作。本地部署一个70B参数以上的模型,其硬件成本和响应速度,在当前阶段并不适合作为轻量级辅助工具的后端。

音频处理与流式传输 为了实现“准实时”的体验,我采用了 流式音频处理 。不是等整场会议录音结束才处理,而是将音频流切成小段(例如每3-5秒),送入Whisper进行“流式转录”。虽然Whisper本身并非为流式设计,但通过重叠分片、上下文窗口等技术,可以实现不错的实时效果。音频库方面, PyAudio sounddevice 用于捕获麦克风输入, librosa pydub 用于基础音频处理。

整体技术栈

  • 核心语言 :Python 3.10+。生态丰富,在AI和音频处理领域有绝对优势。
  • 语音识别 :OpenAI Whisper (本地 faster-whisper 实现,效率更高)。
  • 语言模型 :OpenAI GPT-4 Turbo API 或 Claude 3 Haiku API(兼顾效果、速度与成本)。
  • Web框架 :FastAPI。用于构建一个轻量的后端服务,方便提供API给前端或其它系统集成。
  • 前端(可选) :Streamlit。可以快速构建一个交互式的Web界面,用于展示实时转录和摘要结果。
  • 任务队列 :Celery + Redis。对于较长的录音文件处理,采用异步任务队列,避免阻塞主服务。
  • 数据存储 :SQLite(轻量级原型)或 PostgreSQL(团队部署)。存储会议记录、转录文本、摘要和任务。

注意 :技术选型不是一成不变的。例如,如果团队有强大的GPU服务器且极度注重隐私,可以研究本地部署像 Qwen2.5-72B 这样的开源大模型进行后处理。但就快速构建和效果而言,API方案是当前的最优解。

3. 核心模块实现与实操要点

3.1 音频采集与流式处理实现

音频输入是流水线的源头,其稳定性和质量至关重要。我放弃了简单的单次录音,而是实现了一个双缓冲区的音频流消费者。

import pyaudio
import numpy as np
import threading
from queue import Queue

class AudioStreamHandler:
    def __init__(self, rate=16000, chunksize=1024, buffer_seconds=5):
        self.RATE = rate
        self.CHUNK = chunksize
        self.BUFFER_SECONDS = buffer_seconds
        self.buffer = Queue()
        self.is_recording = False
        self.audio_interface = pyaudio.PyAudio()
        self.stream = None

    def start(self):
        """打开音频流并开始后台线程填充缓冲区"""
        self.is_recording = True
        self.stream = self.audio_interface.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=self.RATE,
            input=True,
            frames_per_buffer=self.CHUNK
        )
        # 启动消费者线程,不断从麦克风读取数据放入缓冲区
        self.thread = threading.Thread(target=self._record_loop)
        self.thread.start()

    def _record_loop(self):
        while self.is_recording:
            data = self.stream.read(self.CHUNK, exception_on_overflow=False)
            audio_int16 = np.frombuffer(data, dtype=np.int16)
            self.buffer.put(audio_int16)
            # 控制缓冲区大小,避免内存溢出
            if self.buffer.qsize() > (self.RATE * self.BUFFER_SECONDS) / self.CHUNK:
                self.buffer.get()

    def get_recent_audio(self, duration_seconds=3):
        """从缓冲区中获取最近N秒的音频数据,用于流式识别"""
        samples_needed = self.RATE * duration_seconds
        audio_chunks = []
        current_samples = 0

        # 临时列表存储取出的数据
        temp_buffer = []
        while not self.buffer.empty() and current_samples < samples_needed:
            chunk = self.buffer.get()
            temp_buffer.append(chunk)
            current_samples += len(chunk)

        # 拼接音频数据
        if temp_buffer:
            recent_audio = np.concatenate(temp_buffer)
            # 将未消费的数据(如果有多余)放回缓冲区头部是不安全的,这里简单设计为丢弃旧数据。
            # 更复杂的实现可以使用双端队列(collections.deque)固定长度。
            return recent_audio[:samples_needed]  # 确保返回指定长度
        return np.array([], dtype=np.int16)

    def stop(self):
        """停止录音并清理资源"""
        self.is_recording = False
        if self.thread.is_alive():
            self.thread.join()
        if self.stream:
            self.stream.stop_stream()
            self.stream.close()
        self.audio_interface.terminate()

实操要点与避坑

  1. 采样率统一 :Whisper模型通常期望16kHz的音频。确保你的音频输入、处理和输出全程保持16kHz,避免重采样带来的音质损失和错误。
  2. 异常处理 pyaudio 在部分系统或虚拟音频设备上可能会遇到 overflow 错误。使用 exception_on_overflow=False 参数并配合稳健的缓冲区设计,可以避免程序因短暂的音频卡顿而崩溃。
  3. 缓冲区设计 :上述示例是一个简化的生产者-消费者模型。在生产环境中,建议使用 collections.deque 并设置最大长度,实现一个真正的滑动窗口缓冲区,能更精确地控制内存使用和获取“最近N秒”的音频。
  4. 环境噪音 :对于嘈杂的会议室环境,可以在音频数据送入Whisper前,使用 noisereduce 库进行简单的降噪预处理,能有效提升识别准确率。

3.2 基于Faster-Whisper的流式语音识别

直接使用原版Whisper进行流式识别效率较低。 faster-whisper 是一个重写实现,使用了CTranslate2推理引擎,速度更快,内存占用更少,并且天然支持时间戳输出,这对后续的说话人分离和定位关键语句非常有帮助。

from faster_whisper import WhisperModel

class StreamTranscriber:
    def __init__(self, model_size="medium", device="cuda", compute_type="float16"):
        # 加载模型,首次运行会下载模型文件
        self.model = WhisperModel(model_size, device=device, compute_type=compute_type)
        self.segments_buffer = []  # 用于存储最近识别的文本片段,用于提供上下文

    def transcribe_chunk(self, audio_numpy):
        """转录一小段音频,并返回带时间戳的文本"""
        # audio_numpy 是 int16 的 numpy 数组
        if audio_numpy.size == 0:
            return ""
        
        # 转换为 float32 并归一化
        audio_float = audio_numpy.astype(np.float32) / 32768.0
        
        # 使用vad_filter过滤静音段,提升实时性
        segments, info = self.model.transcribe(
            audio_float,
            language="zh",
            vad_filter=True,
            initial_prompt="以下是技术讨论会议,涉及产品、开发、设计和市场等术语。" # 可选的初始提示,提升特定领域准确率
        )
        
        text_chunk = ""
        for seg in segments:
            # seg.text: 识别文本, seg.start/seg.end: 时间戳
            formatted_seg = f"[{seg.start:.1f}s-{seg.end:.1f}s] {seg.text}"
            text_chunk += formatted_seg + " "
            self.segments_buffer.append(formatted_seg)
            # 保持缓冲区大小,例如只保留最近30秒的上下文
            if len(self.segments_buffer) > 20:
                self.segments_buffer.pop(0)
        
        return text_chunk.strip()

    def get_recent_context(self):
        """获取最近一段时间内的转录上下文,用于提供给LLM做连贯性理解"""
        return " ".join(self.segments_buffer[-10:])  # 返回最近10个片段作为上下文

参数选择与调优经验

  • model_size tiny , base , small , medium , large-v3 。准确率依次增加,资源消耗也依次增大。经过测试,对于中文会议, medium 模型在准确率和速度上取得了最佳平衡。 small 模型速度更快,但在专业术语较多的技术会议上,准确率下降明显。
  • compute_type float16 在支持GPU的机器上能大幅提升速度且精度损失可接受。如果只有CPU,则使用 int8 (如果模型支持)或 float32
  • vad_filter :强烈建议开启。语音活动检测(VAD)可以自动过滤掉音频中的静音片段,极大减少送入模型的无用数据,提升“实时感”和整体处理速度。
  • initial_prompt :这是一个被很多人忽略但极其有效的技巧。你可以提供一个与会议主题相关的提示词,例如“这是一个关于量子计算架构评审的会议”,这能引导模型更好地识别领域专有名词,显著提升特定场景下的转录准确率。

3.3 利用大语言模型进行智能后处理

这是将原始转录文本转化为有价值信息的关键一步。我的策略是 分阶段、异步处理

阶段一:实时增量摘要 在会议进行中,每积累一定量的新文本(例如过去2分钟的内容),就触发一次LLM调用,生成一个非常简短的“最新进展”摘要。这能帮助参会者,尤其是中途加入者,快速跟上节奏。

import openai
# 或 from anthropic import Anthropic

def generate_live_summary(transcript_chunk, previous_context):
    """生成实时增量摘要"""
    prompt = f"""
你是一个专业的会议助理。请根据以下最新的会议对话片段,结合之前的讨论背景,生成一段不超过100字的简要总结,说明刚才这几分钟主要讨论了什么。

【之前的讨论背景(仅供参考)】
{previous_context}

【最新的对话片段】
{transcript_chunk}

请直接输出总结内容,不要加任何前缀或说明。
"""
    # 使用OpenAI API
    response = openai.ChatCompletion.create(
        model="gpt-4-turbo-preview", # 或使用更快的 "gpt-3.5-turbo"
        messages=[{"role": "user", "content": prompt}],
        temperature=0.2, # 低温度,保证摘要的稳定性和事实性
        max_tokens=150
    )
    return response.choices[0].message.content.strip()

阶段二:会后结构化提取 会议结束后,将完整的转录文本送入LLM,进行深度结构化解析。这里需要精心设计提示词(Prompt)来“约束”LLM的输出格式。

def extract_structured_notes(full_transcript):
    """从完整转录稿中提取结构化信息"""
    system_prompt = """你是一个顶尖的会议纪要整理专家。请仔细阅读会议转录文本,并严格按以下JSON格式输出信息。确保提取的信息准确、无遗漏。

输出格式:
{
  "meeting_title": "根据内容概括的会议主题",
  "attendees": ["从对话中推断出的参会人姓名或角色列表"],
  "key_decisions": [
    {"decision": "具体决策描述", "reason": "决策原因或背景", "timestamp": "在录音中的大致时间点"}
  ],
  "action_items": [
    {"task": "具体任务描述", "owner": "负责人(从对话中推断)", "deadline": "截止时间(如果提及)", "timestamp": "在录音中的大致时间点"}
  ],
  "main_topics": ["讨论的核心议题1", "核心议题2", ...],
  "summary": "一段全面的、段落式的会议总结,涵盖目标、讨论过程和结论。"
}
请只输出JSON,不要有任何其他解释文字。"""

    user_prompt = f"会议转录文本:\n{full_transcript}"

    response = openai.ChatCompletion.create(
        model="gpt-4-turbo-preview", # 此任务复杂,建议使用能力更强的模型
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt}
        ],
        temperature=0.1, # 极低的温度,确保格式稳定
        response_format={"type": "json_object"} # 要求API返回JSON对象,这是GPT-4 Turbo的新特性
    )
    
    import json
    try:
        structured_data = json.loads(response.choices[0].message.content)
        return structured_data
    except json.JSONDecodeError:
        # 如果JSON解析失败,可以记录日志并返回一个空结构,或进行重试
        return {"error": "Failed to parse LLM response as JSON"}

Prompt工程心得

  1. 角色设定与指令明确 :在System Prompt中清晰定义AI的角色和任务边界,这能显著提升输出的专业性和符合度。
  2. 输出格式锁定 :使用“严格按以下JSON格式输出”并给出具体样例,是保证后续程序能自动化处理的关键。利用GPT-4 Turbo的 response_format 参数可以进一步保证JSON输出的有效性。
  3. 提供上下文与约束 :在User Prompt中,除了提供文本,还可以加入约束,如“如果未提及,则对应字段为空字符串或空数组”,避免AI胡编乱造。
  4. 温度(Temperature)设置 :对于事实提取和结构化任务,温度应设置得很低(0.1-0.3),以减少随机性,保证输出稳定。对于创意性摘要,可以适当调高(0.7)。

4. 系统集成与前端展示

4.1 使用FastAPI构建后端服务

将上述模块封装成API,便于前端调用和集成到其他系统(如Teams、Slack机器人)。

from fastapi import FastAPI, WebSocket, BackgroundTasks
from pydantic import BaseModel
import json
import asyncio

app = FastAPI(title="AI Meeting Assistant API")

class MeetingProcessingRequest(BaseModel):
    audio_file_path: str = None
    meeting_id: str

@app.post("/process_meeting/")
async def process_meeting_full(request: MeetingProcessingRequest, background_tasks: BackgroundTasks):
    """异步处理完整会议录音文件"""
    meeting_id = request.meeting_id
    # 将任务放入后台队列,立即返回任务ID
    background_tasks.add_task(process_audio_file, request.audio_file_path, meeting_id)
    return {"status": "processing_started", "meeting_id": meeting_id, "message": "会议处理已进入队列"}

@app.websocket("/ws/live_transcribe/{meeting_id}")
async def websocket_live_transcribe(websocket: WebSocket, meeting_id: str):
    """WebSocket端点,用于接收前端传来的实时音频流(Base64编码)并进行实时转录和摘要"""
    await websocket.accept()
    transcriber = StreamTranscriber()
    try:
        while True:
            # 接收前端发送的音频数据块
            data = await websocket.receive_json()
            audio_chunk_base64 = data.get("audio")
            # 解码base64音频数据,转换为numpy数组...
            # audio_np = decode_audio_base64(audio_chunk_base64)
            
            # 进行流式转录
            # text_chunk = transcriber.transcribe_chunk(audio_np)
            
            # 获取近期上下文并生成实时摘要
            # recent_context = transcriber.get_recent_context()
            # if len(recent_context) > 500: # 积累一定文本后再摘要,避免频繁调用API
            #     live_summary = generate_live_summary(text_chunk, recent_context)
            #     await websocket.send_json({"type": "summary", "content": live_summary})
            
            # 将转录文本发回前端
            # await websocket.send_json({"type": "transcript", "content": text_chunk, "timestamp": time.time()})
            
            await asyncio.sleep(0.1) # 防止循环过紧
    except Exception as e:
        print(f"WebSocket error: {e}")
    finally:
        await websocket.close()

4.2 使用Streamlit打造轻量级Web界面

对于快速原型和内部工具,Streamlit是绝佳选择,它可以用纯Python脚本快速创建交互式应用。

# app_streamlit.py
import streamlit as st
import whisper
import openai
from audio_recorder_streamlit import audio_recorder # 一个Streamlit录音组件库
import json

st.set_page_config(page_title="AI会议记忆助手", layout="wide")
st.title("🎙️ AI会议记忆助手")

tab1, tab2 = st.tabs(["实时会议", "上传录音"])

with tab1:
    st.header("实时转录与摘要")
    meeting_id = st.text_input("输入本次会议标识")
    
    if 'transcriber' not in st.session_state:
        st.session_state.transcriber = WhisperModel("medium")
        st.session_state.full_transcript = ""
        st.session_state.summaries = []
    
    # 录音组件
    audio_bytes = audio_recorder(text="点击开始录音", pause_threshold=120.0)
    
    if audio_bytes:
        # 保存录音字节到临时文件,或直接处理
        with open("temp_audio.wav", "wb") as f:
            f.write(audio_bytes)
        
        # 调用本地Whisper进行转录
        with st.spinner("正在转录..."):
            segments, info = st.session_state.transcriber.transcribe("temp_audio.wav", language="zh", vad_filter=True)
            chunk_text = " ".join([seg.text for seg in segments])
            st.session_state.full_transcript += chunk_text + " "
        
        st.subheader("实时转录文本")
        st.write(st.session_state.full_transcript[-2000:]) # 显示最近2000字
        
        # 每积累一定文本,触发一次实时摘要
        if len(st.session_state.full_transcript) > 1000 and len(st.session_state.full_transcript) % 800 < 100:
            with st.spinner("生成实时摘要..."):
                summary = generate_live_summary(chunk_text, st.session_state.full_transcript[-3000:])
                st.session_state.summaries.append(summary)
                st.subheader("📌 最新摘要")
                st.info(summary)
    
    if st.button("结束会议并生成最终纪要"):
        if st.session_state.full_transcript:
            with st.spinner("正在生成结构化纪要..."):
                final_notes = extract_structured_notes(st.session_state.full_transcript)
                st.subheader("📋 最终会议纪要")
                st.json(final_notes) # 以JSON格式展示
                
                # 提供下载
                notes_str = json.dumps(final_notes, ensure_ascii=False, indent=2)
                st.download_button(
                    label="下载会议纪要 (JSON)",
                    data=notes_str,
                    file_name=f"meeting_notes_{meeting_id}.json",
                    mime="application/json"
                )
        else:
            st.warning("没有可处理的转录文本。")

with tab2:
    st.header("处理已有录音文件")
    uploaded_file = st.file_uploader("上传会议录音文件", type=['wav', 'mp3', 'm4a'])
    if uploaded_file is not None:
        # 保存上传的文件并处理
        # ... 处理逻辑类似,调用 process_audio_file 函数
        st.success("文件上传成功,开始处理...")

5. 部署、优化与常见问题排查

5.1 本地与服务器部署方案

开发环境(本地Mac/Windows)

  • 安装Python 3.10+,创建虚拟环境。
  • 安装 faster-whisper :它依赖 ctranslate2 ,在Mac上可能需要从源码编译,建议先尝试 pip install faster-whisper ,如果出错,参考其GitHub仓库的安装指南。
  • 对于GPU支持(CUDA),需要额外安装对应版本的CTranslate2和CUDA工具包。这步可能比较繁琐,如果仅用于测试,CPU模式( device="cpu" )也可运行,只是速度较慢。

生产环境(Linux服务器)

  1. 使用Docker容器化 :这是最推荐的方式。编写Dockerfile,基于NVIDIA CUDA镜像(如果需要GPU)或普通Python镜像,将依赖和代码打包。这保证了环境的一致性。
    FROM nvidia/cuda:12.1.1-runtime-ubuntu22.04
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
    
  2. 使用进程管理 :使用 systemd supervisord 来管理你的FastAPI服务(Uvicorn/Gunicorn进程),确保服务崩溃后能自动重启。
  3. 处理长任务 :对于上传整段录音文件处理这种耗时操作,务必使用Celery等异步任务队列,将任务丢到后台 worker 执行,通过WebSocket或轮询API向前端反馈状态。绝对不要在HTTP请求线程中执行长时间阻塞的操作。

5.2 性能优化与成本控制

  1. Whisper模型量化 faster-whisper 支持 int8 量化。对于CPU部署,使用 compute_type="int8" 可以大幅提升推理速度,且精度损失在可接受范围内。命令如: WhisperModel("medium", device="cpu", compute_type="int8")
  2. LLM API调用优化
    • 缓存 :对于内容相似的会议(如每日站会),可以设计一个简单的缓存机制,如果转录文本的语义哈希相似,则直接返回之前的处理结果,避免重复调用API。
    • 模型选择 :实时摘要对速度要求高,可以使用更小、更快的模型(如 gpt-3.5-turbo Claude 3 Haiku )。会后深度分析对质量要求高,再用 GPT-4 Claude 3 Sonnet
    • 合并请求 :如果处理多个会议,可以将它们的转录文本适当合并后一次性发送给LLM(注意上下文长度限制),比多次调用小文本更节省token。
  3. 音频预处理 :在音频送入Whisper前,使用 librosa 进行简单的预处理,如 librosa.effects.trim() 自动切除首尾静音, librosa.util.normalize 进行音量归一化,这些小操作能提升识别效果。

5.3 常见问题与排查实录

在实际开发和团队试用中,我遇到了不少问题,这里记录下最典型的几个及其解决方案。

问题一:实时转录延迟高,跟不上说话速度。

  • 现象 :说话结束好几秒后,文字才显示出来。
  • 排查
    1. 检查音频缓冲区大小。如果每次送给Whisper的音频片段太长(比如10秒),那么必须等这10秒录完才能开始识别,自然有延迟。尝试将片段缩短到2-3秒。
    2. 检查Whisper模型大小。 large-v3 模型虽然准,但速度慢。在实时场景下, medium small 是更合适的选择。
    3. 检查是否开启了 vad_filter=True 。开启后,模型会跳过静音部分,能有效减少处理数据量,降低延迟。
  • 解决 :综合调整后,我采用的参数是: model_size="medium" , chunk_length_seconds=3 , vad_filter=True 。在RTX 4060笔记本GPU上,延迟可以控制在1-2秒内,基本满足实时辅助的需求。

问题二:LLM返回的JSON格式经常出错,导致程序解析失败。

  • 现象 json.loads() 抛出异常,返回的文本可能缺少引号、多了换行或根本不是JSON。
  • 排查
    1. 检查Prompt。最初我的Prompt只说了“请输出JSON”,但没有给出严格的格式示例。LLM的自由度太高。
    2. 检查 temperature 参数。如果温度设置过高(如0.7),LLM的输出随机性太强。
    3. 检查模型。 gpt-3.5-turbo 在复杂格式遵循上不如 gpt-4-turbo 稳定。
  • 解决
    1. 强化Prompt :在System Prompt中明确写出“严格按以下JSON格式输出”,并提供一个完整的、带样例值的JSON结构。
    2. 降低温度 :将 temperature 设为0.1或0.2。
    3. 使用GPT-4 :对于关键的结构化提取任务,切换到 gpt-4-turbo-preview ,并利用其 response_format={"type": "json_object"} 参数,这能极大保证输出合法性。
    4. 添加容错 :在代码中添加 try-except ,解析失败时记录原始响应,并尝试用字符串匹配或正则表达式进行二次提取,或给用户一个友好的错误提示。

问题三:说话人分离(谁说了哪句话)效果不理想。

  • 现象 :转录文本混在一起,无法区分不同发言人。
  • 分析 :Whisper本身不提供说话人分离(Diarization)功能。这是一个独立的课题。
  • 解决方案
    1. 集成专业工具 :使用 pyannote.audio 这样的专业说话人分离库。它可以先识别音频中有几个不同的声音,并为每一段音频打上“说话人A”、“说话人B”的标签。然后,将带有时间戳的Whisper转录结果,与 pyannote.audio 输出的说话人时间段进行对齐匹配。这是效果最好的方法,但计算复杂度高,且 pyannote.audio 需要单独申请许可。
    2. 基于规则的简单区分 :对于小型固定团队会议,可以做一个简化版。在会议开始时,让每个参会者依次说一句固定的话(如自己的名字)。程序记录下每个人声音的简短声纹特征(通过 librosa 提取MFCC等)。在后续会议中,通过比较音频片段的特征与预录声纹的相似度,来猜测发言人。这种方法实现简单,但在多人、声音相似或环境嘈杂时准确率很低。
    3. LLM智能推断 :将完整的、不带说话人标签的转录文本交给GPT-4,在Prompt中要求它根据对话内容、语气和上下文,推断出哪些话可能是谁说的。例如:“请分析以下会议记录,推断不同发言人所讲的内容。已知参会者有:张三(产品经理)、李四(后端开发)、王五(前端开发)。请以‘张三:’、‘李四:’的形式重新整理记录。” 这种方法完全依赖文本上下文,对于角色职责清晰的对话有时有奇效,但并非真正的声学分离。

问题四:会议涉及大量专业术语、英文缩写,识别错误率高。

  • 现象 :将“API”识别为“阿皮”、“K8s”识别为“k八s”。
  • 解决
    1. 使用 initial_prompt 参数 :这是Whisper的一个隐藏利器。在调用 transcribe 时,传入一个包含本次会议可能涉及的关键词和领域的提示文本。例如: initial_prompt="本次会议讨论Kubernetes集群部署、API网关设计和React前端性能优化。" 这能极大地引导模型向这些词汇靠拢。
    2. 后处理替换 :建立一个公司内部的“术语词典”(例如:{“k八s”: “K8s”, “阿皮爱”: “API”}),在转录完成后,对全文进行一次快速的查找替换。
    3. LLM纠错 :将转录文本交给LLM,Prompt为“请纠正以下技术会议转录稿中的专业术语错误,只输出纠正后的文本”。LLM在上下文理解的基础上,纠错效果通常比简单替换更好。

这个项目从构思到落地,是一个典型的“用AI解决具体场景问题”的过程。技术本身并非高不可攀,关键在于对需求痛点的精准把握,以及对现有工具链(Whisper, GPT API)的灵活组合和调优。最大的体会是,在AI应用开发中,Prompt工程和系统架构设计的重要性,有时甚至超过了编写复杂算法。现在,我们的团队每周通过这个助手自动生成数十份会议纪要,节省了大量的手工劳动,让会议信息真正流动和沉淀了下来。你可以根据自己团队的具体情况,对这个方案进行裁剪和增强,比如集成到飞书或钉钉机器人中,实现更无缝的体验。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐