feat(ai写作): 内网检索接口执行诊断写入 material_package.intranet_debug(排查用)
- material_package 新增 intranet_debug:是否配置/端点/知识库/超时/每个检索词命中数与 报错/原始响应片段/总命中数/是否被采用为兜底/status(not_configured|pending|ran| cancelled|crashed)。随 values 事件回前端,前端忽略该字段,仅供 devtools 排查。 - 新增 config ai_writing.intranet_search_debug(默认 false):开启后即使技能成功也跑完 内网并记录完整结果,用于验证内网接口是否真能通。 - 前端不改动。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1ed004fbb1
commit
158d157af4
2 files changed
+61
-7
No files matched your search
@@ -440,6 +440,10 @@ ai_writing:
|
|||||||
# 接口契约:POST {url} 体 {"query","knowledge_base_name","ts":"yes"} → [{"page_content","metadata":{m_title,content,recUuid,m_publish,source1}}]
|
# 接口契约:POST {url} 体 {"query","knowledge_base_name","ts":"yes"} → [{"page_content","metadata":{m_title,content,recUuid,m_publish,source1}}]
|
||||||
intranet_search_url: "" # 例如 http://10.106.57.180:7863/knowledge_base/search_docs_es
|
intranet_search_url: "" # 例如 http://10.106.57.180:7863/knowledge_base/search_docs_es
|
||||||
intranet_knowledge_base: "samples" # 兜底检索用的知识库名
|
intranet_knowledge_base: "samples" # 兜底检索用的知识库名
|
||||||
|
# 内网接口诊断开关:开启后即使技能已检索成功,也照样把内网接口跑完并把执行结果
|
||||||
|
# (端点/每个检索词命中数/报错/原始响应片段)记到 material_package.intranet_debug 里,
|
||||||
|
# 便于在接口返回(devtools)排查内网接口到底通不通。生产建议关掉(会多跑一次内网)。
|
||||||
|
intranet_search_debug: false
|
||||||
# 四阶段「内置功能型智能体」开关(按阶段灰度)。true = 该阶段节点改为加载
|
# 四阶段「内置功能型智能体」开关(按阶段灰度)。true = 该阶段节点改为加载
|
||||||
# .deer-flow/agents/ai-writing-<stage>/ 的 SOUL.md + config(admin 可在智能体
|
# .deer-flow/agents/ai-writing-<stage>/ 的 SOUL.md + config(admin 可在智能体
|
||||||
# 管理页编辑人设/模型),false / 缺失 = 走旧的硬编码 prompt 路径。
|
# 管理页编辑人设/模型),false / 缺失 = 走旧的硬编码 prompt 路径。
|
||||||
|
|||||||
+57
-7
@@ -554,6 +554,9 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
# 本轮新检索到的原始结果(统一在下方合并段转 Material、与旧素材去重)
|
# 本轮新检索到的原始结果(统一在下方合并段转 Material、与旧素材去重)
|
||||||
new_results: list[SearchResult] = []
|
new_results: list[SearchResult] = []
|
||||||
provider = None
|
provider = None
|
||||||
|
# 内网检索接口执行诊断(排查用,最终挂到 material_package["intranet_debug"],前端忽略)。
|
||||||
|
# 记录:是否配置 / 端点 / 每个检索词命中数与报错 / 原始响应片段 / 是否被采用为兜底。
|
||||||
|
intranet_debug: dict | None = None
|
||||||
|
|
||||||
if material_source == "notebook" and notebook_search_space_id:
|
if material_source == "notebook" and notebook_search_space_id:
|
||||||
# ── 纯笔记模式 ──
|
# ── 纯笔记模式 ──
|
||||||
@@ -772,12 +775,27 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
# 预跑好的内网结果兜底(省去"技能全失败后再串行等一遍内网"的时间)。需在 config.yaml
|
# 预跑好的内网结果兜底(省去"技能全失败后再串行等一遍内网"的时间)。需在 config.yaml
|
||||||
# 的 ai_writing.intranet_search_url 配地址才启用;技能拿到结果则取消这个任务、不浪费。
|
# 的 ai_writing.intranet_search_url 配地址才启用;技能拿到结果则取消这个任务、不浪费。
|
||||||
intranet_url = (ai_writing_config.get("intranet_search_url") or "").strip()
|
intranet_url = (ai_writing_config.get("intranet_search_url") or "").strip()
|
||||||
|
# 诊断开关:开启后即使技能成功也照样跑完内网并记录完整结果(用于验证内网接口是否真能通)。
|
||||||
|
intranet_debug_force = bool(ai_writing_config.get("intranet_search_debug", False))
|
||||||
intranet_task: "asyncio.Task | None" = None
|
intranet_task: "asyncio.Task | None" = None
|
||||||
intranet_holder: dict = {}
|
intranet_holder: dict = {}
|
||||||
|
intranet_debug = {
|
||||||
|
"configured": bool(intranet_url),
|
||||||
|
"endpoint": intranet_url or "(未配置 ai_writing.intranet_search_url)",
|
||||||
|
"knowledge_base": ai_writing_config.get("intranet_knowledge_base", "samples"),
|
||||||
|
"timeout": ai_writing_config.get("intranet_search_timeout", 600),
|
||||||
|
"debug_force": intranet_debug_force,
|
||||||
|
"status": "pending" if intranet_url else "not_configured",
|
||||||
|
"queries": [], # 每个检索词:{query, count, error?}
|
||||||
|
"errors": [], # provider 聚合的报错
|
||||||
|
"raw_responses": [], # 解析失败时的原始响应片段
|
||||||
|
"result_count": 0, # 内网总命中条数
|
||||||
|
"used_as_fallback": False, # 内网结果是否被真正采用为素材
|
||||||
|
}
|
||||||
|
|
||||||
async def _run_intranet_search() -> list[SearchResult]:
|
async def _run_intranet_search() -> list[SearchResult]:
|
||||||
# 全程吞异常:并行内网任务**绝不**能把主流程带挂(实测内网接口可能瞬间失败)。
|
# 全程吞异常:并行内网任务**绝不**能把主流程带挂(实测内网接口可能瞬间失败)。
|
||||||
# 失败只记 search_errors + 日志,返回空列表,交由技能结果或空结果文案兜底。
|
# 失败只记 search_errors + 诊断 + 日志,返回空列表,交由技能结果或空结果文案兜底。
|
||||||
try:
|
try:
|
||||||
from ..search.intranet_stub import IntranetSearchProvider
|
from ..search.intranet_stub import IntranetSearchProvider
|
||||||
|
|
||||||
@@ -795,31 +813,56 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
return_exceptions=True,
|
return_exceptions=True,
|
||||||
)
|
)
|
||||||
collected: list[SearchResult] = []
|
collected: list[SearchResult] = []
|
||||||
for res in gathered:
|
for kw, res in zip(fb_keywords, gathered):
|
||||||
if isinstance(res, Exception):
|
if isinstance(res, Exception):
|
||||||
logger.warning("[intranet-parallel] one keyword failed: %s", res)
|
logger.warning("[intranet-parallel] keyword=%r failed: %s", kw, res)
|
||||||
search_errors.append(f"{type(res).__name__}: {res}")
|
search_errors.append(f"{type(res).__name__}: {res}")
|
||||||
|
intranet_debug["queries"].append({"query": kw, "count": 0, "error": f"{type(res).__name__}: {res}"})
|
||||||
continue
|
continue
|
||||||
|
intranet_debug["queries"].append({"query": kw, "count": len(res or [])})
|
||||||
collected.extend(res or [])
|
collected.extend(res or [])
|
||||||
|
intranet_debug["errors"] = list(dict.fromkeys(getattr(intranet, "errors", [])))[:5]
|
||||||
|
intranet_debug["raw_responses"] = list(dict.fromkeys(getattr(intranet, "raw_responses", [])))[:3]
|
||||||
|
intranet_debug["result_count"] = len(collected)
|
||||||
|
intranet_debug["status"] = "ran"
|
||||||
return collected
|
return collected
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
|
intranet_debug["status"] = "cancelled"
|
||||||
raise # 取消要正常传播(技能已拿到结果时主动取消)
|
raise # 取消要正常传播(技能已拿到结果时主动取消)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
logger.warning("[intranet-parallel] intranet fallback crashed: %s", e, exc_info=True)
|
logger.warning("[intranet-parallel] intranet fallback crashed: %s", e, exc_info=True)
|
||||||
search_errors.append(f"内网兜底异常:{type(e).__name__}: {e}")
|
search_errors.append(f"内网兜底异常:{type(e).__name__}: {e}")
|
||||||
|
intranet_debug["status"] = "crashed"
|
||||||
|
intranet_debug["errors"].append(f"{type(e).__name__}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if intranet_url:
|
if intranet_url:
|
||||||
intranet_task = asyncio.create_task(_run_intranet_search())
|
intranet_task = asyncio.create_task(_run_intranet_search())
|
||||||
|
|
||||||
async def _cancel_intranet_task() -> None:
|
async def _cancel_intranet_task() -> None:
|
||||||
"""技能已拿到素材:取消并行的内网检索任务,避免无谓占用 / 未取回异常告警。"""
|
"""技能已拿到素材:默认取消并行的内网检索任务,避免无谓占用。
|
||||||
if intranet_task is not None and not intranet_task.done():
|
|
||||||
intranet_task.cancel()
|
诊断模式(intranet_search_debug=true)下**不取消**,而是等它跑完拿到完整诊断,
|
||||||
|
这样即使技能成功也能在 material_package.intranet_debug 里看到内网接口的真实返回。
|
||||||
|
"""
|
||||||
|
if intranet_task is None or intranet_task.done():
|
||||||
|
if intranet_task is not None:
|
||||||
|
try:
|
||||||
|
await intranet_task
|
||||||
|
except BaseException: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
if intranet_debug_force:
|
||||||
try:
|
try:
|
||||||
await intranet_task
|
await intranet_task
|
||||||
except BaseException: # noqa: BLE001 — CancelledError / 任务内异常都吞掉
|
except BaseException: # noqa: BLE001
|
||||||
pass
|
pass
|
||||||
|
return
|
||||||
|
intranet_task.cancel()
|
||||||
|
try:
|
||||||
|
await intranet_task
|
||||||
|
except BaseException: # noqa: BLE001 — CancelledError / 任务内异常都吞掉
|
||||||
|
pass
|
||||||
|
|
||||||
for sk in ordered:
|
for sk in ordered:
|
||||||
added, error_detail = await _execute_skill(sk)
|
added, error_detail = await _execute_skill(sk)
|
||||||
@@ -871,6 +914,8 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
fb_results = []
|
fb_results = []
|
||||||
search_errors.append(f"{type(e).__name__}: {e}")
|
search_errors.append(f"{type(e).__name__}: {e}")
|
||||||
fb_added = _collect(fb_results)
|
fb_added = _collect(fb_results)
|
||||||
|
if intranet_debug is not None:
|
||||||
|
intranet_debug["used_as_fallback"] = True
|
||||||
intranet = intranet_holder.get("provider")
|
intranet = intranet_holder.get("provider")
|
||||||
if intranet is not None:
|
if intranet is not None:
|
||||||
search_errors.extend(getattr(intranet, "errors", []))
|
search_errors.extend(getattr(intranet, "errors", []))
|
||||||
@@ -1004,6 +1049,11 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
pkg["skill_search_used"] = existing_pkg.get("skill_search_used", False)
|
pkg["skill_search_used"] = existing_pkg.get("skill_search_used", False)
|
||||||
pkg["skill_search_steps"] = existing_pkg.get("skill_search_steps", [])
|
pkg["skill_search_steps"] = existing_pkg.get("skill_search_steps", [])
|
||||||
|
|
||||||
|
# 内网检索接口执行诊断:挂进 material_package,随 values 事件回前端(前端忽略此字段,
|
||||||
|
# 仅供在浏览器 devtools / 接口返回里排查内网接口到底有没有被调、返回/报错是什么)。
|
||||||
|
if intranet_debug is not None:
|
||||||
|
pkg["intranet_debug"] = intranet_debug
|
||||||
|
|
||||||
# 容错告警:检索接口 / 模型调用出错时以非致命 notice 提示用户,不打断写作流程
|
# 容错告警:检索接口 / 模型调用出错时以非致命 notice 提示用户,不打断写作流程
|
||||||
notices: list[dict] = []
|
notices: list[dict] = []
|
||||||
if kw_result.get("_llm_error"):
|
if kw_result.get("_llm_error"):
|
||||||
|
|||||||
Reference in new issue
Block a user