项目目录
项目简介
深度研搜是一个整合网络资料、业务数据库、内部知识库和上传附件的研究助手。用户提出调研任务后,主智能体按需调用专家助手获取信息,汇总为回答或 Markdown、PDF 报告,并在页面展示执行进度与生成文件。
如何写进简历
研策 · 新能源产业情报研究平台 | 场景化示例
项目描述:面向新能源企业的战略与产品团队,整合政策、竞品公告、内部产品资料和业务数据,生成技术路线对比、产品布局与量产进展专题报告。
技术栈:Python、FastAPI、DeepAgents、LangGraph、MySQL、RAGFlow
主要工作:
- 多源子任务与上下文分工:基于
DeepAgents将政策公告、业务数据和内部产品资料分派给网络、数据库、知识库子智能体,利用独立子任务消息和task结果回填汇总新能源研究,隔离各来源的中间检索过程。 - 会话历史与文件上下文隔离:以
thread_id关联研究历史,按会话保存附件与报告,通过ContextVar将目录和会话标识传入深层工具,避免异步任务共用全局路径导致材料串用。 - 异步研搜与阶段事件推送:利用
FastAPI与asyncio后台执行研究,将工具调用、子助手调用和结果封装为事件,经WebSocket定向推送到当前会话,让耗时研究可跟踪、可请求取消并交付报告文件。
面试题与回答
你的项目采用单 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 协作。
子智能体返回的结果互相矛盾怎么办?
先比较数据来源、时间范围和统计口径,判断是否确实在回答同一件事。例如数据库返回实际销售记录,内部文档描述计划销量,两者不能直接互相覆盖。主智能体应保留各自依据,补查能验证分歧的证据,仍无法确认时在最终结果中说明缺口。
对应教程
- 深度研搜:项目总览与工程初始化:查看主智能体、三个业务专家和信息来源的对应关系,以及从查询到文件交付的完整流程。
- 深度研搜:主智能体搭建与异步执行:查看文件工具为何由主智能体持有,以及主从组装、会话标识和流式执行如何连接。
- 深度研搜配套源码:主智能体组装:对照
tools、subagents和checkpointer的实际配置,区分业务角色与框架提供的运行能力。
参考资料
- 国内:JavaGuide:多 Agent 协作系统设计:借鉴文中对独立执行边界的划分,解释正文中多 Agent 与多阶段 Prompt Chain 的区别及拆分的代价。
- 国内:zero2Agent:多智能体协作:角色分工、通信机制与冲突仲裁:参考角色分工与结构化交付的设计,说明正文中子 Agent 如何接收必要背景、交付结果,以及单 Agent 与多 Agent 的选型条件。
- 国内:Datawhale:第六章 框架开发实践:借鉴软件开发团队与图式编排案例,区分正文中的角色职责设计和执行顺序安排。
- 国内:zero2Agent:记忆与上下文:参考运行状态、模型上下文和记忆的分工,解释正文中会话标识、检查点与文件产物为什么需要分别管理。
- 国外:LangChain:Deep Agents Subagents:依据子智能体委派、上下文隔离及模型配置约定,解释正文中不同角色为何可以使用同一模型。
- 国外:Anthropic:How we built our multi-agent research system:借鉴主研究者拆分方向、子代理独立取证后汇总的实践,说明正文中并行研究的适用条件与协调成本。
- 主智能体:理解目标、拆分任务、选择专家,拿到结果后判断是否还缺信息,最后汇总回答。读取上传文件、生成
多 Agent 如何隔离上下文和状态?
展开收起
核心回答
深度研搜把子任务的模型消息、会话检查点、运行上下文和文件目录分别管理。
DeepAgents通过task工具委派网络、数据库或知识库子任务,子智能体独立完成工具交互,再把结果交回主智能体。应用以session_id作为thread_id定位会话状态,同时为该会话建立附件与报告目录,用ContextVar把当前目录和会话标识传给深层工具及事件推送。这样,共用一套 Agent 定义也能让不同会话使用各自的历史、附件和产物。同一会话的上下文可以继续使用;其中
InMemorySaver保存的是进程内检查点,磁盘文件保存的是附件和报告,两者不能互相替代。主子智能体怎样交换上下文
主智能体根据专家的
description选择委派对象,把子目标和必要背景传给task。例如数据库助手需要统计对象、时间范围与口径,知识库助手需要待核实的问题;网络检索的完整过程不必因此广播给数据库助手。在本项目使用的
DeepAgents 0.5.7中,委派会用任务描述建立子智能体的新消息输入,完成后将其最终消息作为ToolMessage交回主智能体。因此主智能体可以汇总结果,而不必接收每轮工具对话。不过,独立消息不等于所有状态都独立:框架仍会传递或合并部分非消息字段,共享内容要按具体字段管理。该版本的委派实现可以看到输入过滤和结果回写。会话标识怎样串起历史、附件和报告
run_deep_agent()为一次会话完成四项关联:- 定位历史。 把
session_id放入configurable.thread_id,交给配置了InMemorySaver的主智能体。同一标识用于延续该会话,不是操作系统线程编号。 - 整理文件。 创建
app/output/session_{session_id},把app/updated/session_{session_id}中的上传文件复制进去,并把文件名和工作目录加入本轮输入。模型仍需调用read_file_content才能读取附件内容。 - 传递运行上下文。 将目录与会话标识写入两个
ContextVar。文件工具读取目录,monitor读取会话标识,将执行事件发送到对应前端连接;模型不需要在每个工具参数里重新填写这两个值。 - 恢复绑定。 流式执行结束、取消或异常后,在
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 恢复与浅复制机制,解释当前会话绑定如何传递,以及它与持久化和资源权限的区别。
- 定位历史。 把
向页面推送任务进度时,后端选择 WebSocket 还是 SSE,为什么?
展开收起
核心回答
深度研搜当前采用
HTTP提交和取消任务,WebSocket推送执行事件。研究过程可能调用网络搜索、数据库和知识库,页面需要看到正在使用哪个助手、哪个工具,以及最终结果。后端用FastAPI承接请求,借助asyncio后台任务执行研搜,再由统一的monitor把事件定向发给当前会话。这里选择
WebSocket,实现上是把会话内的进度和结果集中到一条长连接。当前业务消息主要从服务端发往页面,接收端循环只处理心跳,没有通过这条连接提交或取消任务,因此SSE也足以承载这类单向通知。 不能把已有选型解释成双向业务交互的刚性要求。一次进度怎样到达页面
- 先建立会话连接。 页面连接
/ws/{thread_id},ConnectionManager保存thread_id到连接对象的映射。随后提交POST /api/task,带上同一会话标识和研究问题。 - 把长任务交给后台。 接口通过
asyncio.create_task()调度run_deep_agent(),返回started和thread_id,不等待最终报告。任务运行时把会话标识和输出目录写入ContextVar,让深层工具能读取本次执行的上下文。 - 统一上报业务事件。 工具调用、子智能体调用和结果分别经过
report_tool()、report_assistant()、report_task_result()。monitor组装包含event、message、data和时间戳的monitor_event,读取当前thread_id,再找到对应连接发送。 - 回到连接所属的事件循环发送。 如果上报发生在连接所在的循环,使用
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的选择。- 先建立会话连接。 页面连接