项目总览与初始化

三个子智能体

子智能体 负责内容 典型工具
网络搜索助手 查询公开网络资料,适合最新信息、公开网页、外部知识 Tavily 搜索工具
数据库查询助手 查询企业业务数据库,读取表名、表结构、数据预览并执行 SQL 查表、查结构、执行 SQL
RAGFlow 助手 查询企业内部知识库,先获取可用助手,再向知识库提问 创建会话、向知识库提问

总体思路

image

四个智能体和九个工具分布

类型 名称 作用
主智能体 Main Agent 理解任务、规划步骤、调度助手、汇总结果、生成交付物
子智能体 网络搜索助手 查询公开网络资料
子智能体 数据库查询助手 查询业务数据库
子智能体 RAGFlow 助手 查询企业内部知识库

工具:

归属 工具数量 工具内容
主智能体 3 个 读取上传文件、生成 Markdown、Markdown 转 PDF
网络搜索助手 1 个 Tavily 网络搜索
数据库查询助手 3 个 获取表名、获取表结构和数据预览、执行 SQL
RAGFlow 助手 2 个 获取可用助手或会话、向知识库助手提问

工具名称:

主智能体:

工具名 作用
upload_file_read_tool 读取用户本次上传的文件内容
generate_markdown 把最终结果写成标准 Markdown 文档
convert_md_to_pdf 将 Markdown 文件转换为 PDF

网络搜索助手工具:

工具名 作用
internet_search 基于 Tavily 查询互联网公开信息,适合最新资料和多角度检索

数据库查询助手工具:

工具名 作用
list_sql_tables 列出数据库中的可用表
get_table_data 读取表结构和部分数据预览,帮助模型理解表里有什么
execute_sql_query 执行模型生成或整理后的 SQL 查询

RAGFlow 知识库助手工具:

工具名 作用
get_assistant_list 获取 RAGFlow 中可用的知识库助手
create_ask_delete 创建会话、向知识库提问,并在完成后清理会话

技术栈速览:

层次 技术 在项目中的作用
智能体框架 DeepAgents、LangChain、LangGraph、langchain-core 组织主智能体、子智能体、工具调用和思考循环
模型接口 OpenAI 兼容接口、init_chat_model 统一创建大模型对象,供所有智能体使用
Web 服务 FastAPI、Uvicorn 提供任务提交、文件上传、下载等后端接口
实时通信 WebSocket 将 Agent 执行过程实时推送给前端
参数校验 Pydantic、typing-extensions 定义请求参数、状态结构或工具入参,并提供类型兼容能力
网络搜索 Tavily Search API 查询互联网公开信息,并返回结构化搜索结果
私有知识库 RAGFlow、ragflow_sdk 连接企业内部知识库,查询私有文档内容
数据库 MySQL、mysql-connector-python 查询业务数据、表结构和 SQL 结果
文件处理 File IO、python-docx、pypdf、pandas、ReportLab 生成报告、读取上传文件或解析文档内容
环境配置 python-dotenv 从 .env 读取模型、搜索、数据库和 RAGFlow 连接配置
HTTP 请求 requests 做外部服务连通性检查,或在需要时直接访问 HTTP 接口
异步执行 asyncioContextVar 支持并发请求隔离和跨步骤传递上下文
路径管理 pathlibshutil 处理路径拼接、文件移动、上传文件归档

前后端注意的两个ID

名称 解决的问题 可以怎么理解
thread_id 当前任务的进度应该推给哪个前端连接 本次对话任务的身份 ID
session_dir 当前任务生成的文件应该放在哪个目录 本次任务的工作文件夹

 

context.py 负责保存身份,monitor.py 负责对外汇报进度

文件 负责什么 一句话记忆
app/api/context.py 保存当前任务的 thread_id 和 session_dir 我是谁,我的文件夹在哪
app/api/monitor.py 把工具调用、助手调用、最终结果等事件推送给前端 我现在正在做什么

后面写工具时,只要工具里调用 monitor.report_tool(...),前端就有机会看到执行过程

 

项目目录结构:

