发布时间:2026/9/4 22:29:07
基于FastAPI与Chroma实现AI长期记忆系统:从概念到代码实践 EverMind-AI/EverOS 从命名上可以读出三层信号Ever 强调时间维度上的持久Mind 指向认知和记忆OS 则暗示 AI 应用需要一套类似操作系统的基础设施而不是只有一次性的对话请求。这类以“AI 原生知识工作空间”为目标的项目核心命题可以概括为如何让 AI 在多次会话、多天工作时长、多条项目任务之间稳定地保留信息并能在需要时把信息重新取回。这里不计论某个仓库的具体功能清单而是把 EverMind-AI/EverOS 当作一个工程方向来看。真正值得拆解的是它提出的一个问题当前大模型虽然有长上下文能力但每次对话结束后模型并不会天然记住用户说过什么、决定过什么、推进到什么状态。如果 AI 要成为长期可用的工作伴侣就必须在模型之外拥有一套“记忆系统”。这篇内容会给出一个可以本地运行的参考实现。它基于 Python FastAPI 和向量数据库 Chroma实现一个最小记忆服务接收一条新记忆、在语义空间里索引、在下一次提问时召回最相关的若干条记忆并把召回内容交给大模型组织回答。整体代码量不大适合作为理解 EverOS 类项目记忆子系统的起点。动手跑通后这套结构可以迁移到个人知识库、会议纪要助手、用户画像记忆、客服多轮对话甚至多智能体调度等场景。1. 先看懂 EverOS 这类项目真正要解决的技术命题1.1 持久记忆和上下文窗口是两个不同的问题很长一段时间里大模型工程讨论主要围绕“上下文窗口能塞多少 token”。窗口确实在变大但它不等于记忆。窗口里的内容来自当前请求或者开发者临时拼接的上下文会话结束后这些内容不会自动沉淀为可复用的长期信息。EverOS 这类项目的切入点就是沿着“长期记忆”方向做基础设施。它们的目标不是挑战模型的能力而是让模型拥有一个“体外大脑”模型在推理时读取记忆在工作后写入新记忆。模型本身不需要把所有历史都背下来只需要把可以压缩、检索和更新的记忆层放在模型之外。从工程结构上看这一层很像操作系统里的文件系统或数据库。AI 应用要运行得久、运行得稳前提是数据能被写入、索引、查询和删除。只把一句提示词越拼越长并不会形成真正的项目记忆。1.2 记忆层承担的五个动作回忆一个完整的记忆闭环至少要覆盖以下链路动作说明常见实现接收接受一条新的原始记忆文本输入、对话记录、任务结果结构化给记忆打上类型、时间、项目等元数据分类、字段抽取索引让记忆可以被语义检索生成向量并写入向量库召回根据当前问题筛选最相关内容向量相似度检索反馈把召回内容注入模型回答流程拼接提示词或上下文只完成接收和索引不完成召回等于把笔记本写满却永远不打开。只完成召回不完成反馈记忆也不会影响 AI 的答案。判断一个自建记忆系统是否成熟最简单的方式就是看这五步是否都能闭环。1.3 参考实现的范围与边界后面的演示不会假设 EverOS 当前代码长什么样也不去复刻它的真实源码。而是构造一个命名为 evermind-ref 的小服务用它演示“AI 记忆系统”最基本的工程形态。服务分为两个边界记忆层负责写入、索引、检索。这是本文核心。回答层负责把召回的上下文交给大模型并生成最终答案。在记忆层中输入可以是任意文本片段输出是一组相似记忆及其距离分数。在回答层中输入是用户问题输出是结合记忆后的回答文本。学习阶段重点看记忆层。生产阶段还要额外考虑权限、审计、数据删除、多租户隔离等事情相关内容会在最后一章展开。2. 搭建环境和项目骨架选择一个本地可跑的最小技术栈2.1 为什么使用 FastAPI、Chroma 和 SentenceTransformer一个便于演示的记忆服务必须满足三个条件本地能跑、依赖不复杂、能够体现语义检索真实过程。FastAPI 负责提供 HTTP API它和 Pydantic 配合方便适合快速写出可测试服务。Chroma 负责存储文本和向量并把向量索引放在本地目录中无需额外启动一个数据库服务。SentenceTransformer 负责把文本转成向量它在本地完成计算不需要配置外部模型服务也能规避“没有 API Key 就无法运行”的问题。这套组合不是生产环境唯一答案但它很适合进入学习链路。生产上如果数据量很大把 Chroma 替换为支持分布式部署的向量数据库并不困难因为上层面向的是统一的 collection 查询语义。2.2 环境要求与依赖版本建议使用 Python 3.10 到 3.12。太新的 Python 版本有时会和 Chroma 底层依赖兼容性不一致如果遇到 sqlite 或 pydantic 相关报错先确认 Python 版本再继续排查。创建的 requirements.txt 内容如下fastapi0.110.0 uvicorn[standard]0.29.0 chromadb0.4.24 sentence-transformers3.0.0 openai1.24.0 python-dotenv1.0.0说明chromadb 负责本地持久化集合。sentence-transformers 负责本地向量化。openai 用来请求兼容 OpenAI Chat Completions 接口的大模型服务。python-dotenv 用来读取 .env 配置文件。安装命令pip install -r requirements.txt在不使用大模型的情况下只依赖 fastapi、uvicorn、chromadb、sentence-transformers、python-dotenv 就能跑通记忆写入和检索。openai 包只有在调用最终问答接口时才需要。2.3 目录结构设计项目结构如下evermind-ref/ ├── app │ ├── __init__.py │ ├── config.py │ ├── schemas.py │ ├── memory_store.py │ ├── llm.py │ └── main.py ├── .env.example ├── requirements.txt └── data/data 目录是运行时动态生成的存放 Chroma 的持久化文件。如果数据需要备份只需要备份这个目录并通过环境变量重新指向项目外的独立路径。2.4 配置入口与数据约定config.py 的职责是从环境变量和 .env 文件读取配置import os from pathlib import Path from dotenv import load_dotenv load_dotenv() class Settings: data_dir: Path Path(os.getenv(EVEROS_DATA_DIR, ./data)) collection_name: str os.getenv(EVEROS_COLLECTION_NAME, everos_memory) embedding_model: str os.getenv(EMBEDDING_MODEL, all-MiniLM-L6-v2) llm_api_key: str os.getenv(LLM_API_KEY, ) llm_base_url: str os.getenv(LLM_BASE_URL, ) llm_model: str os.getenv(LLM_MODEL, gpt-4o-mini) settings Settings()配置项含义如下环境变量默认值说明EVEROS_DATA_DIR./data向量数据库持久化目录EVEROS_COLLECTION_NAMEeveros_memoryChroma collection 名称EMBEDDING_MODELall-MiniLM-L6-v2文本向量化模型LLM_API_KEY空调用大模型服务的密钥LLM_BASE_URL空兼容接口服务地址空时使用 SDK 默认地址LLM_MODELgpt-4o-mini实际模型名需要注意load_dotenv 默认只从当前工作目录读取 .env。如果 uvicorn 不是从项目根目录启动环境变量可能加载不到。稳妥做法是显式指定路径或者在启动前手动 export。3. 核心实现把“记住”和“回忆”变成 HTTP API3.1 先用 Pydantic 定义记忆的数据模型开始写代码前先定义输入输出的数据结构后续所有模块都围绕这套结构展开。schemas.py 内容如下from typing import Any, Dict, List, Literal, Optional from pydantic import BaseModel, Field MemoryKind Literal[fact, episode, task] class MemoryCreate(BaseModel): content: str Field(..., min_length1, max_length2000, description记忆正文) kind: MemoryKind Field(defaultfact, description记忆类型) memory_id: Optional[str] Field(defaultNone, description业务侧记忆ID为空则自动生成) metadata: Dict[str, Any] Field(default_factorydict, description附加元数据) class MemoryRecord(BaseModel): memory_id: str kind: str content: str created_at: str metadata: Dict[str, Any] class RecallRequest(BaseModel): query: str Field(..., min_length1, description用户问题或检索语句) top_k: int Field(default3, ge1, le20, description返回记忆条数) kind: Optional[str] Field(defaultNone, description按记忆类型过滤) class MemoryHit(BaseModel): memory: MemoryRecord distance: float class RecallResponse(BaseModel): query: str hits: List[MemoryHit] class AskRequest(BaseModel): question: str Field(..., min_length1) top_k: int Field(default3, ge1, le20) class AskResponse(BaseModel): answer: str recalled: List[MemoryHit]这里把记忆类型限制为 fact、episode、task 三种分别表示事实、交互片段和任务状态。类型不是严格的数据库约束但它在实际项目中很有用。例如回答“用户公司主营什么”时只需要查 fact 类型而复盘“上次执行到哪一步”时更适合查 task 类型。3.2 MemoryStore处理元数据归一化Chroma 的 metadata 只能保存 str、int、float、bool 这类标量值。如果直接写入 dict 或 list会抛类型异常。因此写入前需要做一次拍平处理把非标量转换成 JSON 字符串。memory_store.py 的完整实现如下import json import uuid from datetime import datetime, timezone from typing import Any, Optional import chromadb from chromadb.utils import embedding_functions from .schemas import MemoryCreate, MemoryRecord def _normalise_value(value: Any) - Any: if isinstance(value, (str, int, float, bool)): return value return json.dumps(value, ensure_asciiFalse) def _normalise_metadata(metadata: dict[str, Any]) - dict[str, Any]: return {str(key): _normalise_value(value) for key, value in metadata.items()} class MemoryStore: def __init__(self, settings): self.settings settings settings.data_dir.mkdir(parentsTrue, exist_okTrue) embed_fn embedding_functions.SentenceTransformerEmbeddingFunction( model_namesettings.embedding_model ) self.client chromadb.PersistentClient(pathstr(settings.data_dir)) self.collection self.client.get_or_create_collection( namesettings.collection_name, embedding_functionembed_fn, metadata{hnsw:space: cosine}, ) def add_memory(self, payload: MemoryCreate) - MemoryRecord: content payload.content.strip() if not content: raise ValueError(memory content cannot be empty) memory_id payload.memory_id or str(uuid.uuid4()) created_at datetime.now(timezone.utc).isoformat() metadata { kind: payload.kind, created_at: created_at, } metadata.update(_normalise_metadata(payload.metadata)) self.collection.add( ids[memory_id], documents[content], metadatas[metadata], ) return MemoryRecord( memory_idmemory_id, kindpayload.kind, contentcontent, created_atcreated_at, metadatapayload.metadata, ) def recall( self, query: str, top_k: int, kind: Optional[str] None, ) - tuple[list[MemoryRecord], list[float]]: count self.collection.count() if count 0: return [], [] safe_k max(1, min(top_k, count)) kwargs: dict[str, Any] { query_texts: [query], n_results: safe_k, include: [documents, metadatas, distances], } if kind: kwargs[where] {kind: kind} result self.collection.query(**kwargs) ids result[ids][0] documents result[documents][0] metadatas result[metadatas][0] distances result[distances][0] memories [] for index in range(len(ids)): meta metadatas[index] or {} extra_metadata { key: value for key, value in meta.items() if key not in (kind, created_at) } memories.append( MemoryRecord( memory_idids[index], kindmeta.get(kind, fact), contentdocuments[index], created_atmeta.get(created_at, ), metadataextra_metadata, ) ) return memories, distances几个关键点需要解释存储时把 kind 和 created_at 放进 Chroma 的 metadata是为了支持按类型过滤和返回时间。返回给调用方时又把 kind 和 created_at 从普通 metadata 中拆出来让 API 响应更清晰。safe_k max(1, min(top_k, count))是为了避免分页边界错误。Chroma 在请求条数超过集合内文档数时可能直接报错。配置{hnsw:space: cosine}表示使用余弦距离计算相似度。distance 越小表示语义越接近。3.3 召回入口与过滤逻辑“回忆”接口其实不需要单独写另一个类。真正要做的是把用户的查询文本转成向量再在集合中做向量相似度搜索。向量搜索和关键词搜索不一样。用户如果问“上周末我设计了什么功能”关键词搜索只能匹配“上周末”这类字面词很难把“周六下午完成了记忆模块设计”这条记录捞回来。向量搜索把两句话映射到同一个语义空间里即使字面上不重叠只要含义接近距离也会较近。因此 recall 方法保留了 kind 过滤能力。在多条记忆混合存放时可以继续使用 metadata。比如检索任务状态时限定kindtask避免把用户闲聊内容也带进答案。3.4 LLM 模块把召回记忆组装成提示词记忆检索完成后需要把召回结果送给大模型。这里的关键问题是提示词组装不能让模型把所有召回都当成真实结论必须让模型区分“相关记忆”和“无关记忆”。llm.py 内容如下from typing import Any from openai import OpenAI from .config import settings def build_prompt(question: str, recalled_items: list[dict[str, Any]]) - list[dict[str, str]]: lines [] for item in recalled_items: lines.append(f- 类型({item[kind]}): {item[content]}) memory_text \n.join(lines) if lines else - 暂无相关记忆 system_content ( 你是一个带长期记忆的 AI 助手。 回答问题时请优先依据“召回记忆”中与当前问题相关的信息。 如果召回记忆与问题无关请明确说明没有找到相关记忆

相关新闻

2026/9/4 22:29:07

Milvus Bootcamp入门指南:从零跑通向量检索示例

如果你最近在 GitHub 上搜索过向量数据库相关的学习材料,大概率会碰到milvus-io/bootcamp这个仓库。关于它,一个直接的结论是:如果你想系统性地入门 Milvus,这个仓库比零散的技术博客和纯文档更适合作为第一份学习材料。因为它解决…

2026/9/4 22:29:07

Milvus Bootcamp上手指南:从向量数据库部署到语义搜索与RAG

Milvus 官方 Bootcamp 仓库到底提供了什么?这是很多刚接触向量数据库的程序员第一个想问的问题。它的定位不是 SDK 源码,也不是简单的 Hello World 集合,而是一整套面向真实业务的“示例项目 落地教程”。如果你打算做 RAG 知识库、以图搜图…

2026/9/4 22:24:07

基于51单片机与Proteus的货车侧翻检测系统仿真全流程解析

简介:本资源是一套面向嵌入式初学者与课程设计者的51单片机实践项目,聚焦货车侧翻风险实时监测这一典型安全应用场景。系统以Proteus仿真为核心,通过滑动变阻器模拟车身两侧高度差,实现倾斜度阈值可设、超限自动报警与模拟刹车功能…

2026/9/4 23:39:40

一切皆插件:DSH 插件机制如何让命令行 AI 助手从思考走向执行

如果你已经在命令行里跑过几轮 AI 助手做真实任务,大概率遇到过这样一个卡点:任务进行到一半,模型明明知道该怎么做,却没有能力去执行。你想让它读取一份 PDF、解析某个页面、把上一步的结果落到指定目录,它只能给你一…

2026/9/4 23:39:40

观察者模式与 Spring 事件机制解耦核心业务通知

观察者模式与 Spring 事件机制解耦核心业务通知在大型企业级应用开发中,随着业务迭代推进,核心主链路代码往往会面临严重的“功能膨胀”与“强耦合”危机。以电商系统的“订单创建成功”或“支付成功”为例,最初的代码可能只有简洁的几行订单…

2026/9/4 23:39:40

从大厂到创业:技术人系统性决策的五步实操框架

余家辉离职Meta创业,这条新闻这几天在技术社区传得很快。“7亿年薪留不住”这几个字天然带冲突感,如果只看热闹,很容易把讨论变成两派:有人说看不懂,有人当作励志样本。我的看法不太一样,一个做过多年技术、…

2026/9/4 23:39:40

C#实现EASY521工业控制器Modbus通讯实战指南

简介:本资源是一个面向C#开发者、聚焦工业或企业级网络通讯场景的EASY521协议实践项目,适用于具备基础.NET框架与Socket编程能力的中高级学习者,解决C#环境下快速集成与调试EASY521协议的实际需求。压缩包共34个文件,含7个核心C#源…

2026/9/3 18:28:26

vSound小提琴数字处理器实操指南:从接线到演出的完整配置

电小提琴或者原声小提琴插电演出,第一个绕不开的坎就是声音难听。原声琴的共鸣和空气感一旦进了拾音器,出来的往往是一坨干瘪、发尖、带着奇怪塑料味的信号。我当初第一次把琴接上乐队调音台,直接被主唱吐槽"你这声音像在锯钢丝"。…

2026/9/3 14:29:47

传感器接口IC如何攻克生物化学传感的微弱信号难题?

1. 从电极到比特流:为什么生物化学传感必须依赖专用接口IC 做生物化学传感的人都有过类似的经历:明明传感器本身性能很好,信号输出却一塌糊涂——噪声大、漂移明显、重复性差,怎么调都达不到预期。很多时候问题并不在传感器&#…

2026/9/3 14:30:35

STM32F411CEU6多通道ADC采集:扫描模式+DMA实现详解

1. 多通道 ADC 的用武之地把“Multichannel ADC”和“STM32F411CEU6”这两个关键字放在一起,其实就是嵌入式开发里最常遇到的一类需求:用一块不算贵的 MCU,同时采集多路模拟信号。STM32F411CEU6 是 48 引脚的 Cortex-M4F 主控,主频…

2026/9/4 0:00:58

STM32H743 SPI从机DMA双缓冲通信实战

简介:本资源是面向嵌入式开发工程师与STM32进阶学习者的SPI DMA双机通信从机端完整实现方案,聚焦STM32H743高性能Cortex-M7单片机在工业控制与高速数据交互场景下的从机通信开发痛点。压缩包含1355个文件,主体为599个C源码与321个头文件&…

2026/9/4 0:00:58

CPU开盖降温教程:20元成本让温度直降30度的原理与实践

最近很多朋友都在抱怨,自己的电脑一到夏天就变成"烤箱",玩游戏时CPU温度动不动就飙到90度以上,风扇噪音堪比直升机。更让人头疼的是,明明配置不错,却因为高温降频导致性能大打折扣。如果你也遇到了类似问题&…

2026/9/4 0:00:58

ArkTS 表单工程:场地预约页的三态场次 Grid 与校验

ArkTS 表单工程:场地预约页的三态场次 Grid 与校验 App 14「运动场地预约」场地 Tab(Func1Tab),是整 App 交互最丰富的页面——场地横向切换 三色图例 渐变预约预览卡 快捷模板 今日场次 Grid(可选/已选/已满三态&…

2026/9/3 20:43:36

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/3 17:51:43

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/3 21:06:57

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…