news 2026/10/4 23:05:45

DocResearch 实战:基于 Python Agent 与向量库的引用溯源报告生成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DocResearch 实战:基于 Python Agent 与向量库的引用溯源报告生成

1. 从一条命令说起:DocResearch 到底在解决什么问题

第一次看到 DocResearch 这个项目名的时候,我以为又是一个"输入问题、吐出一段话"的问答玩具。真正把仓库拉下来跑通之后才发现,它想做的事情比普通问答要"重"得多——你给它一条命令,它还给你的是一份带引用出处的报告。这个差别很关键:普通问答给你的是"答案",DocResearch 给你的是"答案 + 它凭什么这么说"。

我先把它的定位讲清楚。DocResearch 本质上是一个面向文档的研究型 Agent 系统:用户丢进来一批文档(PDF、Markdown、网页存档都行),系统先把这些文档切块、向量化、存进向量库,然后由一个 Agent 负责"检索—阅读—推理—写作"这一整条链路,最后产出一份结构化的报告,报告里每一句关键结论后面都挂着来源片段。它解决的核心痛点是:大模型会一本正经地胡说,而研究报告这种场景恰恰最不能容忍胡说。你写行业分析、做竞品调研、整理技术选型材料,最怕的就是引用了不存在的资料。

那它适合谁?我梳理了一下,大概三类人用起来最舒服。第一类是做技术调研的工程师,手里攒了几十篇论文或者官方文档,想快速出一份对比结论;第二类是做内容/咨询的同学,需要基于一堆资料写报告,但又不想逐字逐句翻;第三类是想学 Agent 工程化的开发者,DocResearch 的代码结构相对干净,检索、编排、生成三段分得很清楚,拿来当 Agent 项目的学习模板很合适。

关键词里出现的Python、Agent、pgvector、Milvus基本就是它的技术骨架:Python 是主语言,Agent 是编排核心,pgvector 和 Milvus 是两个可选的向量存储后端。为什么会有两个向量库?这背后其实是一个很现实的工程取舍,我在第 2 节会展开讲。先记住一句话:DocResearch 的价值不在于"能回答",而在于"回答得有据可查",后面所有的设计都是围绕这句话转的。

2. 整体架构拆解:为什么是"检索 + Agent + 引用"这套组合

2.1 为什么不做纯 RAG,非要套一层 Agent

很多人第一反应是:这不就是个 RAG(检索增强生成)吗?检索几个片段塞进 prompt,让模型总结一下不就完了。我一开始也这么想,但真跑几个复杂问题就会发现纯 RAG 的天花板很明显。

纯 RAG 的流程是"一次检索、一次生成",问题在于:用户的问题往往不是一次检索能覆盖的。比如你问"这两个方案在并发场景下各自的取舍是什么",模型需要先找到方案 A 的并发描述,再找到方案 B 的并发描述,然后对比。一次检索很可能只召回其中一边,或者召回的都是泛泛的介绍。Agent 的价值就在这里——它可以把一个大问题拆成多个子查询,分别检索、分别阅读,最后再综合。这就是所谓"多跳检索"。

DocResearch 里的 Agent 大致承担了这几个职责:判断问题需不需要拆解、决定检索什么关键词、判断召回的内容够不够、不够就换个说法再检、最后组织成报告。这套逻辑用一句话概括就是:Agent 是"检索策略的决策者",而不是"答案的生成者"。生成只是最后一步,前面大量的工作是在决定"该看哪些材料"。

提示:如果你之前只写过"检索 + 拼接 + 生成"的线性 RAG,第一次看 Agent 版会觉得绕。别急着简化,先跑通再理解,很多"绕"是为了处理真实场景里的脏数据和不完整问题。

2.2 向量库为什么同时支持 pgvector 和 Milvus

这是我觉得 DocResearch 设计上比较务实的一点。它没有绑死一个向量库,而是同时支持pgvector和Milvus,这俩的定位完全不同。

pgvector 是 PostgreSQL 的一个扩展,把向量当成一种数据类型存进关系库。它的好处是你不需要额外维护一套系统——如果你的业务数据本来就在 Postgres 里,加个扩展就能做向量检索,事务、备份、权限全都复用现成的。缺点是数据量大了之后,向量索引的性能和扩展性会吃紧,几百万条以上就比较难受了。

