1. 项目概述:一个能记住会议所有细节的AI助手

如果你和我一样,经常需要参加各种线上会议,那你一定也经历过这样的场景:会议开到一半,突然有人问起“刚才我们讨论的那个数据是多少来着?”,或者会议结束后,你看着密密麻麻的笔记,却死活想不起来某个关键结论是谁、在哪个时间点提出的。更别提后续整理会议纪要时,要从一两个小时的录音或录像里大海捞针,这个过程既耗时又痛苦。

“AI Meeting Memory Assistant”这个项目,就是为了解决这个痛点而生的。它本质上是一个基于Python开发的智能会议助手,核心目标就是 让AI成为你的“第二大脑” ,帮你全程记录、理解并结构化会议中的所有信息。它不是一个简单的录音笔,而是一个能听懂对话、识别发言人、提炼要点、并随时回答你关于会议内容提问的智能伙伴。想象一下,会后你只需要问它:“刚才张三对项目时间线的建议是什么?”或者“我们一共讨论了几个风险点?”,它就能像一位记忆力超群的同事一样,立刻给你准确的答案和上下文。

这个项目非常适合需要频繁开会、进行深度讨论的团队或个人,比如产品经理、项目经理、研发团队、咨询顾问以及远程协作的团队。它不仅能解放你的双手,让你更专注于会议本身,更能将宝贵的会议内容从“一次性消耗品”转变为可随时检索、可分析的结构化知识资产。接下来,我将从零开始,拆解如何用Python构建这样一个实用的AI会议记忆助手,分享我在实现过程中的核心思路、踩过的坑以及那些让工具真正好用的实战技巧。

2. 核心架构设计与技术选型

构建一个完整的AI会议记忆助手,需要一套能够处理音频、理解自然语言、存储和检索信息的流水线。我们不能只依赖某个单一的“魔法”API,而是需要将多个模块有机组合起来。下图清晰地展示了整个系统的核心工作流程与数据流转:

flowchart TD
    A[“会议音频/视频输入”] --> B[“音频处理模块<br>(分离、转写、分轨)”]
    B --> C[“核心AI引擎<br>(LLM理解与摘要)”]
    C --> D[“向量知识库<br>(存储与索引)”]
    D --> E[“查询接口<br>(自然语言问答)”]
    
    B -- “原始文本<br>+时间戳+说话人” --> C
    C -- “结构化摘要<br>+关键点+动作项” --> D
    E -- “用户自然语言提问” --> D
    D -- “基于语义的<br>相似度匹配” --> E
    E --> F[“返回带上下文的精准答案”]

2.1 整体技术栈的考量

基于上图的工作流,我们的技术选型需要覆盖每一个环节。我选择以Python作为主力语言,因为它拥有最丰富的AI和数据处理生态。

  • 音频处理与语音转文本(ASR) :这是数据入口。我放弃了需要复杂本地环境的传统方案,选择了 OpenAI的Whisper 。理由很充分:它的识别准确率,尤其是对中文和带有专业术语的对话,在开源模型中表现突出,且API调用简单。虽然Whisper也开源,但直接使用其API能省去大量的环境配置和模型优化工作,快速获得可用结果。对于需要区分不同发言人的场景(声纹分离),则配合使用 pyannote.audio 这个专业的说话人日志工具。
  • 自然语言理解与摘要(核心AI) :这是大脑。转写出来的文本是冗长的流水账,需要被理解、提炼。这里我选择了 大型语言模型 。初期可以使用OpenAI的GPT-3.5/4 API,它的对话理解、摘要和指令跟随能力极强,能快速验证想法。后期为了成本、隐私和定制化,可以转向开源的Llama 3、Qwen等模型,通过 LangChain 这类框架来组织提示词和调用流程。
  • 记忆存储与检索 :这是海马体。传统的数据库(如SQLite)适合存储结构化结果(如最终纪要),但无法应对“用自然语言查找相关内容”的需求。因此,必须引入 向量数据库 。我将会议文本切成片段,通过文本嵌入模型转换成向量,存入 ChromaDB Qdrant 。当用户提问时,问题也被转换成向量,数据库能快速找到语义最相关的会议片段,提供给LLM生成最终答案。这是实现“记忆”功能的关键。
  • 应用层与调度 :这是神经系统。使用 FastAPI 来构建提供问答接口的Web服务,用 Celery Dramatiq 来异步处理耗时的音频转写和摘要任务,避免阻塞用户请求。整个项目的依赖和环境用 Poetry 管理,清晰又干净。

