diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_utils.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_utils.py
index f5877ecb..9d2085d7 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_utils.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_utils.py
@@ -15,10 +15,15 @@ from langchain_core.runnables import RunnableConfig
logger = logging.getLogger(__name__)
-def get_model(config: RunnableConfig):
+def get_model(config: RunnableConfig, model_name: str | None = None):
+ """创建对话模型。
+
+ model_name 显式传入时优先使用(如素材收集专家的检索词/排序专用快模型),
+ 否则回退到 configurable.model_name(用户在写作表单里选的主模型)。
+ """
from deerflow.models import create_chat_model
- model_name = config.get("configurable", {}).get("model_name", "") or None
- return create_chat_model(name=model_name)
+ name = model_name or config.get("configurable", {}).get("model_name", "") or None
+ return create_chat_model(name=name)
# 弱模型常把结构性标点写成中文全角符号,仅在常规解析失败后作为修复手段
@@ -220,11 +225,16 @@ class ThinkTagParser:
return 0
-async def llm_json(system: str, user: str, fallback: dict, config: RunnableConfig) -> dict:
- """调用 LLM,解析 JSON 输出,失败用 fallback。"""
- model = get_model(config)
+async def llm_json(system: str, user: str, fallback: dict, config: RunnableConfig, model_name: str | None = None) -> dict:
+ """调用 LLM,解析 JSON 输出,失败用 fallback。
+
+ model_name 可覆盖默认模型(如检索词/排序使用配置的专用快模型)。
+ """
messages = [SystemMessage(content=system), HumanMessage(content=user)]
try:
+ # get_model 放进 try:模型名在 config.yaml 找不到时 create_chat_model 会抛
+ # ValueError,若留在 try 外会逐层上抛使整个图硬崩溃。
+ model = get_model(config, model_name=model_name)
resp = await model.ainvoke(messages)
return parse_json(_content_to_text(resp.content), fallback)
except Exception as e:
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/editor.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/editor.py
index 438dac98..6aa7019b 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/editor.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/editor.py
@@ -89,7 +89,19 @@ async def editor_node(state: AIWritingState, config: RunnableConfig) -> dict:
("language", LANGUAGE_REVIEW_USER.format(article=article)),
]
- model = get_model(config)
+ # 模型创建守卫:模型名配错时 create_chat_model 抛 ValueError,
+ # 不拦截会硬崩溃整个图;此处转成可恢复的 status:error。
+ try:
+ model = get_model(config)
+ except Exception as e:
+ logger.error("editor model creation failed: %s", e, exc_info=True)
+ return {
+ "status": "error",
+ "progress_events": [{
+ "type": "error", "agent_name": "编辑",
+ "title": "模型错误", "detail": str(e), "message": "审核失败",
+ }],
+ }
results: dict[str, dict] = {}
review_errors: list[str] = [] # 分项审查的模型异常,供完成后非致命提示
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/intent_parser.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/intent_parser.py
index a575d902..92825d68 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/intent_parser.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/intent_parser.py
@@ -1,6 +1,8 @@
"""意图解析节点:将用户自然语言意图结构化为写作配置。"""
from __future__ import annotations
+import re
+
from langchain_core.runnables import RunnableConfig
from ..state import AIWritingState
@@ -43,6 +45,33 @@ def _build_type_options(types: list[dict]) -> str:
return " / ".join(f'{t["key"]}({t["label"]})' for t in types)
+def _coerce_word_count(raw) -> int | None:
+ """稳健地把模型返回的字数字段转成正整数;无法解析时返回 None。
+
+ 弱模型(尤其老模型被陌生词带偏时)常返回「约800字」「两千」「800.0」
+ 这类非纯数字形式,直接 int() 会抛 ValueError/TypeError 使整个图崩溃。
+ 此处用「提取首个数字串」策略容错,解析不出就返回 None 让调用方保留默认值。
+ """
+ if raw is None:
+ return None
+ if isinstance(raw, bool): # bool 是 int 子类,需先排除
+ return None
+ if isinstance(raw, (int, float)):
+ value = int(raw)
+ return value if value > 0 else None
+ if not isinstance(raw, str):
+ # dict / list 等结构化类型:str() 化后可能误含数字,直接判为无法解析
+ return None
+ m = re.search(r"\d[\d,]*", raw)
+ if not m:
+ return None
+ try:
+ value = int(m.group().replace(",", ""))
+ except ValueError:
+ return None
+ return value if value > 0 else None
+
+
async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> dict:
# 从 configurable 读取动态类型列表,未配置时退回默认值
configurable = (config or {}).get("configurable", {})
@@ -73,10 +102,19 @@ async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> d
config=config,
)
- updates: dict = {
- "status": "researching",
- "progress_events": [{"type": "step_done", "agent_name": "系统", "message": "意图解析完成"}],
- }
+ # 意图解析模型调用失败时以非致命 notice 提示用户(llm_json 已用 fallback 兜底,
+ # 流程仍可继续,但用户需知道结构化配置可能不准)。
+ progress_events: list[dict] = []
+ if result.get("_llm_error"):
+ progress_events.append({
+ "type": "notice",
+ "agent_name": "系统",
+ "title": "模型错误",
+ "detail": f"意图解析失败,已按默认配置兜底。原因:{result['_llm_error']}",
+ })
+ progress_events.append({"type": "step_done", "agent_name": "系统", "message": "意图解析完成"})
+
+ updates: dict = {"status": "researching", "progress_events": progress_events}
# 校验 LLM 返回的 article_type 是否在有效范围内,不合法时回退到默认值
raw_type = result.get("article_type", "")
@@ -84,8 +122,9 @@ async def intent_parser_node(state: AIWritingState, config: RunnableConfig) -> d
if result.get("target_audience"):
updates["target_audience"] = result["target_audience"]
- if result.get("word_count_target"):
- updates["word_count_target"] = int(result["word_count_target"])
+ word_count = _coerce_word_count(result.get("word_count_target"))
+ if word_count is not None:
+ updates["word_count_target"] = word_count
if result.get("refined_intent"):
updates["user_intent"] = result["refined_intent"]
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py
index 3b8f19b4..688b9a02 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py
@@ -40,6 +40,12 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
sse_queue = cfg.get("sse_queue")
sse_session_id: str = cfg.get("sse_session_id", "")
+ # 素材收集专家专用模型:检索词生成 / 素材排序 可在 config.yaml 的
+ # ai_writing 段单独配置(keyword_model / rank_model),留空则回退写作主模型。
+ ai_writing_config = cfg.get("ai_writing_config", {}) or {}
+ keyword_model: str | None = ai_writing_config.get("keyword_model") or None
+ rank_model: str | None = ai_writing_config.get("rank_model") or None
+
async def _emit(message: str) -> None:
if sse_queue is None:
return
@@ -59,13 +65,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
),
fallback={"keywords": [intent]},
config=config,
+ model_name=keyword_model,
)
keywords: list[str] = kw_result.get("keywords", [intent])
if extra_keywords:
keywords = list(dict.fromkeys(keywords + extra_keywords))
# Step 2: 并发搜索
- ai_writing_config = config.get("configurable", {}).get("ai_writing_config", {})
provider = get_search_provider(ai_writing_config)
search_keywords = keywords[:6]
@@ -136,6 +142,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
RANK_MATERIALS_USER.format(user_intent=intent, materials_list=materials_list),
fallback={"ranked_ids": [m["id"] for m in raw_materials[:_MAX_MATERIALS]], "summary": f"共搜索到{len(raw_materials)}条素材"},
config=config,
+ model_name=rank_model,
)
rank_error = rank_result.get("_llm_error", "")
ranked_ids: list[str] = rank_result.get("ranked_ids", [m["id"] for m in raw_materials])
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_draft.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_draft.py
index bf3da60f..14013dd1 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_draft.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_draft.py
@@ -73,7 +73,19 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
def _section_title(idx: int, sec: dict) -> str:
return sec.get("section_title") or sec.get("sectionTitle") or f"章节{idx + 1}"
- model = get_model(config)
+ # 模型创建守卫:模型名配错时 create_chat_model 抛 ValueError,
+ # 不拦截会硬崩溃整个图;此处转成可恢复的 status:error。
+ try:
+ model = get_model(config)
+ except Exception as e:
+ logger.error("draft model creation failed: %s", e, exc_info=True)
+ return {
+ "status": "error",
+ "progress_events": [{
+ "type": "error", "agent_name": "作家",
+ "title": "模型错误", "detail": str(e), "message": "草稿生成失败",
+ }],
+ }
section_titles: list[str] = []
section_bufs: list[str] = [] # 各章节正文(不含 ## 标题),供扩写兜底逐节替换
section_errors: list[str] = [] # 分节写作的模型异常,供完成后非致命提示
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py
index 404f9d5d..bd3ad1e6 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/writer_outline.py
@@ -62,11 +62,13 @@ async def writer_outline_node(state: AIWritingState, config: RunnableConfig) ->
sse_queue: asyncio.Queue | None = cfg.get("sse_queue")
session_id: str = cfg.get("sse_session_id", "")
- model = get_model(config)
full_text = "" # 只累积正文(非思考)用于 JSON 解析
parser = ThinkTagParser()
try:
+ # get_model 放进 try:模型名配错时 create_chat_model 抛 ValueError,
+ # 留在 try 外会硬崩溃;放进来则统一走下方的 status:error 兜底。
+ model = get_model(config)
async for chunk in model.astream([
SystemMessage(content=OUTLINE_SYSTEM),
HumanMessage(content=user_prompt),
diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/researcher_prompts.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/researcher_prompts.py
index 70b41eba..b8f7e576 100644
--- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/researcher_prompts.py
+++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/prompts/researcher_prompts.py
@@ -6,6 +6,11 @@ EXTRACT_KEYWORDS_SYSTEM = """你是一名专业情报研究员(素材收集专
1. 从写作意图中提取 3-6 个搜索关键词
你不能写文章,不能评价内容好坏,只做关键词提取。
+【严禁思考】
+- 这是一个简单的结构化抽取任务,禁止任何思考、分析、推理或规划过程。
+- 禁止输出 标签及其中的任何内容,禁止输出"让我想想""首先"之类的过渡语。
+- 收到输入后必须立即、直接输出最终 JSON,不得有任何前置内容。
+
【输出要求】
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
- 必须使用英文双引号 ",不要用中文引号 “”。
@@ -31,6 +36,11 @@ RANK_MATERIALS_SYSTEM = """你是一名专业情报研究员(素材收集专
1. 从提供的素材列表中,选出与写作意图最相关的素材ID,按相关度从高到低排序
你不能写文章,只做素材筛选排序。
+【严禁思考】
+- 这是一个简单的筛选排序任务,禁止任何思考、分析、推理或规划过程。
+- 禁止输出 标签及其中的任何内容,禁止输出"让我想想""首先"之类的过渡语。
+- 收到输入后必须立即、直接输出最终 JSON,不得有任何前置内容。
+
【输出要求】
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
- 必须使用英文双引号 "。