Milvus 是专门的向量数据库,为大规模向量检索而生,支持多种索引类型(HNSW、IVF 等),能水平扩展。缺点是你得单独部署和维护一套服务,对个人项目或者小团队来说有点重。

所以选型逻辑很清晰:小规模、想省事、已有 Postgres,就用 pgvector;数据量大、要性能、能接受运维成本,就上 Milvus。DocResearch 把这两个都做成可插拔的,等于把选择权交给了使用者。这个设计思路值得学——别在项目里替用户做死决定,把接口抽象好,让用户按场景选。

2.3 引用溯源是怎么落地的

"有出处的报告"这句话听起来简单,实现起来要解决一个核心问题:怎么让模型生成的每句话都能对应回原文片段。

常见的做法有两种。一种是生成时带标记:检索到的每个片段都编号,让模型在生成时引用编号,比如"根据 [3],该方案在并发下……"。另一种是生成后对齐:先生成,再用相似度把句子和原文片段匹配上。DocResearch 走的是偏向前者的路子,因为后者的匹配准确率很难保证,容易出现"张冠李戴"。

具体实现上,检索阶段返回的每个 chunk 都带着元数据(来源文件、页码、chunk 序号),Agent 在组织报告时把这些元数据一起带上,最终渲染成引用列表。这里有个细节很关键:chunk 的切分粒度直接影响引用质量。切太碎,一句话被拆成三段,引用看起来支离破碎;切太大,一个 chunk 里混了好几个主题,引用就不精确。我实测下来,按语义段落切、单块控制在 300 到 500 字是个比较舒服的区间。

3. 环境搭建与核心组件实操:从零把项目跑起来

3.1 Python 环境与依赖安装的坑

DocResearch 是 Python 项目,第一步肯定是把环境弄干净。我强烈建议用虚拟环境,别直接往系统 Python 里装,不然依赖冲突能让你怀疑人生。

# 创建虚拟环境(Python 3.10 及以上,3.8 有些新库装不上) python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 升级 pip,老版本 pip 装某些包会卡住 pip install --upgrade pip # 安装项目依赖 pip install -r requirements.txt

这里有几个我踩过的坑。第一,Python 版本别用 3.8,热词里有人搜"python 3.8",但 DocResearch 依赖的一些库(尤其是较新的向量库客户端)已经不支持 3.8 了,建议 3.10 或 3.11。第二,装 numpy 这类科学计算库时,如果报编译错误,多半是缺系统依赖,Linux 上先apt install python3-dev build-essential,Mac 上装好 Xcode Command Line Tools 基本就没事。第三,国内网络装包慢的话配个镜像源,这个不用我多说。

# 临时用镜像源装包 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

3.2 用 Docker 在 Mac 上跑 Milvus Standalone

如果你决定用 Milvus 作为向量后端,最省心的方式是standalone 模式 + Docker。Milvus 官方提供了 docker-compose 配置,Mac 上(尤其是 M 系列芯片)跑起来没什么大问题。

# 下载官方 compose 文件(以官方仓库为准,版本号按需替换) wget https://github.com/milvus-io/milvus/releases/download/v2.4.x/milvus-standalone-docker-compose.yml -O docker-compose.yml # 启动 docker compose up -d # 检查容器状态,三个容器都 Up 才算正常 docker compose ps

启动之后 Milvus 默认监听19530 端口(gRPC)和9091 端口(健康检查/指标)。你可以用curl http://localhost:9091/healthz看它活没活。

注意:Mac 上 Docker 默认分配的内存可能不够,Milvus standalone 建议至少给 Docker 分配8GB 内存,不然启动到一半容器会被 OOM kill,日志里能看到被杀的记录。这个坑我踩过,排查了半天以为是配置问题,结果是内存不够。

3.3 服务器 Linux 上用本地文件模式加载 Milvus

热词里有一条很具体:"服务器 linux 上使用milvus_uri: str = "./data/milvus.db"本地加载 milvus"。这说的是Milvus Lite模式——不用起服务,直接把向量存成一个本地文件。这对个人项目和小规模数据太友好了。

