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

四个智能体和九个工具分布
| 类型 | 名称 | 作用 |
|---|---|---|
| 主智能体 | 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 接口 |
| 异步执行 | asyncio、ContextVar |
支持并发请求隔离和跨步骤传递上下文 |
| 路径管理 | pathlib、shutil |
处理路径拼接、文件移动、上传文件归档 |
前后端注意的两个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 各自带着自己的上下文往下走:

实时进度: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 |
当前任务的会话目录,用来隔离不同任务生成和读取的文件 |
核心功能可以概括成四件事:
- 清洗模型常见的虚拟路径前缀,例如
/workspace、/mnt/data、/home/user。 - 识别
updated/上传目录,并优先按项目根目录下的真实上传路径解析。 - 结合
session_dir处理相对路径和绝对路径,让普通任务产物尽量落在当前会话目录里。 - 防止路径重复嵌套,例如
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)}"
SimpleDocTemplate is None:缺少依赖时返回可读提示,不让整个服务启动失败。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 尤其重要。它不是随便写一句“这是一个助手”,而是告诉主智能体什么时候应该调用它。
更清楚的写法应该接近:
- 当用户问题需要联网查询最新公开资料时,调用网络搜索助手。
- 当用户问题需要查询药品数据库、表结构或执行 SQL 时,调用数据库查询助手。
- 当用户问题需要查询企业内部知识库或制度文档时,调用 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。.env、pyproject.toml、examples/ 这些仍在 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 时,如果希望保留分段和格式,优先使用 |

浙公网安备 33010602011771号