deepsearch-agents/
├── app/                    # 后端业务代码主目录
│   ├── agent/              # 模型初始化、提示词加载、主智能体和子智能体组装逻辑
│   │   ├── llm.py          # 统一创建大模型对象
│   │   ├── prompts.py      # 读取 app/prompt/prompts.yml
│   │   ├── main_agent.py   # 后续章节补充:主智能体组装入口
│   │   └── sub_agents/     # 后续章节补充:网络、数据库、RAGFlow 子智能体
│   ├── api/                # FastAPI、WebSocket、上下文隔离和执行过程监控相关代码
│   │   ├── context.py      # 保存 thread_id 和 session_dir 上下文
│   │   ├── monitor.py      # 推送工具调用、助手调用和任务结果
│   │   └── server.py       # 后续章节补充:FastAPI 服务入口
│   ├── prompt/             # YAML 提示词配置,让提示词和 Python 逻辑分开维护
│   │   └── prompts.yml     # 主智能体和子智能体提示词配置
│   ├── tools/              # 后续章节补充:Agent 可调用工具,例如搜索、查库、生成文件
│   └── utils/              # 普通 Python 工具函数,给后端代码或 Agent Tool 内部调用
│       ├── path_utils.py   # 统一解析上传文件、输出文件和会话目录路径
│       └── word_converter.py # Markdown 转 PDF 的底层转换工具
├── examples/               # 前面章节学习 DeepAgents API 时用到的示例代码
├── output/                 # 运行时生成:存放 Markdown、PDF 等任务产物
├── updated/                # 运行时生成:存放用户上传文件
├── .env.example            # 环境变量示例
├── .env                    # 本地真实配置,不提交仓库    
├── .python-version         # Python 版本提示
├── pyproject.toml          # 项目依赖声明
└── uv.lock                 # 依赖锁定文件
模块 解决什么问题
.env 把模型、搜索、数据库、RAGFlow 等配置集中管理
api/context.py 在一次请求链路中保存当前任务身份和会话目录
api/monitor.py 把工具调用、助手调用和最终结果推送给前端
utils/path_utils.py 统一解析模型、工具和用户传入的文件路径
utils/word_converter.py 基于 ReportLab 提供 Markdown 转 PDF 的底层转换能力
agent/llm.py 统一创建大模型对象
prompt/prompts.yml 用 YAML 管理主智能体和子智能体提示词
agent/prompts.py 从 YAML 中读取提示词配置,供智能体组装时使用

配置入口:.env

# LLM 配置
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
OPENAI_API_KEY=你的大模型_API_KEY
LLM_QWEN_MAX=qwen-max

# Tavily 配置
# 在 https://app.tavily.com/ 注册账号,然后创建API KEY
TAVILY_API_KEY=你的_TAVILY_API_KEY

# RAGFlow 配置
RAGFLOW_API_URL=http://your-ragflow-host
RAGFLOW_API_KEY=ragflow-your-api-key

# MySQL 配置
MYSQL_USER=root
MYSQL_PASSWORD=root
MYSQL_DATABASE=pharma_db
MYSQL_HOST=localhost
MYSQL_PORT=3306

请求上下文:api/context.py

项目是异步 Web 服务,同一时间可能有多个用户请求在执行。如果用全局变量保存当前用户的 thread_id 或会话目录,就可能出现数据覆盖

每个任务都必须记住自己的身份:任务 A 要记住你的 thread_id 和 session_dir,任务 B 要记住另一个用户的 thread_id 和 session_dir所以这里不能简单依赖“线程隔离”来保存请求数据:

方案 为什么不合适或适合
全局变量 所有请求共用一份值,后来的请求会覆盖前面的请求
threading.local 只能隔离线程,不能隔离同一线程里的多个 asyncio Task
ContextVar 隔离当前异步上下文,适合保存每个请求自己的上下文数据

ContextVar 是 Python 3.7+ 为异步编程设计的上下文变量。可以先把它理解成“当前异步任务上下文里的本地变量(协程级别的本地变量)”:同一个请求链路里,深层函数可以拿到前面设置的值;另一个请求进来时,拿到的是自己那份值

