ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/10/4 23:05:58 拓冰建站 浏览量
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-essentialMac 上装好 Xcode Command Line Tools 基本就没事。第三国内网络装包慢的话配个镜像源这个不用我多说。# 临时用镜像源装包 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple3.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(urimilvus_uri) # 建集合维度要和你的 embedding 模型对齐 client.create_collection( collection_namedoc_chunks, dimension768, # 举例实际看你用的 embedding 模型 )这个模式的好处是零运维一个文件搞定。但要注意Milvus Lite 有数据量上限官方说法是适合百万级以下的向量再大就得切到 standalone 或集群。另外本地文件模式不支持多进程并发写如果你打算多 worker 同时写库会出问题这种场景还是老老实实上 standalone。3.4 pgvector 方案已有 Postgres 就直接加扩展如果你不想引入 Milvuspgvector 是更轻的选择。前提是你得有个 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_len500, min_len300): # 先按段落切 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_k8): q_vec embed_model.encode(query).tolist() results client.search( collection_namedoc_chunks, data[q_vec], limittop_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_k6) # 2. 评估召回是否足够不够就改写重试 if not is_sufficient(chunks, sq): sq2 llm.rewrite(sq) chunks retriever.retrieve(sq2, top_k6) 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 项目最实在的一条经验。