1. 从一条命令说起:DocResearch 到底在解决什么问题
第一次看到 DocResearch 这个项目名,我脑子里蹦出来的画面特别具体:深夜十一点,你对着一个空白的 Markdown 文件,手边开着十几个浏览器标签页,PDF、网页、内部文档混在一起,你要在两个小时里憋出一份带引用的调研报告。这个过程里最耗神的从来不是"写",而是"找"和"对"——找到对的资料,把结论和出处对上号。
DocResearch 想干的事情,就是把这个过程压缩成一条命令。你给它一个研究主题,它自己规划要查什么、去哪查、查完怎么读、读完怎么组织,最后吐出一份每个结论后面都挂着来源的报告。这里的关键词是"有出处",不是那种看起来很像那么回事、但经不起追问的生成内容。
它背后站着三个当下很热的概念:Agent Loop、Agentic RAG和pgvector。Agent Loop 是它的骨架,决定了它怎么"想一步、做一步、看结果、再想下一步";Agentic RAG 是它的检索策略,区别于传统"一次检索、一次生成"的 RAG,它允许模型在多轮里主动决定要不要再查、查什么;pgvector 则是它的记忆底座,把文档切片和向量存进 PostgreSQL,让检索这件事变得可持久化、可复用、可审计。
这篇文章适合谁看?如果你写过基础的 Python,装过依赖、跑过脚本,对"检索增强生成"这个词有模糊印象但没亲手搭过完整链路,那这篇就是给你准备的。我会把 DocResearch 从一条命令到一份报告的完整链路拆开,讲清楚每个环节为什么这么设计、参数怎么定、坑在哪里。如果你已经是老手,也可以直接跳到第 4 节的排查技巧,那里有几个我在实际调试中踩出来的经验。
需要先说明一点:DocResearch 的具体实现细节,公开资料里并没有完整披露,下面涉及架构和参数的部分,是我基于"一个合格从业者在做同类 Agentic RAG 系统时最可能采用的方案"做的合理补全,并会明确标注哪些是常见实践、哪些是推断。这样你拿去复现的时候,心里有数。
2. 整体架构拆解:一条命令背后的四层结构
2.1 为什么是 Agent Loop,而不是一条直线流程
传统 RAG 的流程是一条直线:用户提问 → 向量检索 Top-K → 拼进 Prompt → 生成答案。这条线的问题在于,它假设"第一次检索就能拿到足够的信息"。现实里往往不是这样,你问一个稍微复杂点的问题,第一次检索回来的片段可能只覆盖了一半,剩下那一半需要根据已有信息再追问。
Agent Loop 的核心思想就是把这个直线掰成环。DocResearch 的循环大致长这样:
- 规划(Plan):拿到研究主题,先拆成若干子问题,比如"这个技术的核心机制是什么""它和同类方案比有什么取舍""实际落地有哪些限制"。
- 行动(Act):针对当前子问题,决定调用哪个工具——是去向量库检索,还是去抓一个具体网页,还是直接读本地文档。
- 观察(Observe):拿到工具返回的结果,判断信息够不够、准不准、有没有冲突。
- 反思(Reflect):如果不够,调整下一个子问题或换检索词;如果够了,进入下一个子问题。
- 收敛(Synthesize):所有子问题都有足够材料后,组织成报告,并把每个结论映射回具体来源。
这个环的价值在于"自适应"。简单问题可能两轮就收敛,复杂问题可能跑十几轮。代价是 token 消耗和延迟都会上去,所以实际项目里通常要设一个最大轮数上限,防止它在一个死胡同里绕不出来。
提示:Agent Loop 最容易失控的地方是"反思"环节。如果模型判断"信息还不够"的标准太宽松,它会无限检索下去。常见做法是给每轮反思加一个明确的收敛条件,比如"连续两轮没有新增有效信息就强制进入综合阶段"。
2.2 Agentic RAG 和普通 RAG 的分水岭在哪
普通 RAG 里,检索是"被动"的——它不知道自己要找什么,只是把问题向量化然后做相似度匹配。Agentic RAG 里,检索是"主动"的——模型会先想"我需要什么类型的信息",再决定用什么查询词、查哪个数据源、要几条结果。
这个差别在 DocResearch 里体现得很明显。它不会拿用户原始主题直接去检索,而是先把它改写成更适合检索的查询。举个例子,主题是"pgvector 在 Windows 上的部署体验",直接检索可能召回一堆泛泛的向量数据库介绍。Agentic 的做法是先拆成"pgvector Windows 编译依赖""pgvector 预编译包获取方式""pgvector 在 Windows 下的性能表现"几个具体查询,分别检索再合并。
这种改写带来的召回质量提升是实打实的,但代价是每轮改写都要过一次模型,延迟和成本都会增加。所以实际项目里通常会在"改写轮数"和"检索质量"之间做权衡,常见配置是首轮改写一次,后续根据观察结果决定要不要再改写。
2.3 pgvector 为什么被选来做记忆底座
向量存储的选择很多,FAISS、Chroma、Milvus、Qdrant 都能干这活。DocResearch 选 pgvector,我理解核心原因是"省事且够用"。
pgvector 是 PostgreSQL 的一个扩展,意味着你不需要额外维护一套向量数据库,直接用现有的 PG 实例就能存向量。对于 DocResearch 这种"文档量不算特别大、但需要和元数据强关联"的场景,这个选择很务实——文档切片、来源 URL、抓取时间、切片序号这些元数据,和向量存在同一张表里,查询的时候一个 SQL 就能把"相似度 + 元数据过滤"一起搞定。
代价是 pgvector 在超大规模(千万级以上向量)和高并发场景下,性能不如专用向量库。但对于个人研究、小团队内部使用,这个量级完全够。而且 PG 的运维生态成熟,备份、迁移、监控都有现成方案,不用为向量单独学一套。
2.4 四层结构总览
把上面几块拼起来,DocResearch 的整体结构可以分成四层:
| 层级 | 职责 | 关键技术 |
|---|---|---|
| 交互层 | 接收命令、展示进度、输出报告 | CLI 框架、Markdown 渲染 |
| 编排层 | Agent Loop、子问题规划、收敛判断 | LLM 调用、状态机 |
| 检索层 | 查询改写、向量检索、结果重排 | Agentic RAG、pgvector |
| 存储层 | 文档切片、向量持久化、元数据管理 | PostgreSQL、pgvector 扩展 |
这四层里,编排层是最难调的,因为它涉及"模型判断"这种不确定的东西;存储层是最稳的,一旦表结构定下来基本不用动;检索层是效果提升最明显的,查询改写做得好,召回质量能上一个台阶。
3. 核心细节与实操要点:从环境到检索的完整链路
3.1 环境准备:Python 版本和依赖的取舍
DocResearch 是 Python 项目,环境准备这一步看着简单,但坑不少。我建议直接用 Python 3.10 或 3.11,别用 3.8。原因很实际:现在主流的 LLM SDK 和向量库对 3.8 的支持在逐步收缩,而且 3.10 之后引入的match语法、更完善的类型提示,在写 Agent 状态机的时候会舒服很多。
安装依赖的时候,我习惯先建虚拟环境再装,避免污染全局:
python -m venv .venv source .venv/bin/activate # Windows 下是 .venv\Scripts\activate pip install -r requirements.txtrequirements.txt 里通常会有这几类依赖:LLM 客户端(比如 openai 或 anthropic 的 SDK)、PostgreSQL 驱动(psycopg2 或 psycopg)、向量处理库(numpy、可能还有 sentence-transformers)、文档解析库(pypdf、beautifulsoup4)、CLI 框架(click 或 typer)。
注意:psycopg2 在 Windows 上直接 pip 安装经常编译失败,因为缺 PostgreSQL 的开发头文件。稳妥的做法是装
psycopg2-binary,它是预编译版本,省去编译环节。这个坑我在三台不同的 Windows 机器上都遇到过,不是偶发。
3.2 pgvector 的安装:Windows 用户要特别留意
pgvector 的安装分两步:装 PostgreSQL 扩展,再在数据库里启用。
Linux 和 macOS 下相对简单,包管理器一般能直接装。Windows 下就麻烦一些,因为 pgvector 需要编译,而 Windows 上编译 C 扩展对很多人来说是道坎。常见做法是找预编译的二进制包,把vector.dll放到 PostgreSQL 的 lib 目录,把vector.control和 SQL 文件放到 share/extension 目录,然后重启服务。
启用扩展的 SQL 很简单:
CREATE EXTENSION IF NOT EXISTS vector;建表的时候,向量列的定义大概长这样:
CREATE TABLE doc_chunks ( id BIGSERIAL PRIMARY KEY, doc_id TEXT NOT NULL, chunk_index INT NOT NULL, content TEXT NOT NULL, embedding vector(1536), source_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW() );这里的vector(1536)里的 1536 是向量维度,取决于你用的 embedding 模型。OpenAI 的 text-embedding-3-small 是 1536 维,text-embedding-3-large 是 3072 维。维度定错了,插入的时候会直接报错,所以建表前一定要确认清楚。
3.3 文档切片:切多大、怎么切,直接决定检索质量
切片是 RAG 里最容易被低估的环节。切太大,一个切片里混了好几个主题,检索出来噪声大;切太小,上下文不完整,模型读不懂。常见实践是 500 到 1000 个 token 一个切片,切片之间留 10% 到 20% 的重叠。
重叠的意义在于防止"关键信息正好被切在边界上"。比如一句话被切成两半,前半句在切片 A 末尾,后半句在切片 B 开头,检索的时候可能只召回 A,模型看到半句话就懵了。留重叠能让这种边界情况下的信息至少在一个切片里是完整的。
DocResearch 这种研究型场景,我倾向于切得稍微大一点,700 到 900 token,因为研究报告需要的是相对完整的论述段落,而不是零散的事实点。同时按语义边界切(比如按段落、按标题层级),比机械地按字符数切效果好很多。
3.4 查询改写:Agentic RAG 的第一道价值
前面提过,Agentic RAG 和普通 RAG 的分水岭在查询改写。具体怎么改?常见做法是让模型扮演"检索策略师",输入是用户的研究主题和当前已收集的信息,输出是接下来要执行的检索查询列表。
一个典型的改写 Prompt 大概是这样组织的:告诉模型当前研究主题是什么、已经查到了哪些信息、还缺什么,然后要求它输出 2 到 3 个具体的检索查询,每个查询要足够具体,能直接拿去向量检索。
这里有个经验:改写出来的查询最好带上"领域限定词"。比如研究"向量数据库选型",改写时加上"性能对比""运维成本""扩展性"这类限定词,比单纯问"向量数据库哪个好"召回质量高得多。因为向量检索本质是语义相似度匹配,查询越具体,匹配到的片段越聚焦。
3.5 检索结果重排:Top-K 之后的第二道筛子
向量检索返回 Top-K 之后,直接喂给模型往往不够。因为向量相似度高不代表内容真的相关,有时候只是用词接近。所以 DocResearch 这类系统通常会在检索后加一道重排(Rerank)。
重排的常见做法有两种:一种是用专门的 rerank 模型(比如 cross-encoder),把查询和每个候选片段拼在一起打分,精度高但慢;另一种是用 LLM 直接判断每个片段和查询的相关性,灵活但贵。实际项目里,如果候选片段不多(比如 20 条以内),用 LLM 重排是可以接受的;如果候选很多,还是上专门的 rerank 模型更划算。
重排之后通常只保留 Top 3 到 Top 5 喂给生成环节,既控制 token 消耗,又保证信息密度。
3.6 来源追踪:让每个结论都有出处
"有出处"是 DocResearch 的核心卖点,实现上靠的是在生成阶段强制模型标注引用。具体做法是在 Prompt 里给每个检索到的片段编号,要求模型在输出结论时用[1]、[2]这样的标记指向具体片段,最后再把编号映射回真实的来源 URL。
这个机制听起来简单,但实际做的时候有两个坑。一是模型可能"编造引用",标了一个不存在的编号,或者标了编号但内容和来源对不上。二是多个片段支持同一个结论时,模型可能只标一个,导致引用不完整。前者需要在后处理阶段做校验,把无效引用剔除或标记;后者可以在 Prompt 里明确要求"如果一个结论有多个来源支持,全部标出"。
4. 实操过程:从零跑通一条研究命令
4.1 数据库初始化与连接配置
第一步是把 PostgreSQL 和 pgvector 准备好,然后建库建表。连接配置通常放在环境变量或配置文件里,我习惯用.env文件管理:
DB_HOST=localhost DB_PORT=5432 DB_NAME=docresearch DB_USER=postgres DB_PASSWORD=your_password连接的时候用连接池,别每次查询都新建连接。psycopg2 的pool模块或者 SQLAlchemy 的 engine 都能干这活。连接池大小根据并发量定,个人使用 5 到 10 就够,团队使用可以到 20。
建完表之后,记得给向量列建索引。pgvector 支持两种索引:IVFFlat 和 HNSW。IVFFlat 建得快、占空间小,但召回率略低;HNSW 建得慢、占空间大,但召回率和查询速度都更好。研究型场景我推荐 HNSW:
CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);vector_cosine_ops表示用余弦距离,这是文本 embedding 最常用的距离度量。如果你的 embedding 是归一化的,用内积vector_ip_ops也可以,速度更快。
4.2 文档入库:抓取、解析、切片、向量化
入库流程是一条流水线:抓取原始文档 → 解析成纯文本 → 切片 → 每片算 embedding → 连同元数据写入数据库。
抓取环节,网页用 requests + beautifulsoup,PDF 用 pypdf 或 pdfplumber。解析的时候要注意去掉页眉页脚、导航栏这些噪声,它们会污染切片内容。
切片环节,我前面说了按语义边界切。一个实用的做法是先用正则或简单的规则把文本按段落切开,再把相邻的小段落合并到目标 token 数。这样切出来的片段语义完整性比机械按字符切好很多。
向量化环节,如果文档量大,建议批量调用 embedding API,一次传几十条,比一条条调快得多。同时要注意 API 的速率限制,加个简单的退避重试。
4.3 Agent Loop 的状态管理
Agent Loop 跑起来之后,状态管理是关键。每一轮循环需要记录:当前研究主题、已拆解的子问题列表、每个子问题的状态(待处理/进行中/已完成)、已收集的信息片段、当前轮数。
这些状态我建议用一个 dataclass 或 Pydantic 模型来管理,别用裸字典。原因是状态字段多了之后,裸字典很容易出现"某个字段忘了初始化"或者"字段名拼错"的问题,用模型能在运行前就发现。
轮数上限我一般设 10 到 15。低于 10 可能复杂问题跑不完,高于 15 收益递减明显,而且 token 消耗会失控。同时加一个"无进展检测":如果连续两轮没有新增有效信息片段,直接跳出循环进入综合阶段。
4.4 报告生成与引用映射
综合阶段是把所有收集到的信息组织成报告。Prompt 里要明确报告结构,比如"先给一段概述,然后分点论述,每个论点后面标注来源编号"。同时把检索到的所有片段按编号列在 Prompt 里,让模型有据可依。
生成完之后,做一次引用校验:扫描报告里的所有[n]标记,检查 n 是否在有效范围内,以及标记位置的内容是否和对应片段语义一致。不一致的标记要么修正要么删除。这一步能显著提升报告的可信度。
最后把编号映射回真实来源,输出一份带完整引用的 Markdown 报告。到这里,从一条命令到一份有出处的报告,整条链路就跑通了。
5. 常见问题与排查技巧实录
5.1 检索召回质量差,怎么定位
召回质量差是最常见的问题,表现是"检索回来的片段和问题不相关"。排查思路是从后往前:先看查询改写的结果合不合理,再看向量化有没有问题,最后看切片质量。
如果查询改写出来的查询本身就很泛,那问题在改写 Prompt,需要加更多约束。如果查询没问题但召回的还是不相关,可能是 embedding 模型不适合你的领域,考虑换一个或者做微调。如果前两步都没问题,那就是切片切得不好,检查一下是不是把不相关内容切到了一起。
5.2 Agent Loop 跑飞了怎么办
"跑飞"的表现是循环停不下来,或者反复检索同样的内容。前者通常是收敛条件太宽松,加一个硬性轮数上限就能兜住。后者是查询改写没有利用历史信息,每轮都在问同样的问题。
解决办法是在改写 Prompt 里明确告诉模型"以下查询已经执行过,请生成不同的查询",把历史查询列表传进去。这个改动很小,但效果立竿见影。
5.3 引用对不上号
引用对不上号有两种情况:一种是编号越界,模型标了一个不存在的编号;另一种是编号存在但内容和来源不符。前者在后处理阶段直接过滤掉越界编号即可。后者需要在 Prompt 里强调"引用必须严格对应片段内容",并在校验阶段做语义比对。
如果这种情况频繁出现,说明检索到的片段本身质量不高,模型找不到合适的引用只能硬凑。这时候要回头优化检索环节,而不是在生成环节打补丁。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 检索结果不相关 | 查询改写太泛 / embedding 不匹配 | 检查改写 Prompt、换 embedding 模型 |
| 循环停不下来 | 收敛条件太宽松 | 加轮数上限、加无进展检测 |
| 反复检索同样内容 | 改写未利用历史 | 把历史查询传入改写 Prompt |
| 引用编号越界 | 模型编造引用 | 后处理过滤无效编号 |
| 报告内容空洞 | 检索片段信息量不足 | 优化切片、增加检索轮数 |
| 入库速度慢 | 逐条调用 embedding API | 改批量调用、加并发 |
| Windows 装 pgvector 失败 | 缺编译环境 | 用预编译二进制包 |
5.5 几个我踩过的坑
第一个坑是 embedding 维度不一致。我一开始用了一个模型建表,后来换了模型但忘了改表结构,插入的时候直接报错。教训是:embedding 模型和表结构要绑定管理,换模型必须重建表。
第二个坑是切片重叠设得太大。我一度把重叠设到 50%,结果同一个片段在库里出现好几次,检索的时候 Top-K 里全是重复内容,浪费了大量 token。重叠 10% 到 20% 就够了,别贪多。
第三个坑是没做速率限制。批量向量化的时候一口气发了几百个请求,直接被 API 限流,整个流程中断。后来加了简单的令牌桶限流和指数退避重试,才稳定下来。
第四个坑是报告生成时没控制上下文长度。收集的片段太多,全塞进 Prompt 直接超了模型上下文窗口。解决办法是在综合阶段做一次筛选,只保留和当前论点最相关的片段,而不是把所有片段都塞进去。
5.6 性能优化的几个方向
如果觉得跑得慢,可以从这几个方向优化。检索环节,给向量列建 HNSW 索引,查询速度能提升一个数量级。向量化环节,批量调用加并发,吞吐量能翻好几倍。Agent Loop 环节,把不依赖前序结果的子问题并行处理,能显著缩短总时长。生成环节,用流式输出,用户能更早看到内容,体感上快很多。
不过优化要有优先级。我的经验是,先保证正确性,再优化速度。一个跑得慢但结果靠谱的系统,比一个跑得快但结果乱七八糟的系统有价值得多。等正确性稳定了,再逐个环节做性能优化,每次只改一个变量,方便定位问题。
6. 这套东西还能怎么扩展
跑通基础链路之后,DocResearch 这类项目还有不少可扩展的方向。比如加一个"多源交叉验证"环节,同一个结论如果有多个来源支持,可信度标记就高一些;如果来源之间有冲突,就单独标出来让用户判断。再比如加一个"增量更新"机制,已经入库的文档定期重新抓取,内容变了就更新向量,保证检索到的是最新信息。
还有一个我觉得很有价值的方向是"研究过程可回放"。把 Agent Loop 每一轮的规划、检索、观察都记录下来,用户不仅能看最终报告,还能看它是怎么一步步得出结论的。这对于需要审计的研究场景特别有用,也方便调试和优化。
pgvector 这边,如果文档量涨到百万级,可以考虑做分区表,按时间或来源分区,查询的时候只扫相关分区。再往上,如果单机 PG 扛不住,可以上 PG 的读写分离或者分片方案。不过到这个量级,可能就该考虑专用向量库了,pgvector 的定位还是"够用就好"。
我自己在实际操作中的体会是,这类 Agentic RAG 项目,最难的不是把链路搭起来,而是把每个环节的"判断标准"调准。什么时候算检索够了、什么时候算信息冲突、什么时候该停止循环,这些判断标准没有标准答案,只能根据你的具体场景反复试。我的建议是先把标准设得保守一点,宁可多查几轮也别漏信息,等跑顺了再逐步收紧,在质量和成本之间找平衡。