"""
请求上下文管理模块

负责在异步请求链路中保存当前任务的 thread_id 和 session_dir
工具、智能体和监控模块可以在深层调用中读取这些值,而不需要层层传参
"""

from contextvars import ContextVar, Token
from typing import Optional

# ContextVar 是协程级上下文变量,适合 FastAPI 这类异步 Web 服务
# 它可以避免多个并发请求共用全局变量时出现 thread_id 或 session_dir 串台
_session_dir_ctx: ContextVar[Optional[str]] = ContextVar(
    "session_dir",  # 保存当前任务生成文件的会话目录
    default=None,
)
_thread_id_ctx: ContextVar[Optional[str]] = ContextVar(
    "thread_id",  # 保存当前任务对应的前端连接和 Agent 执行线程
    default=None,
)

用并发视角看,ContextVar 的作用是让任务 A 和任务 B 各自带着自己的上下文往下走:

image

实时进度:api/monitor.py

monitor.py 负责把 Agent 执行过程中的事件统一包装,再推送给前端

常见事件包括:

方法 作用
report_tool 报告开始执行某个工具
report_assistant 报告正在调用某个子智能体
report_task_result 报告任务最终结果
report_session_dir 报告当前会话输出目录

内部会先构造统一消息,再根据当前上下文里的 thread_id 定向推送。这样工具函数不需要知道前端连接对象,只要调用 monitor.report_tool(...) 之类的方法即可:

def _emit(
    self,
    event_type: str,
    message: str,
    data: Optional[dict[str, Any]] = None,
) -> None:
    """
    构造统一监控事件,并尝试推送到当前 thread_id 对应的前端连接

    :param event_type: 事件类型,例如 tool_start、assistant_call
    :param message: 面向前端展示的事件说明
    :param data: 附加结构化数据
    """
    payload = {
        "type": "monitor_event",
        "event": event_type,
        "message": message,
        "data": data or {},
        "timestamp": datetime.datetime.now().isoformat(),
    }

    if self.websocket_manager:
        try:
            # thread_id 来自 ContextVar,确保事件只推给当前任务对应的前端连接
            thread_id = get_thread_context()
            manager_loop = self.websocket_manager.loop

            if manager_loop and thread_id:
                self._send_to_websocket(payload, thread_id, manager_loop)
        except Exception as e:
            print(f"[Monitor] WebSocket send failed: {e}")

    # DeepAgents 脚本调试时,如果运行时暴露了 stream_writer,也同步写入流式输出
    if hasattr(builtins, "runtime") and hasattr(builtins.runtime, "stream_writer"):
        try:
            builtins.runtime.stream_writer(payload)
        except Exception:
            pass

    # 控制台保底输出,便于无前端场景下观察执行过程
    print(f"\n[Monitor:{event_type}] {message}")

这里还有一个容易忽略的点:Agent 运行逻辑和 WebSocket 管理器不一定在同一个事件循环里。真实代码里会判断当前事件循环,如果跨线程或跨 loop,就用 asyncio.run_coroutine_threadsafe 把发送任务投递回 WebSocket 所在的 loop:

def _send_to_websocket(
    self,
    payload: dict[str, Any],
    thread_id: str,
    manager_loop: asyncio.AbstractEventLoop,
) -> None:
    """
    将监控事件投递到 WebSocket 所在事件循环

    FastAPI 的 WebSocket 必须在创建它的事件循环中发送消息
    如果当前代码已经在同一个循环里,直接 create_task;否则使用线程安全投递
    """
    try:
        current_loop = asyncio.get_running_loop()
    except RuntimeError:
        current_loop = None

    coroutine = self.websocket_manager.send_to_thread(payload, thread_id)
    if current_loop and current_loop == manager_loop:
        # 已经在 WebSocket 所属事件循环中,直接创建异步任务即可
        current_loop.create_task(coroutine)
    else:
        # 不在同一个事件循环中时,线程安全地投递到 manager_loop
        asyncio.run_coroutine_threadsafe(coroutine, manager_loop)