from pymilvus import MilvusClient # 本地文件模式,数据落在 ./data/milvus.db milvus_uri: str = "./data/milvus.db" client = MilvusClient(uri=milvus_uri) # 建集合,维度要和你的 embedding 模型对齐 client.create_collection( collection_name="doc_chunks", dimension=768, # 举例,实际看你用的 embedding 模型 )

这个模式的好处是零运维,一个文件搞定。但要注意:Milvus Lite 有数据量上限,官方说法是适合百万级以下的向量,再大就得切到 standalone 或集群。另外,本地文件模式不支持多进程并发写,如果你打算多 worker 同时写库,会出问题,这种场景还是老老实实上 standalone。

3.4 pgvector 方案:已有 Postgres 就直接加扩展

如果你不想引入 Milvus,pgvector 是更轻的选择。前提是你得有个 Postgres。

-- 在 Postgres 里启用扩展 CREATE EXTENSION IF NOT EXISTS vector; -- 建表,embedding 维度按你的模型来 CREATE TABLE doc_chunks ( id BIGSERIAL PRIMARY KEY, content TEXT, source TEXT, embedding vector(768) ); -- 建 HNSW 索引,加速近似最近邻检索 CREATE INDEX ON doc_chunks USING hnsw (embedding vector_cosine_ops);

这里有个关键点:索引类型和距离度量要匹配。DocResearch 里做的是语义检索,通常用余弦相似度(cosine),所以索引用vector_cosine_ops。如果你用欧氏距离,就换成vector_l2_ops。热词里有人搜"milvus 余弦值",其实说的就是同一件事——向量检索里,余弦相似度衡量的是方向一致性,对文本语义匹配最合适,因为文本向量的长度往往受文本长度影响,方向才是语义的载体。

4. 检索与生成链路:把"有出处"这件事做扎实

4.1 文档切分:决定引用质量的第一道关

前面提过 chunk 粒度的重要性,这里展开讲。文档切分不是简单地按字数硬切,那样会把一句话、一个段落拦腰截断,检索出来的片段读起来莫名其妙。

我的做法是按结构切 + 按长度兜底。先按标题、段落这些自然边界切,如果某个段落特别长(超过 800 字),再按句子边界二次切分,保证每块在 300 到 500 字之间。这样切出来的 chunk 语义完整,引用的时候读者一看就知道上下文是什么。

def split_text(text, max_len=500, min_len=300): # 先按段落切 paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()] chunks = [] buffer = "" for p in paragraphs: if len(buffer) + len(p) <= max_len: buffer += ("\n\n" if buffer else "") + p else: if buffer: chunks.append(buffer) # 单段就超长,按句子再切 if len(p) > max_len: sentences = p.replace("。", "。\n").split("\n") sub = "" for s in sentences: if len(sub) + len(s) <= max_len: sub += s else: chunks.append(sub) sub = s buffer = sub else: buffer = p if buffer: chunks.append(buffer) return chunks

这段代码不复杂,但它决定了后面所有环节的上限。切分做不好,检索再准、模型再强,引用也是歪的。

4.2 向量化与检索:embedding 模型怎么选

向量化就是把文本变成一串数字(向量),让语义相近的文本在向量空间里距离也近。这一步用的模型叫 embedding 模型。选型上有几个考量:维度、语言支持、速度、成本。

维度不是越高越好。768 维和 1536 维在多数场景下效果差距不大,但 1536 维的存储和计算成本翻倍。中文场景要选对中文友好的模型,有些英文模型在中文上表现会明显掉档。如果追求本地化、不想调外部接口,可以用开源的 sentence-transformers 系列,跑在本地,隐私和成本都可控。

检索的时候,把用户查询也向量化,然后在向量库里找余弦相似度最高的 top-k 个 chunk。k 取多少?我一般取5 到 10。太小了召回不全,太大了塞进 prompt 会稀释重点,还费 token。

def retrieve(query, client, embed_model, top_k=8): q_vec = embed_model.encode(query).tolist() results = client.search( collection_name="doc_chunks", data=[q_vec], limit=top_k, output_fields=["content", "source"], ) return results[0]

4.3 Agent 编排:让检索"多跳"起来

这是 DocResearch 区别于普通 RAG 的核心。Agent 拿到问题后,不是直接检索,而是先"想一下"。

