文档 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 是必填的。各个路径的默认值都指向当前发行版内部。

环境变量

默认值

用途

RAG_API_KEY

(未设置)

处理器调用网关时使用的用户 API key(提供服务时必填)

RAG_API_BASE_URL

http://localhost:8080/v1

处理器调用的网关(自调用,为的是日志和配额)

RAG_INDEX_PATH

若存在 overlay,则为它的 content/rag/docs_index.json

向量索引位置

RAG_CORPUS_DIR

若存在 overlay,则为它的 content/docs/docs/source

markdown 语料

RAG_EMBEDDER

gateway

gateway(经由 RAG_EMBED_MODEL 调用真实的 embedding 模型)或 hash(离线)

RAG_GATEWAY_BASE_URL

http://localhost:8080/v1

ingest 使用的网关(gateway 模式)

RAG_EMBED_MODEL

bge-m3

embedding 模型 id(gateway 模式)

RAG_CHAT_MODEL

qwen3.6-35b

生成回答用的模型。请设为你的网关实际提供的模型:默认值是一个本仓库并不自带的模型。

RAG_GATEWAY_API_KEY

回落到 LOCAL_API_KEY

ingest 向 RAG_GATEWAY_BASE_URL 出示的用户 API key;必须在那个网关上有效

RAG_TOP_K

4

每次查询检索的 chunk 数

RAG_MAX_TOKENS

1024

答案的 token 预算

RAG_TEMPERATURE

0.3

生成温度

局限

  • 索引是手工刷新的(make rag-ingest),没有定时任务——在重新生成之前,它可能落后于文档。

  • HashEmbedder 这个回退方案只是为了让流水线在没有网关时也能跑(开发/CI);检索质量很差。

  • 没有答案缓存,没有重排序,历史只保留最近几轮。向量库在每个进程里各加载一份到内存(带缓存,按 mtime 失效)。

代码位置

路径

作用

apps/backend/serving/rag/chunker.py

按标题切分 markdown

apps/backend/serving/rag/embedder.py

GatewayHTTPEmbedder,以及离线用的 HashEmbedder

apps/backend/serving/rag/store.py

JSON 向量库 + 余弦检索

apps/backend/serving/rag/pipeline.py

prompt 组装 + 来源(sources)载荷

apps/backend/serving/rag/ingest.py

python -m serving.rag.ingest 命令行工具

apps/backend/serving/servers/routers/rag.py

/v1/rag/status + /v1/rag/chat

apps/frontend/src/app/chat/page.tsx

聊天界面(/chat,受 ProtectedRoute 保护)

apps/frontend/src/lib/api/chat.ts

流式 SSE 客户端