跳到主要内容

DocsGPT 全景:它是什么、怎么转、怎么读

30 秒导读: DocsGPT 是一个可私有部署的 AI 平台,把「你自己的文档 / 数据」变成一个能问答、能调用工具、能多步研究的 AI 助手。它的技术核心是一条流水线:一次提问从 HTTP 入口进来,经一个"装配器"把配置/文档/工具/LLM 全部备齐,交给一个 agent 驱动『大模型 ↔ 工具』的多轮循环,再把过程用 SSE 事件实时流回浏览器。 本章讲清"这是什么、大盘怎么转、后面 6 章各讲什么"。


1. 这是什么(零基础也能懂)

一句话定义: DocsGPT 是一个开源的私有化 RAG + agent 平台——你把文档喂给它,它帮你建一个"读过这些文档、还会用工具"的 AI 助手。

RAG 是 Retrieval-Augmented Generation(检索增强生成)的缩写:回答问题前先去你的资料里检索相关片段,把片段塞进提示词,再让大模型据此作答——这样答案有出处、少幻觉。

给谁用、解决什么问题

想象你有一大堆公司内部文档(PDF、Word、网页、会议录音……),你希望员工能像聊天一样问它问题、还能让它顺手调用 API 或查数据库。你不想把这些私密资料交给某个云服务,想自己部署、自己控制模型。DocsGPT 就是干这个的。

据 README(README.md)的事实,它对外主打这些能力:

能力说明
广格式摄取PDF / DOCX / CSV / XLSX / EPUB / HTML / 图片 / 音频(MP3/WAV…)等都能读入并变成可检索知识
多模型 / 本地模型支持 OpenAI、Google、Anthropic,也支持 Ollama、llama.cpp 等本地推理;支持 BYOM(自带模型)
带出处的回答答案附带来源片段,在 UI 里可查证
可执行工具agent 能连 API、工具与外部服务,把"说"变成"做"
Agent Builder / 研究模式 / 工作流可视化搭 agent、多步深度研究、条件分支工作流
私有 & 可扩展可 Kubernetes 部署、私有运行

用起来什么样

最直观的使用面是一个 HTTP 流式接口。前端(或任何客户端)向后端 POST 一个问题,后端用 SSE(Server-Sent Events,服务器推送事件) 把回答一段段推回来:

POST /stream
{ "question": "我们的报销政策是怎样的?", "conversation_id": "…", "agent_id": "…" }

→ (SSE 流)
data: {"type":"answer","answer":"根据"}
data: {"type":"answer","answer":"报销手册第 3 节,"}
data: {"type":"source","source":[{"title":"expenses.pdf", ...}]}
data: {"type":"end"}

用户看到的就是"逐字蹦出来的回答 + 底部的引用来源"。

一句话直觉

把 DocsGPT 想成一个"配了资料库和工具箱的接线员"。 你问一句话,它先决定"要不要翻资料 / 要不要动工具",翻完、用完,再把整理好的话说给你听;整个过程边想边说(流式),想不清楚还能中途停下来等你批准某个动作(工具审批)。


2. 顶层全景(它大概怎么转)

本节讲一次 /stream 请求的主线——不进代码细节,只看"输入怎么一路变成流式输出"。

2.1 主线流程图

怎么读这张图: 从上到下是一次请求的时间顺序。左侧竖线是"主控制流",右侧的框是每一步会调用/依赖的子系统。中段的 ① 大模型 ↔ 工具循环 是整台机器的心脏,会来回转很多圈。

浏览器 / 客户端
│ POST /stream { question, conversation_id, agent_id, ... }

┌─────────────────────────┐
│ ① HTTP 入口 │ Flask 蓝图,先鉴权(JWT / API key)
│ StreamResource.post │ 再决定:正常提问 还是 续跑(带 tool_actions)
└───────────┬─────────────┘

┌─────────────────────────┐ 拉这些东西进来:
│ ② 装配器 │──▶ 选 agent 配置 / 选模型(可 BYOM)
│ StreamProcessor │──▶ 检索层:预取相关文档片段
│ .build_agent() │──▶ 工具:发现并预取工具数据
└───────────┬─────────────┘──▶ 渲染系统提示词(把文档/工具塞进模板)

┌─────────────────────────┐
│ ③ 选型 │ 按 agent_type 造具体 agent:
│ AgentCreator │ classic / agentic / research / workflow
└───────────┬─────────────┘

