diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml index ee7b9d07..1fd52073 100644 --- a/offline-backend-20260512/backend/config.yaml +++ b/offline-backend-20260512/backend/config.yaml @@ -425,6 +425,16 @@ ai_writing: keyword_model: deepseek-chat # 检索词生成专用模型,留空回退写作主模型 rank_model: deepseek-chat # 素材排序专用模型,留空回退写作主模型 max_materials: 20 # 合并去重后的素材总量上限,范围 [1, 100],默认 20 + # 用「真实智能体运行时」跑配置技能(read SKILL.md + 全套工具 + 沙箱),让技能的真实 + # 检索逻辑(调内网接口/MCP/读文件)真正执行,而不是塞进通用 web/ES 检索 provider。 + # 内网技能必须开 true,否则技能会走通用 provider、连不上内网而报 ConnectError。 + # 仅对非「知识库检索(knowledge-base-search)」的配置技能生效;失败回退直连检索。 + researcher_skill_agent: true + # 最终兜底:当所有技能都没检索到素材时,直连内网知识库 ES 接口兜底检索(避免素材全空)。 + # 配上你内网 ES 接口地址即启用;留空则不启用该兜底。 + # 接口契约: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_knowledge_base: "samples" # 兜底检索用的知识库名 # 四阶段「内置功能型智能体」开关(按阶段灰度)。true = 该阶段节点改为加载 # .deer-flow/agents/ai-writing-/ 的 SOUL.md + config(admin 可在智能体 # 管理页编辑人设/模型),false / 缺失 = 走旧的硬编码 prompt 路径。 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py index b89c9acd..b0951756 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/_skill_agent.py @@ -114,7 +114,25 @@ async def run_skill_via_real_agent( logger.warning("skill real-agent[%s]: build tools failed: %s", skill_name, e) return [], f"工具初始化失败: {e}" - system_prompt = (soul + "\n\n" if soul else "") + _MATERIAL_OUTPUT_CONTRACT + # 技能在容器内的目录:注入 SKILL.md 时模型只有内容、不知道脚本在哪,会用相对路径 + # (python3 scripts/xxx.py)在错误的 CWD 下跑 → No such file。把绝对目录告诉它,让它 + # 先 cd 进去再执行(与「别的智能体」里 skill_view/read_file 拿到 location 后的行为一致)。 + try: + base_path = getattr(getattr(app_config, "skills", None), "container_path", None) or "/mnt/skills" + except Exception: # noqa: BLE001 + base_path = "/mnt/skills" + try: + skill_dir = skill.get_container_path(base_path) + except Exception: # noqa: BLE001 + skill_dir = "" + skill_loc_hint = ( + f"\n\n【技能目录(重要)】技能「{skill_name}」在容器内的目录是:{skill_dir}\n" + f"- 运行该技能自带的脚本时,**务必先 `cd {skill_dir}` 再执行**" + f"(例如 `cd {skill_dir} && python3 scripts/xxx.py ...`),或对脚本用绝对路径;\n" + "- **不要用相对路径**(如 `python3 scripts/xxx.py`),否则当前目录不对会报 No such file。" + ) if skill_dir else "" + + system_prompt = (soul + "\n\n" if soul else "") + _MATERIAL_OUTPUT_CONTRACT + skill_loc_hint task_lines = [f"写作意图:{intent or '(无)'}"] if user_query: 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 59797efa..7f4afede 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 @@ -725,7 +725,11 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict user_query=effective_user_query if is_user_query_search else "", config=config, state=state, - model_name_fallback=keyword_model, + # 跑技能的 agent 用**用户选定的写作主模型**(state.model_name),不要用 + # keyword_model:那是「检索词生成快模型」,常配成外网模型(如 deepseek-chat + # → api.deepseek.com),内网跑技能 agent 会连不上模型本身。传 None 让 + # resolve_agent_model_name 回退到 state.model_name(用户在表单选的、确定可达)。 + model_name_fallback=None, max_results=per_skill, timeout_s=_SKILL_SEARCH_TIMEOUT_SECONDS, emit_query=_emit_q, @@ -810,6 +814,43 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict # 兜底已拿到素材即止,不再无谓尝试后续技能 break + # ── 最终兜底:技能(含兜底技能)都没检索到 → 直连内网知识库接口(ES)兜一把 ── + # 用户诉求:技能因网络/配置连不上而 0 条时,至少用内网 ES 接口直检,避免素材全空。 + # 需在 config.yaml 的 ai_writing 段配 intranet_search_url 才启用;接口契约见 + # IntranetSearchProvider(POST {query,knowledge_base_name,ts} → [{page_content,metadata}])。 + if not new_results: + intranet_url = (ai_writing_config.get("intranet_search_url") or "").strip() + if intranet_url: + from ..search.intranet_stub import IntranetSearchProvider + + _emit_skill_step({ + "type": "fallback", + "message": "技能均未检索到素材,改为直连内网知识库接口兜底检索…", + }) + intranet = IntranetSearchProvider( + base_url=intranet_url, + knowledge_base_name=ai_writing_config.get("intranet_knowledge_base", "samples"), + ) + fb_keywords = keywords[:keyword_count] or [intent] + fb_gathered = await asyncio.gather( + *[intranet.search(kw, max_results=per_skill) for kw in fb_keywords], + return_exceptions=True, + ) + fb_added = 0 + for fb_results in fb_gathered: + if isinstance(fb_results, Exception): + search_errors.append(f"{type(fb_results).__name__}: {fb_results}") + continue + fb_added += _collect(fb_results) + search_errors.extend(intranet.errors) + # 让下方 notice 拿到内网接口的原始响应体(解析失败时便于定位) + provider = intranet + _emit_skill_step({ + "type": "skill_result", + "message": f"内网知识库接口兜底检索到 {fb_added} 条新素材", + "matched_count": fb_added, + }) + # 合并去重 # - 首次检索 / 普通 re_search(无输入,用户只想重出):旧素材丢弃,覆盖式替换 # - 素材不足求助 supplement_search:保留旧素材 + 追加新检索结果