从零实现 ChromaDB 与 ChatGPT Plugin 的无缝集成:FastChat 和 NextChat 最佳实践
·
背景与痛点
把大模型接进业务系统,最怕“最后一公里”卡壳。
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 模板库,克隆即可用。祝你玩得开心,踩坑记得回来留言。
更多推荐


所有评论(0)