1. 项目概述与核心价值开完会大家一拍即合任务也分配了感觉充满了干劲。但没过两天当你想确认“上次会上说谁负责那个接口文档”或者“我们上次讨论的那个方案最终定的预算是多少来着”的时候往往就傻眼了。聊天记录翻半天笔记东一块西一块甚至不同参会者记的要点都有出入。这种“会议记忆流失”的问题几乎困扰着每一个团队直接导致了重复沟通、任务遗漏和决策执行不到位。我自己在带项目和参与协作时也深受其苦。于是我动手构建了一个“AI会议记忆助手”。它的核心目标很简单让每一次会议的讨论和决策都能被系统性地记住并能在需要时被智能地回忆起来。这不是一个简单的笔记应用而是一个结合了持久化存储、语义检索和大语言模型推理能力的智能体。你可以告诉它会议内容它帮你记住你可以用自然语言向它提问它结合所有历史记忆给你准确的答案。比如你问“谁负责后端开发”它能立刻从过去几次会议的记录中找到并回答“是张三”而不是让你去翻几周前的聊天记录。我选择用Groq的云API作为大模型引擎主要是看中其极致的推理速度。在需要实时交互的助手场景下用户问完问题等上好几秒才出结果体验会大打折扣。Groq凭借其独特的LPU架构能提供近乎瞬时的响应这对于打造一个“无感”的智能助手至关重要。整个项目基于Python用Streamlit快速搭建了交互界面记忆存储则用了最朴素的JSON文件确保轻量、可控且易于扩展。下面我就把这个项目的设计思路、技术实现细节、踩过的坑以及一些扩展思考完整地分享出来。无论你是想直接复用这个工具还是希望了解如何为AI应用添加“记忆”能力相信都能从中获得启发。2. 系统架构与核心组件解析2.1 整体设计思路从“聊天”到“记忆体”市面上很多AI对话工具本质是“健忘的”。每次对话都是独立的模型并不记得你几分钟前说过什么。我们要构建的是一个有“记忆”的智能体。其核心工作流可以概括为一个决策循环输入判别系统接收到用户输入后首先需要判断这是一个需要存储的“新记忆”会议笔记还是一个需要解答的“问题”。记忆存储如果是笔记则对其进行结构化处理并存入持久化的记忆库。记忆检索如果是问题则从记忆库中搜索与问题最相关的历史记录。智能合成将检索到的相关记忆作为上下文连同用户问题一并提交给大语言模型要求其基于这些记忆生成答案。响应输出将模型生成的答案返回给用户。这个流程的关键在于大模型本身并不存储记忆记忆的存储和检索是由外部系统管理的。模型只负责在给定的上下文即检索到的记忆中进行理解和生成。这种架构也被称为“检索增强生成”RAG Retrieval-Augmented Generation它有效扩展了模型的知识边界并让其回答具备了事实依据。2.2 技术栈选型与考量为什么是这套技术组合每一个选择背后都有具体的权衡后端语言Python理由在AI和数据处理领域Python拥有最丰富的生态系统如NumPy, Pandas, LangChain等。无论是调用AI API还是处理文本、文件都能找到成熟、高效的库。开发速度快社区支持好。备选考量对于追求极高并发或性能的场景可能会考虑Go或Rust但本项目更侧重快速原型验证和智能逻辑Python是效率最高的选择。大模型APIGroq理由这是本项目的关键决策。Groq并非一个模型提供商而是一个提供超高速推理服务的平台。它支持运行诸如Llama、Mixtral等开源模型。速度优势其自研的LPU语言处理单元推理引擎速度远超传统GPU方案。对于交互式应用响应延迟通常低于1秒体验流畅。成本优势提供免费的额度非常适合个人项目、原型开发和中小规模使用。模型选择支持Meta的Llama 3等顶尖开源模型在性能和效果上有保障。备选考量OpenAI的GPT系列模型效果稳定但API调用有延迟且成本较高本地部署模型如通过Ollama虽无网络延迟和费用但对硬件有要求且推理速度远慢于Groq。综合权衡速度、成本和易用性Groq在现阶段是此类实时助手应用的最佳选择之一。Web框架Streamlit理由Streamlit的理念是“将脚本变为可分享的Web应用”。它允许你用纯Python代码快速创建交互式界面无需前端知识。对于数据科学和机器学习项目来说能极大地降低UI开发门槛让我们专注于核心逻辑。操作意图我们只需要一个简单的输入框、按钮和结果显示区域。Streamlit几行代码就能实现并且天然支持会话状态管理非常适合快速构建原型。记忆存储JSON文件理由简单、直观、无需额外服务。所有记忆以列表形式保存在一个memory.json文件中每条记忆包含内容、时间戳等元数据。读取和追加操作都非常方便。局限性当记忆条数非常多例如数万条时全量加载和检索效率会变低。但这对于大多数团队的会议记录场景几百上千条完全足够。这是一个典型的“先用起来再优化”的选择。扩展路径未来如果需要可以轻松迁移到更专业的向量数据库如Chroma, Pinecone实现基于语义相似度的更精准检索。配置管理python-dotenv理由安全地将敏感信息如Groq API Key与代码分离。通过一个.env文件管理配置代码中通过os.getenv读取。这避免了将密钥硬编码在代码中并误上传至GitHub的风险。3. 核心模块实现细节3.1 记忆系统的构建存储与检索记忆系统是助手的大脑皮层。我们设计它不仅要能存还要能高效、准确地取。3.1.1 记忆的数据结构每条记忆不应该只是一段文本。为了后续的检索和管理我们需要附加一些元数据。# 一条记忆的典型结构 { “id”: “550e8400-e29b-41d4-a716-446655440000”, # 唯一标识符 “content”: “项目启动会确定前端由李四负责使用Vue3框架后端由王五负责使用Spring Boot。下次会议时间是下周一。”, # 记忆的核心内容 “timestamp”: “2023-10-27T10:30:00Z”, # 创建时间用于排序和时效性判断 “type”: “meeting_note” # 可扩展的类型如“action_item”, “decision”等 }注意id使用UUID生成确保全局唯一。timestamp使用ISO 8601格式便于在不同系统间解析和排序。3.1.2 记忆的存储逻辑存储逻辑的核心是原子化操作和异常处理。import json import uuid from datetime import datetime, timezone MEMORY_FILE “memory.json” def save_memory(new_content): “”“将新内容作为一条记忆保存到JSON文件”“” # 1. 准备新记忆条目 new_memory { “id”: str(uuid.uuid4()), “content”: new_content, “timestamp”: datetime.now(timezone.utc).isoformat(), # 使用UTC时间 “type”: “meeting_note” } # 2. 读取现有记忆如果文件不存在则初始化为空列表 try: with open(MEMORY_FILE, ‘r’, encoding‘utf-8’) as f: memories json.load(f) if not isinstance(memories, list): # 防止文件内容被意外篡改 memories [] except (FileNotFoundError, json.JSONDecodeError): memories [] # 3. 追加新记忆 memories.append(new_memory) # 4. 写回文件使用临时文件原子替换模式防止写入过程中崩溃导致数据损坏 temp_file f“{MEMORY_FILE}.tmp” with open(temp_file, ‘w’, encoding‘utf-8’) as f: json.dump(memories, f, ensure_asciiFalse, indent2) # 美化输出方便调试 os.replace(temp_file, MEMORY_FILE) # 原子性替换原文件 return new_memory3.1.3 记忆的检索策略当用户提问时我们需要从所有记忆中找出最相关的部分。这里采用了简单的关键词匹配作为初版检索方案。def search_memories(query, memories, top_k5): “”“从记忆列表中检索与查询最相关的top_k条记忆。 初版采用基于关键词的TF-IDF或简单字符串匹配。 ”“” # 简化版包含查询关键词即视为相关 query_words set(query.lower().split()) scored_memories [] for mem in memories: content mem[“content”].lower() # 计算匹配的关键词数量作为相关性分数 score sum(1 for word in query_words if word in content) if score 0: scored_memories.append((score, mem)) # 按分数降序排序返回分数最高的top_k条 scored_memories.sort(keylambda x: x[0], reverseTrue) return [mem for _, mem in scored_memories[:top_k]]实操心得初版使用关键词匹配是为了快速实现。但它的缺点很明显无法处理语义相似但用词不同的情况如“后端开发”和“服务器端编程”。这是第一个待优化的关键点。生产环境应升级为使用文本嵌入模型如OpenAI的text-embedding-3-small或开源的BGE模型将记忆和查询都转换为向量然后通过计算余弦相似度来检索这才是真正的“语义搜索”。3.2 智能推理引擎Groq API集成记忆检索提供“材料”Groq模型负责“烹饪”出最终答案。集成的关键在于构造一个清晰的提示词Prompt。3.2.1 API调用封装import os from groq import Groq from dotenv import load_dotenv load_dotenv() # 从.env文件加载环境变量 class GroqClient: def __init__(self): # 从环境变量读取API Key确保安全 api_key os.getenv(“GROQ_API_KEY”) if not api_key: raise ValueError(“请在 .env 文件中设置 GROQ_API_KEY”) self.client Groq(api_keyapi_key) # 选择模型例如Llama 3最新版本 self.model “llama3-70b-8192” # 可根据需要更换为 mixtral-8x7b-32768 等 def generate_response(self, prompt, max_tokens500): “”“调用Groq API生成回复”“” try: chat_completion self.client.chat.completions.create( messages[{“role”: “user”, “content”: prompt}], modelself.model, temperature0.7, # 控制创造性0.7在准确性和流畅性间取得平衡 max_tokensmax_tokens, streamFalse, # 非流式一次性返回完整结果 ) return chat_completion.choices[0].message.content except Exception as e: # 详细的错误处理便于排查 error_msg f“调用Groq API时出错: {type(e).__name__}: {str(e)}” # 可以在这里加入重试逻辑或降级方案 return f“抱歉处理您的请求时出现错误{error_msg}”3.2.2 提示词工程这是决定答案质量的核心。我们需要明确告诉模型你的角色是什么你拥有哪些上下文你需要如何回答问题。def build_prompt(user_question, relevant_memories): “”“构建给大模型的提示词”“” # 1. 系统指令定义助手的行为准则 system_instruction “““你是一个专业的会议记忆助手。你的任务是严格基于用户提供的“会议记忆”来回答问题。 如果答案在提供的记忆中明确存在请直接、准确地给出答案。 如果记忆中的信息不足以完全回答问题请基于已有信息进行回答并说明哪些部分是根据已知信息推断的。 如果记忆内容与问题完全无关请如实告知“根据现有会议记录无法回答此问题”。 请使用简洁、清晰、专业的语言。不要编造记忆中不存在的信息。 ”“” # 2. 组织上下文将检索到的记忆格式化 context_parts [] for i, mem in enumerate(relevant_memories, 1): # 可以加入时间信息让模型了解时效性 time_str mem.get(“timestamp”, “未知时间”)[:10] # 取日期部分 context_parts.append(f“记忆片段{i} ({time_str}): {mem[‘content’]}”) context “\n\n”.join(context_parts) if context_parts else “【暂无相关记忆】” # 3. 组合成最终提示词 prompt f“““ {system_instruction} 以下是相关的会议记忆记录 {context} 用户问题{user_question} 请根据以上记忆回答问题 “““ return prompt注意事项提示词中的指令必须清晰、无歧义。强调“严格基于提供的信息”是为了防止模型幻觉Hallucination即编造不存在的内容。temperature参数设置为0.7在保证答案有一定自然语言灵活性的同时尽量维持事实准确性。如果追求绝对稳定可以调低至0.3。3.3 用户界面Streamlit应用搭建Streamlit让前端开发变得极其简单。我们的界面主要包含四个功能区。import streamlit as st # 页面配置 st.set_page_config(page_title“AI会议记忆助手”, layout“wide”) st.title(“ AI会议记忆助手”) st.caption(“记录会议要点并向你的智能记忆库提问”) # 初始化会话状态和核心组件 if ‘memories’ not in st.session_state: st.session_state.memories load_all_memories() # 从JSON文件加载所有记忆 if ‘groq_client’ not in st.session_state: st.session_state.groq_client GroqClient() # 功能区1输入区域 with st.container(): st.subheader(“记录或提问”) user_input st.text_area(“输入新的会议记录或者提出你的问题”, height100, key“input_box”) col1, col2 st.columns(2) with col1: submit_as_note st.button(“ 保存为会议记录”, use_container_widthTrue) with col2: submit_as_question st.button(“❓ 提问”, use_container_widthTrue) # 功能区2处理逻辑与结果显示 if submit_as_note and user_input: with st.spinner(“正在保存记忆...”): new_mem save_memory(user_input) st.session_state.memories.append(new_mem) # 更新会话状态 st.success(f“已保存记忆ID: {new_mem[‘id’][:8]}...”) # 清空输入框的小技巧 st.rerun() # 或者使用 st.session_state.input_box “” elif submit_as_question and user_input: with st.spinner(“正在检索记忆并思考...”): # 检索相关记忆 relevant_mems search_memories(user_input, st.session_state.memories, top_k3) # 构建提示词并生成答案 prompt build_prompt(user_input, relevant_mems) answer st.session_state.groq_client.generate_response(prompt) # 显示答案 st.subheader(“助手回答”) st.markdown(f“** {answer}**”) # 可选项显示本次参考了哪些记忆 with st.expander(“查看本次参考的记忆片段”): for mem in relevant_mems: st.caption(f“{mem[‘timestamp’][:10]} - {mem[‘content’][:100]}...”) # 功能区3记忆库浏览 with st.expander(“ 浏览全部记忆库”, expandedFalse): if st.session_state.memories: # 按时间倒序排列最新的在最前面 sorted_mems sorted(st.session_state.memories, keylambda x: x[‘timestamp’], reverseTrue) for mem in sorted_mems: st.markdown(f“**{mem[‘timestamp’][:10]}** - {mem[‘content’]}”) st.caption(f“ID: {mem[‘id’][:8]}”) st.divider() else: st.info(“记忆库为空。”) # 功能区4管理操作 with st.sidebar: st.subheader(“系统管理”) if st.button(“️ 清空所有记忆”, type“secondary”): # 添加确认步骤防止误操作 if st.checkbox(“我确认要清空所有记忆此操作不可撤销。”): clear_all_memories() st.session_state.memories [] st.rerun() st.caption(f“当前共有 {len(st.session_state.memories)} 条记忆。”)这个界面虽然简洁但功能完整。它清晰地分离了“记录”和“提问”两种意图提供了记忆的浏览和管理入口并在回答时给出了可追溯的参考来源增加了系统的可信度。4. 部署、优化与问题排查实录4.1 从开发到部署关键步骤项目写好了怎么让别人也能用这里提供两种最实用的方式。4.1.1 本地运行开发与测试环境准备确保安装Python 3.8。创建一个新的虚拟环境是好的习惯。python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows安装依赖将项目所需的库写入requirements.txt文件。streamlit1.28.0 groq0.3.0 python-dotenv1.0.0然后安装pip install -r requirements.txt配置密钥在项目根目录创建.env文件填入你的Groq API Key。GROQ_API_KEYyour_actual_groq_api_key_here重要安全提醒务必在.gitignore文件中添加.env绝对不要将此文件提交到Git等版本控制系统。运行应用streamlit run app.pyStreamlit会自动在浏览器打开本地地址通常是http://localhost:8501。4.1.2 云端部署以Streamlit Community Cloud为例想让团队成员通过链接直接访问部署到云端是最佳选择。Streamlit提供了极其简单的免费部署服务。准备仓库将代码除了.env和memory.json推送到GitHub仓库。配置部署登录 share.streamlit.io 。点击“New app”选择你的仓库、分支和主文件app.py。设置密钥在部署页面的“Secrets”选项卡中以TOML格式添加你的环境变量。# .streamlit/secrets.toml GROQ_API_KEY “your_actual_groq_api_key_here”这样云端应用就能安全地读取到API Key了。部署点击“Deploy”。几分钟后你就会获得一个公开的URL可以分享给任何人。实操心得Streamlit Community Cloud对免费用户有资源限制但对于中小型团队的记忆助手完全够用。注意它默认是公开的如果你的会议记录涉密需要考虑设置密码保护或寻找私有化部署方案如部署在内部服务器或使用支持私有应用的Paas平台。4.2 性能与功能优化方向初始版本可用但还有巨大的优化空间。4.2.1 检索系统升级从关键词到向量如前所述关键词检索是短板。升级到向量检索的步骤选择嵌入模型可以使用OpenAI的text-embedding-3-small便宜且效果好或者开源的BAAI/bge-small-zh-v1.5中文优化无需API调用。向量化记忆在保存每条记忆时不仅存文本还用嵌入模型计算出其向量表示一并存储。向量化查询当用户提问时同样将问题转换为向量。相似度计算使用向量数据库如ChromaDB或简单的余弦相似度计算找出与问题向量最相似的记忆向量。这能极大提升“找得准”的能力比如问“谁负责和客户对接”即使记忆里写的是“张三担任客户联络人”也能被准确检索到。4.2.2 记忆的“遗忘”与总结记忆库会无限增长旧的无用信息会成为检索的噪音。可以引入两种机制基于时间的衰减给每条记忆一个“活跃度”分数随着时间推移而降低检索时优先考虑高活跃度的记忆。自动总结定期如每周让AI对过去一段时间的大量细碎记忆进行总结生成一条高度凝练的“周报”式记忆并归档或删除原始细节保持记忆库的精炼。4.2.3 支持文件上传允许用户直接上传会议录音转文字、会议纪要文档Word、PDF由系统自动解析并提取关键信息存入记忆库这将大幅降低用户的使用门槛。4.3 常见问题与排查技巧在实际开发和运行中我遇到了以下几个典型问题这里分享排查思路。4.3.1 Groq API调用失败或超时症状界面长时间转圈最后报错“调用API出错”。排查步骤检查网络确保运行环境能正常访问Groq的API端点api.groq.com。验证API Key在.env文件或云端Secrets中确认GROQ_API_KEY是否正确无误且没有过期。可以尝试在命令行用curl简单测试。查看额度登录Groq控制台确认免费额度或付费额度是否用尽。捕获具体错误在代码的except块中打印完整的错误信息。常见错误如AuthenticationError密钥错、RateLimitError超频、APIConnectionError网络问题。解决方案根据错误信息对应解决。对于超频问题需要在代码中加入指数退避的重试机制。4.3.2 检索结果不相关导致回答不准症状回答的内容明显是错的或者“答非所问”。排查步骤检查检索函数在提问后打印出search_memories函数返回的“相关记忆”列表。看看系统到底找到了什么。分析查询与记忆对比用户的问题和检索到的记忆文本看关键词是否匹配或者是否因为语义不同而漏检。检查提示词打印出构建好的完整prompt确认提供给模型的上下文是否准确、完整。解决方案如果关键词匹配失败考虑引入更宽松的匹配如分词、词干提取或直接升级为向量检索。如果是提示词问题调整系统指令使其更严格地要求“基于上下文”。4.3.3 Streamlit应用刷新后状态丢失症状点击按钮后页面刷新输入框内容清空或者某些临时状态没了。原因Streamlit脚本在每次交互后都会从头到尾重新执行。默认情况下变量会被重新初始化。解决方案正确使用st.session_state。所有需要在多次交互间保持的数据如记忆列表、客户端对象都必须存入st.session_state。# 正确做法 if ‘memories’ not in st.session_state: st.session_state.memories load_memories() # 之后都使用 st.session_state.memories 进行操作4.3.4 JSON文件读写冲突或格式损坏症状保存记忆时程序崩溃或者再次启动时无法读取memory.json文件。原因多线程/进程同时写文件或者写入过程中程序被中断。解决方案使用原子写入如前文代码所示先写入临时文件.tmp成功后再用os.replace原子性地替换原文件。这能保证在任何时刻磁盘上都有一个完整的JSON文件。添加文件锁如果应用可能有并发写入如部署后多用户同时访问需要考虑使用文件锁fcntl或portalocker来防止冲突。做好异常处理和备份在读取文件时捕获JSONDecodeError并尝试从备份中恢复。构建这个AI会议记忆助手的过程是一次将前沿AI能力与具体生产力场景结合的愉快实践。它验证了“外部记忆体大语言模型”这一架构的可行性。目前这个版本已经能解决团队会议信息留存和查找的核心痛点。最大的体会是AI应用的开发难点往往不在模型调用本身而在于如何围绕模型设计好数据流、交互逻辑和异常处理。从简单的关键词检索升级到向量检索从本地文件升级到数据库从单轮对话扩展到支持多模态输入每一个环节都有大量的优化空间。这个项目就像一个乐高底座你可以根据自己的需求不断往上添加更强大的功能模块。