diff --git a/backend/package/yuxi/agents/skills/buildin/__init__.py b/backend/package/yuxi/agents/skills/buildin/__init__.py index df3e75f1..e05a69a1 100644 --- a/backend/package/yuxi/agents/skills/buildin/__init__.py +++ b/backend/package/yuxi/agents/skills/buildin/__init__.py @@ -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", diff --git a/backend/package/yuxi/agents/skills/buildin/deep-research/SKILL.md b/backend/package/yuxi/agents/skills/buildin/deep-research/SKILL.md new file mode 100644 index 00000000..0a7c12f5 --- /dev/null +++ b/backend/package/yuxi/agents/skills/buildin/deep-research/SKILL.md @@ -0,0 +1,50 @@ +--- +name: deep-research +description: "深度研究编排方法论:澄清范围、拆解规划、并行调度子智能体调研、对抗式核验、综合成带引用的结构化报告。当任务需要多来源、可追溯、需事实核查的深度研究时使用此技能。" +--- + +# 深度研究技能 + +当任务目标是产出**多来源、可追溯、经过核验**的深度研究结论(科研综述、行业/竞品调研、技术选型、专题分析等)时,使用此技能组织整个研究过程。本技能的核心是**编排**:你负责整体把控与子智能体调度,把繁重的检索与核验工作派发出去,自己专注规划与综合。 + +## 可用子智能体 + +通过 `task` 工具调度(可并行多开,互不依赖的子任务同时派发): + +- `research-explorer`(调研探索员):围绕一个明确子问题做多轮网页/知识库检索,返回按要点组织、带 `` 引用的结构化发现。**这是主力,按子问题并行多开。** +- `fact-verifier`(事实核查员):对给定的关键论断做对抗式核验,逐条给出 支持 / 存疑 / 反驳 + 依据来源 + 置信度,并标注冲突。 + +## 编排流程 + +### 1. 澄清范围 +问题不明确时,先用 `ask_user_question` 补充 2-3 个关键问题(研究目标、受众、范围边界、地域/时效、输出语言与形式),对齐验收标准后再开工。已经清晰的任务不要反复追问。 + +### 2. 规划拆解 +用 `write_todos` 把研究目标拆成**可独立调研**的子问题,每个子问题写明产出标准(要回答什么、需要哪类证据)。子问题应正交、覆盖完整,避免重叠或遗漏关键角度。 + +### 3. 并行派发调研 +- 把互不依赖的子问题用**多个 `task` 调用并行**派发给 `research-explorer`。 +- 每次派发在 `description` 中写清:子问题目标、已知上下文、期望输出格式(要点 + `$INDEX` 引用 + 参考来源列表)。 +- 何时派发 vs 自己直检:子问题复杂、需多轮检索、可隔离上下文、可并行时一律派发子智能体;仅在澄清范围、补一两个零散事实、或快速校正方向时才自己少量直接检索。 +- 子问题之间有依赖时,先派发前置子问题,拿到结果后再派发后续。 + +### 4. 核验关键结论 +对**影响最终结论的关键论断**、数字、以及子智能体之间相互冲突的发现,派发 `fact-verifier` 做对抗式核验。要求其默认倾向「证据不足即标注存疑」。核验未通过的结论不要写进正文,或必须明确降级标注。 + +### 5. 综合成稿 +证据充分后,由你统一综合为结构化报告,**不要**简单拼接子智能体返回的原文。组织顺序:问题定义 → 证据整理 → 分析比较 → 结论与建议 → 来源。围绕「论证」而非「资料堆砌」,每个结论都要有证据支撑。 + +### 6. 停止准则 +信息饱和、或确认无法获取更多有效信息即停。明确标注证据缺口与不确定性,不臆断、不编造来源。 + +## 引用规范 + +- 报告中关键结论、数据、观点必须绑定来源。 +- 沿用 `$INDEX` 标注,$INDEX 从 1 起递增,引用紧跟结论后、不单独成行。 +- 文末单列「来源」章节,逐条列出标题与 URL;引用用户附件/知识库时标明文件名或路径。 + +## 输出约束 + +- 最终交付的是一份可直接使用的报告,而不是「我打算怎么研究」。 +- 不要外泄中间推理过程、原始检索日志,也不要把待办清单原样输出成正文。 +- 报告语言与用户提问语言一致,使用正式、克制、可复核的书面表达。 diff --git a/backend/package/yuxi/repositories/agent_repository.py b/backend/package/yuxi/repositories/agent_repository.py index fae6cb36..d8a1f925 100644 --- a/backend/package/yuxi/repositories/agent_repository.py +++ b/backend/package/yuxi/repositories/agent_repository.py @@ -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. 优先采信权威、时效性强且彼此印证的来源;对存在冲突的信息要说明分歧。 + +输出要求: +- 返回一份围绕该子问题、按要点组织的结构化发现,不要展开成完整报告。 +- 每条关键结论后使用 $INDEX 标注引用来源,$INDEX 从 1 开始递增。 +- 引用紧跟结论后、不单独成行。 +- 结尾汇总「参考来源」列表,逐条列出标题与 URL。 +- 不要编造来源或链接;无法验证的信息要明确标注证据缺口。""" + +FACT_VERIFIER_AGENT_SLUG = "fact-verifier" +FACT_VERIFIER_AGENT_NAME = "事实核查员" +FACT_VERIFIER_AGENT_DESCRIPTION = "对给定论断做对抗式核验,逐条给出支持/存疑/反驳判定、依据来源与置信度,并标注冲突。" +FACT_VERIFIER_SYSTEM_PROMPT = """你是「事实核查员」子智能体,专注于对调用方给定的论断做对抗式核验。 + +你的职责:对每一条论断独立查证,默认持怀疑态度——证据不足时倾向判定「存疑」,而不是默认相信。 + +工作方式: +1. 逐条拆出待核验的论断(事实、数字、因果、时间等)。 +2. 主动检索权威、独立的来源交叉比对;优先寻找能反驳该论断的证据。 +3. 对来源之间的冲突如实呈现,不强行调和。 + +输出要求: +- 对每条论断给出:判定(支持 / 存疑 / 反驳)+ 简要依据 + 依据来源 + 置信度(高/中/低)。 +- 关键依据后使用 $INDEX 标注来源,$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: diff --git a/backend/server/utils/lifespan.py b/backend/server/utils/lifespan.py index 1da21cbb..5b8567ea 100644 --- a/backend/server/utils/lifespan.py +++ b/backend/server/utils/lifespan.py @@ -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}") diff --git a/backend/test/unit/repositories/test_agent_repository_deep_research.py b/backend/test/unit/repositories/test_agent_repository_deep_research.py new file mode 100644 index 00000000..dc94b1f4 --- /dev/null +++ b/backend/test/unit/repositories/test_agent_repository_deep_research.py @@ -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() diff --git a/docs/develop-guides/changelog.md b/docs/develop-guides/changelog.md index 8791518d..a2349e89 100644 --- a/docs/develop-guides/changelog.md +++ b/docs/develop-guides/changelog.md @@ -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。 - 图片生成能力迁移为 Skill:Qwen-Image 从内置 Python 生成工具迁移到内置 Skill `image-gen`,模型调用与图片下载在 Agent 沙盒中完成,生成结果保存到 outputs 并通过 `present_artifacts` 展示,为多图片生成模型接入复用同一产物展示链路。