最后由 ConnectionManager 保存前端连接。它用 thread_id 作为字典 key,所以同一时刻多个任务并发执行时,也能把事件发回各自页面:

class ConnectionManager:
    """
    WebSocket 连接管理器

    active_connections 使用 thread_id 作为 key,保证监控事件只推送给对应任务的前端连接
    """

    def __init__(self) -> None:
        self.active_connections: dict[str, WebSocket] = {}
        # WebSocket 发送必须回到创建连接的事件循环,因此启动时需要显式绑定 loop
        self.loop: Optional[asyncio.AbstractEventLoop] = None
    # 智能体任务可能被放到后台线程里执行,而 WebSocket 连接属于 FastAPI 的事件循环。后台线程不能直接 await websocket.send_json(...),
    # 所以监控模块会先拿到 manager.loop,再用 asyncio.run_coroutine_threadsafe(...) 把发送动作投递回正确的事件循环
    def set_loop(self, loop: asyncio.AbstractEventLoop) -> None:
        """记录 FastAPI/WebSocket 所在的事件循环,供后台线程安全投递消息"""
        self.loop = loop

    async def connect(self, websocket: WebSocket, thread_id: str) -> None:
        """接受 WebSocket 连接,并按 thread_id 保存"""
        await websocket.accept()
        self.active_connections[thread_id] = websocket

    async def send_to_thread(self, message: dict[str, Any], thread_id: str) -> None:
        """向指定 thread_id 对应的前端连接发送 JSON 消息"""
        if thread_id in self.active_connections:
            websocket = self.active_connections[thread_id]
            await websocket.send_json(message)

文件基础工具:utils/

path_utils.py:统一路径解析

在智能体项目里,模型可能会返回各种路径:

/workspace/report.md
/mnt/data/report.md
output/report.md
session_xxx/report.md
updated/upload/file.pdf

这些路径不一定能直接在当前机器上使用,所以需要统一解析。path_utils.py 主要负责清洗虚拟路径、识别上传目录、拼接当前会话目录,并避免文件写到不该写的位置

参数 含义
filename 模型、工具或用户传入的文件名 / 路径
session_dir 当前任务的会话目录,用来隔离不同任务生成和读取的文件

核心功能可以概括成四件事:

  1. 清洗模型常见的虚拟路径前缀,例如 /workspace/mnt/data/home/user
  2. 识别 updated/ 上传目录,并优先按项目根目录下的真实上传路径解析。
  3. 结合 session_dir 处理相对路径和绝对路径,让普通任务产物尽量落在当前会话目录里。
  4. 防止路径重复嵌套,例如 session_123/session_123/report.md

用一组场景把它讲透。假设当前环境是 Windows,项目根目录是 D:/Project,当前会话目录是 D:/Project/output/session_123

场景 输入 filename 核心处理 解析结果
虚拟路径清洗 /workspace/report.md 剥离 /workspace,再拼到会话目录 D:/Project/output/session_123/report.md
上传目录处理 abc/updated/upload/file.pdf 提取 updated/ 之后的路径,按项目根目录解析 D:/Project/updated/upload/file.pdf
无会话目录 sub/test.md 没有 session_dir,直接按当前工作目录解析 D:/Project/sub/test.md
会话内绝对路径 D:/Project/output/session_123/sub/report.md 确认在会话目录内,检查是否嵌套后返回 D:/Project/output/session_123/sub/report.md
会话外绝对路径 D:/OtherDir/file.md 不在会话目录内,保留真实绝对路径 D:/OtherDir/file.md
Windows Unix 风格路径 /sub/test.md Windows 下 /sub/test.md 没有盘符,按会话内相对路径处理 D:/Project/output/session_123/sub/test.md
路径嵌套防护 D:/Project/output/session_123/session_123/report.md 检测连续 session_123,修正为会话目录下文件 D:/Project/output/session_123/report.md
相对路径含会话名 session_123/report.md 避免重复拼接会话目录,只保留文件名 D:/Project/output/session_123/report.md
相对路径含 output 前缀 output/report.md 避免把 output 再嵌进会话目录,只保留文件名 D:/Project/output/session_123/report.md
普通相对路径 sub1/sub2/test.md 直接拼到当前会话目录 D:/Project/output/session_123/sub1/sub2/test.md
虚拟路径加上传目录 /mnt/data/updated/doc.md 先剥离 /mnt/data,再触发 updated/ 处理 D:/Project/updated/doc.md

