异常处理
This commit is contained in:
1 parent
17b28f43f8
commit
b25c4e33cb
7 files changed
+108
-16
No files matched your search
+16
-6
@@ -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:
|
||||
|
||||
+13
-1
@@ -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] = [] # 分项审查的模型异常,供完成后非致命提示
|
||||
|
||||
|
||||
+45
-6
@@ -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"]
|
||||
|
||||
|
||||
+8
-1
@@ -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])
|
||||
|
||||
+13
-1
@@ -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] = [] # 分节写作的模型异常,供完成后非致命提示
|
||||
|
||||
+3
-1
@@ -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),
|
||||
|
||||
+10
@@ -6,6 +6,11 @@ EXTRACT_KEYWORDS_SYSTEM = """你是一名专业情报研究员(素材收集专
|
||||
1. 从写作意图中提取 3-6 个搜索关键词
|
||||
你不能写文章,不能评价内容好坏,只做关键词提取。
|
||||
|
||||
【严禁思考】
|
||||
- 这是一个简单的结构化抽取任务,禁止任何思考、分析、推理或规划过程。
|
||||
- 禁止输出 <think></think> 标签及其中的任何内容,禁止输出"让我想想""首先"之类的过渡语。
|
||||
- 收到输入后必须立即、直接输出最终 JSON,不得有任何前置内容。
|
||||
|
||||
【输出要求】
|
||||
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
|
||||
- 必须使用英文双引号 ",不要用中文引号 “”。
|
||||
@@ -31,6 +36,11 @@ RANK_MATERIALS_SYSTEM = """你是一名专业情报研究员(素材收集专
|
||||
1. 从提供的素材列表中,选出与写作意图最相关的素材ID,按相关度从高到低排序
|
||||
你不能写文章,只做素材筛选排序。
|
||||
|
||||
【严禁思考】
|
||||
- 这是一个简单的筛选排序任务,禁止任何思考、分析、推理或规划过程。
|
||||
- 禁止输出 <think></think> 标签及其中的任何内容,禁止输出"让我想想""首先"之类的过渡语。
|
||||
- 收到输入后必须立即、直接输出最终 JSON,不得有任何前置内容。
|
||||
|
||||
【输出要求】
|
||||
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
|
||||
- 必须使用英文双引号 "。
|
||||
|
||||
Reference in new issue
Block a user