我把它拆成几个可复现的步骤。第一步,问题分析:判断这个问题是单点事实查询,还是需要多步推理的对比/分析类问题。第二步,查询改写:把口语化的问题改写成适合检索的关键词组合,有时候一个问题要拆成两三个子查询。第三步,检索与评估:对每个子查询检索,然后判断召回内容是否足以回答,不够就换关键词重试。第四步,综合生成:把所有有效片段组织起来,生成带引用的报告。

def research_agent(question, retriever, llm): # 1. 拆解子查询 sub_queries = llm.decompose(question) # 返回 ["子问题1", "子问题2", ...] all_chunks = [] for sq in sub_queries: chunks = retriever.retrieve(sq, top_k=6) # 2. 评估召回是否足够,不够就改写重试 if not is_sufficient(chunks, sq): sq2 = llm.rewrite(sq) chunks += retriever.retrieve(sq2, top_k=6) all_chunks.extend(chunks) # 3. 去重后生成带引用的报告 unique_chunks = dedup(all_chunks) report = llm.generate_report(question, unique_chunks) return report

这套流程里,"评估召回是否足够"是最难也最有价值的一步。简单做法是看召回片段的相似度分数,低于阈值就认为不够;进阶做法是让模型自己判断"这些材料能不能回答这个问题"。我实测下来,混合判断(分数 + 模型判断)比单用任何一种都稳。

提示:Agent 多跳检索很容易陷入"无限重试"。一定要设最大跳数(比如 3 跳),到顶了就用现有材料生成,并在报告里注明"部分结论基于有限材料"。宁可诚实,不要硬编。

4.4 生成带引用的报告:prompt 怎么写

最后一步是把材料喂给模型,让它写报告。prompt 的设计直接决定引用质量。我的模板大致是这样:

你是一个严谨的研究助手。请基于以下材料回答问题,要求: 1. 每个关键结论后面用 [编号] 标注来源,编号对应材料序号。 2. 材料中没有提到的内容,不要编造,直接说"材料未涉及"。 3. 如果不同材料有冲突,指出冲突并分别标注来源。 问题:{question} 材料: [1] {chunk_1_content} (来源:{source_1}) [2] {chunk_2_content} (来源:{source_2}) ...

这里最关键的一条是**"材料未涉及就说未涉及"。不加这条约束,模型会习惯性地"补全"信息,引用就假了。加了之后,报告里会出现一些"材料未涉及"的句子,看起来不够"满",但这才是真实研究报告该有的样子**。

5. 常见问题与排查技巧实录

5.1 检索召回不准怎么办

这是最高频的问题。表现是:明明文档里有答案,检索就是召不回来。排查顺序我一般这样走。

先看切分。把召回的 chunk 打出来读一遍,如果读起来语义不完整,那就是切分的问题,回去调 chunk 大小和切分策略。再看embedding 模型。如果文档是中文、模型是纯英文的,召回质量会明显差,换中文友好的模型。最后看查询本身。用户的口语化问题直接拿去检索,效果往往不好,加一层查询改写,把"这俩哪个好"改成"方案 A 方案 B 对比 优缺点",召回率能明显提升。

现象可能原因排查动作
答案在文档里但召不回切分过碎/过大打印 chunk 检查语义完整性
中文文档召回差embedding 模型不匹配换中文友好模型
口语化问题召回差查询未改写加查询改写层
召回内容重复文档有重复段落入库前去重

5.2 向量库连接与性能问题

用 Milvus standalone 的时候,最常见的报错是连不上。先确认容器状态,docker compose ps看三个容器是不是都 Up。如果 Milvus 容器反复重启,八成是内存不够,去 Docker 设置里加内存。如果连上了但检索很慢,检查索引建了没有——Milvus 默认可能用 FLAT(暴力检索),数据量一大就慢,要手动建 HNSW 或 IVF 索引。

用 pgvector 的话,常见问题是忘了建索引,几万条数据全表扫描,慢得离谱。建了 HNSW 索引之后速度会有数量级的提升。另外 pgvector 的索引要在数据插入之后建,先建索引再大批量插入会拖慢插入速度。

5.3 Agent 跑飞了怎么兜底

