数据截至 (上游 commit 4a030776a31a)
总览:PrivateGPT 是什么、检索在其中怎么转
30 秒导读: PrivateGPT 1.0 是一个开源、本地优先的 AI 应用 API 层,对外长得像 Anthropic 的 Messages API。它自己不跑模型——你把它指向任意 OpenAI 兼容的推理服务;它补上的是"应用层积木":文档摄取、带引用的检索、工具调用、流式 agent 循环。本库这一整套文档只盯其中一条线——RAG 检索:文件怎么变成可检索的东西,一次提问怎么把相关片段取回来、锚回原文、喂给模型流式作答。
1. 这是什么(零基础也能懂)
一句话定义: PrivateGPT 是一个本地部署的 AI 应用后端 API——你把本地模型接上它,它替你把"聊天 + 读文档 + 带引用检索 + 工具调用"这些后端积木做好,对外暴露成一套 Anthropic Messages API 兼容的接口。
它不是什么(这点最容易误解)。 它不负责跑模型。README.md:31 说得很直白:PrivateGPT 通过 OPENAI_API_BASE 连到任意 OpenAI 兼容推理服务(Ollama、llama.cpp、vLLM…),只要对方实现了 /v1/chat/completions 和 /v1/models 就能用。
用一张分层图理解它站在哪:
你的 App / Agent / 工作流 / UI
│
PrivateGPT API ← 应用层积木(本项目)
│
OpenAI 兼容推理服务(Ollama / llama.cpp / vLLM…) ← 只管"把模型跑起来"
这张图直接来自 README.md:23-28。记住上下分工:下层回答"怎么把一个模型跑起来",PrivateGPT 回答"怎么在这个模型之上搭出有用的 AI 应用"。
它从哪来(一句话历史)。 PrivateGPT 2023 年出道时,只是一个"和你的文档离线聊天"的脚本,在 GitHub 上爆火、破 5 万星(README.md:196)。1.0 是把那个想法从头重写成一个正经的 API 层(README.md:200)——本文讲的就是这个 1.0。
给谁用 / 解决什么问题。 给要搭私有、不出本机的 AI 产品的开发者:你不想为了"和文档聊天"重新造一遍摄取、切块、向量检索、引用、工具循环这些后端;PrivateGPT 把它们标准化好,你在 API 之上写自己的应用即可。
它对外的形态(读了哪些文件得出的)。
| 你看到的入口 | 是什么 | 在哪 |
|---|---|---|
private-gpt serve 命令 | CLI 启动服务器 | pyproject.toml:27-28([project.scripts] → private_gpt.cli.main:main) |
| 一个 FastAPI 应用 | 挂了 13 个路由的 HTTP API | private_gpt/launcher.py:78(create_app)、:198-210(include_router) |
/v1/messages 等接口 | Anthropic 兼容的 Messages API | private_gpt/server/chat/chat_router.py:30 |
/ui 工作台 | 自带的演示 UI(非核心产品) | private_gpt/launcher.py:206(挂静态目录) |
| 可选依赖分组 | 按需装 LLM/嵌入/向量库/摄取/工具 | pyproject.toml:30-299 |
"按需装"是它的一个基调。 pyproject.toml 把能力切成一堆 optional 分组:llm-*(openai / anthropic / mistral)、embedding-*、vectorstore-qdrant、ingest-*、tool-*…… 装 core 这个 flavor(pyproject.toml:297-299)就等于一次拉齐"OpenAI 兼容模型 + Qdrant 向量库 + 全套摄取 + 工具"。换句话说:向量库默认走 Qdrant,摄取和工具是可插拔的。
用起来什么样(最小真实示例)。 启动后,一次带检索的提问就是一个标准 HTTP 请求(简化自 chat_router.py:30 的 /v1/messages 与其 ChatBody):
# 示意,非源码:向本地 PrivateGPT 发一次流式提问
curl -N http://localhost:8080/v1/messages \
-H "content-type: application/json" \
-d '{
"model": "private-gpt",
"stream": true,
"messages": [{"role":"user","content":"我上传的合同里 违约条款怎么说?"}],
"tools": [{"type":"semantic_search_v1","name":"semantic_search"}]
}'
响应是一串 SSE 事件(message_start → content_block_delta …→ message_stop),模型边检索边把带引用的答案流回来。
一句话直觉。 把 PrivateGPT 想成**"本地版的 Anthropic API 后端"**:同样的 Messages 接口、同样的工具与引用概念,只是模型在你自己机器上、数据不出本机。
本节不碰任何底层实现;检索到底怎么转,下一节给全景,再往后各章拆开。
2. 顶层全景(它大概怎么转)
本节给两样东西:一张把"从文件到带引用回答"串起来的 ASCII 全景图,和一张"各部件干什么、在哪"的表。怎么读这张图:上半截是离线的"摄取/索引"(把文件变成可检索的东西),下半截是在线的"一次提问"(检索 + agent 循环 + 流式作答);两截通过中间的向量索引相接。
【离线:摄取 → 索引】
文件/内容 ──▶ 摄取 IngestComponent ──▶ 解析+切块成节点 ──▶ 写入 artifact 索引 / Qdrant
(/v1/artifacts) (树状节点) (VectorArtifactIndex)
│
▼
┌──────────────────┐
│ 向量索引 (Qdrant) │
└──────────────────┘
▲
【在线:一次提问 /v1/messages】 │ 语义检索
HTTP 请求 ──▶ chat_router ──▶ ChatAsyncFacade ──▶ ChatLoopEngine (agent 循环)
│
每一轮:调 LLM ──▶ 模型要用工具? ──是──▶ semantic_search 工具
│ │(取回带上下文的片段)
否 ▼
│ 结果 + SourceBlock 引用
▼ │
流式 SSE 事件(带 citations)◀────────┘
部件一句话职责(部件 | 干什么 | 在哪个文件)。
| 部件 | 职责 | 所在文件(符号) |
|---|---|---|
| CLI / launcher | 启动 FastAPI、挂路由、预热组件 | private_gpt/cli/main.py、private_gpt/launcher.py:78(create_app) |
| Messages 路由 | 暴露 /v1/messages 等 Anthropic 兼容接口 | private_gpt/server/chat/chat_router.py:30(chat_messages) |
| Chat 门面(async) | 起流、处理断连取消、决定 SSE 还是整包返回 | private_gpt/server/chat_async/chat_async_facade.py:32(ChatAsyncFacadeService.chat) |
| Chat 服务 | 构建 agent 循环引擎、包装成事件流 | private_gpt/server/chat/chat_service.py:202(build_loop_engine)、:222(stream_chat) |
| Agent 循环引擎 | 迭代式"调 LLM → 执行工具 → 再调"直到收敛,并把一切转成事件 | private_gpt/components/engines/chat/chat_engine.py:204(ChatLoopEngine) |
| 语义检索工具 | 把"检索"封成一个 LLM 可调用的工具 | private_gpt/components/tools/builders/semantic_search_builder.py:216(build_tool) |
| 检索工作流 | 查询改写(condense)+ 取回节点 + 组装带引用上下文 | private_gpt/components/workflows/retrieval/semantic_search.py:86(SemanticSearchWorkflow) |
| Artifact 索引 | 节点的读写与索引结构(上层) | private_gpt/artifact_index/base_artifact_index.py:51(BaseArtifactIndex)、vector_artifact_index.py:31(VectorArtifactIndex) |
| 摄取组件 | 把文件解析、切块成可检索节点 | private_gpt/components/ingest/ingest_component.py:46(IngestComponent) |
| 向量库组件 | Qdrant 存取、构造带过滤的 retriever | private_gpt/components/vector_store/vector_store_component.py:28(VectorStoreComponent)、:111(get_retriever) |
| 上下文栈 | 把系统指令、文档、工具分层组装进 prompt | private_gpt/components/context/models/context_stack.py:17(ContextStack) |
| 摄取路由 | /v1/artifacts 上传/列出/删除文档 | private_gpt/server/ingest/ingest_router.py:33(prefix="/v1/artifacts") |
一个反直觉但关键的点:检索不是一个独立的"RAG 管线",而是 agent 循环里的一个工具。 模型在对话中自己决定要不要调
semantic_search(tool_names.py:4)。这就是"Agentic RAG"——第 04 章专门讲。
3. 主线走一遍(一次问答,从 HTTP 到 SSE)
用一句自然语言提问("我上传的合同里违约条款怎么说?")把高层路径走一遍,只点部件、不进代码。想看某一步的机制,跟着链接跳到对应章。
-
请求进来。
POST /v1/messages命中chat_router.py:222(chat_messages)。它做两件事:用ChatRequestMapper把请求体ChatBody映射成内部的ChatRequest,然后交给 chat 门面。 -
门面起流。 实际处理落在
chat_async_facade.py:32(ChatAsyncFacadeService.chat):它发起一个流、拿到事件生成器,并包一层"客户端断连就取消"的保护(:69,_cancellable_stream_generator)。请求里stream=true就走 SSE(:52,to_sse_stream),否则把事件折叠成一条完整消息返回(:59,fold)。 -
进入 agent 循环。 事件流最终由
chat_service.py:222(stream_chat)构建的ChatLoopEngine产出(chat_engine.py:233,run)。循环每一轮(_run_iteration,:377)向 LLM 发起带工具的流式请求(:420,astream_chat_with_tools)。 -
模型决定检索。 若模型在这一轮要求调用
semantic_search工具,引擎在流式过程中就地把工具调用派发执行(:834,_handle_tool_use)。这一步会触发检索工作流:必要时先把口语化问题改写成独立查询(condense),再到 Qdrant 取回相关节点,并组装成带SourceBlock引用的上下文(semantic_search.py:225,retrieve步骤)。→ 机制见 02 与 03。 -
把工具结果喂回模型。 工具结果作为一条
tool消息追加进对话,循环进入下一轮;模型这次带着检索到的原文,生成带引用的答案。收不到新的工具调用就收敛停止(:427)。→ 循环与工具装配见 04 与 05。 -
流式吐出。 整个过程以 Anthropic 风格的事件流出:
message_start→content_block_start/delta/stop→(工具块)→message_delta→message_stop(事件样例见chat_router.py:136-147)。
一句话记住主线: 请求 → 门面起流 → agent 循环反复"调模型/按需检索" → 带引用的答案以 SSE 流回。