2.2 为什么是模块化设计?

你可能注意到,我的架构图中每个模块都是相对独立的。这是有意为之。模块化设计带来了几个巨大优势:

  1. 可替换性 :如果明天有了比Whisper更准、更快的ASR服务,我只需要更换音频处理模块,不影响其他部分。同样,LLM和向量数据库都可以根据需求升级或替换。
  2. 易于调试 :当问答结果不准时,我可以分别检查:是转写出错了?还是向量检索没找到正确片段?或者是LLM的理解有偏差?问题被隔离,排查效率极高。
  3. 灵活性 :这个助手不仅可以处理实时会议(接入Zoom/Teams的Webhook),也能处理离线录音文件。核心流水线是通用的。

注意:成本与隐私的平衡 :在技术选型初期,我强烈建议先从成熟的云API(如OpenAI)开始。这能让你在几天内就搭建出一个可用的、效果惊艳的原型,快速验证核心价值。当原型得到认可后,再根据实际需求(如会议内容高度敏感、调用量巨大导致成本激增)来规划向开源模型和本地化部署的迁移。不要一开始就陷入部署百亿参数模型的泥潭。

3. 分步实现与核心代码解析

理论讲完了,我们动手把它建起来。我会按照数据流动的顺序,讲解关键步骤和代码。

3.1 第一步:从声音到文字——高精度会议转录

会议音频通常是一个混合了多人声音的单声道或立体声文件。我们的目标是得到带时间戳和说话人标签的文本。

# 示例:使用 Whisper API 进行音频转写
import openai
from pydub import AudioSegment
import os

def transcribe_meeting(audio_file_path, api_key):
    """
    将会议音频文件转录为文本
    :param audio_file_path: 音频文件路径(支持mp3, wav, m4a等)
    :param api_key: OpenAI API密钥
    :return: 包含文本和时间戳的转录结果
    """
    openai.api_key = api_key
    
    # 1. 检查并预处理音频文件(Whisper API对文件大小和格式有要求)
    audio = AudioSegment.from_file(audio_file_path)
    # 如果音频过长,可以按固定时长(如10分钟)分割处理,避免API超时
    # 这里简化处理,假设会议音频在25分钟以内
    
    with open(audio_file_path, "rb") as audio_file:
        transcript = openai.Audio.transcribe(
            model="whisper-1",
            file=audio_file,
            response_format="verbose_json", # 获取带时间戳的详细结果
            language="zh" # 指定中文,提高准确率
        )
    
    # transcript.segments 包含了分段文本及其起止时间
    raw_segments = transcript.segments
    return raw_segments

# 得到的 raw_segments 结构示例:
# [
#   {"id": 0, "start": 0.0, "end": 4.0, "text": "大家好,我们开始今天的项目评审会。"},
#   {"id": 1, "start": 5.2, "end": 10.5, "text": "我先来回顾一下上周的进展。"},
#   ...
# ]

如果会议中多人同时发言或需要严格区分发言人,单纯的Whisper就不够了。这时需要引入 说话人分离(Speaker Diarization)

# 示例:结合 pyannote.audio 进行说话人分离
from pyannote.audio import Pipeline
import torch

