deepsearch-agents

多智能体研究与报告助手
反馈(在 GitHub 新标签页打开)

主要技术:FastAPI、DeepAgents、LangGraph、MySQL、RAGFlow、WebSocket

项目目录

项目简介

深度研搜是一套基于 Python、DeepAgents 和 FastAPI 的多智能体研究助手。它可以按任务需要搜索公开资料、查询 MySQL 数据、检索 RAGFlow 知识库和读取上传附件,将结果整理成回答或可下载的 Markdown、PDF 报告。

用户可以提出一个行业资料调研任务,也可以查询教学库中的药品库存,筛选库存大于 100 的药品并生成报告。主智能体负责分派任务与汇总,网络、数据库和知识库助手分别获取信息;页面展示执行过程、任务状态和生成文件。仓库提供前后端代码、图文教程与对应章节分支。

深度研搜查询药品库存、生成报告并提供文件下载的界面

项目亮点

  • 主智能体与专家助手分工: 基于 DeepAgents 组装主智能体与网络、数据库、知识库三个助手,各助手使用对应工具完成子任务,再将结果交回主智能体汇总,避免把所有来源的中间过程混在一起。
  • 从附件分析到文件交付: 主智能体直接使用文件工具,读取文本文档、文本型 PDF、Word 正文和 Excel 预览,将整理结果写入 Markdown,并按需转换为 PDF。页面通过文件接口列出和下载实际生成的产物。
  • 会话上下文与目录归档: 通过 thread_id 区分同一进程中的会话上下文,按会话组织附件与报告目录;使用 ContextVar 向深层工具传递当前目录和任务标识,执行结束后恢复上下文。
  • 后台执行与实时进度: FastAPI 在后台运行研究任务,monitor 统一上报工具调用、助手调用、结果与异常事件,经 WebSocket 推送到对应会话;页面支持请求取消执行中的任务。

技术架构

项目分为研究执行和应用交互两条主线:

研究执行: DeepAgents 负责主从任务调度,Tavily、MySQL 和 RAGFlow 分别提供网络、结构化数据和知识库信息。附件读取、Markdown 写入与 PDF 转换由主智能体直接调用。

应用交互: React 页面提交任务和附件,FastAPI 管理后台任务及上传、文件列表、下载接口,WebSocket 将执行事件与最终结果推送到当前页面。

代码位置 主要职责
app/agent/ 组装主智能体与三个专家助手,配置模型、提示词和会话上下文
app/tools/ 封装搜索、数据库、知识库、附件读取与报告生成工具
app/api/ 提供任务接口,通过 context 与 monitor 传递上下文并推送事件
frontend/ 展示对话、助手调用、工具事件和输出文件

上传附件和生成报告按会话保存在本地目录。examples/ 另外提供人工审批、Skills 和不同存储后端的独立示例,可配合章节教程阅读。

如何写进简历

研报通 · 制造业产品情报研究助手 | 制造业场景示例

项目描述:面向制造企业产品研究人员的情报研究助手,汇总网页、业务数据和内部资料,结合上传附件比较产品参数、整理交付动态,生成可下载的研究报告。

技术栈:Python、FastAPI、DeepAgents、LangGraph、MySQL、RAGFlow、WebSocket

主要工作:

  • DeepAgents 多助手任务编排:基于 DeepAgents 将网页搜索、业务查询和内部文档检索封装为子智能体,通过 task 委派型号与参数研究;主智能体保留研究目标并汇总子任务结果,分开承载检索细节和报告主线的上下文。
  • 会话状态与工具上下文传递:以 thread_id 关联 InMemorySaver 的进程内状态,按会话组织附件与报告目录;通过 ContextVar 向深层文件工具传递当前目录和会话标识,并在任务结束、异常或取消时恢复绑定,避免上下文残留。
  • 异步执行与跨线程事件推送:基于 FastAPI 与 asyncio 在后台执行研究,由 monitor 汇总助手调用、工具与结果事件;同步工具产生的事件通过线程安全调度回到连接所属事件循环,再以 WebSocket 定向返回当前会话。
  • 产品参数归一与报告交付:将不同来源的型号别名、参数单位和测试条件整理为对照记录,同一参数冲突时保留版本及来源;结合 RAGFlow 检索与附件内容生成 Markdown,再转换为 PDF 交付研究结果。

药商参谋 · 医药经营数据研究助手 | 医药流通场景示例

项目描述:面向医药流通企业的经营分析助手,围绕药品库存、批次效期及业务台账开展查询分析,生成经营报告。

技术栈:Python、FastAPI、DeepAgents、LangGraph、MySQL、RAGFlow、pandas、WebSocket

