08 Chroma 持久化 + FastAPI 封装

Chroma 持久化 + FastAPI 封装

安装依赖

bash 复制代码
cd D:\yunyanshijie\skill_test\python_study
.venv\Scripts\activate
pip install chromadb langchain-chroma

Chroma 持久化

为什么要换?

InMemoryVectorStore Chroma
存储位置 进程内存 磁盘 chroma_db/
重启后 索引丢失,需重新 Embedding 直接加载,秒级启动
适用场景 学习、调试 接近真实部署

核心 API

py 复制代码
from langchain_chroma import Chroma
def build_vectorstore(chunks, embeddings, chroma_dir: Path):
    chroma_dir.mkdir(parents=True, exist_ok=True)

    if any(chroma_dir.iterdir()):          # 目录非空 → 已有索引,库已存在,直接加载
        print("从磁盘加载向量库...")
        return Chroma(
            persist_directory=str(chroma_dir),
            embedding_function=embeddings,
            collection_name="knowledge",
        )
    else:                                   # 首次 → 建库(文档切分后)
        print("首次运行,正在建索引...")
        return Chroma.from_documents(
            documents=chunks,
            embedding=embeddings,
            persist_directory=str(chroma_dir),
            collection_name="knowledge",
        )

FastAPI 封装

架构

#mermaid-svg-mgw4WWbKdobAgero{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mgw4WWbKdobAgero .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mgw4WWbKdobAgero .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mgw4WWbKdobAgero .error-icon{fill:#552222;}#mermaid-svg-mgw4WWbKdobAgero .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mgw4WWbKdobAgero .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mgw4WWbKdobAgero .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mgw4WWbKdobAgero .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mgw4WWbKdobAgero .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mgw4WWbKdobAgero .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mgw4WWbKdobAgero .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mgw4WWbKdobAgero .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mgw4WWbKdobAgero .marker.cross{stroke:#333333;}#mermaid-svg-mgw4WWbKdobAgero svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mgw4WWbKdobAgero p{margin:0;}#mermaid-svg-mgw4WWbKdobAgero .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mgw4WWbKdobAgero .cluster-label text{fill:#333;}#mermaid-svg-mgw4WWbKdobAgero .cluster-label span{color:#333;}#mermaid-svg-mgw4WWbKdobAgero .cluster-label span p{background-color:transparent;}#mermaid-svg-mgw4WWbKdobAgero .label text,#mermaid-svg-mgw4WWbKdobAgero span{fill:#333;color:#333;}#mermaid-svg-mgw4WWbKdobAgero .node rect,#mermaid-svg-mgw4WWbKdobAgero .node circle,#mermaid-svg-mgw4WWbKdobAgero .node ellipse,#mermaid-svg-mgw4WWbKdobAgero .node polygon,#mermaid-svg-mgw4WWbKdobAgero .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mgw4WWbKdobAgero .rough-node .label text,#mermaid-svg-mgw4WWbKdobAgero .node .label text,#mermaid-svg-mgw4WWbKdobAgero .image-shape .label,#mermaid-svg-mgw4WWbKdobAgero .icon-shape .label{text-anchor:middle;}#mermaid-svg-mgw4WWbKdobAgero .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mgw4WWbKdobAgero .rough-node .label,#mermaid-svg-mgw4WWbKdobAgero .node .label,#mermaid-svg-mgw4WWbKdobAgero .image-shape .label,#mermaid-svg-mgw4WWbKdobAgero .icon-shape .label{text-align:center;}#mermaid-svg-mgw4WWbKdobAgero .node.clickable{cursor:pointer;}#mermaid-svg-mgw4WWbKdobAgero .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mgw4WWbKdobAgero .arrowheadPath{fill:#333333;}#mermaid-svg-mgw4WWbKdobAgero .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mgw4WWbKdobAgero .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mgw4WWbKdobAgero .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mgw4WWbKdobAgero .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mgw4WWbKdobAgero .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mgw4WWbKdobAgero .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mgw4WWbKdobAgero .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mgw4WWbKdobAgero .cluster text{fill:#333;}#mermaid-svg-mgw4WWbKdobAgero .cluster span{color:#333;}#mermaid-svg-mgw4WWbKdobAgero div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mgw4WWbKdobAgero .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mgw4WWbKdobAgero rect.text{fill:none;stroke-width:0;}#mermaid-svg-mgw4WWbKdobAgero .icon-shape,#mermaid-svg-mgw4WWbKdobAgero .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mgw4WWbKdobAgero .icon-shape p,#mermaid-svg-mgw4WWbKdobAgero .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mgw4WWbKdobAgero .icon-shape .label rect,#mermaid-svg-mgw4WWbKdobAgero .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mgw4WWbKdobAgero .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mgw4WWbKdobAgero .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mgw4WWbKdobAgero :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} POST /chat
startup 一次
Client
FastAPI
agent_core
chroma_db
LangGraph
DeepSeek

