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}
相关推荐
鲨鱼辣钊1 天前
FastAPI进阶_Day21_WebSocket实时通讯实战
websocket·网络协议·fastapi
Jialu.1 天前
中文 NLP 模型部署实战:FastAPI 接口 + Streamlit 看板
人工智能·python·自然语言处理·fastapi
知识汲取者1 天前
FastAPI 学习教程(写给想学 FastAPI 的 Java 工程师的)
python·学习·fastapi
Broccoli523026651 天前
FastAPI + SQLAlchemy 异步会话(AsyncSession)核心方法
数据库·oracle·fastapi
典典分享指南1 天前
Excel SUMIFS 跨表汇总实战:多表条件求和的完整指南
django·flask·numpy·pyqt·fastapi
鲨鱼辣钊2 天前
【FastAPI筑基-Day19】APScheduler定时任务全实战|自动执行、动态启停、后台常驻
java·spring·fastapi
qq_322762752 天前
一个接口从能用到稳定,中间差的到底是什么
服务器·开发语言·lua·接口·fastapi·请求
码农大叔的博客2 天前
Python:FastAPI的typing.Annotated参数校验示例
fastapi
千里码aicood2 天前
fastAPI-儿童智能陪伴交互系统设计
交互·fastapi
卷无止境2 天前
FastAPI 的可观测性进化,从打日志到看清整个系统的呼吸
后端·python·fastapi