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}