核心代码可以抓住四步:先清洗模型可能生成的虚拟前缀,再优先识别上传目录,然后处理绝对路径,最后把普通相对路径收敛到当前会话目录里:

import os
from pathlib import Path
from typing import Optional


def resolve_path(filename: str, session_dir: Optional[str] = None) -> str:
    """
    解析文件路径,并尽量把任务产物限制在当前会话目录中

    :param filename: 模型、工具或用户传入的文件名/路径
    :param session_dir: 当前任务的会话目录
    :return: 解析后的绝对路径
    """
    path = Path(filename)
    path_str = filename.replace("\\", "/")

    # 大模型常返回 /workspace、/mnt/data 这类沙箱路径,本地项目需要先剥离虚拟前缀
    for prefix in ["/workspace", "/mnt/data", "/home/user"]:
        if path_str.startswith(prefix):
            cleaned = path_str[len(prefix) :].lstrip("/")
            path = Path(cleaned)
            path_str = str(path).replace("\\", "/")
            break

    # updated/ 用于存放用户上传文件,应优先按项目根目录下的真实上传路径解析
    if "updated/" in path_str:
        idx = path_str.find("updated/")
        relative_part = path_str[idx:]
        return str(Path(relative_part).resolve())

    # 未传入会话目录时,只做普通路径解析,适合脚本调试场景
    if not session_dir:
        return str(path.resolve())

    session_path = Path(session_dir).resolve()
    session_name = session_path.name
    is_unix_abs = path_str.startswith("/")

    if path.is_absolute() or (os.name == "nt" and is_unix_abs):
        # Windows 下 "/xxx" 没有盘符,按会话目录内的相对路径处理
        if os.name == "nt" and is_unix_abs and not path.drive:
            full_path = session_path / path_str.lstrip("/")
        else:
            full_path = path.resolve()

        try:
            if session_path in full_path.parents or full_path == session_path:
                return _fix_nested_session_path(full_path, session_path, session_name)
        except Exception:
            pass

        # 真实绝对路径且不在 session_dir 中时保持原样,避免误改外部资源路径
        return str(full_path)

    parts = path.parts

    # 避免模型把 session 名或 output 前缀重复拼到当前会话目录里
    if session_name in parts:
        return str(session_path / path.name)

    if parts and parts[0] == "output":
        return str(session_path / path.name)

    return str(session_path / path)


def _fix_nested_session_path(
    full_path: Path,
    session_path: Path,
    session_name: str,
) -> str:
    """
    修正 session_xxx/session_xxx/file.md 这类重复嵌套路径
    """
    parts = full_path.parts
    for index in range(len(parts) - 1):
        if parts[index] == session_name and parts[index + 1] == session_name:
            return str(session_path / full_path.name)
    return str(full_path)

word_converter.py:Markdown 转 PDF

负责把 Markdown 转成 PDF。当前代码没有再依赖本机 Microsoft Word,而是使用 ReportLab 直接生成 PDF,因此在 macOS、Linux、Windows 上都更容易运行

核心思路可以这样理解:

读取 Markdown
  -> 解析标题、列表、表格、代码块等常见 Markdown 结构
  -> 转成 ReportLab 的 Paragraph、Table、Spacer 等 story 元素
  -> 注册中文字体,避免中文乱码
  -> 生成 PDF 文件

真实代码把 reportlab 做成可选导入:如果环境里没有安装依赖,导入模块本身不会立刻失败,只有真正调用转换函数时才会返回提示

import html
import logging
import re
from pathlib import Path

