文档 RAG 助手
文档助手是一个基于检索增强生成(RAG)的聊天功能:它根据部署自己的公开用户文档,回答用户关于这个部署的问题。检索和生成都通过网关本身完成。
这个功能还处于早期,有已知的不足,见局限。要运行它,部署需要准备好 Markdown 格式的文档、用这些文档构建的索引,以及助手调用网关用的 API key。仓库里如果没有发行版,这些都不存在,所以在你补齐之前,助手会一直返回 503。
工作原理
/v1/rag/chat 是一层很薄的编排:它在进程内完成检索,然后以普通用户的身份调用网关自己的公开 API 来做模型部分的活。
POST /v1/rag/chat (JWT-gated)
1. embed query ── HTTP ─► POST {RAG_API_BASE_URL}/embeddings (RAG_EMBED_MODEL)
2. cosine top-k over the JSON vector index (in-process)
3. build grounded prompt with citations (in-process)
4. generate ── HTTP ─► POST {RAG_API_BASE_URL}/chat/completions (RAG_CHAT_MODEL)
→ SSE: sources event, then the proxied OpenAI chunks, then [DONE]
两次内部调用都会出示 RAG_API_KEY,也就是助手自己的 API key,因此它们走的是普通的 /v1/embeddings 和 /v1/chat/completions 处理器:和其他请求一样记入 api_logs,并计入成本、配额和并发。它们还会带上 X-On-Behalf-Of: <user id>,这样用量会记在提问的登录用户名下,而不是记在共享的 key 上,并计入该用户自己的每日配额。网关只对用 RAG_API_KEY 发出的请求认这个请求头,其他 key 带上它一律忽略。
语料(corpus):当前生效的发行版 overlay 里的文档源文件(
<overlay>/content/docs/docs/source/*.md),也就是用来构建这个部署公开文档站的那批 markdown。没有 overlay 就没有语料,这时把RAG_CORPUS_DIR指向你自己的文档即可。向量库:一个普通的 JSON 文件,用纯 Python 计算余弦相似度逐条扫描;文档语料规模很小,用不着更复杂的方案。和语料一样,索引属于发行版,而不属于本仓库:默认路径是
<overlay>/content/rag/docs_index.json,在网关当前运行的那个发行版里。RAG_INDEX_PATH和RAG_CORPUS_DIR可以分别覆盖这两个路径。为什么要以用户身份(通过 HTTP)调用网关,而不是调用进程内的 router?为了让 RAG 请求可观测、可计量。直接调用 router 或 adapter,会跳过
/v1/*处理器里的逐请求日志、成本、配额和并发检查。
搭建
把 RAG_API_KEY 设为一个有效的用户 API key,并把 RAG_API_BASE_URL 指向网关自己的地址。默认值 http://localhost:8080/v1 与 Compose 中后端监听的端口一致;如果部署把后端绑定在别的地址上,就必须设置它,否则每个 RAG 请求都会在自调用这一步失败。
重建索引
用默认的 gateway embedder 构建索引,它会经由某个网关调用真实的 embedding 模型:
RAG_CORPUS_DIR=path/to/docs RAG_GATEWAY_API_KEY=hyi-xxx make rag-ingest
只有当你的语料不是生效 overlay 的 content/docs/docs/source 时,才需要设置 RAG_CORPUS_DIR;没有 overlay 时,ingest 会失败,并提示你去设置它。
RAG_GATEWAY_BASE_URL 默认是 http://localhost:8080/v1:做 embedding 是要花钱的,所以克隆下来的仓库默认用你自己的网关,而不是这个默认值作者的网关。把它指向你想用来做 embedding 的网关;key 必须是那个网关上有效的用户 API key。文档 chunk 和查询用的是同一个 RAG_EMBED_MODEL(服务端点在查询时用的就是它),这样查询向量和文档向量才在同一个空间里。
如果想不接网关、不用 key,离线跑一遍(检索效果差,只适合开发/CI):
RAG_EMBEDDER=hash make rag-ingest
服务端点会用当初构建索引的那个 embedder(记录在索引元数据里)来 embed 查询;如果查询向量的维度与索引不符,它会直接报错(HTTP 502)。换过 embedder 之后一定要重建索引。
部署索引
索引是部署产物,不属于镜像构建的一部分:用 make rag-ingest 构建,并保证生成的 JSON 文件能在 RAG_INDEX_PATH 读到。在 Compose 部署中,overlay 目录挂载到了后端里,所以写到 overlay 的 content/rag/ 下的索引不用重建镜像就能读到,直接替换文件即可。部署后可以用 /v1/rag/status 检查,它会返回 index_loaded、embedder 模式和 chunk 数量。如果查询时 embedding 后端不可用,/v1/rag/chat 会正常返回 503,而不是报 500。
端点
两个端点都挂在网关下,用仪表盘的 JWT(get_current_user)鉴权,所以 Next.js 聊天页面拿它手上已有的会话 token 就能调。
GET /v1/rag/status——索引是否已构建、chunk 数量、使用的模型。POST /v1/rag/chat——请求体为{ messages, top_k?, stream? }。生成用的模型在服务端固定(RAG_CHAT_MODEL),不能由客户端选择,所以不能借这个端点访问有角色限制的模型。流式(默认):SSE——先是一个
{"type":"sources", ...}事件,接着是 OpenAI 格式的补全 chunk,最后是[DONE]。非流式:
{ answer, sources, model }。
助手产生的请求日志行带有 metadata.user_agent = "doc_assistant"。上游返回的 429 会原样传给调用方,这可能意味着该用户自己的每日配额已经用完。
curl -sN https://your-gateway.example/v1/rag/chat \
-H "Authorization: Bearer <jwt>" -H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"How do I get an API key?"}]}'
关闭助手
发行版 manifest 启用后,features.rag: false 会对所有人(包括管理员)关闭这两个端点:它们在读取索引或调用模型之前就返回 403。设为 true、null,或者不写这个字段,助手都保持开启。manifest 配置损坏时,两个端点都返回 503;见启用 manifest。无论哪种情况,通用的模型 API 都不受影响。
配置
只有 RAG_API_KEY 是必填的。各个路径的默认值都指向当前发行版内部。
环境变量 |
默认值 |
用途 |
|---|---|---|
|
(未设置) |
处理器调用网关时使用的用户 API key(提供服务时必填) |
|
|
处理器调用的网关(自调用,为的是日志和配额) |
|
若存在 overlay,则为它的 |
向量索引位置 |
|
若存在 overlay,则为它的 |
markdown 语料 |
|
|
|
|
|
ingest 使用的网关(gateway 模式) |
|
|
embedding 模型 id(gateway 模式) |
|
|
生成回答用的模型。请设为你的网关实际提供的模型:默认值是一个本仓库并不自带的模型。 |
|
回落到 |
ingest 向 |
|
|
每次查询检索的 chunk 数 |
|
|
答案的 token 预算 |
|
|
生成温度 |
局限
索引是手工刷新的(
make rag-ingest),没有定时任务——在重新生成之前,它可能落后于文档。HashEmbedder这个回退方案只是为了让流水线在没有网关时也能跑(开发/CI);检索质量很差。没有答案缓存,没有重排序,历史只保留最近几轮。向量库在每个进程里各加载一份到内存(带缓存,按 mtime 失效)。
代码位置
路径 |
作用 |
|---|---|
|
按标题切分 markdown |
|
|
|
JSON 向量库 + 余弦检索 |
|
prompt 组装 + 来源(sources)载荷 |
|
|
|
|
|
聊天界面( |
|
流式 SSE 客户端 |