def diarize_and_transcribe(audio_file_path, whisper_api_key, hf_token):
    """
    先分离说话人,再为每一段语音转写文本
    注意:pyannote.audio 需要 Hugging Face Token 并接受用户协议
    """
    # 1. 加载预训练的说话人分离管道
    pipeline = Pipeline.from_pretrained(
        "pyannote/speaker-diarization-3.1",
        use_auth_token=hf_token
    )
    
    # 2. 应用管道到音频文件,得到“谁在什么时候说话”的结果
    diarization = pipeline(audio_file_path)
    
    # diarization 结果是一个包含多个片段的集合,每个片段有开始、结束时间和说话人标签
    # 例如: speaker_A [00:01 --> 00:05], speaker_B [00:06 --> 00:10]
    
    # 3. 根据分离的时间段,裁剪音频并分别调用 Whisper 转写
    # 这是一个简化示例,实际中需要处理音频裁剪和分段调用
    final_segments = []
    for segment, _, speaker in diarization.itertracks(yield_label=True):
        # 裁剪出该说话人片段的音频(需使用pydub或类似库)
        clip = audio[segment.start*1000:segment.end*1000] # 转换为毫秒
        clip.export(f"temp_{speaker}_{segment.start}.wav", format="wav")
        
        # 转写该片段
        with open(f"temp_{speaker}_{segment.start}.wav", "rb") as f:
            clip_transcript = openai.Audio.transcribe(
                model="whisper-1", 
                file=f,
                response_format="verbose_json"
            )
        
        # 将转写结果与说话人标签结合
        for sub_seg in clip_transcript.segments:
            # 调整时间戳为原始音频中的绝对时间
            adjusted_start = segment.start + sub_seg.start
            adjusted_end = segment.start + sub_seg.end
            final_segments.append({
                "start": adjusted_start,
                "end": adjusted_end,
                "speaker": speaker,
                "text": sub_seg.text
            })
    
    # 按时间排序
    final_segments.sort(key=lambda x: x["start"])
    return final_segments

实操心得:音频预处理是关键 。背景噪音、过低的音量、多人重叠发言都会严重影响转写和分离的准确率。在调用API前,不妨先用 pydub 进行简单的预处理: audio = audio.normalize().high_pass_filter(100) (标准化音量并过滤低频噪音)能有效提升效果。对于重叠发言,目前没有完美的解决方案,在会议开始时请与会者轮流发言、不要抢话,是最有效的“人工预处理”。

3.2 第二步:从流水账到结构化理解——LLM的摘要与提炼

拿到带时间戳和说话人的转录文本后,它仍然是一份冗长的“文字录音”。我们需要LLM来理解内容,并提取出关键信息。这里我设计了一个两阶段处理法:

阶段一:实时或分批的增量摘要 在长会议中,可以每10分钟或当一个议题结束时,将最近的文本发送给LLM,生成该部分的要点。这既降低了单次处理的文本长度,也提供了会议的中间视角。

# 示例:使用 LangChain 调用 LLM 进行会议摘要
from langchain.chat_models import ChatOpenAI
from langchain.schema import HumanMessage, SystemMessage
from langchain.prompts import ChatPromptTemplate

def generate_summary(transcript_segments, api_key):
    """
    根据转录片段生成结构化摘要
    """
    llm = ChatOpenAI(model_name="gpt-3.5-turbo", openai_api_key=api_key, temperature=0)
    
    # 将片段合并成连贯的文本,并标注说话人
    formatted_text = ""
    for seg in transcript_segments:
        speaker = seg.get("speaker", "Unknown")
        formatted_text += f"[{speaker} at {seg['start']:.1f}s]: {seg['text']}\n"
    
    # 构建一个专业的提示词(Prompt)
    prompt_template = ChatPromptTemplate.from_messages([
        SystemMessage(content="你是一个专业的会议秘书,擅长从冗长的会议录音转录稿中提取结构化信息。请保持客观、准确。"),
        HumanMessage(content=f"""请分析以下会议对话内容,并提取以下信息:
1. **核心议题**:会议讨论了哪几个主要话题?
2. **关键结论与决定**:针对每个话题,达成了什么结论或做出了什么决定?
3. **待办事项(Action Items)**:列出所有明确的任务,包括内容、负责人(从对话中推断)和截止时间(如有提及)。
4. **遗留问题与风险**:记录会议上提出但未解决的分歧、疑问或潜在风险。

会议转录文本:
{formatted_text}

请用清晰的中文,以JSON格式输出,包含以下键:topics, conclusions, action_items, open_issues。""")
    ])
    
    # 调用LLM
    response = llm(prompt_template.format_messages())
    # 假设LLM返回了合法的JSON字符串
    import json
    try:
        structured_summary = json.loads(response.content)
    except json.JSONDecodeError:
        # 如果返回的不是纯净JSON,这里需要添加后处理逻辑,比如用正则提取
        structured_summary = {"error": "Failed to parse LLM response"}
    
    return structured_summary