try:
    from reportlab.lib import colors
    from reportlab.lib.enums import TA_CENTER, TA_LEFT
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
    from reportlab.lib.units import cm
    from reportlab.pdfbase import pdfmetrics
    from reportlab.pdfbase.cidfonts import UnicodeCIDFont
    from reportlab.platypus import (
        Paragraph,
        Preformatted,
        SimpleDocTemplate,
        Spacer,
        Table,
        TableStyle,
    )
except ImportError:
    # 缺少 reportlab 时仍允许模块被导入,方便其它代码正常启动
    SimpleDocTemplate = None


logger = logging.getLogger(__name__)


def convert_md_to_pdf(md_abs_path: Path, pdf_abs_path: Path) -> str:
    """
    将 Markdown 文件转换为 PDF

    :param md_abs_path: Markdown 文件绝对路径
    :param pdf_abs_path: 输出 PDF 文件绝对路径
    :return: 转换结果说明
    """
    if SimpleDocTemplate is None:
        return "缺少依赖库,请安装 reportlab"

    # 读取智能体生成的 Markdown 报告
    with open(md_abs_path, "r", encoding="utf-8") as f:
        md_content = f.read()

    # 确保输出目录存在,避免 doc.build 写文件时报路径不存在
    pdf_abs_path.parent.mkdir(parents=True, exist_ok=True)

    try:
        _register_fonts()
        doc = SimpleDocTemplate(
            str(pdf_abs_path),
            pagesize=A4,
            rightMargin=2 * cm,
            leftMargin=2 * cm,
            topMargin=2 * cm,
            bottomMargin=2 * cm,
        )
        styles = _build_styles()
        story = _markdown_to_story(md_content, styles)
        doc.build(story)
        return f"成功将 Markdown 转换为 PDF: {pdf_abs_path}"
    except Exception as e:
        logger.exception("Markdown 转 PDF 失败")
        return f"Markdown 转 PDF 失败: {str(e)}"
  1. SimpleDocTemplate is None:缺少依赖时返回可读提示,不让整个服务启动失败。
  2. pdf_abs_path.parent.mkdir(...):先创建输出目录,再生成 PDF,避免会话目录不存在导致转换失败。

底层解析函数可以先看名字,不必一次吃透:

def _register_fonts() -> None:
    """注册中文 CID 字体,优先保证中文内容能正常显示"""
    pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light"))


def _markdown_to_story(md_content: str, styles: dict[str, ParagraphStyle]) -> list:
    """
    把 Markdown 文本解析成 ReportLab story 元素

    story 可以理解成 PDF 页面里的内容队列:
    标题、正文、代码块、表格都会被依次放进去,最后交给 doc.build 生成 PDF。
    """
    ...


def _format_inline(text: str) -> str:
    """处理行内加粗、代码等简单 Markdown 样式,并做 HTML 转义"""
    ...

模型与提示词配置

agent/llm.py:统一创建模型对象

后续创建主智能体或子智能体时,可以直接复用:

from app.agent.llm import model

prompt/prompts.yml:把提示词从代码里拿出来

所以本项目把下面几类内容放到 YAML 里:

  • 主智能体的系统提示词;
  • 子智能体的名称;
  • 子智能体的描述;
  • 子智能体自己的系统提示词。

初始结构如下:

# 主智能体配置:负责理解用户任务、规划步骤、调度子智能体并汇总最终结果
main_agent:
  system_prompt: |
    你是沃华医药公司的智能团队负责人,负责协调三个专家助手完成复杂研究任务
    当前章节先保留基础占位提示词,后续会随着工具和子智能体实现逐步完善

# 子智能体配置:description 给主智能体判断是否调用,system_prompt 给子智能体约束执行方式
sub_agents:
  tavily:
    name: "网络搜索助手"
    description: |
      当任务需要查询互联网公开资料、最新信息、政策新闻或外部网页内容时使用
    system_prompt: |
      你是一个专业的网络信息查询助手,负责基于用户目标检索公开网络资料

  db:
    name: "数据库查询助手"
    description: |
      当任务需要查询业务数据库、读取表结构、查看样例数据或执行 SQL 时使用
    system_prompt: |
      你是一个专业的数据库查询助手,负责安全、准确地查询业务数据库

  ragflow:
    name: "RAGFlow 助手"
    description: |
      当任务需要查询企业内部知识库、制度文档、产品资料或私有知识内容时使用
    system_prompt: |
      你是一个专业的 RAGFlow 知识库助手,负责从企业知识库中检索相关信息

