feat(agent): 新增内置深度研究智能体

This commit is contained in:
Wenjie Zhang 2026-06-05 21:02:13 +08:00
parent 8582b94f68
commit 948a5fda9e
6 changed files with 262 additions and 0 deletions

View File

@ -35,6 +35,13 @@ BUILTIN_SKILLS: list[BuiltinSkillSpec] = [
version="2026.03.28",
tool_dependencies=["tavily_search"],
),
BuiltinSkillSpec(
slug="deep-research",
source_dir=_SKILLS_ROOT / "deep-research",
description="深度研究编排方法论:澄清范围、拆解规划、并行调度子智能体调研、对抗式核验、综合成带引用的结构化报告。",
version="2026.06.05",
tool_dependencies=("tavily_search",),
),
BuiltinSkillSpec(
slug="reporter",
source_dir=_SKILLS_ROOT / "reporter",

View File

@ -0,0 +1,50 @@
---
name: deep-research
description: "深度研究编排方法论:澄清范围、拆解规划、并行调度子智能体调研、对抗式核验、综合成带引用的结构化报告。当任务需要多来源、可追溯、需事实核查的深度研究时使用此技能。"
---
# 深度研究技能
当任务目标是产出**多来源、可追溯、经过核验**的深度研究结论(科研综述、行业/竞品调研、技术选型、专题分析等)时,使用此技能组织整个研究过程。本技能的核心是**编排**:你负责整体把控与子智能体调度,把繁重的检索与核验工作派发出去,自己专注规划与综合。
## 可用子智能体
通过 `task` 工具调度(可并行多开,互不依赖的子任务同时派发):
- `research-explorer`(调研探索员):围绕一个明确子问题做多轮网页/知识库检索,返回按要点组织、带 `<cite>` 引用的结构化发现。**这是主力,按子问题并行多开。**
- `fact-verifier`(事实核查员):对给定的关键论断做对抗式核验,逐条给出 支持 / 存疑 / 反驳 + 依据来源 + 置信度,并标注冲突。
## 编排流程
### 1. 澄清范围
问题不明确时,先用 `ask_user_question` 补充 2-3 个关键问题(研究目标、受众、范围边界、地域/时效、输出语言与形式),对齐验收标准后再开工。已经清晰的任务不要反复追问。
### 2. 规划拆解
`write_todos` 把研究目标拆成**可独立调研**的子问题,每个子问题写明产出标准(要回答什么、需要哪类证据)。子问题应正交、覆盖完整,避免重叠或遗漏关键角度。
### 3. 并行派发调研
- 把互不依赖的子问题用**多个 `task` 调用并行**派发给 `research-explorer`
- 每次派发在 `description` 中写清:子问题目标、已知上下文、期望输出格式(要点 + `<cite source="$URL" type="url">$INDEX</cite>` 引用 + 参考来源列表)。
- 何时派发 vs 自己直检:子问题复杂、需多轮检索、可隔离上下文、可并行时一律派发子智能体;仅在澄清范围、补一两个零散事实、或快速校正方向时才自己少量直接检索。
- 子问题之间有依赖时,先派发前置子问题,拿到结果后再派发后续。
### 4. 核验关键结论
对**影响最终结论的关键论断**、数字、以及子智能体之间相互冲突的发现,派发 `fact-verifier` 做对抗式核验。要求其默认倾向「证据不足即标注存疑」。核验未通过的结论不要写进正文,或必须明确降级标注。
### 5. 综合成稿
证据充分后,由你统一综合为结构化报告,**不要**简单拼接子智能体返回的原文。组织顺序:问题定义 → 证据整理 → 分析比较 → 结论与建议 → 来源。围绕「论证」而非「资料堆砌」,每个结论都要有证据支撑。
### 6. 停止准则
信息饱和、或确认无法获取更多有效信息即停。明确标注证据缺口与不确定性,不臆断、不编造来源。
## 引用规范
- 报告中关键结论、数据、观点必须绑定来源。
- 沿用 `<cite source="$URL" type="url">$INDEX</cite>` 标注,$INDEX 从 1 起递增,引用紧跟结论后、不单独成行。
- 文末单列「来源」章节,逐条列出标题与 URL引用用户附件/知识库时标明文件名或路径。
## 输出约束
- 最终交付的是一份可直接使用的报告,而不是「我打算怎么研究」。
- 不要外泄中间推理过程、原始检索日志,也不要把待办清单原样输出成正文。
- 报告语言与用户提问语言一致,使用正式、克制、可复核的书面表达。

View File

@ -35,6 +35,63 @@ WEB_SEARCH_SYSTEM_PROMPT = """你是「网页检索」子智能体,专注于
- 引用不单独成行直接跟在结论后面
- 在结尾汇总参考来源列表逐条列出标题与 URL
- 不要编造来源或链接无法验证的信息要明确标注"""
DEEP_RESEARCH_AGENT_SLUG = "deep-research"
DEEP_RESEARCH_AGENT_NAME = "深度研究"
DEEP_RESEARCH_AGENT_DESCRIPTION = (
"面向多来源、需事实核查的深度研究任务:规划拆解、并行调度调研子智能体、核验并综合成带引用的结构化报告。"
)
DEEP_RESEARCH_SYSTEM_PROMPT = """你是「深度研究」智能体,负责一项深度研究任务的整体把控与子智能体调度。
你的核心定位是编排者而不是亲自完成所有检索把繁重可独立可并行的调研与核验工作派发给子智能体自己专注于规划调度与最终综合
工作方式
1. 接到研究任务后先读取 `deep-research` 技能read_file SKILL.md获取完整方法论并严格据此执行
2. 问题不明确时先澄清范围再用待办拆解出可独立调研的子问题
3. 优先用 `task` 工具把子问题并行派发给调研子智能体仅在澄清范围或补少量零散事实时自己直接检索
4. 对关键结论与相互冲突的发现派发核查子智能体核验未通过的结论不写入正文或明确降级标注
5. 证据充分后由你统一综合为结构化带引用的报告不要简单拼接子智能体返回的原文
始终全程跟踪进度最终交付一份可直接使用围绕论证组织来源可追溯的报告"""
RESEARCH_EXPLORER_AGENT_SLUG = "research-explorer"
RESEARCH_EXPLORER_AGENT_NAME = "调研探索员"
RESEARCH_EXPLORER_AGENT_DESCRIPTION = "围绕单个子问题多轮检索网页与知识库,交叉验证后返回带引用的结构化发现。"
RESEARCH_EXPLORER_SYSTEM_PROMPT = """你是「调研探索员」子智能体。
专注于围绕调用方给定的**单个子问题**收集充分可追溯的证据
你的职责围绕该子问题持续检索网页与知识库直到收集到足以回答它的信息
工作方式
1. 拆解子问题确定需要检索的关键点与检索词
2. 多轮调用检索工具依据上一轮结果调整检索词补充遗漏角度交叉验证关键事实直到信息充分或确认无法获取更多有效信息
3. 优先采信权威时效性强且彼此印证的来源对存在冲突的信息要说明分歧
输出要求
- 返回一份围绕该子问题按要点组织的结构化发现不要展开成完整报告
- 每条关键结论后使用 <cite source="$URL" type="url">$INDEX</cite> 标注引用来源$INDEX 1 开始递增
- 引用紧跟结论后不单独成行
- 结尾汇总参考来源列表逐条列出标题与 URL
- 不要编造来源或链接无法验证的信息要明确标注证据缺口"""
FACT_VERIFIER_AGENT_SLUG = "fact-verifier"
FACT_VERIFIER_AGENT_NAME = "事实核查员"
FACT_VERIFIER_AGENT_DESCRIPTION = "对给定论断做对抗式核验,逐条给出支持/存疑/反驳判定、依据来源与置信度,并标注冲突。"
FACT_VERIFIER_SYSTEM_PROMPT = """你是「事实核查员」子智能体,专注于对调用方给定的论断做对抗式核验。
你的职责对每一条论断独立查证默认持怀疑态度证据不足时倾向判定存疑而不是默认相信
工作方式
1. 逐条拆出待核验的论断事实数字因果时间等
2. 主动检索权威独立的来源交叉比对优先寻找能反驳该论断的证据
3. 对来源之间的冲突如实呈现不强行调和
输出要求
- 对每条论断给出判定支持 / 存疑 / 反驳+ 简要依据 + 依据来源 + 置信度//
- 关键依据后使用 <cite source="$URL" type="url">$INDEX</cite> 标注来源$INDEX 1 开始递增
- 明确标注无法查证或来源相互冲突的论断
- 不要编造来源或链接"""
ACCESS_LEVELS = {"global", "department", "user"}
ADMIN_ROLES = {"admin", "superadmin"}
@ -204,6 +261,77 @@ class AgentRepository:
await self.db.refresh(agent)
return agent
async def _ensure_builtin_agent(
self,
*,
slug: str,
backend_id: str,
name: str,
description: str,
config_context: dict,
is_subagent: bool,
created_by: str | None = None,
) -> Agent:
"""落库一个内置 Agent已存在则原样返回避免覆盖管理员后续修改。"""
agent = await self.get_by_slug(slug)
if agent:
return agent
agent = Agent(
slug=slug,
backend_id=backend_id,
name=name,
description=description,
icon=None,
pics=[],
config_json={"context": config_context},
share_config=DEFAULT_SHARE_CONFIG.copy(),
is_default=False,
is_subagent=is_subagent,
created_by=created_by,
updated_by=created_by,
created_at=utc_now_naive(),
updated_at=utc_now_naive(),
)
self.db.add(agent)
await self.db.commit()
await self.db.refresh(agent)
return agent
async def ensure_deep_research_agents(self, *, created_by: str | None = None) -> None:
"""落库内置「深度研究」编排器及其配套调研、核查子智能体。"""
await self._ensure_builtin_agent(
slug=RESEARCH_EXPLORER_AGENT_SLUG,
backend_id=SUB_AGENT_BACKEND_ID,
name=RESEARCH_EXPLORER_AGENT_NAME,
description=RESEARCH_EXPLORER_AGENT_DESCRIPTION,
config_context={"system_prompt": RESEARCH_EXPLORER_SYSTEM_PROMPT},
is_subagent=True,
created_by=created_by,
)
await self._ensure_builtin_agent(
slug=FACT_VERIFIER_AGENT_SLUG,
backend_id=SUB_AGENT_BACKEND_ID,
name=FACT_VERIFIER_AGENT_NAME,
description=FACT_VERIFIER_AGENT_DESCRIPTION,
config_context={"system_prompt": FACT_VERIFIER_SYSTEM_PROMPT},
is_subagent=True,
created_by=created_by,
)
await self._ensure_builtin_agent(
slug=DEEP_RESEARCH_AGENT_SLUG,
backend_id=DEFAULT_AGENT_BACKEND_ID,
name=DEEP_RESEARCH_AGENT_NAME,
description=DEEP_RESEARCH_AGENT_DESCRIPTION,
config_context={
"system_prompt": DEEP_RESEARCH_SYSTEM_PROMPT,
"subagents": [RESEARCH_EXPLORER_AGENT_SLUG, FACT_VERIFIER_AGENT_SLUG],
"skills": [DEEP_RESEARCH_AGENT_SLUG],
},
is_subagent=False,
created_by=created_by,
)
async def list_visible(self, *, user: User, include_subagents: bool = False) -> list[Agent]:
stmt = select(Agent)
if not include_subagents:

View File

@ -48,6 +48,7 @@ async def lifespan(app: FastAPI):
repository = AgentRepository(session)
await repository.ensure_default_agent()
await repository.ensure_web_search_subagent()
await repository.ensure_deep_research_agents()
except Exception as e:
logger.error(f"Failed to ensure default agent during startup: {e}")

View File

@ -0,0 +1,75 @@
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from yuxi.repositories.agent_repository import (
AgentRepository,
DEEP_RESEARCH_AGENT_SLUG,
DEFAULT_AGENT_BACKEND_ID,
FACT_VERIFIER_AGENT_SLUG,
RESEARCH_EXPLORER_AGENT_SLUG,
SUB_AGENT_BACKEND_ID,
)
class CollectingDb:
def __init__(self):
self.added: list = []
self.commit = AsyncMock()
self.refresh = AsyncMock()
def add(self, item):
self.added.append(item)
@pytest.mark.asyncio
async def test_ensure_deep_research_agents_creates_orchestrator_and_subagents(monkeypatch):
db = CollectingDb()
repo = AgentRepository(db)
async def get_by_slug(_slug):
return None
monkeypatch.setattr(repo, "get_by_slug", get_by_slug)
await repo.ensure_deep_research_agents()
created = {agent.slug: agent for agent in db.added}
assert set(created) == {
DEEP_RESEARCH_AGENT_SLUG,
RESEARCH_EXPLORER_AGENT_SLUG,
FACT_VERIFIER_AGENT_SLUG,
}
explorer = created[RESEARCH_EXPLORER_AGENT_SLUG]
verifier = created[FACT_VERIFIER_AGENT_SLUG]
assert explorer.backend_id == SUB_AGENT_BACKEND_ID and explorer.is_subagent is True
assert verifier.backend_id == SUB_AGENT_BACKEND_ID and verifier.is_subagent is True
orchestrator = created[DEEP_RESEARCH_AGENT_SLUG]
assert orchestrator.backend_id == DEFAULT_AGENT_BACKEND_ID
assert orchestrator.is_subagent is False
assert orchestrator.is_default is False
context = orchestrator.config_json["context"]
assert context["subagents"] == [RESEARCH_EXPLORER_AGENT_SLUG, FACT_VERIFIER_AGENT_SLUG]
assert context["skills"] == [DEEP_RESEARCH_AGENT_SLUG]
assert context["system_prompt"].strip()
@pytest.mark.asyncio
async def test_ensure_deep_research_agents_is_idempotent(monkeypatch):
db = CollectingDb()
repo = AgentRepository(db)
async def get_by_slug(slug):
return SimpleNamespace(slug=slug)
monkeypatch.setattr(repo, "get_by_slug", get_by_slug)
await repo.ensure_deep_research_agents()
assert db.added == []
db.commit.assert_not_awaited()

View File

@ -16,6 +16,7 @@
### 开发记录
- 新增内置「深度研究」多智能体:编排器 Agent`deep-research`ChatbotAgent 后端)负责澄清、拆解、并行调度子智能体与综合成稿,配套两个子智能体 `research-explorer`(围绕单个子问题多轮检索网页/知识库并返回带引用发现)和 `fact-verifier`(对抗式核验关键论断、标注冲突与置信度);完整研究方法论沉淀为新增内置 Skill `deep-research`(依赖 `tavily_search`),编排器运行时读取并据此调度。三者随 `lifespan` 启动通过 `AgentRepository.ensure_deep_research_agents` 幂等落库(已存在不覆盖管理员修改)。
- 收敛 MCP 创建与编辑入口:前端移除整段配置文本入口和模式切换器,仅保留表单字段提交;后端 MCP 创建/更新请求拒绝额外配置字段,避免绕过表单约束。
- 调整内置 MCP 默认项:移除 `sequentialthinking` 的系统内置同步,启动同步时清理历史系统内置记录,保留用户手动创建的同名 MCP。
- 图片生成能力迁移为 SkillQwen-Image 从内置 Python 生成工具迁移到内置 Skill `image-gen`,模型调用与图片下载在 Agent 沙盒中完成,生成结果保存到 outputs 并通过 `present_artifacts` 展示,为多图片生成模型接入复用同一产物展示链路。