阶段二:全局总结与问答素材准备 会议结束后,将所有的增量摘要和完整的转录文本(或代表性片段)再次交给LLM,生成一份最终的、全面的会议纪要。同时,为了后续的问答,我们需要将完整的转录文本切分成有意义的“块”,以便存入向量数据库。

def prepare_chunks_for_vectordb(transcript_segments, chunk_size=500, overlap=50):
    """
    将转录文本切分成有重叠的块,用于构建向量数据库。
    重叠(overlap)可以避免一个完整的句子或观点被割裂在两个块中。
    """
    from langchain.text_splitter import RecursiveCharacterTextSplitter
    
    full_text = " ".join([seg["text"] for seg in transcript_segments])
    
    # 使用递归字符分割器,优先按句子、然后按词语分割,尽量保持语义完整
    text_splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,  # 每个块大约500字符
        chunk_overlap=overlap,   # 块之间重叠50字符
        length_function=len,
        separators=["\n\n", "。", ";", ",", " ", ""]
    )
    
    chunks = text_splitter.split_text(full_text)
    
    # 为每个块附加元数据,如大致的起止时间,方便溯源
    chunk_with_metadata = []
    for chunk in chunks:
        # 这是一个简化的映射,实际项目中需要更精确地将文本块映射回时间区间
        chunk_with_metadata.append({
            "text": chunk,
            "metadata": {"type": "transcript_chunk"}
        })
    return chunk_with_metadata

注意事项:提示词工程决定输出质量 。LLM只是一个强大的“执行者”,你给它的指令(提示词)决定了它工作的质量。我的经验是:

  1. 角色设定 :明确告诉AI它的角色(如“专业会议秘书”),这能引导其采用合适的口吻和关注点。
  2. 结构化输出要求 :明确要求输出格式(如JSON、Markdown),并定义好键名,这能极大简化后续的数据处理。
  3. 提供示例 :对于特别复杂的提取任务,在提示词中给一两个输入输出的例子(Few-shot Learning),效果会立竿见影。
  4. 分而治之 :不要一次性让LLM处理数万字的文本并要求它做所有事情。先总结,再基于总结提问,或者像上面那样分阶段处理,效果更好、成本也更低。

3.3 第三步:构建“记忆”本身——向量数据库的搭建与检索

这是实现“随时问答”功能的核心。我们使用文本嵌入模型将文本块转换为向量(一组高维数字),语义相近的文本,其向量在空间中的距离也更近。

# 示例:使用 ChromaDB 存储和检索会议记忆
import chromadb
from chromadb.config import Settings
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma

class MeetingMemory:
    def __init__(self, persist_directory="./meeting_memory_db", embedding_api_key=None):
        # 初始化嵌入模型
        self.embeddings = OpenAIEmbeddings(openai_api_key=embedding_api_key)
        # 初始化Chroma客户端,设置持久化目录
        self.client = chromadb.PersistentClient(path=persist_directory)
        # 创建一个集合(类似数据库的表),用于存放本次会议的片段
        self.collection = self.client.get_or_create_collection(name="meeting_transcripts")
        
    def add_meeting_chunks(self, chunks_with_metadata):
        """
        将处理好的文本块添加到向量数据库
        """
        texts = [item["text"] for item in chunks_with_metadata]
        metadatas = [item["metadata"] for item in chunks_with_metadata]
        # 生成ID,可以用时间戳+索引
        ids = [f"chunk_{i}" for i in range(len(texts))]
        
        # 直接使用 Chroma 客户端添加,LangChain 的包装有时不够灵活
        self.collection.add(
            documents=texts,
            metadatas=metadatas,
            ids=ids
        )
        print(f"已添加 {len(texts)} 个文本块到记忆库。")
    
    def search_memory(self, query, n_results=3):
        """
        在记忆库中搜索与查询最相关的片段
        """
        results = self.collection.query(
            query_texts=[query],
            n_results=n_results
        )
        # results 结构: {'ids': [...], 'documents': [...], 'metadatas': [...], 'distances': [...]}
        return results
    
    def qa_with_context(self, query, llm, memory_search_results):
        """
        结合检索到的上下文,让LLM生成最终答案
        """
        # 将检索到的文档拼接成上下文
        context = "\n\n---\n\n".join(memory_search_results['documents'][0])
        
        prompt = f"""你是一个会议助手,基于以下会议记录片段来回答问题。如果记录中没有足够信息,请如实告知。
        
        相关会议记录:
        {context}
        
        问题:{query}
        
        请给出准确、简洁的回答,并可以注明信息来源于会议的哪个部分(如果片段中有时间信息)。"""
        
        response = llm.predict(prompt)
        return response

3.4 第四步:让一切运转起来——整合与API服务

最后,我们需要一个简单的应用将以上模块串联起来。我使用FastAPI来创建一个Web服务,提供两个主要端点:一个用于提交和处理会议音频,一个用于问答。

# 示例:FastAPI 主应用骨架
from fastapi import FastAPI, UploadFile, File, HTTPException
from celery import Celery # 用于异步任务
import uuid
import os

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

# 配置异步任务队列(例如使用Redis作为broker)
celery_app = Celery('tasks', broker='redis://localhost:6379/0')

# 内存中存储任务状态(生产环境应使用数据库)
task_status = {}

@celery_app.task
def process_meeting_task(file_path, meeting_id):
    """后台异步处理会议音频的Celery任务"""
    # 1. 转录
    segments = transcribe_meeting(file_path, os.getenv("OPENAI_API_KEY"))
    # 2. 生成摘要
    summary = generate_summary(segments, os.getenv("OPENAI_API_KEY"))
    # 3. 准备文本块并存入向量库
    chunks = prepare_chunks_for_vectordb(segments)
    memory_db = MeetingMemory(persist_directory=f"./db/{meeting_id}")
    memory_db.add_meeting_chunks(chunks)
    
    # 更新任务状态
    task_status[meeting_id] = {"status": "completed", "summary": summary}
    # 清理临时文件
    os.remove(file_path)
    return meeting_id

@app.post("/upload/")
async def upload_meeting(file: UploadFile = File(...)):
    """上传会议音频文件,触发异步处理"""
    meeting_id = str(uuid.uuid4())
    file_location = f"./uploads/{meeting_id}_{file.filename}"
    
    with open(file_location, "wb+") as f:
        f.write(await file.read())
    
    # 异步启动处理任务
    process_meeting_task.delay(file_location, meeting_id)
    task_status[meeting_id] = {"status": "processing"}
    
    return {"meeting_id": meeting_id, "message": "文件已上传,处理中"}

@app.get("/summary/{meeting_id}")
async def get_summary(meeting_id: str):
    """获取会议摘要"""
    status = task_status.get(meeting_id)
    if not status:
        raise HTTPException(status_code=404, detail="会议ID不存在")
    if status["status"] != "completed":
        return {"status": status["status"]}
    return {"status": "completed", "summary": status["summary"]}

@app.post("/ask/{meeting_id}")
async def ask_question(meeting_id: str, question: str):
    """针对特定会议提问"""
    # 加载该会议对应的向量数据库
    memory_db = MeetingMemory(persist_directory=f"./db/{meeting_id}")
    # 在记忆中搜索
    search_results = memory_db.search_memory(question)
    # 初始化LLM
    llm = ChatOpenAI(model_name="gpt-3.5-turbo", openai_api_key=os.getenv("OPENAI_API_KEY"))
    # 结合上下文生成答案
    answer = memory_db.qa_with_context(question, llm, search_results)
    return {"question": question, "answer": answer}

