1. 从一条命令说起:DocResearch 到底在解决什么问题
第一次接触 DocResearch 是在一个内部技术分享会上,当时有人演示了这样一条命令:输入一个研究问题,回车,几分钟后终端里吐出一份带引用来源的 Markdown 报告。说实话,第一反应是"这不就是套壳搜索加总结吗",但仔细看完它的执行日志之后,我改变了看法——它中间经历了问题拆解、多轮检索、证据筛选、交叉验证、引用回填这一整套流程,最后输出的每一句话都能追溯到具体的文档片段。这跟直接把搜索结果丢给大模型让它编一段话,完全是两码事。
DocResearch 本质上是一个面向文档的研究型 Agent 系统。你给它一个研究主题或者一个具体问题,它会自主规划需要查什么、去哪里查、查到的东西够不够、要不要继续追问,最后把散落在多份文档里的信息整合成一份结构化的报告,并且每个结论后面都挂着出处。它解决的核心痛点是:传统 RAG(检索增强生成)只能做"一问一答"式的单轮检索,遇到需要多跳推理、需要对比多个来源、需要判断证据是否充分的问题时,就力不从心了。而 DocResearch 用 Agent Loop 把检索变成了一个可以反复迭代、自我修正的过程。
这篇文章适合三类人看:一是正在做 RAG 应用、想从"单轮检索"升级到"Agentic RAG"的工程师;二是需要处理大量文档、想要自动化产出研究报告的技术人员;三是对 Agent 架构感兴趣、想看看一个完整 Agent Loop 是怎么落地的人。我会从整体设计思路讲起,然后拆解核心细节,再给出可复现的实操过程,最后把我踩过的坑和排查经验整理出来。全文围绕 DocResearch 这个项目展开,涉及 pgvector、Milvus 这些向量库的选型和实操,也会讲到 Milvus standalone 模式在 Mac 和 Linux 上的安装细节。
2. 整体设计思路:为什么是 Agent Loop 而不是单轮 RAG
2.1 单轮 RAG 的天花板在哪里
先说清楚为什么需要 DocResearch 这种东西。单轮 RAG 的流程很简单:用户提问 → 把问题向量化 → 在向量库里找最相似的 K 个片段 → 拼进 prompt → 让大模型生成答案。这个流程在"事实型问答"上表现不错,比如"公司的报销标准是多少",检索到对应文档片段就能答。但它有三个绕不过去的坎。
第一个坎是多跳问题。比如"对比 A 方案和 B 方案在成本上的差异",这个问题需要先分别找到 A 方案的成本文档和 B 方案的成本文档,然后做对比。单轮检索只能召回跟问题向量最接近的片段,很可能只召回了 A 方案的,或者召回了两个方案但都是概述性的、没有具体数字。第二个坎是证据充分性判断。单轮 RAG 不管检索到的东西够不够,都会硬着头皮生成,结果就是"一本正经地胡说"。第三个坎是来源追溯。单轮 RAG 生成的答案往往是把多个片段揉在一起,你很难知道哪句话来自哪份文档。
DocResearch 的设计思路就是针对这三个坎来的。它把"检索-生成"这个一次性动作,拆成了一个循环:规划 → 检索 → 评估 → 再规划 → 再检索,直到证据充分才生成报告。这个循环就是 Agent Loop。
2.2 Agent Loop 的核心循环长什么样
DocResearch 的 Agent Loop 我把它概括成四个阶段,用大白话讲就是"想清楚要查什么、去查、看看查到的够不够、不够就换个角度再查"。
规划阶段(Plan):拿到用户的问题后,Agent 先做任务分解。比如问题是"DocResearch 支持哪些向量库,各自的适用场景是什么",它会拆成子问题:DocResearch 支持哪些向量库?每个向量库的特点是什么?在什么场景下选哪个?这一步通常用大模型来做,输出一个结构化的查询计划。
检索阶段(Retrieve):按照查询计划,逐个执行检索。这里就是 Agentic RAG 跟普通 RAG 的区别所在——它不是把原始问题直接拿去检索,而是把规划出来的子问题分别检索,而且每个子问题可能触发多次检索(比如第一次没查到,换个关键词再查)。
评估阶段(Evaluate):检索回来之后,Agent 要判断"这些证据够不够回答子问题"。这个判断可以基于规则(比如检索到的片段数量、相似度分数),也可以让大模型来评估(把问题和检索结果一起给它,问它"这些信息足够回答吗")。如果不够,就回到规划阶段,调整查询策略。
生成阶段(Generate):所有子问题都有足够证据之后,Agent 把证据整合起来,生成最终报告,并且把每个结论对应的来源标注清楚。
这个循环的关键在于评估阶段。很多号称 Agentic RAG 的项目其实只是把单轮检索包装了一下,没有真正的评估和回退机制。DocResearch 的价值就在于它把这个循环跑通了,而且评估环节做得比较扎实。
2.3 向量库选型:pgvector 还是 Milvus
DocResearch 支持多种向量存储后端,热词里提到的 pgvector 和 Milvus 是两个典型代表。这两个的选择不是拍脑袋决定的,背后有明确的场景考量。
pgvector是 PostgreSQL 的向量扩展。它的优势是跟关系型数据放在一起。如果你的文档元数据(标题、作者、创建时间、权限标签)本来就存在 PostgreSQL 里,那用 pgvector 就不需要额外维护一套向量数据库,检索的时候可以直接用 SQL 做混合查询——比如"在某个时间范围内、某个权限标签下,找跟问题最相似的片段"。这种"向量+标量"的联合过滤是 pgvector 的强项。缺点是当向量数量上到千万级、亿级的时候,性能会明显下降,索引构建也慢。
Milvus是专门的向量数据库,为大规模向量检索而生。它支持多种索引类型(IVF、HNSW、DiskANN 等),可以水平扩展,在亿级向量上依然能保持毫秒级检索。热词里提到的 "milvus standalone 模式" 就是它的一种部署形态——单机版,适合开发和中小规模生产;再往上还有集群版。Milvus 的余弦相似度检索(热词里的 "milvus 余弦值")是它最常用的度量方式之一。
DocResearch 把这两个都支持,逻辑很清晰:小规模、需要跟业务数据联合查询的场景用 pgvector;大规模、纯向量检索的场景用 Milvus。这个设计让项目既能快速上手(用 pgvector 不用额外部署服务),又能支撑生产规模(切到 Milvus)。
2.4 为什么要有"出处"这个硬约束
"有出处的报告"是 DocResearch 标题里最值得说的部分。很多 RAG 系统也号称有引用,但实际做的是"把检索到的文档列在最后",而不是"每句话对应到具体片段"。DocResearch 的做法是在生成阶段强制模型输出结构化的引用标记,比如[1]、[2],然后在后处理阶段把这些标记替换成真实的文档片段 ID 和位置信息。
这个约束带来的好处是可验证性。用户看到报告里的某个结论,可以点开引用直接跳到原文对应位置,自己判断这个结论是否成立。这在研究场景里特别重要——研究报告的价值不在于"读起来像那么回事",而在于"每个论断都有据可查"。代价是生成阶段更复杂,模型需要同时处理"组织语言"和"标注来源"两件事,prompt 设计要更精细。
3. 核心细节解析:Agent Loop 的每个环节怎么落地
3.1 查询规划:把大问题拆成可检索的小问题
查询规划这一步,很多人会低估它的重要性。我见过不少项目直接把用户问题丢给向量库检索,然后抱怨"检索效果不好"。问题往往出在用户问题本身不适合直接检索。比如"DocResearch 的 Agent Loop 是怎么设计的,跟普通 RAG 有什么区别",这个问题里包含了两个意图:一是问 Agent Loop 的设计,二是问跟普通 RAG 的区别。直接拿整句话去检索,向量会偏向"区别"这个词,导致 Agent Loop 设计相关的文档召回不足。
DocResearch 的规划阶段会做两件事:意图拆解和查询改写。意图拆解是把复合问题拆成原子问题,上面那个问题会拆成"DocResearch Agent Loop 的设计"和"DocResearch 与普通 RAG 的区别"两个子问题。查询改写是把口语化的问题改写成更适合检索的形式,比如把"怎么设计的"改成"架构设计"、"实现方案"这类关键词。
这一步的实现通常用大模型加结构化输出。prompt 里会给几个 few-shot 示例,让模型输出 JSON 格式的查询计划:
{ "sub_questions": [ { "id": 1, "question": "DocResearch Agent Loop 的架构设计是什么", "keywords": ["Agent Loop", "架构", "规划", "检索", "评估"], "priority": "high" }, { "id": 2, "question": "DocResearch 与普通 RAG 的区别", "keywords": ["Agentic RAG", "单轮检索", "多跳推理"], "priority": "medium" } ] }这里有个实操心得:子问题的数量要控制。我试过让模型自由拆解,结果一个问题拆出十几个子问题,每个都去检索一遍,耗时直接爆炸。后来加了个约束,最多拆 5 个子问题,并且按优先级排序,优先检索 high 优先级的。这样既保证了覆盖度,又控制了成本。
3.2 检索执行:多路召回与去重
检索阶段不是简单地把子问题向量化然后查库。DocResearch 在这里做了多路召回:每个子问题会同时走向量检索和关键词检索(BM25 之类的稀疏检索),然后把两路结果合并去重。为什么要这么做?因为向量检索擅长语义匹配,但对专有名词、代码标识符这类精确匹配不敏感;关键词检索正好相反。两路结合能覆盖更多情况。
向量检索这块,如果用的是 Milvus,核心参数是top_k和nprobe(或者 HNSW 的ef)。top_k是返回多少个最相似的片段,一般设 10 到 20;nprobe是搜索多少个倒排桶,值越大越准但越慢。如果是 pgvector,核心是索引类型和lists/probes参数。这些参数没有万能值,要根据你的文档规模和查询延迟要求来调。
去重这块有个细节值得说:不能只按片段 ID 去重。因为同一个文档的不同片段可能内容高度重叠(比如滑动窗口切分导致的),按 ID 去重去不掉。DocResearch 用的是语义去重——计算片段之间的相似度,超过阈值的只保留一个。这个阈值一般设 0.9 左右,太高了去重不干净,太低了会误删有用信息。
3.3 证据评估:判断"够不够"的三种策略
评估阶段是 Agent Loop 的灵魂。DocResearch 用了三种策略来判断证据是否充分,可以单独用也可以组合用。
基于规则的评估最简单:看检索到的片段数量是否达到阈值、相似度分数是否高于某个值、是否覆盖了所有子问题。这种评估快但不准,容易漏判。
基于模型的评估更准:把子问题和检索到的片段一起给大模型,让它判断"这些信息是否足够回答这个问题",输出 yes/no 加理由。这种评估准但慢,而且消耗 token。DocResearch 的做法是先用规则做粗筛,规则判断不确定的时候再调模型,这样兼顾了速度和准确率。
基于覆盖度的评估是第三种:把子问题拆成更细的信息点,看检索结果覆盖了哪些信息点。比如子问题是"DocResearch 支持哪些向量库",信息点就是"pgvector"、"Milvus"等,看检索结果里有没有提到这些。这种评估适合事实型问题。
实操中我发现,评估的 prompt 设计比评估策略本身更重要。早期我用的是"这些信息足够回答吗",模型经常回答"足够",但实际上漏了关键信息。后来改成"请逐条检查以下信息点是否在检索结果中出现,列出缺失的信息点",准确率明显提升。这个改动看起来小,但效果差异很大。
3.4 报告生成:引用回填的两种实现
生成阶段要把证据整合成报告,并且标注来源。引用回填有两种实现方式,各有优劣。
方式一:生成时直接标注。在 prompt 里要求模型在每句话后面加[片段ID]标记。优点是简单直接,缺点是模型经常标错或者漏标,尤其是长报告的时候。
方式二:生成后对齐。先让模型生成不带引用的报告,然后用句子级别的语义匹配,把报告里的每句话跟检索到的片段做对齐,找到最相关的片段作为引用。优点是引用准确率高,缺点是需要额外的对齐步骤,而且有些句子可能找不到合适的片段。
DocResearch 用的是混合方式:生成时要求模型标注,生成后再做一次校验和对齐,修正明显错误的引用。这个做法比较务实,既利用了模型的标注能力,又用后处理兜底。
引用格式上,我建议用结构化引用而不是纯文本标记。比如:
{ "sentence": "DocResearch 支持 pgvector 和 Milvus 两种向量存储后端。", "citations": [ {"doc_id": "design_doc_001", "chunk_id": 12, "text": "系统支持 pgvector 和 Milvus..."} ] }这样前端渲染的时候可以做成可点击的引用,用户体验比纯文本的[1]好很多。
4. 实操过程:从零跑通 DocResearch
4.1 环境准备与依赖安装
先把环境搭起来。DocResearch 是 Python 项目,建议用 Python 3.10 以上。依赖管理用 poetry 或者 pip 都行,我用的是 pip + venv。
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install -r requirements.txt核心依赖包括:大模型的 SDK(OpenAI 或者兼容接口的)、向量库客户端(psycopg2 用于 pgvector,pymilvus 用于 Milvus)、文档解析库(pypdf、python-docx 等)、以及 Web 框架(如果用它的 API 服务)。
环境变量要配好,主要是大模型的 API key 和 base url,以及向量库的连接信息。建议用.env文件管理,不要硬编码在代码里。
4.2 向量库部署:pgvector 和 Milvus 两条路
pgvector 路线适合快速上手。如果你已经有 PostgreSQL,直接装扩展:
CREATE EXTENSION vector;然后建表:
CREATE TABLE doc_chunks ( id SERIAL PRIMARY KEY, doc_id VARCHAR(255), chunk_text TEXT, embedding vector(1536), metadata JSONB ); CREATE INDEX ON doc_chunks USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);这里的vector(1536)是向量维度,要跟你用的 embedding 模型对齐。ivfflat是索引类型,lists参数一般设成数据量的平方根左右。数据量小的时候(几万条以内)可以不建索引,直接暴力检索反而更快。
Milvus 路线适合大规模场景。热词里提到的 "milvus standalone 模式" 是最常用的部署方式。在 Mac 上用 Docker 装 Milvus standalone:
# 下载 docker-compose 配置文件 wget https://github.com/milvus-io/milvus/releases/download/v2.3.0/milvus-standalone-docker-compose.yml -O docker-compose.yml # 启动 docker-compose up -d启动后 Milvus 默认监听 19530 端口。在 Linux 服务器上部署类似,注意防火墙要放开这个端口。如果是在内网环境,可以用milvus_uri指定连接地址。
有个细节要注意:热词里提到 "服务器 linux 上使用 milvus_uri: str = './data/milvus.db' 本地加载 milvus 提示 milv...",这说的是Milvus Lite模式——用本地文件作为存储,不需要起 Docker 服务。这个模式适合开发和测试,但不适合生产。如果你在代码里看到milvus_uri指向一个.db文件,那就是 Lite 模式。切到 standalone 模式的时候,要把milvus_uri改成http://localhost:19530这样的地址。
Milvus 里建集合的时候,要指定度量方式。余弦相似度(热词里的 "milvus 余弦值")是最常用的:
from pymilvus import CollectionSchema, FieldSchema, DataType, Collection fields = [ FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True), FieldSchema(name="doc_id", dtype=DataType.VARCHAR, max_length=255), FieldSchema(name="chunk_text", dtype=DataType.VARCHAR, max_length=65535), FieldSchema(name="embedding", dtype=DataType.FLOAT_VECTOR, dim=1536), ] schema = CollectionSchema(fields) collection = Collection(name="doc_chunks", schema=schema) collection.create_index( field_name="embedding", index_params={"index_type": "HNSW", "metric_type": "COSINE", "params": {"M": 16, "efConstruction": 200}} )HNSW 的M和efConstruction是建索引参数,M一般 16 到 64,越大越准但内存占用越高;efConstruction一般 200 到 500。检索的时候还有个ef参数,一般设成top_k的两倍以上。
4.3 文档入库:切分策略决定检索质量
文档入库这一步,切分策略是最关键的。切得太碎,片段缺乏上下文,检索出来答非所问;切得太大,一个片段里混了多个主题,向量表示不准确。我的经验是按语义切分,而不是按固定字数切分。
具体做法是:先用文档解析库把 PDF/Word/Markdown 解析成纯文本,然后按标题层级切分成段落,再对过长的段落做二次切分。二次切分的时候用滑动窗口,窗口大小 500 到 800 字,重叠 100 到 200 字。重叠是为了避免关键信息正好被切在边界上。
切分完之后,每个片段要带上元数据:文档 ID、标题、章节路径、位置信息。这些元数据在检索和引用回填的时候都要用到。元数据存哪里?如果用 pgvector,直接存在 JSONB 字段里;如果用 Milvus,存在标量字段里。
embedding 这一步,选模型很重要。中文场景建议用专门的中文 embedding 模型,不要直接用英文模型。维度上,1536 维是常见选择,但也不是越大越好——维度高检索慢、存储成本高,要权衡。
4.4 跑通第一个查询:观察 Agent Loop 的执行日志
环境搭好、文档入库之后,就可以跑第一个查询了。DocResearch 一般会打印详细的执行日志,这是观察 Agent Loop 的最好窗口。
python -m docresearch.query "DocResearch 支持哪些向量库,各自适用什么场景"日志里你会看到:规划阶段输出了几个子问题、每个子问题检索到了多少片段、评估阶段判断证据是否充分、如果不充分又发起了哪些补充检索、最后生成报告用了哪些片段。第一次跑的时候建议把日志级别调到 DEBUG,把中间结果都打出来,这样能直观看到 Agent Loop 是怎么转的。
我建议先用小规模文档测试,比如放 10 篇文档进去,跑几个问题,观察检索和评估的行为。确认流程通了之后,再上大规模文档。直接上大规模文档的话,一旦检索效果不好,很难定位是切分问题、embedding 问题还是评估问题。
4.5 报告输出与引用校验
报告生成之后,一定要做引用校验。校验的方法是:随机抽几条引用,点开看原文片段,确认报告里的结论确实能从片段里推出来。我见过不少情况是引用指向了片段,但片段内容跟结论对不上——要么是模型标错了,要么是片段本身就不相关。
DocResearch 一般会提供一个校验脚本,自动检查引用的有效性。但自动检查只能查"引用是否存在",查不了"引用是否支持结论"。后者还是要人工抽查。我的做法是每次生成报告后抽 10% 的引用人工看一遍,发现系统性问题就调整 prompt 或者检索策略。
5. 常见问题与排查技巧实录
5.1 检索召回不准的排查路径
检索不准是最常见的问题。排查的时候按这个顺序来:先看切分,再看 embedding,最后看检索参数。
切分问题表现为:检索到的片段内容不完整,或者一个片段里混了多个主题。解决办法是调整切分策略,按语义切分,控制片段大小。
embedding 问题表现为:语义相近的查询和文档,向量距离却很远。解决办法是换 embedding 模型,或者检查是不是用了英文模型处理中文。有个简单的验证方法:拿几个已知相关的查询-文档对,算一下它们的余弦相似度,如果低于 0.7,基本可以确定是 embedding 的问题。
检索参数问题表现为:相关片段在库里,但没被召回。解决办法是调大top_k、调大nprobe或ef。如果调大了还是召回不到,那可能是索引构建的问题,需要重建索引。
5.2 Agent Loop 死循环的终止条件
Agent Loop 有个风险是死循环:评估一直说证据不够,规划一直发起新检索,但检索结果始终不满足。DocResearch 用几个条件来终止循环:最大迭代次数(一般 5 到 10 次)、最大检索次数、以及"连续两次检索结果高度重叠"的检测。
实操中我发现,死循环往往是因为评估标准太严。比如要求"所有信息点都必须覆盖",但有些信息点文档里根本没有,那就会一直循环。解决办法是给评估加个"尽力而为"的兜底——如果迭代次数到了上限,就用现有证据生成报告,并在报告里标注"以下信息未能找到充分来源"。
5.3 Milvus 连接与性能问题速查
Milvus 相关的问题,我整理了一个速查表:
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| 连接超时 | 服务没启动或端口不通 | 检查 docker 容器状态,确认 19530 端口可达 |
| 检索结果为空 | 集合没加载或索引没建 | 调用collection.load(),确认索引已创建 |
| 检索很慢 | 索引参数不合理 | 调小ef,或换用 IVF 索引 |
| 内存占用高 | HNSW 索引吃内存 | 换用 DiskANN 或 IVF_PQ 索引 |
| 余弦值异常 | 向量没归一化 | 入库前对向量做 L2 归一化 |
这里重点说下向量归一化。余弦相似度计算的是向量夹角,跟向量长度无关。但如果你的向量没归一化,有些实现会先算内积再除以模长,数值稳定性会差一些。建议入库前统一做 L2 归一化,这样余弦相似度就等于内积,计算更快也更稳。
5.4 pgvector 索引选择的坑
pgvector 的索引选择有个常见的坑:数据量小的时候不要建 ivfflat 索引。ivfflat 是近似索引,建了之后检索是近似的,可能漏掉真正最相似的片段。数据量在几万条以内的时候,暴力检索(不建索引)反而又快又准。等数据量上去了再建索引。
另一个坑是lists参数。lists设得太小,每个桶里数据太多,检索慢;设得太大,每个桶里数据太少,召回率下降。经验值是lists = rows / 1000,但也要根据实际数据分布调。建完索引后可以用EXPLAIN ANALYZE看查询计划,确认索引有没有被用上。
5.5 引用标注错位的修复
引用标注错位有两种情况:标错了片段和标错了位置。标错片段是模型的问题,解决办法是在 prompt 里给更明确的指令,比如"只引用直接支持该结论的片段,不要引用相关但不直接的片段"。标错位置是后处理的问题,解决办法是改进句子对齐算法,用更细粒度的匹配。
我踩过的一个坑是:模型在生成报告的时候,会把多个片段的信息融合成一句话,然后只标了其中一个片段的引用。这种情况下,引用是不完整的。解决办法是要求模型一句话最多融合两个片段的信息,超过两个就拆成多句话。这个约束看起来限制了表达,但实际上让引用更准确,报告的可验证性更强。
5.6 成本控制的几个实用技巧
Agent Loop 比单轮 RAG 贵,因为要多次调用大模型。控制成本有几个技巧:规划阶段用便宜的小模型,生成阶段用强模型;评估阶段先用规则筛,规则不确定再调模型;缓存检索结果,相同子问题不重复检索;限制迭代次数,避免无意义的循环。
我实测下来,一个中等复杂度的问题,Agent Loop 的 token 消耗大概是单轮 RAG 的 3 到 5 倍。如果成本敏感,可以把迭代次数限制在 3 次以内,大部分问题 3 次迭代就能收敛。另外,embedding 的计算成本也不低,文档入库的时候可以批量计算,利用批处理提升吞吐。
6. 一些个人体会
DocResearch 这个项目最让我欣赏的地方,是它把"有出处"当成了一个硬约束,而不是一个可选功能。很多 RAG 项目把引用当成锦上添花的东西,生成完报告随便附几个链接就完事了。但 DocResearch 是从架构层面保证引用的准确性——检索阶段记录片段来源,评估阶段验证证据充分性,生成阶段强制标注,后处理阶段校验对齐。这一整套下来,报告的可信度跟"让模型自由发挥"完全不是一个量级。
Agent Loop 的落地,我的体会是评估环节是分水岭。没有评估的 Agent Loop 就是个花架子,转了几圈还是那些东西。有了靠谱的评估,Agent 才知道什么时候该继续查、什么时候可以收手。评估的 prompt 设计值得反复打磨,我前后改了七八版才比较满意。
向量库的选择上,我的建议是先用 pgvector 跑通,需要的时候再切 Milvus。pgvector 部署简单,跟业务数据放一起,调试方便。等数据量上来了、检索延迟成为瓶颈了,再迁移到 Milvus。迁移的时候注意 embedding 维度要一致,度量方式要一致(都用余弦),这样迁移成本最低。
最后分享一个小技巧:给 Agent Loop 加一个"检索历史"的记录。每次检索的子问题、检索到的片段 ID、评估结果都记下来。这样当报告出问题的时候,可以回溯整个执行过程,定位是哪一步出了问题。这个记录在调试阶段特别有用,我靠它定位过好几次"评估说够了但实际不够"的 bug。