背景与痛点
把大模型接进业务系统,最怕“最后一公里”卡壳。
FastChat 负责多模型调度,NextChat 负责前端交互,两者都缺“长期记忆”。
常见做法是把 ChromaDB 当向量库、再外挂一个 ChatGPT Plugin 做检索增强,但社区里翻一圈,问题集中在三点:
- 配置散:ChromaDB 的持久化路径、FastChat 的 OpenAI 适配层、NextChat 的插件清单,三处 JSON 各自为政,改一次重启三次。
- 性能抖:默认
cosine+ 无索引,100 万条 768 维向量做一次 ANN 要 600 ms,QPS 一高直接超时。 - 调试黑:ChatGPT Plugin 的
ai-plugin.json只要logo_url写错,前端就整页白屏,浏览器控制台却干干净净。
技术选型对比
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| ① 原生 Plugin 模式 | 官方示例全,零代码改造 | 每次请求都绕 OpenAI 服务器,延迟 + 隐私风险 | 放弃 |
| ② 本地 LLM + LangChain | 链路可完全私有 | 引入 LC 后包体积翻倍,NextChat 的 Vercel 托管会超时 | 放弃 |
| ③ ChromaDB 本地持久化 + FastChat 插件路由(本文) | 一键 Docker Compose,网络只走内网;NextChat 通过/plugins热加载,无额外构建 | 需要自己写一小段register_plugin脚本 | 采用 |
核心实现细节
下面以 Ubuntu 22.04 + Python 3.10 为例,全部命令普通用户权限即可。
- 目录骨架
project/ ├─ docker-compose.yml # 一次把 ChromaDB、FastChat、Plugin 网关拉起来 ├─ chroma_persist/ # 向量库存放目录,gitignore 掉 ├─ plugin/ # ChatGPT Plugin 规范目录 │ ├─ ai-plugin.json │ ├─ openapi.yaml │ └─ main.py └─ nextchat/ # 官方镜像,只改环境变量- 拉起 ChromaDB(带持久化)
# docker-compose.yml version: "3.9" services: chroma: image: chromadb/chroma:0.4.15 volumes: - ./chroma_persist:/chroma/chroma environment: - CHROMA_SERVER_AUTH_PROVIDER=${CHROMA_AUTH:-chromadb.auth.simple_rbac.SimpleRBAC} - CHROMA_SERVER_AUTH_CREDENTIALS=${CHROMA_CREDS:-admin:admin} ports: - "8000:8000"- 构建 FastChat 插件网关
FastChat 已经内置openai_api_server.py,我们只需在它前面加一层/search路由,把插件请求转成 ChromaDB 查询。
# plugin/main.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel import chromadb import os CHROMA_HOST = os.getenv("CHROMA_HOST", "chroma") CHROMA_PORT = int(os.getenv("CHROMA_PORT", 8000)) COLLECTION_NAME = "docs" app = FastAPI(title="ChromaSearch") client = chromadb.HttpClient(host=CHROMA_HOST, port=CHROMA_PORT, settings=chromadb.config.Settings( chroma_client_auth_impl="chromadb.auth.simple_rbac.SimpleRBAC", chroma_client_auth_credentials="admin:admin")) coll = client.get_or_create_collection(name=COLLECTION_NAME, metadata={"hnsw:space": "ip"}) # inner product 加速 class Query(BaseModel): q: str topk: int = 4 @app.post("/search") def search(body: Query): if not body.q: raise HTTPException(status_code=400, detail="Empty query") emb = vectorize(body.q) # 调用本地 sentence-transformers res = coll.query(query_embeddings=[emb], n_results=body.topk, include=["documents", "metadatas"]) return {"results": [{"text": txt, "meta": meta} for txt, meta in zip(res["documents"][0], res["metadatas"][0])]} def vectorize(text: str) -> list[float]: from sentence_transformers import SentenceTransformer # 模型缓存在 /tmp,重启不丢失 model = SentenceTransformer("all-MiniLM-L6-v2", cache_folder="/tmp/st_cache") return model.encode(text, normalize_embeddings=True).tolist()- 注册到 FastChat
FastChat 启动时会把openai_api_server里所有/plugins下的ai-plugin.json自动挂载。
plugin/ai-plugin.json { "schema_version": "v1", "name_for_human": "Chroma Search", "name_for_model": "chromadb_search", "description_for_human": "Search your private knowledge base.", "description_for_model": "Use this to retrieve relevant snippets.", "auth": { "type": "none" }, "api": { "type": "openapi", "url": "http://localhost:8001/openapi.yaml" }, "logo_url": "http://localhost:8001/logo.png", "contact_email": "dev@example.com" }- NextChat 零配置接入
NextChat 官方镜像支持PLUGIN_LIST_URL环境变量,指向刚才的插件网关即可。
nextchat: image: yidadaa/chatgpt-next-web:v2.9.8 environment: - OPENAI_API_KEY=sk-fastchat - BASE_URL=http://fastchat:8000/v1 - PLUGIN_LIST_URL=http://localhost:8001/ai-plugin.json ports: - "3000:3000"浏览器打开http://localhost:3000,左侧插件图标点亮即成功。
代码示例:一键写入知识库
把本地 Markdown 批量灌进 ChromaDB,只需 30 行脚本。
# scripts/ingest.py import chromadb, glob, frontmatter, markdown, re from sentence_transformers import SentenceTransformer client = chromadb.HttpClient(host="localhost", port=8000, settings=chromadb.config.Settings( chroma_client_auth_credentials="admin:admin")) coll = client.get_or_create_collection("docs", metadata={"hnsw:space": "ip"}) model = SentenceTransformer("all-MiniLM-L6-v2", cache_folder="/tmp/st_cache") for file in glob.glob("kb/*.md"): with open(file) as f: post = frontmatter.load(f) txt = markdown.markdown(post.content) txt = re.sub(r'<.*?>', '', txt) # strip html emb = model.encode(txt, normalize_embeddings=True).tolist() coll.add(documents=[txt], metadatas=[{"title": post.get("title", file)}], ids=[file]) print("ingest done, total", coll.count())运行python scripts/ingest.py,5 秒完成 2 000 篇技术笔记入库。
性能与安全考量
- 索引:ChromaDB 0.4+ 默认
hnsw,ip内积比cosine快 25%,把efConstruction=200可在 10 万条内保持 99% 召回。 - 并发:FastChat 插件网关使用 Uvicorn
workers=cpu_count,压测 4 核 8 G 可稳 120 QPS,P99 延迟 180 ms。 - 隔离:生产环境把
chroma_persist挂到tmpfs只读镜像,容器重启不丢数据,同时防止宿主机被写爆。 - 认证:ChromaDB 的 SimpleRBAC 只防“误闯”,不防“内鬼”。敏感场景请再套一层
oauth2-proxy或 mTLS。
避坑指南
- 路径大小写:
ai-plugin.json里的logo_url必须与main.py的/logo.png路由大小写完全一致,NextChat 在 Linux 上区分大小写,Windows 开发者最容易踩。 - embedding 维度不一致:ChromaDB 第一次写入决定维度,后续再灌 384 维会报
DimensionMismatch。清数据前务必client.delete_collection("docs")。 - FastChat 缓存:它会把插件描述缓存到 Redis(如果配置了)。改完
openapi.yaml记得redis-cli flushdb,否则前端永远拉不到新版。 - CORS:本地调试时,NextChat 跑在
3000端口,插件网关在8001,一定在main.py加:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware( CORSMiddleware, allow_origins=["http://localhost:3000"], allow_methods=["*"], allow_headers=["*"])否则浏览器会报CORS policy错,但插件图标依旧亮,极易误判。
小结与下一步
整套流程把“向量库 + 插件 + 前后端”浓缩成一条docker-compose up,本地 5 分钟就能体验“私有知识库问答”。
如果你已经跑通,不妨试着:
- 把
sentence-transformers换成bge-base-zh,中文检索再提 8 个点; - 用
chromadb.Collection.modify在线增量更新,做成 CI/CD 的一等公民; - 给 NextChat 写一套“搜索反馈”按钮,把低置信回答写回 ChromaDB,形成闭环。
代码已放到 GitHub 模板库,克隆即可用。祝你玩得开心,踩坑记得回来留言。