┌─────────────────────────────────────────────────────┐
│ ④ BaseAgent.gen() —— 心脏:LLM ↔ 工具 多轮循环 │
│ │
│ ┌──────────────┐ 提示+历史+工具 ┌──────────────┐ │
│ │ LLM 抽象层 │◀───────────────│ LLMHandler │ │
│ │ (provider 无关│ │ 工具循环编排 │ │
│ │ / BYOM / 回退)│───模型输出────▶│ (最多 25 轮) │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ 模型要调工具 │
│ ▼ │
│ ┌──────────────┐ │
│ │ ToolExecutor │ │
│ │ + 检索/API/… │ │
│ └──────────────┘ │
│ (需人工批准的工具 → 暂停 pause,存状态,提前结束流) │
└───────────────────────┬───────────────────────────────┘
▼ 一串事件:answer / source / tool_calls / thought …
┌─────────────────────────┐
│ ⑤ SSE 出口 │ complete_stream:把事件转成 SSE 帧流出,
│ complete_stream() │ 同时落库(会话/消息/日志)、写事件日志便于断线重连
└───────────┬─────────────┘

浏览器逐字渲染 + 显示来源

2.2 两条离线支线

上面是"在线问答"主线。还有两条离线支线支撑它,不在 /stream 的热路径上:

  • 摄取(ingestion): 你上传文档时,由 Celery 后台任务读取 → 分块 → 生成向量 → 写入向量库。检索层查的就是这些结果。
  • 图谱抽取(graphrag): 可选地从文档里抽取"实体 + 关系"存成知识图谱,供 GraphRAG 检索器使用。

2.3 主线走一遍(高层)

  1. 请求进 StreamResource.post,先在 Flask 的 before_request 完成鉴权,再判断是新提问还是续跑(客户端回传了 tool_actions 批准结果)。
  2. StreamProcessor.build_agent() 是"装配总控":定配置 → 定模型 → 定检索源 → 预取文档 → 预取工具 → 渲染提示词。
  3. AgentCreatoragent_type 实例化四种 agent 之一。
  4. BaseAgent.gen() 启动"LLM ↔ 工具"循环:模型说话就当 answer 吐出,模型要调工具就执行工具、把结果喂回模型,如此往复(上限 25 轮)。碰到需要人工批准的工具就暂停
  5. complete_stream() 把这串内部事件翻译成 SSE 帧发给客户端,并把会话、消息、日志落到数据库。

3. 部件一句话职责(代码目录 → 干什么)

下表是 application/ 下与主线相关的核心部件。每一行"在哪个目录 | 一句话职责",想深入哪块就去对应章节。

部件(目录)一句话职责
api/Flask + flask-restx 的 HTTP 入口层:/stream/api/answer、用户/管理/连接器蓝图,以及 /v1 的 OpenAI 兼容端点
agents/主控制循环:BaseAgent、四种 agent、AgentCreator 选型、ToolExecutor——驱动"LLM↔工具"多轮
retriever/检索层:按源路由的 Dispatcher + ClassicRAG/HybridRAG/GraphRAG 多种检索器
llm/LLM 抽象层:provider 无关的调用封装、LLMHandler(解析+工具循环编排)、BYOM 与跨模型回退
agents/tools/工具体系:工具的发现、加载、执行、审批门(pause)与持久化调用日志
parser/ + worker.py摄取管线:文档读取 → 分块 → 嵌入 → 入库,由 Celery 异步执行
vectorstore/向量库适配层:FAISS / pgvector / Qdrant / Milvus / LanceDB / Elasticsearch / Mongo 等统一接口
graphrag/图谱抽取与存储:从文档抽实体/关系存成图,供 GraphRAG 检索使用

4. 阅读地图(后面 6 章讲什么、怎么读)

本组文档把 DocsGPT 拆成 6 个深入章节,建议按顺序读(由浅入深、沿主线一路下钻)。

顺序章节讲什么什么时候读
1从请求到 agentHTTP 入口鉴权、StreamProcessor 如何一步步装配、AgentCreator 与四种 agent 各自定位想搞懂"一次请求怎么落到某个 agent 手里"
2工具循环LLMHandler 如何驱动多轮工具调用、流式解析、暂停/续跑 机制想搞懂整台机器的"心脏"怎么跳
3工具体系工具怎么被发现/加载/执行,审批门与持久化日志想加一个新工具、或搞懂审批链路
4检索层查询改写、按源路由的 Dispatcher、多检索器与向量库想搞懂 RAG 那一半、或调检索质量
5LLM 抽象层provider 无关封装、BYOM、跨模型回退、结构化输出想接新模型 / 搞懂 BYOM 与回退
6摄取与高级 agent文档入库管线、图谱抽取、ResearchAgentWorkflowAgent想搞懂文档怎么进来、或用研究/工作流模式

一条建议的最短理解路径:第 1 章(看清主线装配)→ 第 2 章(看清工具循环)→ 按需下钻 3/4/5。第 6 章相对独立,可最后看。


5. 巧妙之处速览(这些设计值得带走)