4. 部署优化与成本控制实战

一个能跑起来的原型和一个稳定、可用的服务之间,还有很长的路要走。以下是几个关键的优化方向:

4.1 性能与成本优化策略

  1. 音频预处理降本 :Whisper API按时长收费。在上传前,使用 pydub 检测并 静音修剪 可以显著减少处理时长和成本。 silence_thresh=-40dB, min_silence_len=500 是不错的起始参数。
  2. LLM调用优化
    • 缓存 :对相同的或相似的提问,缓存LLM的回答。可以使用 langchain CacheBackedEmbeddings SemanticCache
    • 小模型优先 :对于简单的信息提取和问答,尝试使用更小、更快的模型(如 gpt-3.5-turbo 甚至 text-embedding-3-small ),在效果可接受的情况下能大幅降低成本。
    • 提示词精简 :去除提示词中不必要的描述,使用更简洁的指令。
  3. 向量检索优化
    • 分库存储 :不要将所有会议的文本都塞进一个向量集合。按会议ID或日期分库,能提升检索速度和准确度。
    • 元数据过滤 :在检索时,除了语义相似度,还可以利用元数据过滤。例如,当用户问“张三说了什么”,你可以先过滤出说话人标签为“张三”的片段,再进行向量检索,结果更精准。

4.2 提升准确性的技巧

  1. 转录后处理 :Whisper的输出可能存在一些口语化赘词或错误。可以编写简单的规则或用一个极小的语言模型进行后处理,比如纠正明显的数字错误、统一专有名词的翻译。
  2. 混合检索(Hybrid Search) :单纯依靠向量检索(语义搜索)有时会漏掉包含关键词但表述方式不同的内容。结合 关键词检索(稀疏检索) ,例如使用BM25算法,将两者的结果融合,能获得更全面、更鲁棒的检索效果。许多现代向量数据库(如Qdrant、Weaviate)都支持混合检索。
  3. 让LLM“引用”来源 :在让LLM基于检索到的上下文生成答案时,在提示词中要求它 注明来源 。例如:“请在你的回答末尾,用括号注明你所依据的原文片段编号或大致时间点。” 这增加了答案的可信度和可追溯性。

4.3 从云服务到本地化部署的路径

当项目成熟,考虑数据隐私和长期成本时,迁移到本地模型是必然选择。

  1. ASR本地化 :Whisper模型本身是开源的,你可以下载 large-v3 模型,使用 faster-whisper (一个CTranslate2的实现)在本地CPU或GPU上运行,速度更快。 pyannote.audio 也可以完全本地运行。
  2. Embedding模型本地化 :替代OpenAI的 text-embedding-ada-002 ,可以选择开源的 BGE GTE Snowflake Arctic 嵌入模型,通过 SentenceTransformers 库调用,效果接近甚至在某些任务上超越闭源模型。
  3. LLM本地化 :这是最具挑战性的一步。需要根据你的硬件(GPU内存)选择合适的模型。7B参数左右的模型(如Llama 3 8B, Qwen 7B)在消费级显卡上可运行,适合总结和简单问答。更大的模型需要更多资源。可以使用 Ollama vLLM text-generation-inference 来部署和管理本地模型。LangChain同样支持连接这些本地端点。

迁移是一个渐进过程。我建议先从Embedding模型开始本地化,因为它的调用最频繁,对延迟敏感,且本地化能完全消除数据外传风险。ASR和LLM可以视情况逐步迁移。

5. 常见问题与排查实录

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