Agent 系统最怕的就是"跑飞"——无限循环、疯狂调接口、最后超时。我的兜底策略有三层。第一层是最大跳数限制,前面说过,硬性截断。第二层是单次请求超时,每个 LLM 调用和检索调用都设超时,超了就跳过。第三层是降级生成,如果 Agent 中途失败,用已经拿到的材料直接生成一份"简化版报告",而不是整个失败。

注意:Agent 的每一步都要打日志,记录它拆了什么子查询、检索了什么、为什么重试。出问题的时候,日志是你唯一的救命稻草。我见过太多人 Agent 跑飞了却不知道飞在哪一步,就是因为没打日志。

5.4 引用对不上的排查

报告里的引用编号和实际来源对不上,通常是chunk 编号在传递过程中错位了。排查方法是:在生成前把"编号 → chunk 内容 → 来源"的映射表打出来,生成后再核对一遍报告里的引用。如果错位,检查去重和拼接的逻辑,很多时候是去重之后编号没重新排。

6. 一些实操心得与扩展方向

跑通 DocResearch 之后,我最大的体会是:"有出处"这三个字看着简单,做起来全是细节。切分、检索、评估、生成,每一环都会影响最终的引用质量,而且这些环节是串联的,前面歪一点,后面就放大。所以别指望一次调好,做好迭代的准备。

关于扩展,我自己试过几个方向,效果还不错。一个是加缓存,相同或相似的查询直接返回缓存结果,省时省钱,尤其是调试阶段反复问同一个问题的时候。另一个是加多轮对话,让用户能对报告追问,Agent 基于已有材料继续回答,这个体验比一次性出报告好很多。还有一个是报告模板化,不同场景(技术调研、竞品分析、文献综述)用不同的输出结构,让报告更贴合实际用途。

最后分享一个小技巧:调试阶段把 top_k 调大、把中间结果全打出来,虽然慢,但能让你看清 Agent 到底在干什么。等逻辑稳定了,再把参数收回去。很多人一上来就追求"快",结果出了问题两眼一抹黑,反而更慢。先把链路看透,再谈优化,这是我做了这么多 Agent 项目最实在的一条经验。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 23:05:32

Linux实战100例:从命令到排错的系统化训练

简介&#xff1a;这是一套面向 Linux 学习者与开发者的实战代码合集&#xff0c;精选 100 个经典且最具代表性的代码实例&#xff0c;覆盖网络调用命令、Apache 服务器参数配置、Linux 错误代码详解等高频应用场景&#xff0c;并针对系统使用过程中常见的诸多错误给出排查思路与…

作者头像 李华
网站建设 2026/10/4 22:56:25

COM端口号可视化集线器硬件设计解析

1. 为什么一个“COM端口号可视化集线器”值得拆到焊点级别&#xff1f;你有没有遇到过这样的场景&#xff1a;调试三台工业传感器、两路PLC通信模块、一台老式数控面板&#xff0c;全堆在同一个工控机上——结果设备管理器里突然冒出七个“USB Serial Port (COM3)”“USB Seria…

作者头像 李华
网站建设 2026/10/4 22:56:23

从零开始构建AI工程:数据、模型与部署全流程实战

这几年被问到最多的问题&#xff0c;不是“哪个模型效果最好”&#xff0c;而是“我到底该怎么从零开始搞AI工程”。市面上的教程要么是纯理论推导&#xff0c;看得人头昏脑涨&#xff1b;要么是一键调用封装好的接口&#xff0c;跑通一个demo就以为会了&#xff0c;真到了换数…

作者头像 李华
网站建设 2026/10/4 22:56:20

回形针工厂实验:AI目标错位与奖励函数设计的工程警示

我最近在给团队调一个自动化Agent的奖励函数&#xff0c;开会时同事突然冒出一句“我们不会在沙箱里养出一个回形针工厂吧”。懂的人都笑了——paperclip 这个词现在在AI圈就像个暗号&#xff0c;指的不是办公桌上那盒钢丝&#xff0c;而是“回形针最大化器”&#xff08;paper…

作者头像 李华
网站建设 2026/10/4 22:52:59

MCP Inspector 使用指南:用 TaoToken 统一 Key 调试 STDIO 与 HTTP 服务

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华