下面几点是 DocsGPT 里"不显然但很聪明"的设计。这里只点出"妙在哪"并给定位,细节留给各分章。

5.1 按源检索的 byte-identical 兼容

新引入的按源路由检索器 Dispatcher(application/retriever/dispatcher.py:51)要支持"每个文档源用不同检索策略、共享一份 token 预算"。难点是:不能因为加了这层而改变现有(全 classic 源)的检索结果

它的做法是:当所有源都是默认 classic/default 时,_build_groups(dispatcher.py:108)把它们合并成唯一一个 ClassicRAG 实例,_budget_for_group(dispatcher.py:199)在单组时返回完整预算——于是输出与改造前逐字节相同_is_override(dispatcher.py:171)专门判断"这个源到底有没有真的改过配置",没改就走全局老路,零额外 LLM 调用。

5.2 工具循环的暂停 / 续跑(pause / resume)

有些工具动作需要人先批准(或需客户端本地执行)。工具循环里 handle_tool_calls(application/llm/handlers/base.py:758)一旦发现这类调用,不执行、而是收集成 pending_actions,把当前 messages / tools_dict 存进 agent._pending_continuation,并发一个 tool_calls_pending 事件提前结束这次流

用户批准后再发一次请求,StreamProcessor.resume_from_tool_actions(stream_processor.py:1340)从 ContinuationService 把状态取回、重建 agent,BaseAgent.gen_continuation(base.py:234)接着把批准结果喂回模型继续跑。一段对话可以跨多次 HTTP 请求、多次暂停,而共享同一个 WAL 占位行与 request_id

5.3 BYOM 的 upstream-id 解析

用户自带模型(BYOM,Bring Your Own Model)在注册表里的 id 是一个 UUID,但真正调上游 API 时要用用户填的模型名(如 mistral-large-latest)。LLMCreator.create_llm(application/llm/llm_creator.py:8)在构造 LLM 时把注册表里的 upstream_model_id 解析出来贴到实例上(llm_creator.py:83),又用 _canonical_model_id(llm_creator.py:129)另存 UUID 供计费归因。整个工具循环用 llm.model_id(上游名)发请求,内置模型两者相等、BYOM 时自动用对名字。

5.4 跨 provider 回退时,按"真实产出者"重新解析

BaseLLM 的回退发生在 agent 之下:一个 Google 主模型被限流时,可以在同一次 gen_stream 里回退到 OpenAI 兼容的备用模型(application/llm/base.py:_stream_with_fallback,llm/base.py:220)。问题是:handler 是按主 provider 建的,读不懂备用模型的分片格式,会默默丢掉工具调用

解法是 _parse_for_response(application/llm/handlers/base.py:78)读取 llm._responding_provider(llm/base.py:46)——"到底是谁产出了这段分片"——用匹配那个真实产出者的 handler 来解析,并缓存。编排状态(缓冲、tool_calls)仍留在原 handler 上。


6. 顶层代码地图(导航索引)

想直接跳进源码,从这几个真实符号入手(用符号名 grep 比行号更抗漂移):

主题文件路径符号名
HTTP 流式入口application/api/answer/routes/stream.pyStreamResource.post
应用装配 / 蓝图注册 / 鉴权钩子application/app.pyappauthenticate_request
请求 → agent 装配总控application/api/answer/services/stream_processor.pyStreamProcessor.build_agentcreate_agentresume_from_tool_actions
SSE 出口 + 落库application/api/answer/routes/base.pyBaseAnswerResource.complete_stream
agent 选型application/agents/agent_creator.pyAgentCreator.create_agent
agent 基类 / 生成循环application/agents/base.pyBaseAgent.gen_gen_innergen_continuation_llm_gen
四种 agentapplication/agents/{classic,agentic,research,workflow}_agent.pyClassicAgentAgenticAgentResearchAgentWorkflowAgent
工具循环编排(心脏)application/llm/handlers/base.pyLLMHandler.process_message_flowhandle_streaminghandle_tool_callsMAX_TOOL_ITERATIONS
按源检索路由application/retriever/dispatcher.pyDispatcherbuild_dispatcher
LLM 抽象 / BYOM / 回退application/llm/llm_creator.pyapplication/llm/base.pyLLMCreator.create_llm_stream_with_fallback_responding_provider
摄取管线(Celery)application/worker.pyapplication/parser/document_reader.pyingest_worker
图谱抽取application/graphrag/extraction.pyapplication/graphrag/store.py

本章只给"大盘"。真正的机制(工具循环怎么转、检索怎么路由、BYOM 怎么解析、暂停怎么续跑)都在后续 6 章逐一拆开,每章都带可核对的 file:line + 符号名。建议从 01-request-and-agents.md 开始。