From 3eae4a0f45e5e6164c973b1e3b591df37e9db924 Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 3 Jun 2026 16:34:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../deerflow/agents/lead_agent/agent.py | 10 ++- .../deerflow/agents/lead_agent/prompt.py | 6 +- .../writing_setup_round_cap_middleware.py | 72 +++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/writing_setup_round_cap_middleware.py diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py index f63cc719..3570f0a2 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/agent.py @@ -26,6 +26,9 @@ from deerflow.models import create_chat_model logger = logging.getLogger(__name__) +# 写作配置 Q&A:意图澄清最多几轮(一条用户消息 = 一轮),到顶强制提交配置卡。 +WRITING_SETUP_MAX_ROUNDS = 3 + def _get_runtime_config(config: RunnableConfig) -> dict: """Merge legacy configurable options with LangGraph runtime context.""" @@ -454,12 +457,17 @@ def _make_lead_agent(config: RunnableConfig, *, app_config: AppConfig): # 写作配置 Q&A:最小化 agent —— **只挂 setup_writing 一个工具** + 一份职责单一的独立提示。 # 关键:绝不给全套工具、绝不复用 lead 超级体提示,否则模型会把「新能源汽车市场分析」之类 # 长得像任务的输入直接做掉,而不是转成写作配置提交。 + # 末尾追加 WritingSetupRoundCapMiddleware:澄清最多 3 轮,到顶强制提交配置卡。 from deerflow.agents.lead_agent.prompt import build_writing_setup_prompt + from deerflow.agents.middlewares.writing_setup_round_cap_middleware import WritingSetupRoundCapMiddleware return create_agent( model=create_chat_model(name=model_name, thinking_enabled=thinking_enabled, app_config=resolved_app_config), tools=[setup_writing], - middleware=_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), + middleware=[ + *_build_middlewares(config, model_name=model_name, app_config=resolved_app_config), + WritingSetupRoundCapMiddleware(max_rounds=WRITING_SETUP_MAX_ROUNDS), + ], system_prompt=build_writing_setup_prompt(writing_setup_article_types), state_schema=ThreadState, ) diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py index 25908c3c..914d30f7 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/lead_agent/prompt.py @@ -927,9 +927,9 @@ def build_writing_setup_prompt(article_types: list[dict] | None = None) -> str: - 你能做的动作只有两个:① 用自然语言向用户提问或确认;② 调用 `setup_writing` 工具。**除此之外你没有任何其它工具**,也不要假装调用别的工具、不要做检索、不要做研究。 - 不要在回复里输出文章段落、要点清单或大纲(用户主动粘贴的大纲除外——原样放进 `outline` 参数)。 -【工作流程】 -1. 读用户输入。如果「写什么」已经明确,关键信息(文章类型、字数、读者)即使缺失也可以用合理默认补全——**信息基本够了就直接进入第 3 步,不要为了凑齐而反复追问**。 -2. 只有当「写什么」本身都不清楚时,才用一句话问 1-2 个最关键的问题(最多一轮)。 +【工作流程】(与用户交互确认,但最多 3 轮内必须提交) +1. 读用户输入,先用一两句话回应并和用户**确认写作意图**;就缺失或模糊的关键项(文章类型、目标字数、目标读者)提问澄清,一次最多问 1-2 个最关键的问题。 +2. 与用户的澄清来回**最多 2 次**。一旦关键信息明确、或已经问过 2 次,就**立即进入第 3 步提交**,缺失字段用合理默认值,绝不无休止追问。 3. 调用 `setup_writing` 提交配置: - `user_intent`:精炼成一句话的写作目标(必填)。 - `article_type`:见下方可选类型;不确定就留空。 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/writing_setup_round_cap_middleware.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/writing_setup_round_cap_middleware.py new file mode 100644 index 00000000..5fadb642 --- /dev/null +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/middlewares/writing_setup_round_cap_middleware.py @@ -0,0 +1,72 @@ +"""写作配置 Q&A 的「轮数上限」中间件。 + +意图阶段允许与用户来回澄清确认,但**最多 N 轮后必须出结果**:到上限时,在模型请求里 +注入强制指令 + 用 ``tool_choice`` 锁定 ``setup_writing``,逼模型立刻提交配置卡,不再提问。 + +「一轮」= 一条用户消息(HumanMessage)。达到 ``max_rounds`` 条用户消息时强制提交。 +""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from typing import override + +from langchain.agents import AgentState +from langchain.agents.middleware import AgentMiddleware +from langchain.agents.middleware.types import ModelCallResult, ModelRequest, ModelResponse +from langchain_core.messages import HumanMessage, SystemMessage + + +class WritingSetupRoundCapMiddleware(AgentMiddleware[AgentState]): + """达到澄清轮数上限时,强制模型调用 ``setup_writing`` 提交。 + + 软约束(注入 SystemMessage)+ 硬约束(``tool_choice`` 锁定工具)双保险:即使个别模型 + 无视提示,``tool_choice`` 也会逼它产出一次 ``setup_writing`` 调用。 + """ + + def __init__(self, max_rounds: int = 3, tool_name: str = "setup_writing") -> None: + super().__init__() + self._max_rounds = max(1, max_rounds) + self._tool_name = tool_name + + def _should_force(self, messages: list) -> bool: + human_count = sum(1 for m in messages if isinstance(m, HumanMessage)) + return human_count >= self._max_rounds + + def _reminder(self) -> SystemMessage: + return SystemMessage( + content=( + f"【系统强制】意图澄清已达 {self._max_rounds} 轮上限。" + "现在必须立即调用 setup_writing 提交写作配置,禁止再向用户提任何问题。" + "缺失或不确定的字段一律用合理默认值:" + "word_count_target 默认 800、target_audience 默认「普通读者」、" + "writing_mode 默认 loose、material_source 默认 knowledge_base、" + "article_type 不确定就留空。" + ) + ) + + def _patch(self, request: ModelRequest) -> ModelRequest: + return request.override( + messages=[*request.messages, self._reminder()], + tool_choice=self._tool_name, + ) + + @override + def wrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], ModelResponse], + ) -> ModelCallResult: + if self._should_force(request.messages): + request = self._patch(request) + return handler(request) + + @override + async def awrap_model_call( + self, + request: ModelRequest, + handler: Callable[[ModelRequest], Awaitable[ModelResponse]], + ) -> ModelCallResult: + if self._should_force(request.messages): + request = self._patch(request) + return await handler(request)