5.1 音频处理相关

  • 问题:转录结果中专业术语或人名错误百出。

    • 排查 :检查Whisper API调用是否指定了正确的 language 参数。对于中英文混合的会议,不指定语言或指定中文可能效果更好。
    • 解决 :使用 prompt 参数!Whisper支持提供上下文提示。你可以将本次会议涉及的项目名、成员姓名、专业词汇作为提示词传入,能显著提升这些词汇的识别准确率。例如: prompt="本次会议涉及项目‘天枢’,参与者有张三、李四、王五。讨论术语包括KPI、ROI、API网关。"
  • 问题:说话人分离将同一个人分成了多个不同ID。

    • 排查 :这通常是因为音频质量不佳(有回声、背景音)或说话人声音变化较大(有时激动有时平静)。
    • 解决 :优化音频输入源(使用好的麦克风)。对于 pyannote.audio ,可以尝试调整其管道中的 min_duration_on min_duration_off 参数,让算法对短促的停顿更不敏感。或者,在后期处理中,根据语音特征(如音高、语速)对相似的片段进行聚类合并。

5.2 LLM与摘要相关

  • 问题:LLM生成的摘要遗漏了重要细节,或者自己“编造”了内容。

    • 排查 :首先检查提供给LLM的上下文是否包含了这些细节。可能是文本块切分时把关键信息割裂了。
    • 解决 :调整文本分割器的 chunk_size chunk_overlap 。对于重要会议,可以尝试不分割全文,而是让LLM先识别出“重要段落”,再对这些段落进行精读摘要。在提示词中强调“严格基于提供文本,不要臆测”。
  • 问题:Action Items(待办事项)提取不全,尤其是负责人提取错误。

    • 排查 :中文会议中,任务分配常常很隐晦,比如“这个小明你来跟进一下”。
    • 解决 :在提示词中强化对Action Item的提取要求,并给出明确的格式示例。例如:“任务描述:[具体内容],负责人:[从对话中推断的人名,如不确定则标记‘待确认’],截止时间:[如果提及]”。可以让LLM分两步走:先提取所有包含任务意向的句子,再逐一分析这些句子分配负责人。

5.3 检索与问答相关

  • 问题:问答时,AI总是回答“根据会议记录,没有相关信息”,但实际上有。

    • 排查 :检查向量检索返回的 top_k 个结果是否真的包含答案。可能是检索到的片段不相关,或者相关但语义匹配度不够高没排进前几名。
    • 解决 :1. 增加 top_k (例如从3增加到5)。2. 尝试使用不同的嵌入模型,有些模型对中文语义相似度的捕捉更好。3. 启用上文提到的 混合检索 ,确保关键词匹配的内容也能被召回。
  • 问题:回答正确,但无法定位到原文具体位置。

    • 解决 :这是在存储文本块时埋下的伏笔。确保每个文本块都带有足够精确的元数据,比如 start_time end_time 。在检索到相关块后,可以将这个时间信息一并返回给用户。更高级的做法是,让LLM在生成答案时,从它使用的原文块中复制出最相关的原句,作为“引用”展示。

5.4 部署与运维相关

  • 问题:处理一个1小时的会议音频,API调用耗时太长,用户等待不耐烦。

    • 解决 :这正是我们使用 Celery 等异步任务队列的原因。上传接口应立即返回一个 task_id ,处理在后台进行。前端可以通过轮询另一个接口(如 /task_status/{task_id} )来获取处理进度和最终结果。对于用户体验至关重要。
  • 问题:随着会议数量增加,向量数据库查询变慢。

    • 解决 :1. 索引优化 :ChromaDB默认使用HNSW索引,确保其参数 M ef_construction 设置合理(文档有推荐值)。2. 分集合 :如前所述,按会议分集合或分目录存储。3. 硬件 :向量检索是计算密集型,考虑使用更快的CPU或支持相似度计算加速的硬件。

构建一个AI会议记忆助手,就像打造一个数字化的会议协作者。它不会取代人类的思考和决策,但能完美地承担起“记录员”和“记忆库”的角色。从简单的脚本开始,逐步迭代,加入说话人识别、混合检索、本地模型,你会发现这个工具的价值随着每一次迭代而增长。最让我有成就感的时刻,不是在代码跑通的那一刻,而是在一次激烈的脑暴会议后,团队成员自然地转向这个助手提问:“我们刚才否决的那个方案,主要反对理由是什么?” 而它能瞬间给出清晰、准确的回答。那一刻,你会觉得所有的折腾都是值得的。

更多推荐