主要工作:

  • 数据库子智能体与聚合查询:将库表发现、字段预览和 SQL 执行封装到数据库子智能体,由主智能体通过 task 下发药品、批次与效期目标;子智能体基于真实结构组织聚合查询,主控接收结果后继续研究与报告生成。
  • 异构附件解析与台账计算:以 pypdf、python-docx 读取文本型附件,利用 pandas 返回表格结构与摘要,控制明细进入模型的规模;针对库存台账扩展全量计算工具,按药品、批次和仓库完成归并,将差异摘要与明细文件分开交付。
  • RAGFlow 工具封装与会话收敛:封装 RAGFlow SDK 的助手发现和临时会话调用,将业务问题路由到绑定知识库的助手;兼容流式接口的增量与累计答案,查询结束后清理临时会话,减少外部问答历史对本次研究的干扰。
  • 会话文件与报告生成衔接:按 session_id 将上传文件复制到当前任务目录,通过 ContextVar 让附件读取、Markdown 写入与 PDF 转换共享工作路径;结合文件列表及下载接口返回产物,保留查询结果与台账文件的关联。

面试题与回答

3 题
  1. 你的项目采用单 Agent 还是多 Agent,分别承担什么职责?

    展开收起

    核心回答

    下面以一个整合公开资料、企业数据库和内部知识库的研搜助手为参考示例。

    项目采用主从式多 Agent:一个主智能体统一面对用户,配置三个自定义业务专家子智能体,按信息来源分工。后端使用 Python 和 FastAPI,DeepAgents 负责主从调度,底层由 LangGraph 承接执行状态;大模型负责理解问题、选择调用和整理结果,Tavily、MySQL、RAGFlow 分别提供真实信息。

    • 主智能体:理解目标、拆分任务、选择专家,拿到结果后判断是否还缺信息,最后汇总回答。读取上传文件、生成 Markdown 和转换 PDF 由它调用文件工具完成。
    • 网络搜索子智能体:通过 Tavily 搜索公开网页,补充最新公开信息。
    • 数据库查询子智能体:先确认表和字段,再生成、执行 SQL,获取结构化业务数据。
    • RAGFlow 子智能体:选择合适的知识库助手,查询企业内部的非结构化文档。

    例如新能源团队研究某类电池的技术路线与量产进展,主智能体把政策和竞品公告查询交给网络助手,把相关业务统计交给数据库助手,把内部产品资料查询交给 RAGFlow 助手;结果回来后统一解释,再按要求生成文件。拆开的主要原因是三类数据源的工具、查询步骤和上下文不同,能把各领域的细节留在子任务里;代价是多了调度、信息交接和模型调用开销。若需求只有单一知识库问答,一个 Agent 配检索工具通常就足够。

    展开说明

    选型的依据是任务边界,而不是模型数量

    这个例子按信息来源拆分,因为“查公开网页”“查业务表”“查内部文档”的处理过程不同。数据库助手需要表结构和查询结果,网络助手需要网页与搜索记录,把这些原始材料都放进主智能体会挤占上下文,也容易让它混淆工具用途。

    子智能体在自己的上下文中选择工具、读取反馈并完成局部任务,主智能体主要接收结果。它和“一个 Agent 挂很多工具”的区别,在于存在独立的任务执行边界;给几次模型调用换上角色名称,并不能体现这种分工。

    多 Agent 也不要求多种模型。这个参考实现通过统一配置创建模型,三个业务子智能体没有覆盖 model,因此沿用主智能体的模型。职责差异来自提示词、工具集合和任务上下文。需要进一步控制成本时,可以评估给不同角色配置不同模型,但要比较工具调用、结果质量、延迟和费用,而不是默认小模型一定适合 Worker。

    一次调用如何完成交接

    主智能体根据子智能体的能力描述,利用框架的 task 工具下发子目标和必要背景。例如给数据库助手的任务应包含统计对象、时间范围和指标口径;给知识库助手的任务应说明要核实哪项产品或制度信息。子智能体执行后把结果交回主智能体,由主智能体决定补查还是汇总,子智能体不分别给用户输出三份最终答案。

    数据库查询与文档查询只有在输入已经齐备、互不依赖时才适合并行。如果要先查出产品型号,再检索对应内部资料,就必须先完成前一步。接口采用异步执行、流式推送进度,也不代表每个子任务都会自动并行。

    角色分工与权限控制要分别落实

    配置不同工具集合可以缩小角色的业务范围,但实际权限仍由工具和数据源执行。例如数据库场景应使用只读账号、查询校验、行数和执行时间限制,不能把“只查询”的提示词当作数据库权限。该参考代码的自定义 SQL 工具直接执行传入语句,进一步加固属于后续工程工作。

    示例使用 thread_id 关联会话,用 InMemorySaver 保存进程内状态。它有助于同一会话持续执行;若要在服务重启后恢复任务,还需要持久化检查点和产物记录。WebSocket 用于推送进度,不承担这些状态的持久化。

    常见追问

    为什么不用一个 Agent,把三个数据源都做成工具?

    完全可以作为简单版本。若请求主要落在同一领域,工具选择稳定,单 Agent 的成本和排障负担更小。拆分的理由应是领域上下文、工具选择或独立执行已有明确需求;用相同任务比较成功率、来源准确性、延迟和 Token 消耗,才能判断拆分是否值得。

    主智能体和子智能体用相同模型,为什么还算多 Agent?

    模型是能力底座,Agent 是围绕目标、指令、上下文和工具组织起来的执行单元。同一个模型可以驱动多个独立子任务;反过来,先后调用两个不同模型,也不自动构成多 Agent 协作。

    子智能体返回的结果互相矛盾怎么办?

    先比较数据来源、时间范围和统计口径,判断是否确实在回答同一件事。例如数据库返回实际销售记录,内部文档描述计划销量,两者不能直接互相覆盖。主智能体应保留各自依据,补查能验证分歧的证据,仍无法确认时在最终结果中说明缺口。

    对应教程

    参考资料

  2. 多 Agent 如何隔离上下文和状态?

    展开收起

    核心回答

    深度研搜把子任务的模型消息、会话检查点、运行上下文和文件目录分别管理。DeepAgents 通过 task 工具委派网络、数据库或知识库子任务,子智能体独立完成工具交互,再把结果交回主智能体。应用以 session_id 作为 thread_id 定位会话状态,同时为该会话建立附件与报告目录,用 ContextVar 把当前目录和会话标识传给深层工具及事件推送。

    这样,共用一套 Agent 定义也能让不同会话使用各自的历史、附件和产物。同一会话的上下文可以继续使用;其中 InMemorySaver 保存的是进程内检查点,磁盘文件保存的是附件和报告,两者不能互相替代。

    主子智能体怎样交换上下文

    主智能体根据专家的 description 选择委派对象,把子目标和必要背景传给 task。例如数据库助手需要统计对象、时间范围与口径,知识库助手需要待核实的问题;网络检索的完整过程不必因此广播给数据库助手。

    在本项目使用的 DeepAgents 0.5.7 中,委派会用任务描述建立子智能体的新消息输入,完成后将其最终消息作为 ToolMessage 交回主智能体。因此主智能体可以汇总结果,而不必接收每轮工具对话。不过,独立消息不等于所有状态都独立:框架仍会传递或合并部分非消息字段,共享内容要按具体字段管理。该版本的委派实现可以看到输入过滤和结果回写。

    会话标识怎样串起历史、附件和报告

    run_deep_agent() 为一次会话完成四项关联:

    1. 定位历史。 把 session_id 放入 configurable.thread_id,交给配置了 InMemorySaver 的主智能体。同一标识用于延续该会话,不是操作系统线程编号。
    2. 整理文件。 创建 app/output/session_{session_id},把 app/updated/session_{session_id} 中的上传文件复制进去,并把文件名和工作目录加入本轮输入。模型仍需调用 read_file_content 才能读取附件内容。
    3. 传递运行上下文。 将目录与会话标识写入两个 ContextVar。文件工具读取目录,monitor 读取会话标识,将执行事件发送到对应前端连接;模型不需要在每个工具参数里重新填写这两个值。
    4. 恢复绑定。 流式执行结束、取消或异常后,在 finally 中用设置时取得的 token 恢复上下文,结束本轮绑定的作用域。

    主执行入口负责这些关联,上下文模块负责设置、读取与恢复绑定。

    例如两个会话都上传 材料.pdf 并生成 报告.md,各自的目录提供文件归属,thread_id 提供历史归属,当前运行上下文让文件工具找到对应目录。只修改报告文件名,解决不了读取错会话历史或把进度发给另一连接的问题。

    这些机制各自解决到哪一层

    ContextVar 避免用一个可变全局变量保存“当前会话”,但它不保存长期记忆,也不会自动深拷贝其中的可变对象。会话目录便于组织附件和交付物,也便于继续读取文件;服务重启后能否恢复 Agent 状态,还取决于检查点是否持久化。

    文件访问权限是另一层。本项目的 resolve_path() 会处理相对路径和重复目录,但仍允许目录外的真实绝对路径,因此当前工作目录不能当作文件沙箱。若用于多用户服务,需要在资源入口核验身份和归属,并在路径解析后检查允许范围;这些约束由程序执行。路径解析源码展示了现有行为。

    对应教程

    参考资料

    • 国内:JavaGuide:多 Agent 协作系统设计:采用独立任务、显式交接和共享结果的分工,解释主智能体为何不需要广播全部工具历史。
    • 国内:zero2Agent:多智能体协作:借鉴公共约束、子任务过程与交付结果的分层,说明委派时应保留必要条件。
    • 国内:WeThinkIn:记忆与上下文工程:参考记忆主体、读取作用域和派生副本,区分文件存在、状态已保存与模型实际取得的内容。
    • 国内:zero2Agent:记忆与上下文:参考异步任务污染及会话文件命名空间,解释运行上下文、检查点和磁盘产物各自的职责。
    • 国外:LangChain:Deep Agents Subagents:采用委派任务、独立工具过程和精简结果交付的说明;本例具体的状态传递行为以 0.5.7 实现为准。
    • 国外:Python 3.12:contextvars:依据异步上下文变量、token 恢复与浅复制机制,解释当前会话绑定如何传递,以及它与持久化和资源权限的区别。
  3. 向页面推送任务进度时,后端选择 WebSocket 还是 SSE,为什么?

    展开收起

    核心回答

    深度研搜当前采用 HTTP 提交和取消任务,WebSocket 推送执行事件。研究过程可能调用网络搜索、数据库和知识库,页面需要看到正在使用哪个助手、哪个工具,以及最终结果。后端用 FastAPI 承接请求,借助 asyncio 后台任务执行研搜,再由统一的 monitor 把事件定向发给当前会话。

    这里选择 WebSocket,实现上是把会话内的进度和结果集中到一条长连接。当前业务消息主要从服务端发往页面,接收端循环只处理心跳,没有通过这条连接提交或取消任务,因此 SSE 也足以承载这类单向通知。 不能把已有选型解释成双向业务交互的刚性要求。

    一次进度怎样到达页面

    1. 先建立会话连接。 页面连接 /ws/{thread_id},ConnectionManager 保存 thread_id 到连接对象的映射。随后提交 POST /api/task,带上同一会话标识和研究问题。
    2. 把长任务交给后台。 接口通过 asyncio.create_task() 调度 run_deep_agent(),返回 started 和 thread_id,不等待最终报告。任务运行时把会话标识和输出目录写入 ContextVar,让深层工具能读取本次执行的上下文。
    3. 统一上报业务事件。 工具调用、子智能体调用和结果分别经过 report_tool()、report_assistant()、report_task_result()。monitor 组装包含 event、message、data 和时间戳的 monitor_event,读取当前 thread_id,再找到对应连接发送。
    4. 回到连接所属的事件循环发送。 如果上报发生在连接所在的循环,使用 create_task();否则通过 asyncio.run_coroutine_threadsafe() 投递回该循环。页面按事件类型更新执行记录、最终回答或错误状态。

    这套分工让工具只负责上报“发生了什么”,不需要自己保存或操作页面连接。任务接口和事件发送分别负责调度和通知。

    推送了哪些内容

    当前事件包括 session_created、tool_start、assistant_call、task_result、task_cancelled 和 error。例如调用网络搜索助手时展示助手名称,调用文件工具时展示工具名称,收到 task_result 后展示返回的完整结果。

    虽然主智能体使用 astream() 执行,现有代码解析的是节点状态及其中的消息,再生成这些阶段事件,没有把模型 token 增量直接转发给页面。具体分支见主智能体执行。

    心跳、重连和取消分别解决什么

    前端定时发送文本 ping,服务端返回 pong;断开后,前端会用同一 thread_id 重新连接。连接管理器删除旧连接前还会比较对象身份,避免旧连接稍后触发断开,把已经登记的新连接一起删除。

    但当前连接表和后台任务表都在进程内,没有持久事件日志,也没有按事件游标补发的接口。断线期间已产生的通知不会因为重连自动恢复,丢失最终结果事件时,页面也不能仅凭连接恢复就判断任务已完成。若要补齐,应增加任务状态查询或历史事件补发,而不是只缩短重连间隔。

    取消通过 POST /api/task/{thread_id}/cancel 向后台任务发出协作式取消请求,关闭页面连接不会自动执行该接口。另一个同会话请求会先对旧任务调用 cancel(),随后创建新任务,未等待旧任务结束,因此不能据此保证两次任务严格互斥。这些行为独立于 WebSocket 或 SSE 的选择。