name、description、system_prompt 的区别

字段 给谁看 作用
name 框架和主智能体 子智能体名称
description 主智能体 判断什么时候调用这个助手
system_prompt 子智能体自己的模型 约束这个助手如何完成任务

 

description 尤其重要。它不是随便写一句“这是一个助手”,而是告诉主智能体什么时候应该调用它。

更清楚的写法应该接近:

  1. 当用户问题需要联网查询最新公开资料时,调用网络搜索助手。
  2. 当用户问题需要查询药品数据库、表结构或执行 SQL 时,调用数据库查询助手。
  3. 当用户问题需要查询企业内部知识库或制度文档时,调用 RAGFlow 助手。

agent/prompts.py:读取 YAML 配置

先写一个加载函数:

"""
提示词配置加载模块

负责读取 app/prompt/prompts.yml 中的主智能体和子智能体配置
后续组装 DeepAgent 时,可以直接复用 main_agent_content 和 sub_agents_content
"""

from pathlib import Path
from typing import Any

import yaml


def load_yaml(file_path: Path) -> dict[str, Any]:
    """
    加载 YAML 配置文件

    :param file_path: YAML 文件路径
    :return: YAML 解析后的字典
    """
    with open(file_path, "r", encoding="utf-8") as f:
        # safe_load 只按数据解析 YAML,避免 yaml.load 可能触发的对象构造风险
        return yaml.safe_load(f)

这里使用 yaml.safe_load(),而不是 yaml.load()

safe_load() 只按数据读取 YAML,不执行里面可能藏着的 Python 对象构造逻辑,更适合读取配置文件

再找到项目根路径和 YAML 文件:

# 当前文件位于 app/agent/prompts.py,parents[1] 即 app 目录
app_root_path = Path(__file__).parents[1]
yaml_file_path = app_root_path / "prompt" / "prompts.yml"

如果当前文件是:deepsearch-agents/app/agent/prompts.py

那么:

Path(__file__).parents[0]  # deepsearch-agents/app/agent
Path(__file__).parents[1]  # deepsearch-agents/app
Path(__file__).parents[2]  # deepsearch-agents

所以这里用 parents[1] 拿到 app/ 目录,再拼出 app/prompt/prompts.yml.envpyproject.tomlexamples/ 这些仍在 deepsearch-agents 根目录下,和 app/ 同级

最后拆出主智能体和子智能体配置:

prompt_yaml_content = load_yaml(yaml_file_path)

# 主智能体提示词配置
main_agent_content = prompt_yaml_content["main_agent"]

# 子智能体配置集合,包含 name、description 和 system_prompt
sub_agents_content = prompt_yaml_content["sub_agents"]

# 当前学习阶段可以临时打印,确认 YAML 是否被正确读取;生产代码中可以去掉
print(sub_agents_content)

后续创建主智能体时,可以用:

main_agent_content["system_prompt"]

创建子智能体时,可以用:

ub_agents_content["tavily"]["name"]
sub_agents_content["tavily"]["description"]
sub_agents_content["tavily"]["system_prompt"]

YAML 中的 | 和 > 怎么看

提示词经常是多行文本,所以 YAML 里会看到 |

system_prompt: |
  第一行提示词
  第二行提示词
  - 可以保留列表结构

| 会保留换行,适合写系统提示词、步骤、列表。

还有一种写法是 >

description: >
  第一行描述
  第二行描述

> 通常会把多数换行折叠成空格,更适合普通段落描述。写 system_prompt 时,如果希望保留分段和格式,优先使用 |

 

posted @ 2026-06-30 15:00  幻影之舞  阅读(11)  评论(0)    收藏  举报