重点:向量库和 Graph 在 服务启动时初始化一次,不是每个请求重建。

生命周期(lifespan)

FastAPI 推荐用 lifespan,在 startup 里完成初始化:

py 复制代码
from contextlib import asynccontextmanager

graph = None

@asynccontextmanager
async def lifespan(app: FastAPI):
    global graph # 在函数里要赋值模块级变量时必须写,否则 Python 会当成函数局部变量
    graph = init_graph()   # 内部调 build_vectorstore + build_graph
    yield # 关服务   → yield 之后的代码再跑(清理)
    # shutdown 清理

app = FastAPI(lifespan=lifespan)

接口设计

py 复制代码
class ChatRequest(BaseModel):
    message: str
    thread_id: str = "default"

class ChatResponse(BaseModel):
    reply: str
    sources: list[str] = [] # 这是 RAG 的 溯源(citation):不只给答案,还告诉用户「依据哪些文档」。
路由 方法 作用
/health GET 健康检查,返回 {"status": "ok", "chunks": N}
/chat POST 发送消息,返回回答 + 引用来源

/chat 核心逻辑

py 复制代码
@app.post("/chat", response_model=ChatResponse)
def chat(req: ChatRequest):
    config = {"configurable": {"thread_id": req.thread_id}}
    result = graph.invoke(
        {"messages": [HumanMessage(content=req.message)], "context": ""},
        config=config,
    )
    reply = result["messages"][-1].content
    sources = result.get("sources", [])   # 见下方「溯源」
    return ChatResponse(reply=reply, sources=sources)

检索溯源

在 AgentState 里加一个字段:

py 复制代码
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    context: str
    sources: list[str]   # 新增

retrieve 节点里:

py 复制代码
sources = [str(doc.metadata.get("source", "unknown")) for doc in results]
return {"context": context, "sources": sources}
相关推荐
京东云开发者6 小时前
【全栈实践】第一个 AI Agent 项目:从零搭建 AI 音频创作助手(进阶篇)
typescript·fastapi·vuex
云和数据.ChenGuang1 天前
fastapi项目拆分实战数据模型
java·服务器·数据库·人工智能·深度学习·fastapi·强化学习
燐妤1 天前
中老年智能健康管理项目
python·ai·pycharm·vue3·fastapi·项目开发·健康
circuitsosk1 天前
构建高可用AI后端服务:REST API设计、数据库交互及异步任务编排经验总结
数据库·人工智能·python·交互·fastapi·数据库连接池·rest api
java1234_小锋2 天前
【免费】基于Python的微信小程序网约车(打车,在线叫车,移动出行)系统(FastAPI+Vue3) 锋哥原创出品,必属精品
微信小程序·小程序·fastapi·网约车系统·在线叫车系统
卷无止境2 天前
用 FastAPI 撑起大文件的上传下载:从流式处理到断点续传的完整实践
后端·python·fastapi
碧水澜庭2 天前
头条【vue+fastapi 】全栈教学项目系列之《04_项目架构与文件结构说明书》
vue.js·架构·fastapi
船长@2 天前
FastAPI 快速上手:从零基础到实操入门
python·ai·fastapi
碧水澜庭2 天前
头条【vue+fastapi 】全栈教学项目系列之《02_双系统零基础环境搭建手册》
vue·fastapi