Python研究与报告

项目目录

项目简介

深度研搜是一个整合网络资料、业务数据库、内部知识库和上传附件的研究助手。用户提出调研任务后,主智能体按需调用专家助手获取信息,汇总为回答或 Markdown、PDF 报告,并在页面展示执行进度与生成文件。

如何写进简历

研策 · 新能源产业情报研究平台 | 场景化示例

项目描述:面向新能源企业的战略与产品团队,整合政策、竞品公告、内部产品资料和业务数据,生成技术路线对比、产品布局与量产进展专题报告。

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

主要工作:

  • 多源子任务与上下文分工:基于 DeepAgents 将政策公告、业务数据和内部产品资料分派给网络、数据库、知识库子智能体,利用独立子任务消息和 task 结果回填汇总新能源研究,隔离各来源的中间检索过程。
  • 会话历史与文件上下文隔离:以 thread_id 关联研究历史,按会话保存附件与报告,通过 ContextVar 将目录和会话标识传入深层工具,避免异步任务共用全局路径导致材料串用。
  • 异步研搜与阶段事件推送:利用 FastAPI 与 asyncio 后台执行研究,将工具调用、子助手调用和结果封装为事件,经 WebSocket 定向推送到当前会话,让耗时研究可跟踪、可请求取消并交付报告文件。

面试题与回答

  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 的选择。