基于FastAPI与Chroma实现AI长期记忆系统:从概念到代码实践

发布时间:2026/11/7 10:34:54

基于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/10/30 0:05:34

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

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

2026/11/6 2:24:46

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

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

2026/11/4 20:44:50

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

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

2026/11/7 11:28:49

那种电工证含金量大 通过率揭秘:工地太忙根本没时间复习考试

那种电工证含金量大 通过率揭秘:工地太忙根本没时间复习考试 工地太忙根本没时间复习考试,这几乎是每个在一线作业的电工都遇到过的痛点。尤其在高压电工证、低压电工证这类高门槛证书上,考试内容繁杂,复习时间却总是被各种任务挤压得所剩无几。但你有没…

2026/11/7 11:28:49

钢筋工算特种作业吗在哪考别再被中介骗了

钢筋工算特种作业吗在哪考别再被中介骗了 你是不是也在纠结钢筋工算不算特种作业?报名又不知道在哪考,怕被中介骗钱?现在就给你讲明白,别再踩坑了。…

2026/11/7 11:28:49

魏县哪有报考电工证 别踩坑!工地太忙根本没时间复习考试

魏县哪有报考电工证 别踩坑!工地太忙根本没时间复习考试 工地太忙根本没时间复习考试,这是很多一线电工的真实写照。尤其是魏县的建筑工地上,电工证几乎是上岗的“硬门槛”,但很多电工因为工作节奏快、时间紧,根本没时间准备考试,更别说去找到靠谱的培…

2026/11/7 11:28:49

挂靠建筑电工证网上查询怎么查?怕考不过白交培训费的看过来

挂靠建筑电工证网上查询怎么查?怕考不过白交培训费的看过来 怕考不过白交培训费?你不是一个人。在建筑工地上,很多工人因为不清楚【挂靠建筑电工证】的具体要求和考试流程,花了几千块报名,结果连考试都没通过,白白浪费了时间和钱。尤其是像信阳这样的劳…

2026/11/7 11:28:49

双柏县电工证办理怎么报名?实操考试心里没底怕挂科别慌

双柏县电工证办理怎么报名?实操考试心里没底怕挂科别慌 实操考试心里没底怕挂科?你不是一个人。特别是那些在工地、工厂一线摸爬滚打的电工,考个电工证不仅是上岗的门槛,更是保护自己和他人的“护身符”。今天就以郑州一位在职建筑工人为例,带大家看看双…

2026/11/7 11:23:49

电工作业穿啥鞋郑州报考避坑指南

电工作业穿啥鞋郑州报考避坑指南 你手里有特种作业操作证吗?没有证,别说找工作,连面试资格都没有。现在是实打实的“持证上岗”时代,尤其是像电工作业这种高危岗位, 电工作业穿啥鞋…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/6 10:49:01

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/7 0:03:08

郑州报考避坑指南:电工证和钳工证怎么选?实操考试心里没底怕挂科

郑州报考避坑指南:电工证和钳工证怎么选?实操考试心里没底怕挂科 实操考试心里没底怕挂科?你不是一个人。特别是在郑州这样的城市,很多施工企业负责人和一线作业人员在报考电工证和钳工证时都面临着同样的困惑。到底该考哪个证?哪个更适合自己?哪个证书…

2026/11/7 0:03:08

广西电工证办理流程全解析:外省证怎么转?在哪考最靠谱?

广西电工证办理流程全解析:外省证怎么转?在哪考最靠谱? 之前考的证在外省能不能转过来?这个问题我遇到太多了,今天就拿广西电工证办理流程来具体说说,别再走弯路了。广西电工证的报考、复审、继续教育这些环节,如果你不了解,真的会吃大亏,尤其对于从…

2026/11/7 0:03:08

电气电工证都有哪些保姆级教程

电气电工证都有哪些保姆级教程 你是不是正愁不知道去哪报名,怕被中介坑?别急,这篇保姆级教程专为河南初次报考人员准备,帮你搞清楚 电气电工证都有哪些 ,以及如何避开那些坑。 电气电工证都有哪些?别被五花八门的证书搞懵了…

2026/11/5 15:39:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/6 10:49:01

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